mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
tpl/internal: Synch Go templates fork with Go 1.16dev
This commit is contained in:
parent
66beac99c6
commit
cf3e077da3
25 changed files with 2520 additions and 137 deletions
|
@ -18,7 +18,7 @@ import (
|
|||
|
||||
func main() {
|
||||
// TODO(bep) git checkout tag
|
||||
// The current is built with Go version b68fa57c599720d33a2d735782969ce95eabf794 / go1.15dev
|
||||
// The current is built with Go version da54dfb6a1f3bef827b9ec3780c98fde77a97d11 / go1.16dev
|
||||
fmt.Println("Forking ...")
|
||||
defer fmt.Println("Done ...")
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ const KnownEnv = `
|
|||
GOSUMDB
|
||||
GOTMPDIR
|
||||
GOTOOLDIR
|
||||
GOVCS
|
||||
GOWASM
|
||||
GO_EXTLINK_ENABLED
|
||||
PKG_CONFIG
|
||||
|
|
|
@ -130,7 +130,7 @@ func compare(aVal, bVal reflect.Value) int {
|
|||
default:
|
||||
return -1
|
||||
}
|
||||
case reflect.Ptr:
|
||||
case reflect.Ptr, reflect.UnsafePointer:
|
||||
a, b := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case a < b:
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var compareTests = [][]reflect.Value{
|
||||
|
@ -32,6 +33,7 @@ var compareTests = [][]reflect.Value{
|
|||
ct(reflect.TypeOf(complex128(0+1i)), -1-1i, -1+0i, -1+1i, 0-1i, 0+0i, 0+1i, 1-1i, 1+0i, 1+1i),
|
||||
ct(reflect.TypeOf(false), false, true),
|
||||
ct(reflect.TypeOf(&ints[0]), &ints[0], &ints[1], &ints[2]),
|
||||
ct(reflect.TypeOf(unsafe.Pointer(&ints[0])), unsafe.Pointer(&ints[0]), unsafe.Pointer(&ints[1]), unsafe.Pointer(&ints[2])),
|
||||
ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]),
|
||||
ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}),
|
||||
ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}),
|
||||
|
@ -118,6 +120,10 @@ var sortTests = []sortTest{
|
|||
pointerMap(),
|
||||
"PTR0:0 PTR1:1 PTR2:2",
|
||||
},
|
||||
{
|
||||
unsafePointerMap(),
|
||||
"UNSAFEPTR0:0 UNSAFEPTR1:1 UNSAFEPTR2:2",
|
||||
},
|
||||
{
|
||||
map[toy]string{{7, 2}: "72", {7, 1}: "71", {3, 4}: "34"},
|
||||
"{3 4}:34 {7 1}:71 {7 2}:72",
|
||||
|
@ -159,6 +165,14 @@ func sprintKey(key reflect.Value) string {
|
|||
}
|
||||
}
|
||||
return "PTR???"
|
||||
case "unsafe.Pointer":
|
||||
ptr := key.Interface().(unsafe.Pointer)
|
||||
for i := range ints {
|
||||
if ptr == unsafe.Pointer(&ints[i]) {
|
||||
return fmt.Sprintf("UNSAFEPTR%d", i)
|
||||
}
|
||||
}
|
||||
return "UNSAFEPTR???"
|
||||
case "chan int":
|
||||
c := key.Interface().(chan int)
|
||||
for i := range chans {
|
||||
|
@ -185,6 +199,14 @@ func pointerMap() map[*int]string {
|
|||
return m
|
||||
}
|
||||
|
||||
func unsafePointerMap() map[unsafe.Pointer]string {
|
||||
m := make(map[unsafe.Pointer]string)
|
||||
for i := 2; i >= 0; i-- {
|
||||
m[unsafe.Pointer(&ints[i])] = fmt.Sprint(i)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func chanMap() map[chan int]string {
|
||||
m := make(map[chan int]string)
|
||||
for i := 2; i >= 0; i-- {
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
func TestAddParseTree(t *testing.T) {
|
||||
func TestAddParseTreeHTML(t *testing.T) {
|
||||
root := Must(New("root").Parse(`{{define "a"}} {{.}} {{template "b"}} {{.}} "></a>{{end}}`))
|
||||
tree, err := parse.Parse("t", `{{define "b"}}<a href="{{end}}`, "", "", nil, nil)
|
||||
if err != nil {
|
||||
|
@ -174,7 +174,7 @@ func TestCloneThenParse(t *testing.T) {
|
|||
t.Error("adding a template to a clone added it to the original")
|
||||
}
|
||||
// double check that the embedded template isn't available in the original
|
||||
err := t0.ExecuteTemplate(ioutil.Discard, "a", nil)
|
||||
err := t0.ExecuteTemplate(io.Discard, "a", nil)
|
||||
if err == nil {
|
||||
t.Error("expected 'no such template' error")
|
||||
}
|
||||
|
@ -188,13 +188,13 @@ func TestFuncMapWorksAfterClone(t *testing.T) {
|
|||
|
||||
// get the expected error output (no clone)
|
||||
uncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))
|
||||
wantErr := uncloned.Execute(ioutil.Discard, nil)
|
||||
wantErr := uncloned.Execute(io.Discard, nil)
|
||||
|
||||
// toClone must be the same as uncloned. It has to be recreated from scratch,
|
||||
// since cloning cannot occur after execution.
|
||||
toClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}"))
|
||||
cloned := Must(toClone.Clone())
|
||||
gotErr := cloned.Execute(ioutil.Discard, nil)
|
||||
gotErr := cloned.Execute(io.Discard, nil)
|
||||
|
||||
if wantErr.Error() != gotErr.Error() {
|
||||
t.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr)
|
||||
|
@ -216,7 +216,7 @@ func TestTemplateCloneExecuteRace(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
if err := tmpl.Execute(ioutil.Discard, "data"); err != nil {
|
||||
if err := tmpl.Execute(io.Discard, "data"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -240,7 +240,7 @@ func TestCloneGrowth(t *testing.T) {
|
|||
tmpl = Must(tmpl.Clone())
|
||||
Must(tmpl.Parse(`{{define "B"}}Text{{end}}`))
|
||||
for i := 0; i < 10; i++ {
|
||||
tmpl.Execute(ioutil.Discard, nil)
|
||||
tmpl.Execute(io.Discard, nil)
|
||||
}
|
||||
if len(tmpl.DefinedTemplates()) > 200 {
|
||||
t.Fatalf("too many templates: %v", len(tmpl.DefinedTemplates()))
|
||||
|
@ -260,7 +260,7 @@ func TestCloneRedefinedName(t *testing.T) {
|
|||
for i := 0; i < 2; i++ {
|
||||
t2 := Must(t1.Clone())
|
||||
t2 = Must(t2.New(fmt.Sprintf("%d", i)).Parse(page))
|
||||
err := t2.Execute(ioutil.Discard, nil)
|
||||
err := t2.Execute(io.Discard, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -404,11 +404,11 @@ func TestTypedContent(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that we print using the String method. Was issue 3073.
|
||||
type stringer struct {
|
||||
type myStringer struct {
|
||||
v int
|
||||
}
|
||||
|
||||
func (s *stringer) String() string {
|
||||
func (s *myStringer) String() string {
|
||||
return fmt.Sprintf("string=%d", s.v)
|
||||
}
|
||||
|
||||
|
@ -421,7 +421,7 @@ func (s *errorer) Error() string {
|
|||
}
|
||||
|
||||
func TestStringer(t *testing.T) {
|
||||
s := &stringer{3}
|
||||
s := &myStringer{3}
|
||||
b := new(bytes.Buffer)
|
||||
tmpl := Must(New("x").Parse("{{.}}"))
|
||||
if err := tmpl.Execute(b, s); err != nil {
|
||||
|
|
|
@ -125,6 +125,8 @@ func (e *escaper) escape(c context, n parse.Node) context {
|
|||
switch n := n.(type) {
|
||||
case *parse.ActionNode:
|
||||
return e.escapeAction(c, n)
|
||||
case *parse.CommentNode:
|
||||
return c
|
||||
case *parse.IfNode:
|
||||
return e.escapeBranch(c, &n.BranchNode, "if")
|
||||
case *parse.ListNode:
|
||||
|
|
|
@ -1825,7 +1825,7 @@ func TestIndirectPrint(t *testing.T) {
|
|||
}
|
||||
|
||||
// This is a test for issue 3272.
|
||||
func TestEmptyTemplate(t *testing.T) {
|
||||
func TestEmptyTemplateHTML(t *testing.T) {
|
||||
page := Must(New("page").ParseFiles(os.DevNull))
|
||||
if err := page.ExecuteTemplate(os.Stdout, "page", "nothing"); err == nil {
|
||||
t.Fatal("expected error")
|
||||
|
|
1711
tpl/internal/go_templates/htmltemplate/exec_test.go
Normal file
1711
tpl/internal/go_templates/htmltemplate/exec_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -240,8 +240,7 @@ func htmlNameFilter(args ...interface{}) string {
|
|||
}
|
||||
s = strings.ToLower(s)
|
||||
if t := attrType(s); t != contentTypePlain {
|
||||
// TODO: Split attr and element name part filters so we can whitelist
|
||||
// attributes.
|
||||
// TODO: Split attr and element name part filters so we can recognize known attributes.
|
||||
return filterFailsafe
|
||||
}
|
||||
for _, r := range s {
|
||||
|
|
292
tpl/internal/go_templates/htmltemplate/multi_test.go
Normal file
292
tpl/internal/go_templates/htmltemplate/multi_test.go
Normal file
|
@ -0,0 +1,292 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Tests for multiple-template execution, copied from text/template.
|
||||
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
var multiExecTests = []execTest{
|
||||
{"empty", "", "", nil, true},
|
||||
{"text", "some text", "some text", nil, true},
|
||||
{"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true},
|
||||
{"invoke x no args", `{{template "x"}}`, "TEXT", tVal, true},
|
||||
{"invoke dot int", `{{template "dot" .I}}`, "17", tVal, true},
|
||||
{"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true},
|
||||
{"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true},
|
||||
{"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true},
|
||||
{"variable declared by template", `{{template "nested" $x:=.SI}},{{index $x 1}}`, "[3 4 5],4", tVal, true},
|
||||
|
||||
// User-defined function: test argument evaluator.
|
||||
{"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true},
|
||||
{"testFunc .", `{{oneArg .}}`, "oneArg=joe", "joe", true},
|
||||
}
|
||||
|
||||
// These strings are also in testdata/*.
|
||||
const multiText1 = `
|
||||
{{define "x"}}TEXT{{end}}
|
||||
{{define "dotV"}}{{.V}}{{end}}
|
||||
`
|
||||
|
||||
const multiText2 = `
|
||||
{{define "dot"}}{{.}}{{end}}
|
||||
{{define "nested"}}{{template "dot" .}}{{end}}
|
||||
`
|
||||
|
||||
func TestMultiExecute(t *testing.T) {
|
||||
// Declare a couple of templates first.
|
||||
template, err := New("root").Parse(multiText1)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 1: %s", err)
|
||||
}
|
||||
_, err = template.Parse(multiText2)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 2: %s", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFiles(t *testing.T) {
|
||||
_, err := ParseFiles("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlob(t *testing.T) {
|
||||
_, err := ParseGlob("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
_, err = New("error").ParseGlob("[x")
|
||||
if err == nil {
|
||||
t.Error("expected error for bad pattern; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseGlob("testdata/file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFS(t *testing.T) {
|
||||
fs := os.DirFS("testdata")
|
||||
|
||||
{
|
||||
_, err := ParseFS(fs, "DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
template := New("root")
|
||||
_, err := template.ParseFS(fs, "file1.tmpl", "file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
{
|
||||
template := New("root")
|
||||
_, err := template.ParseFS(fs, "file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
}
|
||||
|
||||
// In these tests, actual content (not just template definitions) comes from the parsed files.
|
||||
|
||||
var templateFileExecTests = []execTest{
|
||||
{"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\n\ny\ntemplate2\n\nx\n", 0, true},
|
||||
}
|
||||
|
||||
func TestParseFilesWithData(t *testing.T) {
|
||||
template, err := New("root").ParseFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlobWithData(t *testing.T) {
|
||||
template, err := New("root").ParseGlob("testdata/tmpl*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseZipFS(t *testing.T) {
|
||||
z, err := zip.OpenReader("testdata/fs.zip")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing zip: %v", err)
|
||||
}
|
||||
template, err := New("root").ParseFS(z, "tmpl*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
const (
|
||||
cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}`
|
||||
cloneText2 = `{{define "b"}}b{{end}}`
|
||||
cloneText3 = `{{define "c"}}root{{end}}`
|
||||
cloneText4 = `{{define "c"}}clone{{end}}`
|
||||
)
|
||||
|
||||
// Issue 7032
|
||||
func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
|
||||
master := "{{define \"master\"}}{{end}}"
|
||||
tmpl := New("master")
|
||||
tree, err := parse.Parse("master", master, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse err: %v", err)
|
||||
}
|
||||
masterTree := tree["master"]
|
||||
tmpl.AddParseTree("master", masterTree) // used to panic
|
||||
}
|
||||
|
||||
func TestRedefinition(t *testing.T) {
|
||||
var tmpl *Template
|
||||
var err error
|
||||
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parse 1: %v", err)
|
||||
}
|
||||
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err != nil {
|
||||
t.Fatalf("got error %v, expected nil", err)
|
||||
}
|
||||
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err != nil {
|
||||
t.Fatalf("got error %v, expected nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 10879
|
||||
func TestEmptyTemplateCloneCrash(t *testing.T) {
|
||||
t1 := New("base")
|
||||
t1.Clone() // used to panic
|
||||
}
|
||||
|
||||
// Issue 10910, 10926
|
||||
func TestTemplateLookUp(t *testing.T) {
|
||||
t.Skip("broken on html/template") // TODO
|
||||
t1 := New("foo")
|
||||
if t1.Lookup("foo") != nil {
|
||||
t.Error("Lookup returned non-nil value for undefined template foo")
|
||||
}
|
||||
t1.New("bar")
|
||||
if t1.Lookup("bar") != nil {
|
||||
t.Error("Lookup returned non-nil value for undefined template bar")
|
||||
}
|
||||
t1.Parse(`{{define "foo"}}test{{end}}`)
|
||||
if t1.Lookup("foo") == nil {
|
||||
t.Error("Lookup returned nil value for defined template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
// In multiple calls to Parse with the same receiver template, only one call
|
||||
// can contain text other than space, comments, and template definitions
|
||||
t1 := New("test")
|
||||
if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil {
|
||||
t.Fatalf("parsing test: %s", err)
|
||||
}
|
||||
if _, err := t1.Parse(`{{define "test"}}{{/* this is a comment */}}{{end}}`); err != nil {
|
||||
t.Fatalf("parsing test: %s", err)
|
||||
}
|
||||
if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parsing test: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyTemplate(t *testing.T) {
|
||||
cases := []struct {
|
||||
defn []string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{[]string{"x", "y"}, "", "y"},
|
||||
{[]string{""}, "once", ""},
|
||||
{[]string{"", ""}, "twice", ""},
|
||||
{[]string{"{{.}}", "{{.}}"}, "twice", "twice"},
|
||||
{[]string{"{{/* a comment */}}", "{{/* a comment */}}"}, "comment", ""},
|
||||
{[]string{"{{.}}", ""}, "twice", "twice"}, // TODO: should want "" not "twice"
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
root := New("root")
|
||||
|
||||
var (
|
||||
m *Template
|
||||
err error
|
||||
)
|
||||
for _, d := range c.defn {
|
||||
m, err = root.New(c.in).Parse(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := m.Execute(buf, c.in); err != nil {
|
||||
t.Error(i, err)
|
||||
continue
|
||||
}
|
||||
if buf.String() != c.want {
|
||||
t.Errorf("expected string %q: got %q", c.want, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 19249 was a regression in 1.8 caused by the handling of empty
|
||||
// templates added in that release, which got different answers depending
|
||||
// on the order templates appeared in the internal map.
|
||||
func TestIssue19294(t *testing.T) {
|
||||
// The empty block in "xhtml" should be replaced during execution
|
||||
// by the contents of "stylesheet", but if the internal map associating
|
||||
// names with templates is built in the wrong order, the empty block
|
||||
// looks non-empty and this doesn't happen.
|
||||
var inlined = map[string]string{
|
||||
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
|
||||
"xhtml": `{{block "stylesheet" .}}{{end}}`,
|
||||
}
|
||||
all := []string{"stylesheet", "xhtml"}
|
||||
for i := 0; i < 100; i++ {
|
||||
res, err := New("title.xhtml").Parse(`{{template "xhtml" .}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range all {
|
||||
_, err := res.New(name).Parse(inlined[name])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
res.Execute(&buf, 0)
|
||||
if buf.String() != "stylesheet" {
|
||||
t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@ package template
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
|
@ -385,7 +387,7 @@ func Must(t *Template, err error) *Template {
|
|||
// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template
|
||||
// named "foo", while "a/foo" is unavailable.
|
||||
func ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(nil, filenames...)
|
||||
return parseFiles(nil, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// ParseFiles parses the named files and associates the resulting templates with
|
||||
|
@ -397,12 +399,12 @@ func ParseFiles(filenames ...string) (*Template, error) {
|
|||
//
|
||||
// ParseFiles returns an error if t or any associated template has already been executed.
|
||||
func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(t, filenames...)
|
||||
return parseFiles(t, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// parseFiles is the helper for the method and function. If the argument
|
||||
// template is nil, it is created from the first file.
|
||||
func parseFiles(t *Template, filenames ...string) (*Template, error) {
|
||||
func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) {
|
||||
if err := t.checkCanParse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -412,12 +414,11 @@ func parseFiles(t *Template, filenames ...string) (*Template, error) {
|
|||
return nil, fmt.Errorf("html/template: no files named in call to ParseFiles")
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
name, b, err := readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(b)
|
||||
name := filepath.Base(filename)
|
||||
// First template becomes return value if not already defined,
|
||||
// and we use that one for subsequent New calls to associate
|
||||
// all the templates together. Also, if this file has the same name
|
||||
|
@ -480,7 +481,7 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
|
|||
if len(filenames) == 0 {
|
||||
return nil, fmt.Errorf("html/template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
return parseFiles(t, filenames...)
|
||||
return parseFiles(t, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// IsTrue reports whether the value is 'true', in the sense of not the zero of its type,
|
||||
|
@ -489,3 +490,48 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
|
|||
func IsTrue(val interface{}) (truth, ok bool) {
|
||||
return template.IsTrue(val)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
func ParseFS(fs fs.FS, patterns ...string) (*Template, error) {
|
||||
return parseFS(nil, fs, patterns)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error) {
|
||||
return parseFS(t, fs, patterns)
|
||||
}
|
||||
|
||||
func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) {
|
||||
var filenames []string
|
||||
for _, pattern := range patterns {
|
||||
list, err := fs.Glob(fsys, pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
filenames = append(filenames, list...)
|
||||
}
|
||||
return parseFiles(t, readFileFS(fsys), filenames...)
|
||||
}
|
||||
|
||||
func readFileOS(file string) (name string, b []byte, err error) {
|
||||
name = filepath.Base(file)
|
||||
b, err = ioutil.ReadFile(file)
|
||||
return
|
||||
}
|
||||
|
||||
func readFileFS(fsys fs.FS) func(string) (string, []byte, error) {
|
||||
return func(file string) (name string, b []byte, err error) {
|
||||
name = path.Base(file)
|
||||
b, err = fs.ReadFile(fsys, file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,11 @@ import (
|
|||
"testing"
|
||||
|
||||
. "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" // https://golang.org/issue/12996
|
||||
)
|
||||
|
||||
func TestTemplateClone(t *testing.T) {
|
||||
// https://golang.org/issue/12996
|
||||
|
||||
orig := New("name")
|
||||
clone, err := orig.Clone()
|
||||
if err != nil {
|
||||
|
@ -163,6 +164,21 @@ func TestStringsInScriptsWithJsonContentTypeAreCorrectlyEscaped(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSkipEscapeComments(t *testing.T) {
|
||||
c := newTestCase(t)
|
||||
tr := parse.New("root")
|
||||
tr.Mode = parse.ParseComments
|
||||
newT, err := tr.Parse("{{/* A comment */}}{{ 1 }}{{/* Another comment */}}", "", "", make(map[string]*parse.Tree))
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot parse template text: %v", err)
|
||||
}
|
||||
c.root, err = c.root.AddParseTree("root", newT)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot add parse tree to template: %v", err)
|
||||
}
|
||||
c.mustExecute(c.root, nil, "1")
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
t *testing.T
|
||||
root *Template
|
||||
|
|
|
@ -45,12 +45,8 @@ func HasGoBuild() bool {
|
|||
return false
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "android", "js":
|
||||
case "android", "js", "ios":
|
||||
return false
|
||||
case "darwin":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -124,12 +120,8 @@ func GoTool() (string, error) {
|
|||
// using os.StartProcess or (more commonly) exec.Command.
|
||||
func HasExec() bool {
|
||||
switch runtime.GOOS {
|
||||
case "js":
|
||||
case "js", "ios":
|
||||
return false
|
||||
case "darwin":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -137,10 +129,8 @@ func HasExec() bool {
|
|||
// HasSrc reports whether the entire source tree is available under GOROOT.
|
||||
func HasSrc() bool {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return false
|
||||
}
|
||||
case "ios":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -204,6 +194,32 @@ func MustHaveCGO(t testing.TB) {
|
|||
}
|
||||
}
|
||||
|
||||
// CanInternalLink reports whether the current system can link programs with
|
||||
// internal linking.
|
||||
// (This is the opposite of cmd/internal/sys.MustLinkExternal. Keep them in sync.)
|
||||
func CanInternalLink() bool {
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
if runtime.GOARCH != "arm64" {
|
||||
return false
|
||||
}
|
||||
case "ios":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MustInternalLink checks that the current system can link programs with internal
|
||||
// linking.
|
||||
// If not, MustInternalLink calls t.Skip with an explanation.
|
||||
func MustInternalLink(t testing.TB) {
|
||||
if !CanInternalLink() {
|
||||
t.Skipf("skipping test: internal linking on %s/%s is not supported", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
// HasSymlink reports whether the current system can use os.Symlink.
|
||||
func HasSymlink() bool {
|
||||
ok, _ := hasSymlink()
|
||||
|
@ -272,3 +288,23 @@ func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
|
|||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CPUIsSlow reports whether the CPU running the test is suspected to be slow.
|
||||
func CPUIsSlow() bool {
|
||||
switch runtime.GOARCH {
|
||||
case "arm", "mips", "mipsle", "mips64", "mips64le":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SkipIfShortAndSlow skips t if -short is set and the CPU running the test is
|
||||
// suspected to be slow.
|
||||
//
|
||||
// (This is useful for CPU-intensive tests that otherwise complete quickly.)
|
||||
func SkipIfShortAndSlow(t testing.TB) {
|
||||
if testing.Short() && CPUIsSlow() {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test in -short mode on %s", runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,16 +40,17 @@ More intricate examples appear below.
|
|||
Text and spaces
|
||||
|
||||
By default, all text between actions is copied verbatim when the template is
|
||||
executed. For example, the string " items are made of " in the example above appears
|
||||
on standard output when the program is run.
|
||||
executed. For example, the string " items are made of " in the example above
|
||||
appears on standard output when the program is run.
|
||||
|
||||
However, to aid in formatting template source code, if an action's left delimiter
|
||||
(by default "{{") is followed immediately by a minus sign and ASCII space character
|
||||
("{{- "), all trailing white space is trimmed from the immediately preceding text.
|
||||
Similarly, if the right delimiter ("}}") is preceded by a space and minus sign
|
||||
(" -}}"), all leading white space is trimmed from the immediately following text.
|
||||
In these trim markers, the ASCII space must be present; "{{-3}}" parses as an
|
||||
action containing the number -3.
|
||||
However, to aid in formatting template source code, if an action's left
|
||||
delimiter (by default "{{") is followed immediately by a minus sign and white
|
||||
space, all trailing white space is trimmed from the immediately preceding text.
|
||||
Similarly, if the right delimiter ("}}") is preceded by white space and a minus
|
||||
sign, all leading white space is trimmed from the immediately following text.
|
||||
In these trim markers, the white space must be present:
|
||||
"{{- 3}}" is like "{{3}}" but trims the immediately preceding text, while
|
||||
"{{-3}}" parses as an action containing the number -3.
|
||||
|
||||
For instance, when executing the template whose source is
|
||||
|
||||
|
|
|
@ -256,6 +256,7 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
|
|||
if len(node.Pipe.Decl) == 0 {
|
||||
s.printValue(node, val)
|
||||
}
|
||||
case *parse.CommentNode:
|
||||
case *parse.IfNode:
|
||||
s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
|
||||
case *parse.ListNode:
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -1297,7 +1297,7 @@ func TestUnterminatedStringError(t *testing.T) {
|
|||
t.Fatal("expected error")
|
||||
}
|
||||
str := err.Error()
|
||||
if !strings.Contains(str, "X:3: unexpected unterminated raw quoted string") {
|
||||
if !strings.Contains(str, "X:3: unterminated raw quoted string") {
|
||||
t.Fatalf("unexpected error: %s", str)
|
||||
}
|
||||
}
|
||||
|
@ -1330,7 +1330,7 @@ func TestExecuteGivesExecError(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tmpl.Execute(ioutil.Discard, 0)
|
||||
err = tmpl.Execute(io.Discard, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected error; got none")
|
||||
}
|
||||
|
@ -1476,7 +1476,7 @@ func TestEvalFieldErrors(t *testing.T) {
|
|||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tmpl := Must(New("tmpl").Parse(tc.src))
|
||||
err := tmpl.Execute(ioutil.Discard, tc.value)
|
||||
err := tmpl.Execute(io.Discard, tc.value)
|
||||
got := "<nil>"
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
|
@ -1493,7 +1493,7 @@ func TestMaxExecDepth(t *testing.T) {
|
|||
t.Skip("skipping in -short mode")
|
||||
}
|
||||
tmpl := Must(New("tmpl").Parse(`{{template "tmpl" .}}`))
|
||||
err := tmpl.Execute(ioutil.Discard, nil)
|
||||
err := tmpl.Execute(io.Discard, nil)
|
||||
got := "<nil>"
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
|
|
|
@ -8,7 +8,9 @@ package template
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
|
@ -35,7 +37,7 @@ func Must(t *Template, err error) *Template {
|
|||
// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template
|
||||
// named "foo", while "a/foo" is unavailable.
|
||||
func ParseFiles(filenames ...string) (*Template, error) {
|
||||
return parseFiles(nil, filenames...)
|
||||
return parseFiles(nil, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// ParseFiles parses the named files and associates the resulting templates with
|
||||
|
@ -51,23 +53,22 @@ func ParseFiles(filenames ...string) (*Template, error) {
|
|||
// the last one mentioned will be the one that results.
|
||||
func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
|
||||
t.init()
|
||||
return parseFiles(t, filenames...)
|
||||
return parseFiles(t, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// parseFiles is the helper for the method and function. If the argument
|
||||
// template is nil, it is created from the first file.
|
||||
func parseFiles(t *Template, filenames ...string) (*Template, error) {
|
||||
func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) {
|
||||
if len(filenames) == 0 {
|
||||
// Not really a problem, but be consistent.
|
||||
return nil, fmt.Errorf("template: no files named in call to ParseFiles")
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
name, b, err := readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(b)
|
||||
name := filepath.Base(filename)
|
||||
// First template becomes return value if not already defined,
|
||||
// and we use that one for subsequent New calls to associate
|
||||
// all the templates together. Also, if this file has the same name
|
||||
|
@ -126,5 +127,51 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
|
|||
if len(filenames) == 0 {
|
||||
return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
return parseFiles(t, filenames...)
|
||||
return parseFiles(t, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
||||
return parseFS(nil, fsys, patterns)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
||||
t.init()
|
||||
return parseFS(t, fsys, patterns)
|
||||
}
|
||||
|
||||
func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) {
|
||||
var filenames []string
|
||||
for _, pattern := range patterns {
|
||||
list, err := fs.Glob(fsys, pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern)
|
||||
}
|
||||
filenames = append(filenames, list...)
|
||||
}
|
||||
return parseFiles(t, readFileFS(fsys), filenames...)
|
||||
}
|
||||
|
||||
func readFileOS(file string) (name string, b []byte, err error) {
|
||||
name = filepath.Base(file)
|
||||
b, err = ioutil.ReadFile(file)
|
||||
return
|
||||
}
|
||||
|
||||
func readFileFS(fsys fs.FS) func(string) (string, []byte, error) {
|
||||
return func(file string) (name string, b []byte, err error) {
|
||||
name = path.Base(file)
|
||||
b, err = fs.ReadFile(fsys, file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
66
tpl/internal/go_templates/texttemplate/link_test.go
Normal file
66
tpl/internal/go_templates/texttemplate/link_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Issue 36021: verify that text/template doesn't prevent the linker from removing
|
||||
// unused methods.
|
||||
func _TestLinkerGC(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode")
|
||||
}
|
||||
testenv.MustHaveGoBuild(t)
|
||||
const prog = `package main
|
||||
|
||||
import (
|
||||
_ "text/template"
|
||||
)
|
||||
|
||||
type T struct{}
|
||||
|
||||
func (t *T) Unused() { println("THIS SHOULD BE ELIMINATED") }
|
||||
func (t *T) Used() {}
|
||||
|
||||
var sink *T
|
||||
|
||||
func main() {
|
||||
var t T
|
||||
sink = &t
|
||||
t.Used()
|
||||
}
|
||||
`
|
||||
td, err := ioutil.TempDir("", "text_template_TestDeadCodeElimination")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
|
||||
cmd.Dir = td
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go build: %v, %s", err, out)
|
||||
}
|
||||
slurp, err := ioutil.ReadFile(filepath.Join(td, "x.exe"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Contains(slurp, []byte("THIS SHOULD BE ELIMINATED")) {
|
||||
t.Error("binary contains code that should be deadcode eliminated")
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -155,6 +156,35 @@ func TestParseGlob(t *testing.T) {
|
|||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFS(t *testing.T) {
|
||||
fs := os.DirFS("testdata")
|
||||
|
||||
{
|
||||
_, err := ParseFS(fs, "DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
template := New("root")
|
||||
_, err := template.ParseFS(fs, "file1.tmpl", "file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
{
|
||||
template := New("root")
|
||||
_, err := template.ParseFS(fs, "file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
}
|
||||
|
||||
// In these tests, actual content (not just template definitions) comes from the parsed files.
|
||||
|
||||
var templateFileExecTests = []execTest{
|
||||
|
@ -361,6 +391,7 @@ func TestEmptyTemplate(t *testing.T) {
|
|||
in string
|
||||
want string
|
||||
}{
|
||||
{[]string{"x", "y"}, "", "y"},
|
||||
{[]string{""}, "once", ""},
|
||||
{[]string{"", ""}, "twice", ""},
|
||||
{[]string{"{{.}}", "{{.}}"}, "twice", "twice"},
|
||||
|
|
|
@ -41,6 +41,7 @@ const (
|
|||
itemBool // boolean constant
|
||||
itemChar // printable ASCII character; grab bag for comma etc.
|
||||
itemCharConstant // character constant
|
||||
itemComment // comment text
|
||||
itemComplex // complex constant (1+2i); imaginary is just a number
|
||||
itemAssign // equals ('=') introducing an assignment
|
||||
itemDeclare // colon-equals (':=') introducing a declaration
|
||||
|
@ -91,15 +92,14 @@ const eof = -1
|
|||
// If the action begins "{{- " rather than "{{", then all space/tab/newlines
|
||||
// preceding the action are trimmed; conversely if it ends " -}}" the
|
||||
// leading spaces are trimmed. This is done entirely in the lexer; the
|
||||
// parser never sees it happen. We require an ASCII space to be
|
||||
// present to avoid ambiguity with things like "{{-3}}". It reads
|
||||
// parser never sees it happen. We require an ASCII space (' ', \t, \r, \n)
|
||||
// to be present to avoid ambiguity with things like "{{-3}}". It reads
|
||||
// better with the space present anyway. For simplicity, only ASCII
|
||||
// space does the job.
|
||||
// does the job.
|
||||
const (
|
||||
spaceChars = " \t\r\n" // These are the space characters defined by Go itself.
|
||||
leftTrimMarker = "- " // Attached to left delimiter, trims trailing spaces from preceding text.
|
||||
rightTrimMarker = " -" // Attached to right delimiter, trims leading spaces from following text.
|
||||
trimMarkerLen = Pos(len(leftTrimMarker))
|
||||
spaceChars = " \t\r\n" // These are the space characters defined by Go itself.
|
||||
trimMarker = '-' // Attached to left/right delimiter, trims trailing spaces from preceding/following text.
|
||||
trimMarkerLen = Pos(1 + 1) // marker plus space before or after
|
||||
)
|
||||
|
||||
// stateFn represents the state of the scanner as a function that returns the next state.
|
||||
|
@ -107,18 +107,18 @@ type stateFn func(*lexer) stateFn
|
|||
|
||||
// lexer holds the state of the scanner.
|
||||
type lexer struct {
|
||||
name string // the name of the input; used only for error reports
|
||||
input string // the string being scanned
|
||||
leftDelim string // start of action
|
||||
rightDelim string // end of action
|
||||
trimRightDelim string // end of action with trim marker
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
items chan item // channel of scanned items
|
||||
parenDepth int // nesting depth of ( ) exprs
|
||||
line int // 1+number of newlines seen
|
||||
startLine int // start line of this item
|
||||
name string // the name of the input; used only for error reports
|
||||
input string // the string being scanned
|
||||
leftDelim string // start of action
|
||||
rightDelim string // end of action
|
||||
emitComment bool // emit itemComment tokens.
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
items chan item // channel of scanned items
|
||||
parenDepth int // nesting depth of ( ) exprs
|
||||
line int // 1+number of newlines seen
|
||||
startLine int // start line of this item
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
|
@ -203,7 +203,7 @@ func (l *lexer) drain() {
|
|||
}
|
||||
|
||||
// lex creates a new scanner for the input string.
|
||||
func lex(name, input, left, right string) *lexer {
|
||||
func lex(name, input, left, right string, emitComment bool) *lexer {
|
||||
if left == "" {
|
||||
left = leftDelim
|
||||
}
|
||||
|
@ -211,14 +211,14 @@ func lex(name, input, left, right string) *lexer {
|
|||
right = rightDelim
|
||||
}
|
||||
l := &lexer{
|
||||
name: name,
|
||||
input: input,
|
||||
leftDelim: left,
|
||||
rightDelim: right,
|
||||
trimRightDelim: rightTrimMarker + right,
|
||||
items: make(chan item),
|
||||
line: 1,
|
||||
startLine: 1,
|
||||
name: name,
|
||||
input: input,
|
||||
leftDelim: left,
|
||||
rightDelim: right,
|
||||
emitComment: emitComment,
|
||||
items: make(chan item),
|
||||
line: 1,
|
||||
startLine: 1,
|
||||
}
|
||||
go l.run()
|
||||
return l
|
||||
|
@ -248,7 +248,7 @@ func lexText(l *lexer) stateFn {
|
|||
ldn := Pos(len(l.leftDelim))
|
||||
l.pos += Pos(x)
|
||||
trimLength := Pos(0)
|
||||
if strings.HasPrefix(l.input[l.pos+ldn:], leftTrimMarker) {
|
||||
if hasLeftTrimMarker(l.input[l.pos+ldn:]) {
|
||||
trimLength = rightTrimLength(l.input[l.start:l.pos])
|
||||
}
|
||||
l.pos -= trimLength
|
||||
|
@ -277,7 +277,7 @@ func rightTrimLength(s string) Pos {
|
|||
|
||||
// atRightDelim reports whether the lexer is at a right delimiter, possibly preceded by a trim marker.
|
||||
func (l *lexer) atRightDelim() (delim, trimSpaces bool) {
|
||||
if strings.HasPrefix(l.input[l.pos:], l.trimRightDelim) { // With trim marker.
|
||||
if hasRightTrimMarker(l.input[l.pos:]) && strings.HasPrefix(l.input[l.pos+trimMarkerLen:], l.rightDelim) { // With trim marker.
|
||||
return true, true
|
||||
}
|
||||
if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { // Without trim marker.
|
||||
|
@ -294,7 +294,7 @@ func leftTrimLength(s string) Pos {
|
|||
// lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker.
|
||||
func lexLeftDelim(l *lexer) stateFn {
|
||||
l.pos += Pos(len(l.leftDelim))
|
||||
trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker)
|
||||
trimSpace := hasLeftTrimMarker(l.input[l.pos:])
|
||||
afterMarker := Pos(0)
|
||||
if trimSpace {
|
||||
afterMarker = trimMarkerLen
|
||||
|
@ -323,6 +323,9 @@ func lexComment(l *lexer) stateFn {
|
|||
if !delim {
|
||||
return l.errorf("comment ends before closing delimiter")
|
||||
}
|
||||
if l.emitComment {
|
||||
l.emit(itemComment)
|
||||
}
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
}
|
||||
|
@ -336,7 +339,7 @@ func lexComment(l *lexer) stateFn {
|
|||
|
||||
// lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker.
|
||||
func lexRightDelim(l *lexer) stateFn {
|
||||
trimSpace := strings.HasPrefix(l.input[l.pos:], rightTrimMarker)
|
||||
trimSpace := hasRightTrimMarker(l.input[l.pos:])
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
l.ignore()
|
||||
|
@ -363,7 +366,7 @@ func lexInsideAction(l *lexer) stateFn {
|
|||
return l.errorf("unclosed left paren")
|
||||
}
|
||||
switch r := l.next(); {
|
||||
case r == eof || isEndOfLine(r):
|
||||
case r == eof:
|
||||
return l.errorf("unclosed action")
|
||||
case isSpace(r):
|
||||
l.backup() // Put space back in case we have " -}}".
|
||||
|
@ -433,7 +436,7 @@ func lexSpace(l *lexer) stateFn {
|
|||
}
|
||||
// Be careful about a trim-marked closing delimiter, which has a minus
|
||||
// after a space. We know there is a space, so check for the '-' that might follow.
|
||||
if strings.HasPrefix(l.input[l.pos-1:], l.trimRightDelim) {
|
||||
if hasRightTrimMarker(l.input[l.pos-1:]) && strings.HasPrefix(l.input[l.pos-1+trimMarkerLen:], l.rightDelim) {
|
||||
l.backup() // Before the space.
|
||||
if numSpaces == 1 {
|
||||
return lexRightDelim // On the delim, so go right to that.
|
||||
|
@ -520,7 +523,7 @@ func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
|
|||
// day to implement arithmetic.
|
||||
func (l *lexer) atTerminator() bool {
|
||||
r := l.peek()
|
||||
if isSpace(r) || isEndOfLine(r) {
|
||||
if isSpace(r) {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
|
@ -651,15 +654,18 @@ Loop:
|
|||
|
||||
// isSpace reports whether r is a space character.
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
// isEndOfLine reports whether r is an end-of-line character.
|
||||
func isEndOfLine(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
func hasLeftTrimMarker(s string) bool {
|
||||
return len(s) >= 2 && s[0] == trimMarker && isSpace(rune(s[1]))
|
||||
}
|
||||
|
||||
func hasRightTrimMarker(s string) bool {
|
||||
return len(s) >= 2 && isSpace(rune(s[0])) && s[1] == trimMarker
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ var itemName = map[itemType]string{
|
|||
itemBool: "bool",
|
||||
itemChar: "char",
|
||||
itemCharConstant: "charconst",
|
||||
itemComment: "comment",
|
||||
itemComplex: "complex",
|
||||
itemDeclare: ":=",
|
||||
itemEOF: "EOF",
|
||||
|
@ -92,6 +93,7 @@ var lexTests = []lexTest{
|
|||
{"text", `now is the time`, []item{mkItem(itemText, "now is the time"), tEOF}},
|
||||
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
mkItem(itemComment, "/* this is a comment */"),
|
||||
mkItem(itemText, "-world"),
|
||||
tEOF,
|
||||
}},
|
||||
|
@ -313,6 +315,7 @@ var lexTests = []lexTest{
|
|||
}},
|
||||
{"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
|
||||
mkItem(itemText, "hello-"),
|
||||
mkItem(itemComment, "/* hello */"),
|
||||
mkItem(itemText, "-world"),
|
||||
tEOF,
|
||||
}},
|
||||
|
@ -322,7 +325,7 @@ var lexTests = []lexTest{
|
|||
tLeft,
|
||||
mkItem(itemError, "unrecognized character in action: U+0001"),
|
||||
}},
|
||||
{"unclosed action", "{{\n}}", []item{
|
||||
{"unclosed action", "{{", []item{
|
||||
tLeft,
|
||||
mkItem(itemError, "unclosed action"),
|
||||
}},
|
||||
|
@ -391,7 +394,7 @@ var lexTests = []lexTest{
|
|||
|
||||
// collect gathers the emitted items into a slice.
|
||||
func collect(t *lexTest, left, right string) (items []item) {
|
||||
l := lex(t.name, t.input, left, right)
|
||||
l := lex(t.name, t.input, left, right, true)
|
||||
for {
|
||||
item := l.nextItem()
|
||||
items = append(items, item)
|
||||
|
@ -531,7 +534,7 @@ func TestPos(t *testing.T) {
|
|||
func TestShutdown(t *testing.T) {
|
||||
// We need to duplicate template.Parse here to hold on to the lexer.
|
||||
const text = "erroneous{{define}}{{else}}1234"
|
||||
lexer := lex("foo", text, "{{", "}}")
|
||||
lexer := lex("foo", text, "{{", "}}", false)
|
||||
_, err := New("root").parseLexer(lexer)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
|
|
|
@ -70,6 +70,7 @@ const (
|
|||
NodeTemplate // A template invocation action.
|
||||
NodeVariable // A $ variable.
|
||||
NodeWith // A with action.
|
||||
NodeComment // A comment.
|
||||
)
|
||||
|
||||
// Nodes.
|
||||
|
@ -149,6 +150,38 @@ func (t *TextNode) Copy() Node {
|
|||
return &TextNode{tr: t.tr, NodeType: NodeText, Pos: t.Pos, Text: append([]byte{}, t.Text...)}
|
||||
}
|
||||
|
||||
// CommentNode holds a comment.
|
||||
type CommentNode struct {
|
||||
NodeType
|
||||
Pos
|
||||
tr *Tree
|
||||
Text string // Comment text.
|
||||
}
|
||||
|
||||
func (t *Tree) newComment(pos Pos, text string) *CommentNode {
|
||||
return &CommentNode{tr: t, NodeType: NodeComment, Pos: pos, Text: text}
|
||||
}
|
||||
|
||||
func (c *CommentNode) String() string {
|
||||
var sb strings.Builder
|
||||
c.writeTo(&sb)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (c *CommentNode) writeTo(sb *strings.Builder) {
|
||||
sb.WriteString("{{")
|
||||
sb.WriteString(c.Text)
|
||||
sb.WriteString("}}")
|
||||
}
|
||||
|
||||
func (c *CommentNode) tree() *Tree {
|
||||
return c.tr
|
||||
}
|
||||
|
||||
func (c *CommentNode) Copy() Node {
|
||||
return &CommentNode{tr: c.tr, NodeType: NodeComment, Pos: c.Pos, Text: c.Text}
|
||||
}
|
||||
|
||||
// PipeNode holds a pipeline with optional declaration
|
||||
type PipeNode struct {
|
||||
NodeType
|
||||
|
@ -349,7 +382,7 @@ func (i *IdentifierNode) Copy() Node {
|
|||
return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos)
|
||||
}
|
||||
|
||||
// AssignNode holds a list of variable names, possibly with chained field
|
||||
// VariableNode holds a list of variable names, possibly with chained field
|
||||
// accesses. The dollar sign is part of the (first) name.
|
||||
type VariableNode struct {
|
||||
NodeType
|
||||
|
|
|
@ -21,16 +21,26 @@ type Tree struct {
|
|||
Name string // name of the template represented by the tree.
|
||||
ParseName string // name of the top-level template during parsing, for error messages.
|
||||
Root *ListNode // top-level root of the tree.
|
||||
Mode Mode // parsing mode.
|
||||
text string // text parsed to create the template (or its parent)
|
||||
// Parsing only; cleared after parse.
|
||||
funcs []map[string]interface{}
|
||||
lex *lexer
|
||||
token [3]item // three-token lookahead for parser.
|
||||
peekCount int
|
||||
vars []string // variables defined at the moment.
|
||||
treeSet map[string]*Tree
|
||||
funcs []map[string]interface{}
|
||||
lex *lexer
|
||||
token [3]item // three-token lookahead for parser.
|
||||
peekCount int
|
||||
vars []string // variables defined at the moment.
|
||||
treeSet map[string]*Tree
|
||||
actionLine int // line of left delim starting action
|
||||
mode Mode
|
||||
}
|
||||
|
||||
// A mode value is a set of flags (or 0). Modes control parser behavior.
|
||||
type Mode uint
|
||||
|
||||
const (
|
||||
ParseComments Mode = 1 << iota // parse comments and add them to AST
|
||||
)
|
||||
|
||||
// Copy returns a copy of the Tree. Any parsing state is discarded.
|
||||
func (t *Tree) Copy() *Tree {
|
||||
if t == nil {
|
||||
|
@ -178,6 +188,16 @@ func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item {
|
|||
|
||||
// unexpected complains about the token and terminates processing.
|
||||
func (t *Tree) unexpected(token item, context string) {
|
||||
if token.typ == itemError {
|
||||
extra := ""
|
||||
if t.actionLine != 0 && t.actionLine != token.line {
|
||||
extra = fmt.Sprintf(" in action started at %s:%d", t.ParseName, t.actionLine)
|
||||
if strings.HasSuffix(token.val, " action") {
|
||||
extra = extra[len(" in action"):] // avoid "action in action"
|
||||
}
|
||||
}
|
||||
t.errorf("%s%s", token, extra)
|
||||
}
|
||||
t.errorf("unexpected %s in %s", token, context)
|
||||
}
|
||||
|
||||
|
@ -220,7 +240,8 @@ func (t *Tree) stopParse() {
|
|||
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
|
||||
defer t.recover(&err)
|
||||
t.ParseName = t.Name
|
||||
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet)
|
||||
emitComment := t.Mode&ParseComments != 0
|
||||
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim, emitComment), treeSet)
|
||||
t.text = text
|
||||
t.parse()
|
||||
t.add()
|
||||
|
@ -240,12 +261,14 @@ func (t *Tree) add() {
|
|||
}
|
||||
}
|
||||
|
||||
// IsEmptyTree reports whether this tree (node) is empty of everything but space.
|
||||
// IsEmptyTree reports whether this tree (node) is empty of everything but space or comments.
|
||||
func IsEmptyTree(n Node) bool {
|
||||
switch n := n.(type) {
|
||||
case nil:
|
||||
return true
|
||||
case *ActionNode:
|
||||
case *CommentNode:
|
||||
return true
|
||||
case *IfNode:
|
||||
case *ListNode:
|
||||
for _, node := range n.Nodes {
|
||||
|
@ -276,6 +299,7 @@ func (t *Tree) parse() {
|
|||
if t.nextNonSpace().typ == itemDefine {
|
||||
newT := New("definition") // name will be updated once we know it.
|
||||
newT.text = t.text
|
||||
newT.Mode = t.Mode
|
||||
newT.ParseName = t.ParseName
|
||||
newT.startParse(t.funcs, t.lex, t.treeSet)
|
||||
newT.parseDefinition()
|
||||
|
@ -331,19 +355,27 @@ func (t *Tree) itemList() (list *ListNode, next Node) {
|
|||
}
|
||||
|
||||
// textOrAction:
|
||||
// text | action
|
||||
// text | comment | action
|
||||
func (t *Tree) textOrAction() Node {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemText:
|
||||
return t.newText(token.pos, token.val)
|
||||
case itemLeftDelim:
|
||||
t.actionLine = token.line
|
||||
defer t.clearActionLine()
|
||||
return t.action()
|
||||
case itemComment:
|
||||
return t.newComment(token.pos, token.val)
|
||||
default:
|
||||
t.unexpected(token, "input")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tree) clearActionLine() {
|
||||
t.actionLine = 0
|
||||
}
|
||||
|
||||
// Action:
|
||||
// control
|
||||
// command ("|" command)*
|
||||
|
@ -369,12 +401,12 @@ func (t *Tree) action() (n Node) {
|
|||
t.backup()
|
||||
token := t.peek()
|
||||
// Do not pop variables; they persist until "end".
|
||||
return t.newAction(token.pos, token.line, t.pipeline("command"))
|
||||
return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim))
|
||||
}
|
||||
|
||||
// Pipeline:
|
||||
// declarations? command ('|' command)*
|
||||
func (t *Tree) pipeline(context string) (pipe *PipeNode) {
|
||||
func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
|
||||
token := t.peekNonSpace()
|
||||
pipe = t.newPipeline(token.pos, token.line, nil)
|
||||
// Are there declarations or assignments?
|
||||
|
@ -415,12 +447,9 @@ decls:
|
|||
}
|
||||
for {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemRightDelim, itemRightParen:
|
||||
case end:
|
||||
// At this point, the pipeline is complete
|
||||
t.checkPipeline(pipe, context)
|
||||
if token.typ == itemRightParen {
|
||||
t.backup()
|
||||
}
|
||||
return
|
||||
case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
|
||||
itemNumber, itemNil, itemRawString, itemString, itemVariable, itemLeftParen:
|
||||
|
@ -449,7 +478,7 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
|
|||
|
||||
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
|
||||
defer t.popVars(len(t.vars))
|
||||
pipe = t.pipeline(context)
|
||||
pipe = t.pipeline(context, itemRightDelim)
|
||||
var next Node
|
||||
list, next = t.itemList()
|
||||
switch next.Type() {
|
||||
|
@ -535,10 +564,11 @@ func (t *Tree) blockControl() Node {
|
|||
|
||||
token := t.nextNonSpace()
|
||||
name := t.parseTemplateName(token, context)
|
||||
pipe := t.pipeline(context)
|
||||
pipe := t.pipeline(context, itemRightDelim)
|
||||
|
||||
block := New(name) // name will be updated once we know it.
|
||||
block.text = t.text
|
||||
block.Mode = t.Mode
|
||||
block.ParseName = t.ParseName
|
||||
block.startParse(t.funcs, t.lex, t.treeSet)
|
||||
var end Node
|
||||
|
@ -564,7 +594,7 @@ func (t *Tree) templateControl() Node {
|
|||
if t.nextNonSpace().typ != itemRightDelim {
|
||||
t.backup()
|
||||
// Do not pop variables; they persist until "end".
|
||||
pipe = t.pipeline(context)
|
||||
pipe = t.pipeline(context, itemRightDelim)
|
||||
}
|
||||
return t.newTemplate(token.pos, token.line, name, pipe)
|
||||
}
|
||||
|
@ -598,13 +628,12 @@ func (t *Tree) command() *CommandNode {
|
|||
switch token := t.next(); token.typ {
|
||||
case itemSpace:
|
||||
continue
|
||||
case itemError:
|
||||
t.errorf("%s", token.val)
|
||||
case itemRightDelim, itemRightParen:
|
||||
t.backup()
|
||||
case itemPipe:
|
||||
// nothing here; break loop below
|
||||
default:
|
||||
t.errorf("unexpected %s in operand", token)
|
||||
t.unexpected(token, "operand")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -659,8 +688,6 @@ func (t *Tree) operand() Node {
|
|||
// A nil return means the next item is not a term.
|
||||
func (t *Tree) term() Node {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
case itemError:
|
||||
t.errorf("%s", token.val)
|
||||
case itemIdentifier:
|
||||
if !t.hasFunction(token.val) {
|
||||
t.errorf("function %q not defined", token.val)
|
||||
|
@ -683,11 +710,7 @@ func (t *Tree) term() Node {
|
|||
}
|
||||
return number
|
||||
case itemLeftParen:
|
||||
pipe := t.pipeline("parenthesized pipeline")
|
||||
if token := t.next(); token.typ != itemRightParen {
|
||||
t.errorf("unclosed right paren: unexpected %s", token)
|
||||
}
|
||||
return pipe
|
||||
return t.pipeline("parenthesized pipeline", itemRightParen)
|
||||
case itemString, itemRawString:
|
||||
s, err := strconv.Unquote(token.val)
|
||||
if err != nil {
|
||||
|
|
|
@ -252,6 +252,13 @@ var parseTests = []parseTest{
|
|||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
|
||||
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||
`{{template "foo" .}}`},
|
||||
|
||||
{"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
|
||||
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
|
||||
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
|
||||
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
|
||||
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
|
||||
|
||||
// Errors.
|
||||
{"unclosed action", "hello{{range", hasError, ""},
|
||||
{"unmatched end", "{{end}}", hasError, ""},
|
||||
|
@ -350,6 +357,30 @@ func TestParseCopy(t *testing.T) {
|
|||
testParse(true, t)
|
||||
}
|
||||
|
||||
func TestParseWithComments(t *testing.T) {
|
||||
textFormat = "%q"
|
||||
defer func() { textFormat = "%s" }()
|
||||
tests := [...]parseTest{
|
||||
{"comment", "{{/*\n\n\n*/}}", noError, "{{/*\n\n\n*/}}"},
|
||||
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"{{/* hi */}}`},
|
||||
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `{{/* hi */}}"y"`},
|
||||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x"{{/* */}}"y"`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tr := New(test.name)
|
||||
tr.Mode = ParseComments
|
||||
tmpl, err := tr.Parse(test.input, "", "", make(map[string]*Tree))
|
||||
if err != nil {
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
}
|
||||
if result := tmpl.Root.String(); result != test.result {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type isEmptyTest struct {
|
||||
name string
|
||||
input string
|
||||
|
@ -360,6 +391,7 @@ var isEmptyTests = []isEmptyTest{
|
|||
{"empty", ``, true},
|
||||
{"nonempty", `hello`, false},
|
||||
{"spaces only", " \t\n \t\n", true},
|
||||
{"comment only", "{{/* comment */}}", true},
|
||||
{"definition", `{{define "x"}}something{{end}}`, true},
|
||||
{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
|
||||
{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n", false},
|
||||
|
@ -403,23 +435,38 @@ var errorTests = []parseTest{
|
|||
// Check line numbers are accurate.
|
||||
{"unclosed1",
|
||||
"line1\n{{",
|
||||
hasError, `unclosed1:2: unexpected unclosed action in command`},
|
||||
hasError, `unclosed1:2: unclosed action`},
|
||||
{"unclosed2",
|
||||
"line1\n{{define `x`}}line2\n{{",
|
||||
hasError, `unclosed2:3: unexpected unclosed action in command`},
|
||||
hasError, `unclosed2:3: unclosed action`},
|
||||
{"unclosed3",
|
||||
"line1\n{{\"x\"\n\"y\"\n",
|
||||
hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
|
||||
{"unclosed4",
|
||||
"{{\n\n\n\n\n",
|
||||
hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
|
||||
{"var1",
|
||||
"line1\n{{\nx\n}}",
|
||||
hasError, `var1:3: function "x" not defined`},
|
||||
// Specific errors.
|
||||
{"function",
|
||||
"{{foo}}",
|
||||
hasError, `function "foo" not defined`},
|
||||
{"comment",
|
||||
{"comment1",
|
||||
"{{/*}}",
|
||||
hasError, `unclosed comment`},
|
||||
hasError, `comment1:1: unclosed comment`},
|
||||
{"comment2",
|
||||
"{{/*\nhello\n}}",
|
||||
hasError, `comment2:1: unclosed comment`},
|
||||
{"lparen",
|
||||
"{{.X (1 2 3}}",
|
||||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
"{{.X 1 2 3)}}",
|
||||
hasError, `unexpected ")"`},
|
||||
"{{.X 1 2 3 ) }}",
|
||||
hasError, `unexpected ")" in command`},
|
||||
{"rparen2",
|
||||
"{{(.X 1 2 3",
|
||||
hasError, `unclosed action`},
|
||||
{"space",
|
||||
"{{`x`3}}",
|
||||
hasError, `in operand`},
|
||||
|
@ -465,7 +512,7 @@ var errorTests = []parseTest{
|
|||
hasError, `missing value for parenthesized pipeline`},
|
||||
{"multilinerawstring",
|
||||
"{{ $v := `\n` }} {{",
|
||||
hasError, `multilinerawstring:2: unexpected unclosed action`},
|
||||
hasError, `multilinerawstring:2: unclosed action`},
|
||||
{"rangeundefvar",
|
||||
"{{range $k}}{{end}}",
|
||||
hasError, `undefined variable`},
|
||||
|
|
Loading…
Reference in a new issue