tpls

Extendable, Fast Template Engine for Go
git clone git://git.lair.cx/tpls
Log | Files | Refs | README | LICENSE

commit 8664ce4f9243f1278df70f02098658b8af015ba3
parent 199d3f170b2f5e4dfe322b760405726dca995e9c
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Wed, 26 Jan 2022 04:06:01 +0900

Clean up code for built-in tags

Diffstat:
Mgo.mod | 2+-
Mgo.sum | 2++
Ainternal/stacks/bytestack.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtplc/builder.go | 2+-
Atplc/builtin.go | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtplc/builtin_fallback.go | 50++++++++++++++++++++++++++++----------------------
Mtplc/builtin_for.go | 10++++------
Mtplc/builtin_import.go | 25+++++++++----------------
Mtplc/builtin_module.go | 2+-
Mtplc/builtin_resources.go | 19++++---------------
Mtplc/builtin_template.go | 39+++++++++++++++++++++++++--------------
Mtplc/builtin_typedef.go | 35++++++++++++++++++-----------------
12 files changed, 200 insertions(+), 93 deletions(-)

diff --git a/go.mod b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/pkg/errors v0.9.1 github.com/valyala/bytebufferpool v1.0.0 - go.lair.cx/go-core v0.0.0-20220125080507-f09759a0cb54 + go.lair.cx/go-core v0.0.0-20220125190652-563a9b063607 ) require golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba // indirect diff --git a/go.sum b/go.sum @@ -58,6 +58,8 @@ github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7Fw github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw= go.lair.cx/go-core v0.0.0-20220125080507-f09759a0cb54 h1:RLgzH0YIRwixqkwtsW0CCKqHtSjn1gmlJ8eST4oIHVY= go.lair.cx/go-core v0.0.0-20220125080507-f09759a0cb54/go.mod h1:1thuGzWT92YNMkR1/DAJE+8OEoIrx6V6ASu8KTYV7eg= +go.lair.cx/go-core v0.0.0-20220125190652-563a9b063607 h1:kgtZfT/ObsVjGitRad3K3dBLOevrMnaNKTdcK+byIwM= +go.lair.cx/go-core v0.0.0-20220125190652-563a9b063607/go.mod h1:1thuGzWT92YNMkR1/DAJE+8OEoIrx6V6ASu8KTYV7eg= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= diff --git a/internal/stacks/bytestack.go b/internal/stacks/bytestack.go @@ -0,0 +1,52 @@ +package stacks + +import ( + "sync" +) + +type ByteStack struct { + sync.Mutex + buf []byte + stack [][]byte +} + +func NewByteStack(buf []byte) *ByteStack { + return &ByteStack{ + buf: buf, + } +} + +func (s *ByteStack) Put(item []byte) { + s.Lock() + defer s.Unlock() + s.buf = append(s.buf, item...) + s.stack = append(s.stack, s.buf[len(s.buf)-len(item):]) +} + +// Pop removes last item from stack and return it. +// +// The buffer is valid until the next Put is called. +// To ensure data, call Lock directly and use UnsafePop. +func (s *ByteStack) Pop() (item []byte, ok bool) { + s.Lock() + defer s.Unlock() + return s.UnsafePop() +} + +func (s *ByteStack) UnsafePop() (item []byte, ok bool) { + var i = len(s.stack) - 1 + if i < 0 { + return nil, false + } + item, s.stack = s.stack[i], s.stack[:i] + s.buf = s.buf[:len(s.buf)-len(item)] + return item, true +} + +func (s *ByteStack) Len() int { + return len(s.stack) +} + +func (s *ByteStack) Buffer() []byte { + return s.buf +} diff --git a/tplc/builder.go b/tplc/builder.go @@ -70,7 +70,7 @@ func (b *Builder) render(r io.Reader, buf []byte) ([]byte, error) { handler, ok := w.builder.tags[string(tagName)] if !ok { - handler = TagFallback(tagName) + handler = TagFallback(string(tagName)) } err = handler(b, w, t) diff --git a/tplc/builtin.go b/tplc/builtin.go @@ -0,0 +1,55 @@ +package tplc + +import ( + "bytes" + "fmt" + "io" + + "go.lair.cx/go-core/net/htmlx" +) + +func getBodyFromTextOnlyTag(t *htmlx.Tokenizer, name string, allowEmpty bool) ([]byte, error) { + if t.CurrentType() == htmlx.SelfClosingTagToken { + if allowEmpty { + return nil, nil + } + return nil, fmt.Errorf("self-closing tag but empty is not allowed for %s", name) + } + + var buf []byte + + t.SetRawTag(name) + + for { + switch t.Next() { + case htmlx.TextToken: + buf = bytes.TrimSpace(t.Text()) + + case htmlx.EndTagToken: + if tagName, _ := t.TagName(); string(tagName) != name { + return nil, fmt.Errorf( + "%s: unexpected closing tag: %s", + name, tagName, + ) + } + if !allowEmpty && len(buf) == 0 { + return nil, fmt.Errorf("empty content is not allowed for %s", name) + } + return buf, nil + + case htmlx.ErrorToken: + err := t.Err() + if err == io.EOF { + return nil, io.ErrUnexpectedEOF + } + return nil, err + + default: + return nil, fmt.Errorf( + "invalid %s syntax: unexpected token %s", + name, + t.CurrentType().String(), + ) + } + } +} diff --git a/tplc/builtin_fallback.go b/tplc/builtin_fallback.go @@ -2,52 +2,58 @@ package tplc import ( "bytes" + "fmt" "io" + "strconv" "github.com/valyala/bytebufferpool" "go.lair.cx/go-core/net/htmlx" + "go.lair.cx/tpls/internal/stacks" ) -func TagFallback(tagName []byte) TagHandler { +// TagFallback skips current tag. +func TagFallback(name string) TagHandler { return func(_ *Builder, w *Writer, t *htmlx.Tokenizer) error { + if t.CurrentType() == htmlx.SelfClosingTagToken { + return nil + } + stackBuf := bytebufferpool.Get() defer bytebufferpool.Put(stackBuf) - stackBuf.B = append(stackBuf.B, tagName...) - - var stack = [][]byte{ - stackBuf.B[:len(tagName)], - } + stack := stacks.NewByteStack(stackBuf.B) loop: for { switch t.Next() { case htmlx.StartTagToken: - tagName, _ = t.TagName() - - // put stack - stackBuf.B = append(stackBuf.B, tagName...) - stack = append(stack, stackBuf.B[len(stackBuf.B)-len(tagName):]) + tagName, _ := t.TagName() + stack.Put(tagName) case htmlx.EndTagToken: - tagName, _ = t.TagName() - - // search the stack. ignore unmatched tags. - for i := len(stack) - 1; i >= 0; i-- { - var item []byte - item, stack = stack[i], stack[:i] - stackBuf.B = stackBuf.B[:len(stackBuf.B)-len(item)] + tagName, _ := t.TagName() + // Remove items in stack until the matched starting tag is found. + // If the tag is not exists in stack, + // - Root tag's closing. + // - Unexpected closing tag. + for { + item, ok := stack.Pop() + if !ok { // Tag is not found: + if string(tagName) == name { + break loop + } + return fmt.Errorf( + "unexpected closing tag: %s", + strconv.Quote(string(tagName)), + ) + } if bytes.Equal(item, tagName) { break } } - if len(stack) == 0 { - break loop - } - case htmlx.ErrorToken: err := t.Err() if err == io.EOF { diff --git a/tplc/builtin_for.go b/tplc/builtin_for.go @@ -3,20 +3,18 @@ package tplc import ( "strings" - "github.com/pkg/errors" - "go.lair.cx/go-core/net/htmlx" ) func TagFor(b *Builder, w *Writer, t *htmlx.Tokenizer) error { attrs := getAttrs(t) className, ok := attrs["class"] - if !ok { - return errors.New("class is required for 'for' tag") + if ok { + w.WriteGoCode("for ", strings.TrimSpace(className), " {\n") + } else { + w.WriteGoCode("for {\n") } - w.WriteGoCode("for ", strings.TrimSpace(className), " {\n") - err := RenderTag(b, w, t, "for") if err != nil { return err diff --git a/tplc/builtin_import.go b/tplc/builtin_import.go @@ -1,28 +1,21 @@ package tplc import ( - "fmt" - "go.lair.cx/go-core/net/htmlx" ) func TagImport(_ *Builder, w *Writer, t *htmlx.Tokenizer) error { - w.WriteGoCode("import (\n") - - t.SetRawTag("import") - if t.Next() != htmlx.TextToken { - return fmt.Errorf("invalid import tag syntax") + if t.CurrentType() == htmlx.SelfClosingTagToken { + return nil } - - w.WriteGoCodeBytes(t.Text()) - - if t.Next() != htmlx.EndTagToken { - return fmt.Errorf("invalid import tag syntax") + body, err := getBodyFromTextOnlyTag(t, "import", true) + if err != nil { + return err } - if tagName, _ := t.TagName(); string(tagName) != "import" { - return fmt.Errorf("invalid import tag syntax") + if len(body) > 0 { + w.WriteGoCode("import (") + w.WriteGoCodeBytes(body) + w.WriteGoCode(")\n\n") } - - w.WriteGoCode(")\n\n") return nil } diff --git a/tplc/builtin_module.go b/tplc/builtin_module.go @@ -69,7 +69,7 @@ loop: handleBodyTag := func(b *Builder, w *Writer, t *htmlx.Tokenizer) error { if len(moduleBody) > 0 { - err := TagFallback([]byte("body"))(b, w, t) + err := TagFallback("body")(b, w, t) if err != nil { return err } diff --git a/tplc/builtin_resources.go b/tplc/builtin_resources.go @@ -1,9 +1,6 @@ package tplc import ( - "bytes" - "fmt" - "go.lair.cx/go-core/net/htmlx" ) @@ -62,19 +59,11 @@ func TagTitle(_ *Builder, w *Writer, t *htmlx.Tokenizer) error { } func handleResourceTag(t *htmlx.Tokenizer, name string, handler func([]byte)) error { - t.SetRawTag(name) - - if t.Next() != htmlx.TextToken { - return fmt.Errorf("invalid %s syntax: unexpected text token", name) + body, err := getBodyFromTextOnlyTag(t, name, false) + if err != nil { + return err } - handler(bytes.TrimSpace(t.Text())) - - if t.Next() != htmlx.EndTagToken { - return fmt.Errorf("invalid %s syntax", name) - } - if tagName, _ := t.TagName(); string(tagName) != name { - return fmt.Errorf("invalid %s syntax", name) - } + handler(body) return nil } diff --git a/tplc/builtin_template.go b/tplc/builtin_template.go @@ -13,6 +13,7 @@ import ( "github.com/valyala/bytebufferpool" "go.lair.cx/go-core/net/htmlx" + "go.lair.cx/tpls/internal/stacks" ) var regexTemplateClassParser = regexp.MustCompile( @@ -68,9 +69,18 @@ func renderTag( customHandlers map[string]TagHandler, useCustomOnly bool, ) error { + if t.CurrentType() == htmlx.SelfClosingTagToken { + return nil + } + buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) + stackBuf := bytebufferpool.Get() + defer bytebufferpool.Put(stackBuf) + + stack := stacks.NewByteStack(stackBuf.B) + flushBuffer := func() { if len(buf.B) > 0 { w.WriteRawBytesWithBacktick(buf.B) @@ -78,12 +88,10 @@ func renderTag( } } - var stack [][]byte - loop: for { switch t.Next() { - case htmlx.StartTagToken: + case htmlx.StartTagToken, htmlx.SelfClosingTagToken: tagName, _ := t.TagName() handler := b.Handler(string(tagName)) @@ -95,7 +103,7 @@ loop: return fmt.Errorf("unexpected tag: %s", tagName) } buf.B = append(buf.B, t.Raw()...) - stack = append(stack, tagName) + stack.Put(tagName) } else { flushBuffer() @@ -108,25 +116,24 @@ loop: case htmlx.EndTagToken: tagName, _ := t.TagName() - if len(stack) == 0 { + if stack.Len() == 0 { if string(tagName) != name { return errors.New("unexpected end tag: " + string(tagName)) } break loop } - for i := len(stack) - 1; i >= 0; i-- { - var item []byte - item, stack = stack[i], stack[:i] - + stack.Lock() + for { + item, ok := stack.UnsafePop() + if !ok && string(tagName) == name { + break loop + } if bytes.Equal(item, tagName) { break } } - - if len(stack) == 0 && string(tagName) == name { - break loop - } + stack.Unlock() buf.B = append(buf.B, t.Raw()...) @@ -155,6 +162,10 @@ loop: continue } + if useCustomOnly { + return fmt.Errorf("text node is not allowed for %s", name) + } + for r = len(txt); r > 0; { c, width := utf8.DecodeLastRune(txt[:r]) if !unicode.IsSpace(c) { @@ -176,7 +187,7 @@ loop: case htmlx.ErrorToken: err := t.Err() if err == io.EOF { - if len(stack) == 0 && len(name) == 0 { + if stack.Len() == 0 && len(name) == 0 { break loop } return io.ErrUnexpectedEOF diff --git a/tplc/builtin_typedef.go b/tplc/builtin_typedef.go @@ -3,8 +3,6 @@ package tplc import ( "fmt" - "github.com/pkg/errors" - "go.lair.cx/go-core/net/htmlx" ) @@ -20,26 +18,29 @@ func handleTypeTag(w *Writer, t *htmlx.Tokenizer, name string) error { attrs := getAttrs(t) className, ok := attrs["class"] if !ok { - return errors.New("class is required for struct tag") + return fmt.Errorf("class is required for %s tag", name) } w.WriteGoCode("type ", className, " ", name, " {") - t.SetRawTag(name) - if t.Next() != htmlx.TextToken { - return fmt.Errorf("invalid %s tag syntax", name) + if t.CurrentType() == htmlx.SelfClosingTagToken { + w.WriteGoCode("}\n\n") + return nil } - w.WriteGoCodeBytes(t.Text()) - - if t.Next() != htmlx.EndTagToken { - return fmt.Errorf("invalid %s syntax", name) - } - if tagName, _ := t.TagName(); string(tagName) != name { - return fmt.Errorf("invalid %s syntax", name) + t.SetRawTag(name) + for { + switch t.Next() { + case htmlx.TextToken: + w.WriteGoCodeBytes(t.Text()) + + case htmlx.EndTagToken: + if tagName, _ := t.TagName(); string(tagName) != name { + return fmt.Errorf("invalid %s syntax", name) + } + + w.WriteGoCode("}\n\n") + return nil + } } - - w.WriteGoCode("}\n\n") - - return nil }