commit 1b0d19f4294c76a2cb232b7be3fbc774076d7758
parent 1848cd2f09d35f76e70a15b0dffbb84b615961a7
Author: Yongbin Kim <iam@yongbin.kim>
Date: Thu, 21 Apr 2022 07:09:50 +0900
feat: 'plain' template mode was added
The *plain* template allows this library to work with non-html documents.
To use it, you can use the 'plain' attribute in the template tag.
For example, see `example/plaintext`.
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
10 files changed, 197 insertions(+), 33 deletions(-)
diff --git a/examples/plaintext/main.go b/examples/plaintext/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "log"
+ "net/http"
+
+ "go.lair.cx/tpls"
+ "go.lair.cx/tpls/examples/plaintext/templates"
+)
+
+func main() {
+ log.Fatalln(http.ListenAndServe(":8080", http.HandlerFunc(handler)))
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ tplw := tpls.NewWriter(nil)
+ templates.Render(tplw, &templates.Body{
+ Name: "World",
+ })
+ w.Header().Set("Content-Type", "text/html;charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(tplw.Bytes())
+}
diff --git a/examples/plaintext/templates/body.html b/examples/plaintext/templates/body.html
@@ -0,0 +1,15 @@
+<!--suppress HtmlUnknownAttribute, HtmlUnknownTag -->
+
+<struct class="Body">
+ Name string
+</struct>
+
+<raw>
+ func (p *Body) Title() string {
+ return "이메일입니다."
+ }
+</raw>
+
+<template class="(p *Body) Body()" plain>
+ 안녕하세요, <string>p.Name</string>님!
+</template>
diff --git a/examples/plaintext/templates/body.html.go b/examples/plaintext/templates/body.html.go
@@ -0,0 +1,20 @@
+// Code generated by tplc. DO NOT EDIT.
+
+package templates
+
+import "go.lair.cx/tpls"
+
+type Body struct {
+ Name string
+}
+
+func (p *Body) Title() string {
+ return "이메일입니다."
+}
+func (p *Body) Body(w tpls.Writer) {
+ w.WriteRaw(`
+안녕하세요, `)
+ w.WriteString(p.Name)
+ w.WriteRaw(`님!
+`)
+}
diff --git a/examples/plaintext/templates/template.html b/examples/plaintext/templates/template.html
@@ -0,0 +1,17 @@
+<!--suppress HtmlUnknownAttribute, HtmlUnknownTag -->
+
+<interface class="Page">
+ Title() string
+ Body(w tpls.Writer)
+</interface>
+
+<template class="Render(p Page)" plain>
+ Company Name
+
+ <raw>p.Body(w)</raw>
+
+ 문의사항이 있으시다면 아래 이메일 주소로 연락해주세요.
+ email@domain.tld
+
+ 감사합니다.
+</template>
diff --git a/examples/plaintext/templates/template.html.go b/examples/plaintext/templates/template.html.go
@@ -0,0 +1,22 @@
+// Code generated by tplc. DO NOT EDIT.
+
+package templates
+
+import "go.lair.cx/tpls"
+
+type Page interface {
+ Title() string
+ Body(w tpls.Writer)
+}
+
+func Render(w tpls.Writer, p Page) {
+ w.WriteRaw(`
+Company Name
+`)
+ p.Body(w)
+ w.WriteRaw(`
+문의사항이 있으시다면 아래 이메일 주소로 연락해주세요.
+email@domain.tld
+감사합니다.
+`)
+}
diff --git a/tplc/builtin_for.go b/tplc/builtin_for.go
@@ -15,7 +15,7 @@ func TagFor(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
w.WriteGoCode("for {\n")
}
- err := RenderTag(b, w, t, "for")
+ err := RenderTag(b, w, t, "for", false)
if err != nil {
return err
}
diff --git a/tplc/builtin_if.go b/tplc/builtin_if.go
@@ -20,7 +20,7 @@ func TagIf(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
err := RenderTagWithCustomTags(b, w, t, "if", map[string]TagHandler{
"then": handleThen,
"else": handleElse,
- }, true)
+ }, true, false)
if err != nil {
return err
}
@@ -30,7 +30,7 @@ func TagIf(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
}
func handleThen(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
- err := RenderTag(b, w, t, "then")
+ err := RenderTag(b, w, t, "then", false)
if err != nil {
return err
}
@@ -47,7 +47,7 @@ func handleElse(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
}
w.WriteGoCode(" {\n")
- err := RenderTag(b, w, t, "else")
+ err := RenderTag(b, w, t, "else", false)
if err != nil {
return err
}
diff --git a/tplc/builtin_module.go b/tplc/builtin_module.go
@@ -74,9 +74,9 @@ loop:
return err
}
mbt := htmlx.NewTokenizer(bytes.NewReader(moduleBody))
- return RenderTag(b, w, mbt, "")
+ return RenderTag(b, w, mbt, "", false)
} else {
- return RenderTag(b, w, t, "body")
+ return RenderTag(b, w, t, "body", false)
}
}
@@ -87,6 +87,7 @@ loop:
"body": handleBodyTag,
},
false,
+ false,
)
})
diff --git a/tplc/builtin_template.go b/tplc/builtin_template.go
@@ -19,11 +19,17 @@ import (
var regexTemplateClassParser = regexp.MustCompile(
`^\s*(\((?:[^\s)]+[ \t]+)?[^\s)]+\)[ \t]+)?([^\s(]+)\(([^)]+)?\)\s*$`)
+var regexTemplatePlainTextLineParser = regexp.MustCompile(
+ `(?:[\t\f\v ]*([\r\n])|^)\s*([^\n\r]*)`)
+
func TagTemplate(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
attrs := getAttrs(t)
+
+ _, plainText := attrs["plain"]
+
className, ok := attrs["class"]
if !ok {
- return errors.New("class is required for struct tag")
+ return errors.New("class is required for template tag")
}
caps := regexTemplateClassParser.FindStringSubmatch(className)
if caps == nil {
@@ -32,7 +38,7 @@ func TagTemplate(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
w.WriteGoCode("func ", caps[1], caps[2], "(w tpls.Writer,", caps[3], ") {\n")
- err := RenderTag(b, w, t, "template")
+ err := RenderTag(b, w, t, "template", plainText)
if err != nil {
return err
}
@@ -46,8 +52,9 @@ func RenderTag(
w *Writer,
t *htmlx.Tokenizer,
name string,
+ plainText bool,
) error {
- return renderTag(b, w, t, name, nil, false)
+ return renderTag(b, w, t, name, nil, false, plainText)
}
func RenderTagWithCustomTags(
@@ -57,8 +64,9 @@ func RenderTagWithCustomTags(
name string,
customHandlers map[string]TagHandler,
useCustomOnly bool,
+ plainText bool,
) error {
- return renderTag(b, w, t, name, customHandlers, useCustomOnly)
+ return renderTag(b, w, t, name, customHandlers, useCustomOnly, plainText)
}
func renderTag(
@@ -68,6 +76,7 @@ func renderTag(
name string,
customHandlers map[string]TagHandler,
useCustomOnly bool,
+ plainText bool, // Plain text mode - keep spaces, trim prefix indent, etc.
) error {
if t.CurrentType() == htmlx.SelfClosingTagToken {
return nil
@@ -116,6 +125,7 @@ loop:
case htmlx.EndTagToken:
tagName, _ := t.TagName()
+ stack.Lock()
if stack.Len() == 0 {
if string(tagName) != name {
return errors.New("unexpected end tag: " + string(tagName))
@@ -123,7 +133,6 @@ loop:
break loop
}
- stack.Lock()
for {
item, ok := stack.UnsafePop()
if !ok && string(tagName) == name {
@@ -139,17 +148,10 @@ loop:
case htmlx.TextToken:
var (
- txt = t.Text()
- l, r int
+ txt = t.Text()
+ l int
)
- // Reduce spaces.
- // Skip this token if there are only space characters. Replace the
- // spaces at the beginning and end to ' '.
- // Through this, the 'weird padding' between tags which is useless
- // in most cases is removed, the <if> tag works properly and save
- // few bytes while keep a gap between texts.
-
for l < len(txt) {
c, width := utf8.DecodeRune(txt[l:])
if !unicode.IsSpace(c) {
@@ -158,6 +160,7 @@ loop:
l += width
}
+ // Is empty text?
if l == len(txt) {
continue
}
@@ -166,22 +169,43 @@ loop:
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) {
- break
+ if plainText {
+ // Plain Text Mode
+ // In this mode, only the prefix 'space' character on each line
+ // is removed. space characters do not include line breaks.
+
+ lines := regexTemplatePlainTextLineParser.FindAllSubmatch(txt, -1)
+ for _, caps := range lines {
+ buf.B = append(buf.B, caps[1]...)
+ buf.B = append(buf.B, caps[2]...)
+ }
+ } else {
+ // Smart Indent Mode
+ // In this mode, space characters are reduced. The space
+ // between the tags is removed, and the space characters at the
+ // beginning and end of the text node are replaced by ' '.
+ // this removes the 'weird padding' between tags which is
+ // useless in most cases, and make <if> tag works properly
+ // and also saves few bytes while keep a gap.
+
+ var r int
+ for r = len(txt); r > 0; {
+ c, width := utf8.DecodeLastRune(txt[:r])
+ if !unicode.IsSpace(c) {
+ break
+ }
+ r -= width
}
- r -= width
- }
- if l != 0 {
- buf.B = append(buf.B, ' ')
- }
+ if l != 0 {
+ buf.B = append(buf.B, ' ')
+ }
- buf.B = append(buf.B, txt[l:r]...)
+ buf.B = append(buf.B, txt[l:r]...)
- if r != len(txt) {
- buf.B = append(buf.B, ' ')
+ if r != len(txt) {
+ buf.B = append(buf.B, ' ')
+ }
}
case htmlx.ErrorToken:
diff --git a/tplc/builtin_template_test.go b/tplc/builtin_template_test.go
@@ -12,6 +12,7 @@ func Test_renderTag(t *testing.T) {
tests := []struct {
name string
src string
+ plainText bool
tagName string
customHandlers map[string]TagHandler
useCustomOnly bool
@@ -21,6 +22,7 @@ func Test_renderTag(t *testing.T) {
{
"root document",
`<raw>fmt.Println("Hello, World!")</raw>`,
+ false,
"",
nil,
false,
@@ -32,6 +34,7 @@ func Test_renderTag(t *testing.T) {
// When renderTags is called with tagName, starting tag is
// already parsed.
`<raw>fmt.Println("Hello, World!")</raw></wrapper>`,
+ false,
"wrapper",
nil,
false,
@@ -41,6 +44,7 @@ func Test_renderTag(t *testing.T) {
{
"unknown tag with custom handler only flag",
`<div>Hello!</div></test>`,
+ false,
"test",
map[string]TagHandler{},
true,
@@ -51,6 +55,7 @@ func Test_renderTag(t *testing.T) {
"custom tag with standard tag",
`<raw>fmt.Println("Hello, World!")</raw>
<custom>Hi!</custom></test>`,
+ false,
"test",
map[string]TagHandler{
"custom": TagTestTransparent("custom"),
@@ -62,6 +67,7 @@ func Test_renderTag(t *testing.T) {
{
"with self-closing content",
`<custom /></test>`,
+ false,
"test",
map[string]TagHandler{
"custom": func(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
@@ -77,6 +83,34 @@ func Test_renderTag(t *testing.T) {
false,
"Good",
},
+ {
+ "plain text",
+ `
+ Hello, World!
+ And this is second line.
+</template>`,
+ true,
+ "template",
+ nil,
+ false,
+ false,
+ "w.WriteRaw(`\nHello, World!\nAnd this is second line.\n`)\n",
+ },
+ {
+ "plain text with tag",
+ `
+ Hello, <string>"World"</string>!
+ And this is second line.
+</template>`,
+ true,
+ "template",
+ nil,
+ false,
+ false,
+ "w.WriteRaw(`\nHello, `)\n" +
+ "w.WriteString(\"World\")\n" +
+ "w.WriteRaw(`!\nAnd this is second line.\n`)\n",
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -84,7 +118,15 @@ func Test_renderTag(t *testing.T) {
writer = NewTestWriter()
tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src))
)
- err := renderTag(writer.builder, writer, tokenizer, tt.tagName, tt.customHandlers, tt.useCustomOnly)
+ err := renderTag(
+ writer.builder,
+ writer,
+ tokenizer,
+ tt.tagName,
+ tt.customHandlers,
+ tt.useCustomOnly,
+ tt.plainText,
+ )
if (err != nil) != tt.wantErr {
t.Errorf("renderTag() error = %v, wantErr %v", err, tt.wantErr)
return