tpls

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

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:
Aexamples/plaintext/main.go | 23+++++++++++++++++++++++
Aexamples/plaintext/templates/body.html | 15+++++++++++++++
Aexamples/plaintext/templates/body.html.go | 20++++++++++++++++++++
Aexamples/plaintext/templates/template.html | 17+++++++++++++++++
Aexamples/plaintext/templates/template.html.go | 22++++++++++++++++++++++
Mtplc/builtin_for.go | 2+-
Mtplc/builtin_if.go | 6+++---
Mtplc/builtin_module.go | 5+++--
Mtplc/builtin_template.go | 76++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtplc/builtin_template_test.go | 44+++++++++++++++++++++++++++++++++++++++++++-
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