Skip to content

Commit

Permalink
Optimize template replacement speed
Browse files Browse the repository at this point in the history
  • Loading branch information
0fv committed Jun 24, 2024
1 parent a864613 commit 558be3b
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 74 deletions.
4 changes: 2 additions & 2 deletions ImageProcesser.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func processImage(img *Image) (imgXMLStr string, err error) {
Space: "",
Local: "Default",
},
Attrs: []xml.Attr{
Attrs: []*xml.Attr{
{Name: xml.Name{Space: "", Local: "Extension"}, Value: imgExt},
{Name: xml.Name{Space: "", Local: "ContentType"}, Value: "image/" + imgExt},
},
Expand All @@ -76,7 +76,7 @@ func processImage(img *Image) (imgXMLStr string, err error) {
Space: "",
Local: "Relationship",
},
Attrs: []xml.Attr{
Attrs: []*xml.Attr{
{Name: xml.Name{Space: "", Local: "Id"}, Value: rid},
{Name: xml.Name{Space: "", Local: "Type"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"},
{Name: xml.Name{Space: "", Local: "Target"}, Value: "media/" + imgPath},
Expand Down
2 changes: 1 addition & 1 deletion Template.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func OpenTemplateWithURL(docurl string) (tpl *Template, err error) {

// Params - replace template placeholders with params
// "Hello {{ Name }}!"" --> "Hello World!""
func (t *Template) Params(v any) {
func (t *Template) Params(v interface{}) {
// t.params = collectParams("", v)
switch val := v.(type) {
case map[string]interface{}:
Expand Down
32 changes: 32 additions & 0 deletions Template.helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/xml"
"log"
"strings"
)

// Convert given bytes to struct of xml nodes
Expand Down Expand Up @@ -119,3 +120,34 @@ func (t *Template) matchBrokenLeftPlaceholder(content string) bool {
func (t *Template) matchBrokenRightPlaceholder(content string) bool {
return t.matchBrokenPlaceholder(content, false)
}

func (t Template) GetContentPrefixList(content []byte) []string {
var ret []string
var record strings.Builder
start := false
length := len(content)
for i, v := range content {
if i == 0 {
continue
}

if v == '{' && content[i-1] == '{' {
start = true
continue
}
if start {
if v == ' ' || (v == '}' && length-1 > i && content[i+1] == '}') {
ret = append(ret, record.String())
record.Reset()
start = false
}
if v == '.' {
ret = append(ret, record.String())
record.Reset()
continue
}
record.WriteByte(v)
}
}
return ret
}
139 changes: 83 additions & 56 deletions Template.stage.funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,31 +44,50 @@ func (t *Template) triggerMissingParams(xnode *xmlNode) {

// Expand complex placeholders
func (t *Template) expandPlaceholders(xnode *xmlNode) {
type xmlNodeContent struct {
node *xmlNode
contents []byte
}
prefixNodeMap := map[string][]xmlNodeContent{}
xnode.WalkWithEnd(func(nrow *xmlNode) bool {
if nrow.isNew {
return false
}
if !nrow.isRowElement() {
return false
}
contents := nrow.AllContents()
prefixList := t.GetContentPrefixList(contents)
for _, prefix := range prefixList {
prefixNodeMap[prefix] = append(prefixNodeMap[prefix], xmlNodeContent{
contents: contents,
node: nrow,
})
}
return true
})
t.params.Walk(func(p *Param) {
if p.Type != SliceParam {
return
}

prefixes := []string{
p.PlaceholderPrefix(),
p.ToCompact(p.PlaceholderPrefix()),
p.AbsoluteKey,
p.ToCompact(p.AbsoluteKey),
}
if prefixes[0] == prefixes[1] {
prefixes = prefixes[:1]
}

var max int
for _, prefix := range prefixes {
xnode.Walk(func(nrow *xmlNode) {
if nrow.isNew {
return
}
if !nrow.isRowElement() {
return
}
if !nrow.AnyChildContains([]byte(prefix)) {
return
}

contents := nrow.AllContents()
rowParams := rowParams(contents)
nodeList, ok := prefixNodeMap[prefix]
if !ok {
continue
}
for i := range nodeList {
node := nodeList[i]
nrow := node.node
rowParams := rowParams(node.contents)
rowPlaceholders := make(map[string]*placeholder)
// Collect placeholder that for expansion
for _, rowParam := range rowParams {
Expand Down Expand Up @@ -138,7 +157,7 @@ func (t *Template) expandPlaceholders(xnode *xmlNode) {
}
}
}
})
}
}
})

Expand All @@ -151,51 +170,59 @@ func (t *Template) expandPlaceholders(xnode *xmlNode) {

// Replace single params by type
func (t *Template) replaceSingleParams(xnode *xmlNode, triggerParamOnly bool) {
replaceAttr := []*xml.Attr{}
xnodeList := []*xmlNode{}
xnode.Walk(func(n *xmlNode) {
if n == nil || n.isDeleted {
return
}

// node params
t.params.Walk(func(p *Param) {
for i, attr := range n.Attrs {
if strings.Contains(attr.Value, "{{") {
n.Attrs[i].Value = string(p.replaceIn([]byte(attr.Value)))
}
for _, attr := range n.Attrs {
if strings.Contains(attr.Value, "{{") {
replaceAttr = append(replaceAttr, attr)
}
})

// node contentt
if bytes.Contains(n.Content, []byte("{{")) {
// Try to replace on node that contains possible placeholder
t.params.Walk(func(p *Param) {
// Only string and image param to replace
if p.Type != StringParam && p.Type != ImageParam {
return
}
// Prefix check
if !n.ContentHasPrefix(p.PlaceholderPrefix()) {
return
}
// Trigger: does placeholder have trigger
if p.Trigger = p.extractTriggerFrom(n.Content); p.Trigger != nil {
defer func() {
p.RunTrigger(n)
}()
}
if triggerParamOnly {
return
}
// Repalce by type
switch p.Type {
case StringParam:
t.replaceTextParam(n, p)
case ImageParam:
t.replaceImageParams(n, p)
}
})
}
xnodeList = append(xnodeList, n)
})
paramAbsoluteKeyMap := map[string]*Param{}
t.params.Walk(func(p *Param) {
for _, v := range replaceAttr {
v.Value = string(p.replaceIn([]byte(v.Value)))
}
if p.Type != StringParam && p.Type != ImageParam {
return
}
paramAbsoluteKeyMap[p.AbsoluteKey] = p
})
for i := range xnodeList {
n := xnodeList[i]
for _, key := range n.GetContentPrefixList() {
p, ok := paramAbsoluteKeyMap[key]
if !ok {
continue
}
t.replaceAndRunTrigger(p, n, triggerParamOnly)
}
}
}

func (t *Template) replaceAndRunTrigger(p *Param, n *xmlNode, triggerParamOnly bool) {
// Trigger: does placeholder have trigger
if p.Trigger = p.extractTriggerFrom(n.Content); p.Trigger != nil {
// if
defer func() {
p.RunTrigger(n)
}()
}
if triggerParamOnly {
return
}
// Repalce by type
switch p.Type {
case StringParam:
t.replaceTextParam(n, p)
case ImageParam:
t.replaceImageParams(n, p)
}
}

// Enhance some markup (removed when building XML in the end)
Expand All @@ -215,7 +242,7 @@ func (t *Template) enhanceMarkup(xnode *xmlNode) {
}

// n.XMLName.Local = "w-item"
n.Attrs = append(n.Attrs, xml.Attr{
n.Attrs = append(n.Attrs, &xml.Attr{
Name: xml.Name{Local: "list-id"},
Value: listID,
})
Expand Down
68 changes: 68 additions & 0 deletions t.docx_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package docxplate_test

import (
"log"
"testing"

"github.com/bobiverse/docxplate"
)

func BenchmarkLists100(b *testing.B) {
var user = User{
Name: "Walter",
}
for i := 0; i < 100; i++ {
user.Friends = append(user.Friends, &User{Name: "Bob", Age: 28})
}

tdoc, _ := docxplate.OpenTemplate("test-data/lists.docx")
tdoc.Params(user)
if err := tdoc.ExportDocx("test-data/~test-lists.docx"); err != nil {
log.Fatal(err)
}
}

func BenchmarkLists200(b *testing.B) {
var user = User{
Name: "Walter",
}
for i := 0; i < 200; i++ {
user.Friends = append(user.Friends, &User{Name: "Bob", Age: 28})
}

tdoc, _ := docxplate.OpenTemplate("test-data/lists.docx")
tdoc.Params(user)
if err := tdoc.ExportDocx("test-data/~test-lists.docx"); err != nil {
log.Fatal(err)
}
}

func BenchmarkLists400(b *testing.B) {
var user = User{
Name: "Walter",
}
for i := 0; i < 400; i++ {
user.Friends = append(user.Friends, &User{Name: "Bob", Age: 28})
}

tdoc, _ := docxplate.OpenTemplate("test-data/lists.docx")
tdoc.Params(user)
if err := tdoc.ExportDocx("test-data/~test-lists.docx"); err != nil {
log.Fatal(err)
}
}

func BenchmarkLists1000(b *testing.B) {
var user = User{
Name: "Walter",
}
for i := 0; i < 1000; i++ {
user.Friends = append(user.Friends, &User{Name: "Bob", Age: 28})
}

tdoc, _ := docxplate.OpenTemplate("test-data/lists.docx")
tdoc.Params(user)
if err := tdoc.ExportDocx("test-data/~test-lists.docx"); err != nil {
log.Fatal(err)
}
}
Loading

0 comments on commit 558be3b

Please sign in to comment.