Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize template replacement speed #33

Merged
merged 1 commit into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -137,7 +137,7 @@ func OpenTemplateWithURL(docurl string) (tpl *Template, err error) {
func (t *Template) Params(v any) {
// t.params = collectParams("", v)
switch val := v.(type) {
case map[string]interface{}:
case map[string]any:
t.params = mapToParams(val)
case string:
t.params = JSONToParams([]byte(val))
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
Loading