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:
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
}