tpl/internal: Sync go_templates

Closes #10411
This commit is contained in:
Bjørn Erik Pedersen 2022-11-14 19:13:09 +01:00
parent 58a98c7758
commit f6ab9553f4
34 changed files with 739 additions and 514 deletions

View file

@ -17,7 +17,7 @@ import (
) )
func main() { func main() {
// The current is built with 41a82aa9c3 text/template/parse: allow space after continue or break // The current is built with be7068fb0804f661515c678bee9224b90b32869a text/template: correct assignment, not declaration, in range
fmt.Println("Forking ...") fmt.Println("Forking ...")
defer fmt.Println("Done ...") defer fmt.Println("Done ...")

View file

@ -36,19 +36,18 @@ func (o *SortedMap) Swap(i, j int) {
// //
// The ordering rules are more general than with Go's < operator: // The ordering rules are more general than with Go's < operator:
// //
// - when applicable, nil compares low // - when applicable, nil compares low
// - ints, floats, and strings order by < // - ints, floats, and strings order by <
// - NaN compares less than non-NaN floats // - NaN compares less than non-NaN floats
// - bool compares false before true // - bool compares false before true
// - complex compares real, then imag // - complex compares real, then imag
// - pointers compare by machine address // - pointers compare by machine address
// - channel values compare by machine address // - channel values compare by machine address
// - structs compare each field in turn // - structs compare each field in turn
// - arrays compare each element in turn. // - arrays compare each element in turn.
// Otherwise identical arrays compare by length. // Otherwise identical arrays compare by length.
// - interface values compare first by reflect.Type describing the concrete type // - interface values compare first by reflect.Type describing the concrete type
// and then by concrete value as described in the previous rules. // and then by concrete value as described in the previous rules.
//
func Sort(mapValue reflect.Value) *SortedMap { func Sort(mapValue reflect.Value) *SortedMap {
if mapValue.Type().Kind() != reflect.Map { if mapValue.Type().Kind() != reflect.Map {
return nil return nil

View file

@ -147,7 +147,7 @@ func sprint(data any) string {
} }
b.WriteString(sprintKey(key)) b.WriteString(sprintKey(key))
b.WriteRune(':') b.WriteRune(':')
b.WriteString(fmt.Sprint(om.Value[i])) fmt.Fprint(b, om.Value[i])
} }
return b.String() return b.String()
} }

View file

@ -8,7 +8,6 @@
package template package template
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -26,7 +25,7 @@ func TestAddParseTreeHTML(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
added := Must(root.AddParseTree("b", tree["b"])) added := Must(root.AddParseTree("b", tree["b"]))
b := new(bytes.Buffer) b := new(strings.Builder)
err = added.ExecuteTemplate(b, "a", "1>0") err = added.ExecuteTemplate(b, "a", "1>0")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -43,7 +42,7 @@ func TestClone(t *testing.T) {
// In the t2 template, it will be in a JavaScript context. // In the t2 template, it will be in a JavaScript context.
// In the t3 template, it will be in a CSS context. // In the t3 template, it will be in a CSS context.
const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}` const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}`
b := new(bytes.Buffer) b := new(strings.Builder)
// Create an incomplete template t0. // Create an incomplete template t0.
t0 := Must(New("t0").Parse(tmpl)) t0 := Must(New("t0").Parse(tmpl))

View file

@ -284,7 +284,7 @@ func TestTypedContent(t *testing.T) {
[]string{ []string{
`#ZgotmplZ`, `#ZgotmplZ`,
`#ZgotmplZ`, `#ZgotmplZ`,
// Commas are not esacped // Commas are not escaped.
`Hello,#ZgotmplZ`, `Hello,#ZgotmplZ`,
// Leading spaces are not percent escapes. // Leading spaces are not percent escapes.
` dir=%22ltr%22`, ` dir=%22ltr%22`,
@ -389,7 +389,7 @@ func TestTypedContent(t *testing.T) {
tmpl := Must(New("x").Parse(test.input)) tmpl := Must(New("x").Parse(test.input))
pre := strings.Index(test.input, "{{.}}") pre := strings.Index(test.input, "{{.}}")
post := len(test.input) - (pre + 5) post := len(test.input) - (pre + 5)
var b bytes.Buffer var b strings.Builder
for i, x := range data { for i, x := range data {
b.Reset() b.Reset()
if err := tmpl.Execute(&b, x); err != nil { if err := tmpl.Execute(&b, x); err != nil {
@ -423,7 +423,7 @@ func (s *errorer) Error() string {
func TestStringer(t *testing.T) { func TestStringer(t *testing.T) {
s := &myStringer{3} s := &myStringer{3}
b := new(bytes.Buffer) b := new(strings.Builder)
tmpl := Must(New("x").Parse("{{.}}")) tmpl := Must(New("x").Parse("{{.}}"))
if err := tmpl.Execute(b, s); err != nil { if err := tmpl.Execute(b, s); err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -80,7 +80,9 @@ func (c context) mangle(templateName string) string {
// HTML5 parsing algorithm because a single token production in the HTML // HTML5 parsing algorithm because a single token production in the HTML
// grammar may contain embedded actions in a template. For instance, the quoted // grammar may contain embedded actions in a template. For instance, the quoted
// HTML attribute produced by // HTML attribute produced by
// <div title="Hello {{.World}}"> //
// <div title="Hello {{.World}}">
//
// is a single token in HTML's grammar but in a template spans several nodes. // is a single token in HTML's grammar but in a template spans several nodes.
type state uint8 type state uint8

View file

@ -12,14 +12,14 @@ The documentation here focuses on the security features of the package.
For information about how to program the templates themselves, see the For information about how to program the templates themselves, see the
documentation for text/template. documentation for text/template.
Introduction # Introduction
This package wraps package text/template so you can share its template API This package wraps package text/template so you can share its template API
to parse and execute HTML templates safely. to parse and execute HTML templates safely.
tmpl, err := template.New("name").Parse(...) tmpl, err := template.New("name").Parse(...)
// Error checking elided // Error checking elided
err = tmpl.Execute(out, data) err = tmpl.Execute(out, data)
If successful, tmpl will now be injection-safe. Otherwise, err is an error If successful, tmpl will now be injection-safe. Otherwise, err is an error
defined in the docs for ErrorCode. defined in the docs for ErrorCode.
@ -34,38 +34,37 @@ provided below.
Example Example
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
... ...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
produces produces
Hello, <script>alert('you have been pwned')</script>! Hello, <script>alert('you have been pwned')</script>!
but the contextual autoescaping in html/template but the contextual autoescaping in html/template
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
... ...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
produces safe, escaped HTML output produces safe, escaped HTML output
Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;! Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
# Contexts
Contexts
This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
functions to each simple action pipeline, so given the excerpt functions to each simple action pipeline, so given the excerpt
<a href="/search?q={{.}}">{{.}}</a> <a href="/search?q={{.}}">{{.}}</a>
At parse time each {{.}} is overwritten to add escaping functions as necessary. At parse time each {{.}} is overwritten to add escaping functions as necessary.
In this case it becomes In this case it becomes
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a> <a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping where urlescaper, attrescaper, and htmlescaper are aliases for internal escaping
functions. functions.
@ -73,117 +72,113 @@ functions.
For these internal escaping functions, if an action pipeline evaluates to For these internal escaping functions, if an action pipeline evaluates to
a nil interface value, it is treated as though it were an empty string. a nil interface value, it is treated as though it were an empty string.
Namespaced and data- attributes # Namespaced and data- attributes
Attributes with a namespace are treated as if they had no namespace. Attributes with a namespace are treated as if they had no namespace.
Given the excerpt Given the excerpt
<a my:href="{{.}}"></a> <a my:href="{{.}}"></a>
At parse time the attribute will be treated as if it were just "href". At parse time the attribute will be treated as if it were just "href".
So at parse time the template becomes: So at parse time the template becomes:
<a my:href="{{. | urlescaper | attrescaper}}"></a> <a my:href="{{. | urlescaper | attrescaper}}"></a>
Similarly to attributes with namespaces, attributes with a "data-" prefix are Similarly to attributes with namespaces, attributes with a "data-" prefix are
treated as if they had no "data-" prefix. So given treated as if they had no "data-" prefix. So given
<a data-href="{{.}}"></a> <a data-href="{{.}}"></a>
At parse time this becomes At parse time this becomes
<a data-href="{{. | urlescaper | attrescaper}}"></a> <a data-href="{{. | urlescaper | attrescaper}}"></a>
If an attribute has both a namespace and a "data-" prefix, only the namespace If an attribute has both a namespace and a "data-" prefix, only the namespace
will be removed when determining the context. For example will be removed when determining the context. For example
<a my:data-href="{{.}}"></a> <a my:data-href="{{.}}"></a>
This is handled as if "my:data-href" was just "data-href" and not "href" as This is handled as if "my:data-href" was just "data-href" and not "href" as
it would be if the "data-" prefix were to be ignored too. Thus at parse it would be if the "data-" prefix were to be ignored too. Thus at parse
time this becomes just time this becomes just
<a my:data-href="{{. | attrescaper}}"></a> <a my:data-href="{{. | attrescaper}}"></a>
As a special case, attributes with the namespace "xmlns" are always treated As a special case, attributes with the namespace "xmlns" are always treated
as containing URLs. Given the excerpts as containing URLs. Given the excerpts
<a xmlns:title="{{.}}"></a> <a xmlns:title="{{.}}"></a>
<a xmlns:href="{{.}}"></a> <a xmlns:href="{{.}}"></a>
<a xmlns:onclick="{{.}}"></a> <a xmlns:onclick="{{.}}"></a>
At parse time they become: At parse time they become:
<a xmlns:title="{{. | urlescaper | attrescaper}}"></a> <a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a> <a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a> <a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
Errors # Errors
See the documentation of ErrorCode for details. See the documentation of ErrorCode for details.
# A fuller picture
A fuller picture
The rest of this package comment may be skipped on first reading; it includes The rest of this package comment may be skipped on first reading; it includes
details necessary to understand escaping contexts and error messages. Most users details necessary to understand escaping contexts and error messages. Most users
will not need to understand these details. will not need to understand these details.
# Contexts
Contexts
Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows
how {{.}} appears when used in the context to the left. how {{.}} appears when used in the context to the left.
Context {{.}} After Context {{.}} After
{{.}} O'Reilly: How are &lt;i&gt;you&lt;/i&gt;? {{.}} O'Reilly: How are &lt;i&gt;you&lt;/i&gt;?
<a title='{{.}}'> O&#39;Reilly: How are you? <a title='{{.}}'> O&#39;Reilly: How are you?
<a href="/{{.}}"> O&#39;Reilly: How are %3ci%3eyou%3c/i%3e? <a href="/{{.}}"> O&#39;Reilly: How are %3ci%3eyou%3c/i%3e?
<a href="?q={{.}}"> O&#39;Reilly%3a%20How%20are%3ci%3e...%3f <a href="?q={{.}}"> O&#39;Reilly%3a%20How%20are%3ci%3e...%3f
<a onx='f("{{.}}")'> O\x27Reilly: How are \x3ci\x3eyou...? <a onx='f("{{.}}")'> O\x27Reilly: How are \x3ci\x3eyou...?
<a onx='f({{.}})'> "O\x27Reilly: How are \x3ci\x3eyou...?" <a onx='f({{.}})'> "O\x27Reilly: How are \x3ci\x3eyou...?"
<a onx='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f <a onx='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f
If used in an unsafe context, then the value might be filtered out: If used in an unsafe context, then the value might be filtered out:
Context {{.}} After Context {{.}} After
<a href="{{.}}"> #ZgotmplZ <a href="{{.}}"> #ZgotmplZ
since "O'Reilly:" is not an allowed protocol like "http:". since "O'Reilly:" is not an allowed protocol like "http:".
If {{.}} is the innocuous word, `left`, then it can appear more widely, If {{.}} is the innocuous word, `left`, then it can appear more widely,
Context {{.}} After Context {{.}} After
{{.}} left {{.}} left
<a title='{{.}}'> left <a title='{{.}}'> left
<a href='{{.}}'> left <a href='{{.}}'> left
<a href='/{{.}}'> left <a href='/{{.}}'> left
<a href='?dir={{.}}'> left <a href='?dir={{.}}'> left
<a style="border-{{.}}: 4px"> left <a style="border-{{.}}: 4px"> left
<a style="align: {{.}}"> left <a style="align: {{.}}"> left
<a style="background: '{{.}}'> left <a style="background: '{{.}}'> left
<a style="background: url('{{.}}')> left <a style="background: url('{{.}}')> left
<style>p.{{.}} {color:red}</style> left <style>p.{{.}} {color:red}</style> left
Non-string values can be used in JavaScript contexts. Non-string values can be used in JavaScript contexts.
If {{.}} is If {{.}} is
struct{A,B string}{ "foo", "bar" } struct{A,B string}{ "foo", "bar" }
in the escaped template in the escaped template
<script>var pair = {{.}};</script> <script>var pair = {{.}};</script>
then the template output is then the template output is
<script>var pair = {"A": "foo", "B": "bar"};</script> <script>var pair = {"A": "foo", "B": "bar"};</script>
See package json to understand how non-string content is marshaled for See package json to understand how non-string content is marshaled for
embedding in JavaScript contexts. embedding in JavaScript contexts.
# Typed Strings
Typed Strings
By default, this package assumes that all pipelines produce a plain text string. By default, this package assumes that all pipelines produce a plain text string.
It adds escaping pipeline stages necessary to correctly and safely embed that It adds escaping pipeline stages necessary to correctly and safely embed that
@ -197,24 +192,23 @@ exempted from escaping.
The template The template
Hello, {{.}}! Hello, {{.}}!
can be invoked with can be invoked with
tmpl.Execute(out, template.HTML(`<b>World</b>`)) tmpl.Execute(out, template.HTML(`<b>World</b>`))
to produce to produce
Hello, <b>World</b>! Hello, <b>World</b>!
instead of the instead of the
Hello, &lt;b&gt;World&lt;b&gt;! Hello, &lt;b&gt;World&lt;b&gt;!
that would have been produced if {{.}} was a regular string. that would have been produced if {{.}} was a regular string.
# Security Model
Security Model
https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition defines "safe" as used by this package. https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition defines "safe" as used by this package.

View file

@ -33,14 +33,17 @@ type ErrorCode int
// //
// Output: "ZgotmplZ" // Output: "ZgotmplZ"
// Example: // Example:
// <img src="{{.X}}"> //
// where {{.X}} evaluates to `javascript:...` // <img src="{{.X}}">
// where {{.X}} evaluates to `javascript:...`
//
// Discussion: // Discussion:
// "ZgotmplZ" is a special value that indicates that unsafe content reached a //
// CSS or URL context at runtime. The output of the example will be // "ZgotmplZ" is a special value that indicates that unsafe content reached a
// <img src="#ZgotmplZ"> // CSS or URL context at runtime. The output of the example will be
// If the data comes from a trusted source, use content types to exempt it // <img src="#ZgotmplZ">
// from filtering: URL(`javascript:...`). // If the data comes from a trusted source, use content types to exempt it
// from filtering: URL(`javascript:...`).
const ( const (
// OK indicates the lack of an error. // OK indicates the lack of an error.
OK ErrorCode = iota OK ErrorCode = iota

View file

@ -412,13 +412,19 @@ func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode {
// nudge returns the context that would result from following empty string // nudge returns the context that would result from following empty string
// transitions from the input context. // transitions from the input context.
// For example, parsing: // For example, parsing:
// `<a href=` //
// `<a href=`
//
// will end in context{stateBeforeValue, attrURL}, but parsing one extra rune: // will end in context{stateBeforeValue, attrURL}, but parsing one extra rune:
// `<a href=x` //
// `<a href=x`
//
// will end in context{stateURL, delimSpaceOrTagEnd, ...}. // will end in context{stateURL, delimSpaceOrTagEnd, ...}.
// There are two transitions that happen when the 'x' is seen: // There are two transitions that happen when the 'x' is seen:
// (1) Transition from a before-value state to a start-of-value state without // (1) Transition from a before-value state to a start-of-value state without
// consuming any character. //
// consuming any character.
//
// (2) Consume 'x' and transition past the first value character. // (2) Consume 'x' and transition past the first value character.
// In this case, nudging produces the context after (1) happens. // In this case, nudging produces the context after (1) happens.
func nudge(c context) context { func nudge(c context) context {

View file

@ -693,7 +693,7 @@ func TestEscape(t *testing.T) {
t.Errorf("%s: tree not set properly", test.name) t.Errorf("%s: tree not set properly", test.name)
continue continue
} }
b := new(bytes.Buffer) b := new(strings.Builder)
if err := tmpl.Execute(b, data); err != nil { if err := tmpl.Execute(b, data); err != nil {
t.Errorf("%s: template execution failed: %s", test.name, err) t.Errorf("%s: template execution failed: %s", test.name, err)
continue continue
@ -740,7 +740,7 @@ func TestEscapeMap(t *testing.T) {
}, },
} { } {
tmpl := Must(New("").Parse(test.input)) tmpl := Must(New("").Parse(test.input))
b := new(bytes.Buffer) b := new(strings.Builder)
if err := tmpl.Execute(b, data); err != nil { if err := tmpl.Execute(b, data); err != nil {
t.Errorf("%s: template execution failed: %s", test.desc, err) t.Errorf("%s: template execution failed: %s", test.desc, err)
continue continue
@ -882,7 +882,7 @@ func TestEscapeSet(t *testing.T) {
t.Errorf("error parsing %q: %v", source, err) t.Errorf("error parsing %q: %v", source, err)
continue continue
} }
var b bytes.Buffer var b strings.Builder
if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil { if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil {
t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main")) t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main"))
@ -1833,7 +1833,7 @@ func TestIndirectPrint(t *testing.T) {
bp := &b bp := &b
bpp := &bp bpp := &bp
tmpl := Must(New("t").Parse(`{{.}}`)) tmpl := Must(New("t").Parse(`{{.}}`))
var buf bytes.Buffer var buf strings.Builder
err := tmpl.Execute(&buf, ap) err := tmpl.Execute(&buf, ap)
if err != nil { if err != nil {
t.Errorf("Unexpected error: %s", err) t.Errorf("Unexpected error: %s", err)
@ -1876,7 +1876,7 @@ func TestPipeToMethodIsEscaped(t *testing.T) {
t.Errorf("panicked: %v\n", panicValue) t.Errorf("panicked: %v\n", panicValue)
} }
}() }()
var b bytes.Buffer var b strings.Builder
tmpl.Execute(&b, Issue7379(0)) tmpl.Execute(&b, Issue7379(0))
return b.String() return b.String()
} }
@ -1909,7 +1909,7 @@ func TestIdempotentExecute(t *testing.T) {
Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`)) Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`))
Must(tmpl. Must(tmpl.
Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`)) Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`))
got := new(bytes.Buffer) got := new(strings.Builder)
var err error var err error
// Ensure that "hello" produces the same output when executed twice. // Ensure that "hello" produces the same output when executed twice.
want := "Hello, Ladies &amp; Gentlemen!" want := "Hello, Ladies &amp; Gentlemen!"
@ -1952,7 +1952,7 @@ func TestOrphanedTemplate(t *testing.T) {
t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`)) t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`))
t2 := Must(t1.New("foo").Parse(`bar`)) t2 := Must(t1.New("foo").Parse(`bar`))
var b bytes.Buffer var b strings.Builder
const wantError = `template: "foo" is an incomplete or empty template` const wantError = `template: "foo" is an incomplete or empty template`
if err := t1.Execute(&b, "javascript:alert(1)"); err == nil { if err := t1.Execute(&b, "javascript:alert(1)"); err == nil {
t.Fatal("expected error executing t1") t.Fatal("expected error executing t1")
@ -1981,7 +1981,7 @@ func TestAliasedParseTreeDoesNotOverescape(t *testing.T) {
if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil { if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil {
t.Fatalf("AddParseTree error: %v", err) t.Fatalf("AddParseTree error: %v", err)
} }
var b1, b2 bytes.Buffer var b1, b2 strings.Builder
if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil { if err := tpl.ExecuteTemplate(&b1, "foo", data); err != nil {
t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err) t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err)
} }

View file

@ -769,7 +769,7 @@ func mapOfThree() any {
} }
func testExecute(execTests []execTest, template *Template, t *testing.T) { func testExecute(execTests []execTest, template *Template, t *testing.T) {
b := new(bytes.Buffer) b := new(strings.Builder)
funcs := FuncMap{ funcs := FuncMap{
"add": add, "add": add,
"count": count, "count": count,
@ -861,7 +861,7 @@ func TestDelims(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err) t.Fatalf("delim %q text %q parse err %s", left, text, err)
} }
var b = new(bytes.Buffer) var b = new(strings.Builder)
err = tmpl.Execute(b, value) err = tmpl.Execute(b, value)
if err != nil { if err != nil {
t.Fatalf("delim %q exec err %s", left, err) t.Fatalf("delim %q exec err %s", left, err)
@ -1002,7 +1002,7 @@ func TestTree(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("parse error:", err) t.Fatal("parse error:", err)
} }
var b bytes.Buffer var b strings.Builder
const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]" const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
// First by looking up the template. // First by looking up the template.
err = tmpl.Lookup("tree").Execute(&b, tree) err = tmpl.Lookup("tree").Execute(&b, tree)
@ -1196,33 +1196,39 @@ var cmpTests = []cmpTest{
{"eq .Iface1 .Iface1", "true", true}, {"eq .Iface1 .Iface1", "true", true},
{"eq .Iface1 .Iface2", "false", true}, {"eq .Iface1 .Iface2", "false", true},
{"eq .Iface2 .Iface2", "true", true}, {"eq .Iface2 .Iface2", "true", true},
{"eq .Map .Map", "true", true}, // Uncomparable types but nil is OK.
{"eq .Map nil", "true", true}, // Uncomparable types but nil is OK.
{"eq nil .Map", "true", true}, // Uncomparable types but nil is OK.
{"eq .Map .NonNilMap", "false", true}, // Uncomparable types but nil is OK.
// Errors // Errors
{"eq `xy` 1", "", false}, // Different types. {"eq `xy` 1", "", false}, // Different types.
{"eq 2 2.0", "", false}, // Different types. {"eq 2 2.0", "", false}, // Different types.
{"lt true true", "", false}, // Unordered types. {"lt true true", "", false}, // Unordered types.
{"lt 1+0i 1+0i", "", false}, // Unordered types. {"lt 1+0i 1+0i", "", false}, // Unordered types.
{"eq .Ptr 1", "", false}, // Incompatible types. {"eq .Ptr 1", "", false}, // Incompatible types.
{"eq .Ptr .NegOne", "", false}, // Incompatible types. {"eq .Ptr .NegOne", "", false}, // Incompatible types.
{"eq .Map .Map", "", false}, // Uncomparable types. {"eq .Map .V1", "", false}, // Uncomparable types.
{"eq .Map .V1", "", false}, // Uncomparable types. {"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types.
} }
func TestComparison(t *testing.T) { func TestComparison(t *testing.T) {
b := new(bytes.Buffer) b := new(strings.Builder)
var cmpStruct = struct { var cmpStruct = struct {
Uthree, Ufour uint Uthree, Ufour uint
NegOne, Three int NegOne, Three int
Ptr, NilPtr *int Ptr, NilPtr *int
NonNilMap map[int]int
Map map[int]int Map map[int]int
V1, V2 V V1, V2 V
Iface1, Iface2 fmt.Stringer Iface1, Iface2 fmt.Stringer
}{ }{
Uthree: 3, Uthree: 3,
Ufour: 4, Ufour: 4,
NegOne: -1, NegOne: -1,
Three: 3, Three: 3,
Ptr: new(int), Ptr: new(int),
Iface1: b, NonNilMap: make(map[int]int),
Iface1: b,
} }
for _, test := range cmpTests { for _, test := range cmpTests {
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr) text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
@ -1254,7 +1260,7 @@ func TestMissingMapKey(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
var b bytes.Buffer var b strings.Builder
// By default, just get "<no value>" // NOTE: not in html/template, get empty string // By default, just get "<no value>" // NOTE: not in html/template, get empty string
err = tmpl.Execute(&b, data) err = tmpl.Execute(&b, data)
if err != nil { if err != nil {
@ -1423,7 +1429,7 @@ func TestBlock(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
var buf bytes.Buffer var buf strings.Builder
if err := tmpl.Execute(&buf, "hello"); err != nil { if err := tmpl.Execute(&buf, "hello"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1529,7 +1535,7 @@ func TestAddrOfIndex(t *testing.T) {
} }
for _, text := range texts { for _, text := range texts {
tmpl := Must(New("tmpl").Parse(text)) tmpl := Must(New("tmpl").Parse(text))
var buf bytes.Buffer var buf strings.Builder
err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}})) err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}}))
if err != nil { if err != nil {
t.Fatalf("%s: Execute: %v", text, err) t.Fatalf("%s: Execute: %v", text, err)
@ -1585,7 +1591,7 @@ func TestInterfaceValues(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tmpl := Must(New("tmpl").Parse(tt.text)) tmpl := Must(New("tmpl").Parse(tt.text))
var buf bytes.Buffer var buf strings.Builder
err := tmpl.Execute(&buf, map[string]any{ err := tmpl.Execute(&buf, map[string]any{
"PlusOne": func(n int) int { "PlusOne": func(n int) int {
return n + 1 return n + 1
@ -1680,7 +1686,7 @@ func TestIssue31810(t *testing.T) {
t.Skip("broken in html/template") t.Skip("broken in html/template")
// A simple value with no arguments is fine. // A simple value with no arguments is fine.
var b bytes.Buffer var b strings.Builder
const text = "{{ (.) }}" const text = "{{ (.) }}"
tmpl, err := New("").Parse(text) tmpl, err := New("").Parse(text)
if err != nil { if err != nil {

View file

@ -84,10 +84,12 @@ var htmlNormReplacementTable = []string{
// <script>(function () { // <script>(function () {
// var a = [], d = document.getElementById("d"), i, c, s; // var a = [], d = document.getElementById("d"), i, c, s;
// for (i = 0; i < 0x10000; ++i) { // for (i = 0; i < 0x10000; ++i) {
// c = String.fromCharCode(i); //
// d.innerHTML = "<span title=" + c + "lt" + c + "></span>" // c = String.fromCharCode(i);
// s = d.getElementsByTagName("SPAN")[0]; // d.innerHTML = "<span title=" + c + "lt" + c + "></span>"
// if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); } // s = d.getElementsByTagName("SPAN")[0];
// if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); }
//
// } // }
// document.write(a.join(", ")); // document.write(a.join(", "));
// })()</script> // })()</script>
@ -174,7 +176,7 @@ func htmlReplacer(s string, replacementTable []string, badRunes bool) string {
// stripTags takes a snippet of HTML and returns only the text content. // stripTags takes a snippet of HTML and returns only the text content.
// For example, `<b>&iexcl;Hi!</b> <script>...</script>` -> `&iexcl;Hi! `. // For example, `<b>&iexcl;Hi!</b> <script>...</script>` -> `&iexcl;Hi! `.
func stripTags(html string) string { func stripTags(html string) string {
var b bytes.Buffer var b strings.Builder
s, c, i, allText := []byte(html), context{}, 0, true s, c, i, allText := []byte(html), context{}, 0, true
// Using the transition funcs helps us avoid mangling // Using the transition funcs helps us avoid mangling
// `<div title="1>2">` or `I <3 Ponies!`. // `<div title="1>2">` or `I <3 Ponies!`.

View file

@ -8,7 +8,6 @@
package template package template
import ( import (
"bytes"
"math" "math"
"strings" "strings"
"testing" "testing"
@ -324,7 +323,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
// Escape it rune by rune to make sure that any // Escape it rune by rune to make sure that any
// fast-path checking does not break escaping. // fast-path checking does not break escaping.
var buf bytes.Buffer var buf strings.Builder
for _, c := range input { for _, c := range input {
buf.WriteString(test.escaper(string(c))) buf.WriteString(test.escaper(string(c)))
} }

View file

@ -11,8 +11,8 @@ package template
import ( import (
"archive/zip" "archive/zip"
"bytes"
"os" "os"
"strings"
"testing" "testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@ -249,7 +249,7 @@ func TestEmptyTemplate(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
buf := &bytes.Buffer{} buf := &strings.Builder{}
if err := m.Execute(buf, c.in); err != nil { if err := m.Execute(buf, c.in); err != nil {
t.Error(i, err) t.Error(i, err)
continue continue
@ -284,7 +284,7 @@ func TestIssue19294(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
var buf bytes.Buffer var buf strings.Builder
res.Execute(&buf, 0) res.Execute(&buf, 0)
if buf.String() != "stylesheet" { if buf.String() != "stylesheet" {
t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet") t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")

View file

@ -65,6 +65,7 @@ func (t *Template) Templates() []*Template {
// //
// missingkey: Control the behavior during execution if a map is // missingkey: Control the behavior during execution if a map is
// indexed with a key that is not present in the map. // indexed with a key that is not present in the map.
//
// "missingkey=default" or "missingkey=invalid" // "missingkey=default" or "missingkey=invalid"
// The default behavior: Do nothing and continue execution. // The default behavior: Do nothing and continue execution.
// If printed, the result of the index operation is the string // If printed, the result of the index operation is the string
@ -73,7 +74,6 @@ func (t *Template) Templates() []*Template {
// The operation returns the zero value for the map type's element. // The operation returns the zero value for the map type's element.
// "missingkey=error" // "missingkey=error"
// Execution stops immediately with an error. // Execution stops immediately with an error.
//
func (t *Template) Option(opt ...string) *Template { func (t *Template) Option(opt ...string) *Template {
t.text.Option(opt...) t.text.Option(opt...)
return t return t
@ -329,14 +329,7 @@ func (t *Template) Name() string {
return t.text.Name() return t.text.Name()
} }
// FuncMap is the type of the map defining the mapping from names to type FuncMap = template.FuncMap
// functions. Each function must have either a single return value, or two
// return values of which the second has type error. In that case, if the
// second (error) argument evaluates to non-nil during execution, execution
// terminates and Execute returns that error. FuncMap has the same base type
// as FuncMap in "text/template", copied here so clients need not import
// "text/template".
type FuncMap map[string]any
// Funcs adds the elements of the argument map to the template's function map. // Funcs adds the elements of the argument map to the template's function map.
// It must be called before the template is parsed. // It must be called before the template is parsed.
@ -369,6 +362,7 @@ func (t *Template) Lookup(name string) *Template {
// Must is a helper that wraps a call to a function returning (*Template, error) // Must is a helper that wraps a call to a function returning (*Template, error)
// and panics if the error is non-nil. It is intended for use in variable initializations // and panics if the error is non-nil. It is intended for use in variable initializations
// such as // such as
//
// var t = template.Must(template.New("name").Parse("html")) // var t = template.Must(template.New("name").Parse("html"))
func Must(t *Template, err error) *Template { func Must(t *Template, err error) *Template {
if err != nil { if err != nil {

View file

@ -30,7 +30,7 @@ func TestTemplateClone(t *testing.T) {
const want = "stuff" const want = "stuff"
parsed := Must(clone.Parse(want)) parsed := Must(clone.Parse(want))
var buf bytes.Buffer var buf strings.Builder
err = parsed.Execute(&buf, nil) err = parsed.Execute(&buf, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -211,7 +211,7 @@ func (c *testCase) mustNotParse(t *Template, text string) {
} }
func (c *testCase) mustExecute(t *Template, val any, want string) { func (c *testCase) mustExecute(t *Template, val any, want string) {
var buf bytes.Buffer var buf strings.Builder
err := t.Execute(&buf, val) err := t.Execute(&buf, val)
if err != nil { if err != nil {
c.t.Fatalf("execute: %v", err) c.t.Fatalf("execute: %v", err)

View file

@ -5,7 +5,6 @@
package template package template
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
) )
@ -19,15 +18,15 @@ import (
// //
// This filter conservatively assumes that all schemes other than the following // This filter conservatively assumes that all schemes other than the following
// are unsafe: // are unsafe:
// * http: Navigates to a new website, and may open a new window or tab. // - http: Navigates to a new website, and may open a new window or tab.
// These side effects can be reversed by navigating back to the // These side effects can be reversed by navigating back to the
// previous website, or closing the window or tab. No irreversible // previous website, or closing the window or tab. No irreversible
// changes will take place without further user interaction with // changes will take place without further user interaction with
// the new website. // the new website.
// * https: Same as http. // - https: Same as http.
// * mailto: Opens an email program and starts a new draft. This side effect // - mailto: Opens an email program and starts a new draft. This side effect
// is not irreversible until the user explicitly clicks send; it // is not irreversible until the user explicitly clicks send; it
// can be undone by closing the email program. // can be undone by closing the email program.
// //
// To allow URLs containing other schemes to bypass this filter, developers must // To allow URLs containing other schemes to bypass this filter, developers must
// explicitly indicate that such a URL is expected and safe by encapsulating it // explicitly indicate that such a URL is expected and safe by encapsulating it
@ -76,7 +75,7 @@ func urlProcessor(norm bool, args ...any) string {
if t == contentTypeURL { if t == contentTypeURL {
norm = true norm = true
} }
var b bytes.Buffer var b strings.Builder
if processURLOnto(s, norm, &b) { if processURLOnto(s, norm, &b) {
return b.String() return b.String()
} }
@ -85,7 +84,7 @@ func urlProcessor(norm bool, args ...any) string {
// processURLOnto appends a normalized URL corresponding to its input to b // processURLOnto appends a normalized URL corresponding to its input to b
// and reports whether the appended content differs from s. // and reports whether the appended content differs from s.
func processURLOnto(s string, norm bool, b *bytes.Buffer) bool { func processURLOnto(s string, norm bool, b *strings.Builder) bool {
b.Grow(len(s) + 16) b.Grow(len(s) + 16)
written := 0 written := 0
// The byte loop below assumes that all URLs use UTF-8 as the // The byte loop below assumes that all URLs use UTF-8 as the
@ -149,7 +148,7 @@ func srcsetFilterAndEscaper(args ...any) string {
case contentTypeURL: case contentTypeURL:
// Normalizing gets rid of all HTML whitespace // Normalizing gets rid of all HTML whitespace
// which separate the image URL from its metadata. // which separate the image URL from its metadata.
var b bytes.Buffer var b strings.Builder
if processURLOnto(s, true, &b) { if processURLOnto(s, true, &b) {
s = b.String() s = b.String()
} }
@ -157,7 +156,7 @@ func srcsetFilterAndEscaper(args ...any) string {
return strings.ReplaceAll(s, ",", "%2c") return strings.ReplaceAll(s, ",", "%2c")
} }
var b bytes.Buffer var b strings.Builder
written := 0 written := 0
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
if s[i] == ',' { if s[i] == ',' {
@ -183,7 +182,7 @@ func isHTMLSpaceOrASCIIAlnum(c byte) bool {
return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7))) return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
} }
func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) { func filterSrcsetElement(s string, left int, right int, b *strings.Builder) {
start := left start := left
for start < right && isHTMLSpace(s[start]) { for start < right && isHTMLSpace(s[start]) {
start++ start++

View file

@ -0,0 +1,73 @@
// Copyright 2015 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.
package testenv
import (
"os"
"os/exec"
"runtime"
"strings"
"sync"
"testing"
)
// HasExec reports whether the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
func HasExec() bool {
switch runtime.GOOS {
case "js", "ios":
return false
}
return true
}
// MustHaveExec checks that the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
// If not, MustHaveExec calls t.Skip with an explanation.
func MustHaveExec(t testing.TB) {
if !HasExec() {
t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
var execPaths sync.Map // path -> error
// MustHaveExecPath checks that the current system can start the named executable
// using os.StartProcess or (more commonly) exec.Command.
// If not, MustHaveExecPath calls t.Skip with an explanation.
func MustHaveExecPath(t testing.TB, path string) {
MustHaveExec(t)
err, found := execPaths.Load(path)
if !found {
_, err = exec.LookPath(path)
err, _ = execPaths.LoadOrStore(path, err)
}
if err != nil {
t.Skipf("skipping test: %s: %s", path, err)
}
}
// CleanCmdEnv will fill cmd.Env with the environment, excluding certain
// variables that could modify the behavior of the Go tools such as
// GODEBUG and GOTRACEBACK.
func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
if cmd.Env != nil {
panic("environment already set")
}
for _, env := range os.Environ() {
// Exclude GODEBUG from the environment to prevent its output
// from breaking tests that are trying to parse other command output.
if strings.HasPrefix(env, "GODEBUG=") {
continue
}
// Exclude GOTRACEBACK for the same reason.
if strings.HasPrefix(env, "GOTRACEBACK=") {
continue
}
cmd.Env = append(cmd.Env, env)
}
return cmd
}

View file

@ -0,0 +1,12 @@
// Copyright 2022 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.
//go:build noopt
package testenv
// OptimizationOff reports whether optimization is disabled.
func OptimizationOff() bool {
return true
}

View file

@ -0,0 +1,12 @@
// Copyright 2022 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.
//go:build !noopt
package testenv
// OptimizationOff reports whether optimization is disabled.
func OptimizationOff() bool {
return false
}

View file

@ -11,10 +11,12 @@
package testenv package testenv
import ( import (
"bytes"
"errors" "errors"
"flag" "flag"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -23,7 +25,6 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
) )
// Builder reports the name of the builder running this test // Builder reports the name of the builder running this test
@ -34,7 +35,7 @@ func Builder() string {
return os.Getenv("GO_BUILDER_NAME") return os.Getenv("GO_BUILDER_NAME")
} }
// HasGoBuild reports whether the current system can build programs with ``go build'' // HasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command. // and then run them with os.StartProcess or exec.Command.
func HasGoBuild() bool { func HasGoBuild() bool {
if os.Getenv("GO_GCFLAGS") != "" { if os.Getenv("GO_GCFLAGS") != "" {
@ -51,7 +52,7 @@ func HasGoBuild() bool {
return true return true
} }
// MustHaveGoBuild checks that the current system can build programs with ``go build'' // MustHaveGoBuild checks that the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command. // and then run them with os.StartProcess or exec.Command.
// If not, MustHaveGoBuild calls t.Skip with an explanation. // If not, MustHaveGoBuild calls t.Skip with an explanation.
func MustHaveGoBuild(t testing.TB) { func MustHaveGoBuild(t testing.TB) {
@ -63,13 +64,13 @@ func MustHaveGoBuild(t testing.TB) {
} }
} }
// HasGoRun reports whether the current system can run programs with ``go run.'' // HasGoRun reports whether the current system can run programs with “go run.”
func HasGoRun() bool { func HasGoRun() bool {
// For now, having go run and having go build are the same. // For now, having go run and having go build are the same.
return HasGoBuild() return HasGoBuild()
} }
// MustHaveGoRun checks that the current system can run programs with ``go run.'' // MustHaveGoRun checks that the current system can run programs with “go run.”
// If not, MustHaveGoRun calls t.Skip with an explanation. // If not, MustHaveGoRun calls t.Skip with an explanation.
func MustHaveGoRun(t testing.TB) { func MustHaveGoRun(t testing.TB) {
if !HasGoRun() { if !HasGoRun() {
@ -96,6 +97,100 @@ func GoToolPath(t testing.TB) string {
return path return path
} }
var (
gorootOnce sync.Once
gorootPath string
gorootErr error
)
func findGOROOT() (string, error) {
gorootOnce.Do(func() {
gorootPath = runtime.GOROOT()
if gorootPath != "" {
// If runtime.GOROOT() is non-empty, assume that it is valid.
//
// (It might not be: for example, the user may have explicitly set GOROOT
// to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
// rare, and if that happens the user can fix what they broke.)
return
}
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
// without GOROOT and the tree hasn't been moved there yet).
//
// Since this is internal/testenv, we can cheat and assume that the caller
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
// runs the test in the directory containing the packaged under test.) That
// means that if we start walking up the tree, we should eventually find
// GOROOT/src/go.mod, and we can report the parent directory of that.
cwd, err := os.Getwd()
if err != nil {
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
return
}
dir := cwd
for {
parent := filepath.Dir(dir)
if parent == dir {
// dir is either "." or only a volume name.
gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
return
}
if base := filepath.Base(dir); base != "src" {
dir = parent
continue // dir cannot be GOROOT/src if it doesn't end in "src".
}
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
if os.IsNotExist(err) {
dir = parent
continue
}
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
return
}
goMod := string(b)
for goMod != "" {
var line string
line, goMod, _ = strings.Cut(goMod, "\n")
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
// Found "module std", which is the module declaration in GOROOT/src!
gorootPath = parent
return
}
}
}
})
return gorootPath, gorootErr
}
// GOROOT reports the path to the directory containing the root of the Go
// project source tree. This is normally equivalent to runtime.GOROOT, but
// works even if the test binary was built with -trimpath.
//
// If GOROOT cannot be found, GOROOT skips t if t is non-nil,
// or panics otherwise.
func GOROOT(t testing.TB) string {
path, err := findGOROOT()
if err != nil {
if t == nil {
panic(err)
}
t.Helper()
t.Skip(err)
}
return path
}
// GoTool reports the path to the Go tool. // GoTool reports the path to the Go tool.
func GoTool() (string, error) { func GoTool() (string, error) {
if !HasGoBuild() { if !HasGoBuild() {
@ -105,7 +200,11 @@ func GoTool() (string, error) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
exeSuffix = ".exe" exeSuffix = ".exe"
} }
path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) goroot, err := findGOROOT()
if err != nil {
return "", fmt.Errorf("cannot find go tool: %w", err)
}
path := filepath.Join(goroot, "bin", "go"+exeSuffix)
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
return path, nil return path, nil
} }
@ -116,16 +215,6 @@ func GoTool() (string, error) {
return goBin, nil return goBin, nil
} }
// HasExec reports whether the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
func HasExec() bool {
switch runtime.GOOS {
case "js", "ios":
return false
}
return true
}
// HasSrc reports whether the entire source tree is available under GOROOT. // HasSrc reports whether the entire source tree is available under GOROOT.
func HasSrc() bool { func HasSrc() bool {
switch runtime.GOOS { switch runtime.GOOS {
@ -135,33 +224,6 @@ func HasSrc() bool {
return true return true
} }
// MustHaveExec checks that the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
// If not, MustHaveExec calls t.Skip with an explanation.
func MustHaveExec(t testing.TB) {
if !HasExec() {
t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
var execPaths sync.Map // path -> error
// MustHaveExecPath checks that the current system can start the named executable
// using os.StartProcess or (more commonly) exec.Command.
// If not, MustHaveExecPath calls t.Skip with an explanation.
func MustHaveExecPath(t testing.TB, path string) {
MustHaveExec(t)
err, found := execPaths.Load(path)
if !found {
_, err = exec.LookPath(path)
err, _ = execPaths.LoadOrStore(path, err)
}
if err != nil {
t.Skipf("skipping test: %s: %s", path, err)
}
}
// HasExternalNetwork reports whether the current system can use // HasExternalNetwork reports whether the current system can use
// external (non-localhost) networks. // external (non-localhost) networks.
func HasExternalNetwork() bool { func HasExternalNetwork() bool {
@ -194,32 +256,6 @@ 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. // HasSymlink reports whether the current system can use os.Symlink.
func HasSymlink() bool { func HasSymlink() bool {
ok, _ := hasSymlink() ok, _ := hasSymlink()
@ -267,28 +303,6 @@ func SkipFlakyNet(t testing.TB) {
} }
} }
// CleanCmdEnv will fill cmd.Env with the environment, excluding certain
// variables that could modify the behavior of the Go tools such as
// GODEBUG and GOTRACEBACK.
func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
if cmd.Env != nil {
panic("environment already set")
}
for _, env := range os.Environ() {
// Exclude GODEBUG from the environment to prevent its output
// from breaking tests that are trying to parse other command output.
if strings.HasPrefix(env, "GODEBUG=") {
continue
}
// Exclude GOTRACEBACK for the same reason.
if strings.HasPrefix(env, "GOTRACEBACK=") {
continue
}
cmd.Env = append(cmd.Env, env)
}
return cmd
}
// CPUIsSlow reports whether the CPU running the test is suspected to be slow. // CPUIsSlow reports whether the CPU running the test is suspected to be slow.
func CPUIsSlow() bool { func CPUIsSlow() bool {
switch runtime.GOARCH { switch runtime.GOARCH {
@ -309,58 +323,10 @@ func SkipIfShortAndSlow(t testing.TB) {
} }
} }
// RunWithTimeout runs cmd and returns its combined output. If the // SkipIfOptimizationOff skips t if optimization is disabled.
// subprocess exits with a non-zero status, it will log that status func SkipIfOptimizationOff(t testing.TB) {
// and return a non-nil error, but this is not considered fatal. if OptimizationOff() {
func RunWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) { t.Helper()
args := cmd.Args t.Skip("skipping test with optimization disabled")
if args == nil {
args = []string{cmd.Path}
} }
var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
if err := cmd.Start(); err != nil {
t.Fatalf("starting %s: %v", args, err)
}
// If the process doesn't complete within 1 minute,
// assume it is hanging and kill it to get a stack trace.
p := cmd.Process
done := make(chan bool)
go func() {
scale := 1
// This GOARCH/GOOS test is copied from cmd/dist/test.go.
// TODO(iant): Have cmd/dist update the environment variable.
if runtime.GOARCH == "arm" || runtime.GOOS == "windows" {
scale = 2
}
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
if sc, err := strconv.Atoi(s); err == nil {
scale = sc
}
}
select {
case <-done:
case <-time.After(time.Duration(scale) * time.Minute):
p.Signal(Sigquit)
// If SIGQUIT doesn't do it after a little
// while, kill the process.
select {
case <-done:
case <-time.After(time.Duration(scale) * 30 * time.Second):
p.Signal(os.Kill)
}
}
}()
err := cmd.Wait()
if err != nil {
t.Logf("%s exit status: %v", args, err)
}
close(done)
return b.Bytes(), err
} }

View file

@ -0,0 +1,54 @@
// Copyright 2022 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.
package testenv_test
import (
"os"
"path/filepath"
"runtime"
"testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
)
func _TestGoToolLocation(t *testing.T) {
testenv.MustHaveGoBuild(t)
var exeSuffix string
if runtime.GOOS == "windows" {
exeSuffix = ".exe"
}
// Tests are defined to run within their package source directory,
// and this package's source directory is $GOROOT/src/internal/testenv.
// The 'go' command is installed at $GOROOT/bin/go, so if the environment
// is correct then testenv.GoTool() should be identical to ../../../bin/go.
relWant := "../../../bin/go" + exeSuffix
absWant, err := filepath.Abs(relWant)
if err != nil {
t.Fatal(err)
}
wantInfo, err := os.Stat(absWant)
if err != nil {
t.Fatal(err)
}
t.Logf("found go tool at %q (%q)", relWant, absWant)
goTool, err := testenv.GoTool()
if err != nil {
t.Fatalf("testenv.GoTool(): %v", err)
}
t.Logf("testenv.GoTool() = %q", goTool)
gotInfo, err := os.Stat(goTool)
if err != nil {
t.Fatal(err)
}
if !os.SameFile(wantInfo, gotInfo) {
t.Fatalf("%q is not the same file as %q", absWant, goTool)
}
}

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris //go:build unix
package testenv package testenv

View file

@ -18,7 +18,6 @@ structure as execution proceeds.
The input text for a template is UTF-8-encoded text in any format. The input text for a template is UTF-8-encoded text in any format.
"Actions"--data evaluations or control structures--are delimited by "Actions"--data evaluations or control structures--are delimited by
"{{" and "}}"; all text outside actions is copied to the output unchanged. "{{" and "}}"; all text outside actions is copied to the output unchanged.
Except for raw strings, actions may not span newlines, although comments can.
Once parsed, a template may be executed safely in parallel, although if parallel Once parsed, a template may be executed safely in parallel, although if parallel
executions share a Writer the output may be interleaved. executions share a Writer the output may be interleaved.
@ -425,10 +424,10 @@ The syntax of such definitions is to surround each template declaration with a
The define action names the template being created by providing a string The define action names the template being created by providing a string
constant. Here is a simple example: constant. Here is a simple example:
`{{define "T1"}}ONE{{end}} {{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}} {{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}} {{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}` {{template "T3"}}
This defines two templates, T1 and T2, and a third T3 that invokes the other two This defines two templates, T1 and T2, and a third T3 that invokes the other two
when it is executed. Finally it invokes T3. If executed this template will when it is executed. Finally it invokes T3. If executed this template will

View file

@ -94,6 +94,12 @@ type missingValType struct{}
var missingVal = reflect.ValueOf(missingValType{}) var missingVal = reflect.ValueOf(missingValType{})
var missingValReflectType = reflect.TypeOf(missingValType{})
func isMissing(v reflect.Value) bool {
return v.IsValid() && v.Type() == missingValReflectType
}
// at marks the state to be on node n, for error reporting. // at marks the state to be on node n, for error reporting.
func (s *state) at(node parse.Node) { func (s *state) at(node parse.Node) {
s.node = node s.node = node
@ -357,11 +363,19 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
oneIteration := func(index, elem reflect.Value) { oneIteration := func(index, elem reflect.Value) {
// Set top var (lexically the second if there are two) to the element. // Set top var (lexically the second if there are two) to the element.
if len(r.Pipe.Decl) > 0 { if len(r.Pipe.Decl) > 0 {
s.setTopVar(1, elem) if r.Pipe.IsAssign {
s.setVar(r.Pipe.Decl[0].Ident[0], elem)
} else {
s.setTopVar(1, elem)
}
} }
// Set next var (lexically the first if there are two) to the index. // Set next var (lexically the first if there are two) to the index.
if len(r.Pipe.Decl) > 1 { if len(r.Pipe.Decl) > 1 {
s.setTopVar(2, index) if r.Pipe.IsAssign {
s.setVar(r.Pipe.Decl[1].Ident[0], index)
} else {
s.setTopVar(2, index)
}
} }
defer s.pop(mark) defer s.pop(mark)
defer func() { defer func() {
@ -471,7 +485,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
} }
func (s *state) notAFunction(args []parse.Node, final reflect.Value) { func (s *state) notAFunction(args []parse.Node, final reflect.Value) {
if len(args) > 1 || final != missingVal { if len(args) > 1 || !isMissing(final) {
s.errorf("can't give argument to non-function %s", args[0]) s.errorf("can't give argument to non-function %s", args[0])
} }
} }
@ -629,7 +643,7 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
if method := ptr.MethodByName(fieldName); method.IsValid() { if method := ptr.MethodByName(fieldName); method.IsValid() {
return s.evalCall(dot, method, false, node, fieldName, args, final) return s.evalCall(dot, method, false, node, fieldName, args, final)
} }
hasArgs := len(args) > 1 || final != missingVal hasArgs := len(args) > 1 || !isMissing(final)
// It's not a method; must be a field of a struct or an element of a map. // It's not a method; must be a field of a struct or an element of a map.
switch receiver.Kind() { switch receiver.Kind() {
case reflect.Struct: case reflect.Struct:
@ -700,7 +714,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
} }
typ := fun.Type() typ := fun.Type()
numIn := len(args) numIn := len(args)
if final != missingVal { if !isMissing(final) {
numIn++ numIn++
} }
numFixed := len(args) numFixed := len(args)
@ -763,7 +777,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
} }
} }
// Add final value if necessary. // Add final value if necessary.
if final != missingVal { if !isMissing(final) {
t := typ.In(typ.NumIn() - 1) t := typ.In(typ.NumIn() - 1)
if typ.IsVariadic() { if typ.IsVariadic() {
if numIn-1 < numFixed { if numIn-1 < numFixed {

View file

@ -695,6 +695,8 @@ var execTests = []execTest{
{"bug18a", "{{eq . '.'}}", "true", '.', true}, {"bug18a", "{{eq . '.'}}", "true", '.', true},
{"bug18b", "{{eq . 'e'}}", "true", 'e', true}, {"bug18b", "{{eq . 'e'}}", "true", 'e', true},
{"bug18c", "{{eq . 'P'}}", "true", 'P', true}, {"bug18c", "{{eq . 'P'}}", "true", 'P', true},
{"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true},
} }
func zeroArgs() string { func zeroArgs() string {
@ -775,7 +777,7 @@ func mapOfThree() any {
} }
func testExecute(execTests []execTest, template *Template, t *testing.T) { func testExecute(execTests []execTest, template *Template, t *testing.T) {
b := new(bytes.Buffer) b := new(strings.Builder)
funcs := FuncMap{ funcs := FuncMap{
"add": add, "add": add,
"count": count, "count": count,
@ -864,7 +866,7 @@ func TestDelims(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err) t.Fatalf("delim %q text %q parse err %s", left, text, err)
} }
var b = new(bytes.Buffer) var b = new(strings.Builder)
err = tmpl.Execute(b, value) err = tmpl.Execute(b, value)
if err != nil { if err != nil {
t.Fatalf("delim %q exec err %s", left, err) t.Fatalf("delim %q exec err %s", left, err)
@ -1027,7 +1029,7 @@ func TestTree(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("parse error:", err) t.Fatal("parse error:", err)
} }
var b bytes.Buffer var b strings.Builder
const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]" const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]"
// First by looking up the template. // First by looking up the template.
err = tmpl.Lookup("tree").Execute(&b, tree) err = tmpl.Lookup("tree").Execute(&b, tree)
@ -1223,33 +1225,39 @@ var cmpTests = []cmpTest{
{"eq .NilIface .Iface1", "false", true}, {"eq .NilIface .Iface1", "false", true},
{"eq .NilIface 0", "false", true}, {"eq .NilIface 0", "false", true},
{"eq 0 .NilIface", "false", true}, {"eq 0 .NilIface", "false", true},
{"eq .Map .Map", "true", true}, // Uncomparable types but nil is OK.
{"eq .Map nil", "true", true}, // Uncomparable types but nil is OK.
{"eq nil .Map", "true", true}, // Uncomparable types but nil is OK.
{"eq .Map .NonNilMap", "false", true}, // Uncomparable types but nil is OK.
// Errors // Errors
{"eq `xy` 1", "", false}, // Different types. {"eq `xy` 1", "", false}, // Different types.
{"eq 2 2.0", "", false}, // Different types. {"eq 2 2.0", "", false}, // Different types.
{"lt true true", "", false}, // Unordered types. {"lt true true", "", false}, // Unordered types.
{"lt 1+0i 1+0i", "", false}, // Unordered types. {"lt 1+0i 1+0i", "", false}, // Unordered types.
{"eq .Ptr 1", "", false}, // Incompatible types. {"eq .Ptr 1", "", false}, // Incompatible types.
{"eq .Ptr .NegOne", "", false}, // Incompatible types. {"eq .Ptr .NegOne", "", false}, // Incompatible types.
{"eq .Map .Map", "", false}, // Uncomparable types. {"eq .Map .V1", "", false}, // Uncomparable types.
{"eq .Map .V1", "", false}, // Uncomparable types. {"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types.
} }
func TestComparison(t *testing.T) { func TestComparison(t *testing.T) {
b := new(bytes.Buffer) b := new(strings.Builder)
var cmpStruct = struct { var cmpStruct = struct {
Uthree, Ufour uint Uthree, Ufour uint
NegOne, Three int NegOne, Three int
Ptr, NilPtr *int Ptr, NilPtr *int
NonNilMap map[int]int
Map map[int]int Map map[int]int
V1, V2 V V1, V2 V
Iface1, NilIface fmt.Stringer Iface1, NilIface fmt.Stringer
}{ }{
Uthree: 3, Uthree: 3,
Ufour: 4, Ufour: 4,
NegOne: -1, NegOne: -1,
Three: 3, Three: 3,
Ptr: new(int), Ptr: new(int),
Iface1: b, NonNilMap: make(map[int]int),
Iface1: b,
} }
for _, test := range cmpTests { for _, test := range cmpTests {
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr) text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
@ -1281,7 +1289,7 @@ func TestMissingMapKey(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
var b bytes.Buffer var b strings.Builder
// By default, just get "<no value>" // By default, just get "<no value>"
err = tmpl.Execute(&b, data) err = tmpl.Execute(&b, data)
if err != nil { if err != nil {
@ -1451,7 +1459,7 @@ func TestBlock(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
var buf bytes.Buffer var buf strings.Builder
if err := tmpl.Execute(&buf, "hello"); err != nil { if err := tmpl.Execute(&buf, "hello"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1557,7 +1565,7 @@ func TestAddrOfIndex(t *testing.T) {
} }
for _, text := range texts { for _, text := range texts {
tmpl := Must(New("tmpl").Parse(text)) tmpl := Must(New("tmpl").Parse(text))
var buf bytes.Buffer var buf strings.Builder
err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}})) err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}}))
if err != nil { if err != nil {
t.Fatalf("%s: Execute: %v", text, err) t.Fatalf("%s: Execute: %v", text, err)
@ -1613,7 +1621,7 @@ func TestInterfaceValues(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tmpl := Must(New("tmpl").Parse(tt.text)) tmpl := Must(New("tmpl").Parse(tt.text))
var buf bytes.Buffer var buf strings.Builder
err := tmpl.Execute(&buf, map[string]any{ err := tmpl.Execute(&buf, map[string]any{
"PlusOne": func(n int) int { "PlusOne": func(n int) int {
return n + 1 return n + 1
@ -1706,7 +1714,7 @@ func TestExecutePanicDuringCall(t *testing.T) {
// Issue 31810. Check that a parenthesized first argument behaves properly. // Issue 31810. Check that a parenthesized first argument behaves properly.
func TestIssue31810(t *testing.T) { func TestIssue31810(t *testing.T) {
// A simple value with no arguments is fine. // A simple value with no arguments is fine.
var b bytes.Buffer var b strings.Builder
const text = "{{ (.) }}" const text = "{{ (.) }}"
tmpl, err := New("").Parse(text) tmpl, err := New("").Parse(text)
if err != nil { if err != nil {

View file

@ -5,7 +5,6 @@
package template package template
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -436,14 +435,33 @@ func basicKind(v reflect.Value) (kind, error) {
return invalidKind, errBadComparisonType return invalidKind, errBadComparisonType
} }
// isNil returns true if v is the zero reflect.Value, or nil of its type.
func isNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
}
return false
}
// canCompare reports whether v1 and v2 are both the same kind, or one is nil.
// Called only when dealing with nillable types, or there's about to be an error.
func canCompare(v1, v2 reflect.Value) bool {
k1 := v1.Kind()
k2 := v2.Kind()
if k1 == k2 {
return true
}
// We know the type can be compared to nil.
return k1 == reflect.Invalid || k2 == reflect.Invalid
}
// eq evaluates the comparison a == b || a == c || ... // eq evaluates the comparison a == b || a == c || ...
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) { func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
arg1 = indirectInterface(arg1) arg1 = indirectInterface(arg1)
if arg1 != zero {
if t1 := arg1.Type(); !t1.Comparable() {
return false, fmt.Errorf("uncomparable type %s: %v", t1, arg1)
}
}
if len(arg2) == 0 { if len(arg2) == 0 {
return false, errNoComparison return false, errNoComparison
} }
@ -479,11 +497,14 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
case uintKind: case uintKind:
truth = arg1.Uint() == arg.Uint() truth = arg1.Uint() == arg.Uint()
default: default:
if arg == zero || arg1 == zero { if !canCompare(arg1, arg) {
truth = arg1 == arg return false, fmt.Errorf("non-comparable types %s: %v, %s: %v", arg1, arg1.Type(), arg.Type(), arg)
}
if isNil(arg1) || isNil(arg) {
truth = isNil(arg) == isNil(arg1)
} else { } else {
if t2 := arg.Type(); !t2.Comparable() { if !arg.Type().Comparable() {
return false, fmt.Errorf("uncomparable type %s: %v", t2, arg) return false, fmt.Errorf("non-comparable type %s: %v", arg, arg.Type())
} }
truth = arg1.Interface() == arg.Interface() truth = arg1.Interface() == arg.Interface()
} }
@ -620,7 +641,7 @@ func HTMLEscapeString(s string) string {
if !strings.ContainsAny(s, "'\"&<>\000") { if !strings.ContainsAny(s, "'\"&<>\000") {
return s return s
} }
var b bytes.Buffer var b strings.Builder
HTMLEscape(&b, []byte(s)) HTMLEscape(&b, []byte(s))
return b.String() return b.String()
} }
@ -703,7 +724,7 @@ func JSEscapeString(s string) string {
if strings.IndexFunc(s, jsIsSpecial) < 0 { if strings.IndexFunc(s, jsIsSpecial) < 0 {
return s return s
} }
var b bytes.Buffer var b strings.Builder
JSEscape(&b, []byte(s)) JSEscape(&b, []byte(s))
return b.String() return b.String()
} }
@ -729,7 +750,9 @@ func URLQueryEscaper(args ...any) string {
} }
// evalArgs formats the list of arguments into a string. It is therefore equivalent to // evalArgs formats the list of arguments into a string. It is therefore equivalent to
//
// fmt.Sprint(args...) // fmt.Sprint(args...)
//
// except that each argument is indirected (if a pointer), as required, // except that each argument is indirected (if a pointer), as required,
// using the same rules as the default string evaluation during template // using the same rules as the default string evaluation during template
// execution. // execution.

View file

@ -19,6 +19,7 @@ import (
// Must is a helper that wraps a call to a function returning (*Template, error) // Must is a helper that wraps a call to a function returning (*Template, error)
// and panics if the error is non-nil. It is intended for use in variable // and panics if the error is non-nil. It is intended for use in variable
// initializations such as // initializations such as
//
// var t = template.Must(template.New("name").Parse("text")) // var t = template.Must(template.New("name").Parse("text"))
func Must(t *Template, err error) *Template { func Must(t *Template, err error) *Template {
if err != nil { if err != nil {

View file

@ -10,10 +10,10 @@ package template
// Tests for multiple-template parsing and execution. // Tests for multiple-template parsing and execution.
import ( import (
"bytes"
"fmt" "fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"os" "os"
"strings"
"testing" "testing"
) )
@ -245,7 +245,7 @@ func TestClone(t *testing.T) {
} }
} }
// Execute root. // Execute root.
var b bytes.Buffer var b strings.Builder
err = root.ExecuteTemplate(&b, "a", 0) err = root.ExecuteTemplate(&b, "a", 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -284,7 +284,7 @@ func TestAddParseTree(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Execute. // Execute.
var b bytes.Buffer var b strings.Builder
err = added.ExecuteTemplate(&b, "a", 0) err = added.ExecuteTemplate(&b, "a", 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -413,7 +413,7 @@ func TestEmptyTemplate(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
buf := &bytes.Buffer{} buf := &strings.Builder{}
if err := m.Execute(buf, c.in); err != nil { if err := m.Execute(buf, c.in); err != nil {
t.Error(i, err) t.Error(i, err)
continue continue
@ -448,7 +448,7 @@ func TestIssue19294(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
var buf bytes.Buffer var buf strings.Builder
res.Execute(&buf, 0) res.Execute(&buf, 0)
if buf.String() != "stylesheet" { if buf.String() != "stylesheet" {
t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet") t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")

View file

@ -30,6 +30,7 @@ type option struct {
// //
// missingkey: Control the behavior during execution if a map is // missingkey: Control the behavior during execution if a map is
// indexed with a key that is not present in the map. // indexed with a key that is not present in the map.
//
// "missingkey=default" or "missingkey=invalid" // "missingkey=default" or "missingkey=invalid"
// The default behavior: Do nothing and continue execution. // The default behavior: Do nothing and continue execution.
// If printed, the result of the index operation is the string // If printed, the result of the index operation is the string
@ -38,7 +39,6 @@ type option struct {
// The operation returns the zero value for the map type's element. // The operation returns the zero value for the map type's element.
// "missingkey=error" // "missingkey=error"
// Execution stops immediately with an error. // Execution stops immediately with an error.
//
func (t *Template) Option(opt ...string) *Template { func (t *Template) Option(opt ...string) *Template {
t.init() t.init()
for _, s := range opt { for _, s := range opt {

View file

@ -111,31 +111,36 @@ type stateFn func(*lexer) stateFn
// lexer holds the state of the scanner. // lexer holds the state of the scanner.
type lexer struct { type lexer struct {
name string // the name of the input; used only for error reports name string // the name of the input; used only for error reports
input string // the string being scanned input string // the string being scanned
leftDelim string // start of action leftDelim string // start of action marker
rightDelim string // end of action rightDelim string // end of action marker
emitComment bool // emit itemComment tokens. pos Pos // current position in the input
pos Pos // current position in the input start Pos // start position of this item
start Pos // start position of this item atEOF bool // we have hit the end of input and returned eof
width Pos // width of last rune read from input parenDepth int // nesting depth of ( ) exprs
items chan item // channel of scanned items line int // 1+number of newlines seen
parenDepth int // nesting depth of ( ) exprs startLine int // start line of this item
line int // 1+number of newlines seen item item // item to return to parser
startLine int // start line of this item insideAction bool // are we inside an action?
breakOK bool // break keyword allowed options lexOptions
continueOK bool // continue keyword allowed }
// lexOptions control behavior of the lexer. All default to false.
type lexOptions struct {
emitComment bool // emit itemComment tokens.
breakOK bool // break keyword allowed
continueOK bool // continue keyword allowed
} }
// next returns the next rune in the input. // next returns the next rune in the input.
func (l *lexer) next() rune { func (l *lexer) next() rune {
if int(l.pos) >= len(l.input) { if int(l.pos) >= len(l.input) {
l.width = 0 l.atEOF = true
return eof return eof
} }
r, w := utf8.DecodeRuneInString(l.input[l.pos:]) r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = Pos(w) l.pos += Pos(w)
l.pos += l.width
if r == '\n' { if r == '\n' {
l.line++ l.line++
} }
@ -149,23 +154,41 @@ func (l *lexer) peek() rune {
return r return r
} }
// backup steps back one rune. Can only be called once per call of next. // backup steps back one rune.
func (l *lexer) backup() { func (l *lexer) backup() {
l.pos -= l.width if !l.atEOF && l.pos > 0 {
// Correct newline count. r, w := utf8.DecodeLastRuneInString(l.input[:l.pos])
if l.width == 1 && l.input[l.pos] == '\n' { l.pos -= Pos(w)
l.line-- // Correct newline count.
if r == '\n' {
l.line--
}
} }
} }
// emit passes an item back to the client. // thisItem returns the item at the current input point with the specified type
func (l *lexer) emit(t itemType) { // and advances the input.
l.items <- item{t, l.start, l.input[l.start:l.pos], l.startLine} func (l *lexer) thisItem(t itemType) item {
i := item{t, l.start, l.input[l.start:l.pos], l.startLine}
l.start = l.pos l.start = l.pos
l.startLine = l.line l.startLine = l.line
return i
}
// emit passes the trailing text as an item back to the parser.
func (l *lexer) emit(t itemType) stateFn {
return l.emitItem(l.thisItem(t))
}
// emitItem passes the specified item to the parser.
func (l *lexer) emitItem(i item) stateFn {
l.item = i
return nil
} }
// ignore skips over the pending input before this point. // ignore skips over the pending input before this point.
// It tracks newlines in the ignored text, so use it only
// for text that is skipped without calling l.next.
func (l *lexer) ignore() { func (l *lexer) ignore() {
l.line += strings.Count(l.input[l.start:l.pos], "\n") l.line += strings.Count(l.input[l.start:l.pos], "\n")
l.start = l.pos l.start = l.pos
@ -191,25 +214,31 @@ func (l *lexer) acceptRun(valid string) {
// errorf returns an error token and terminates the scan by passing // errorf returns an error token and terminates the scan by passing
// back a nil pointer that will be the next state, terminating l.nextItem. // back a nil pointer that will be the next state, terminating l.nextItem.
func (l *lexer) errorf(format string, args ...any) stateFn { func (l *lexer) errorf(format string, args ...any) stateFn {
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine} l.item = item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine}
l.start = 0
l.pos = 0
l.input = l.input[:0]
return nil return nil
} }
// nextItem returns the next item from the input. // nextItem returns the next item from the input.
// Called by the parser, not in the lexing goroutine. // Called by the parser, not in the lexing goroutine.
func (l *lexer) nextItem() item { func (l *lexer) nextItem() item {
return <-l.items l.item = item{itemEOF, l.pos, "EOF", l.startLine}
} state := lexText
if l.insideAction {
// drain drains the output so the lexing goroutine will exit. state = lexInsideAction
// Called by the parser, not in the lexing goroutine. }
func (l *lexer) drain() { for {
for range l.items { state = state(l)
if state == nil {
return l.item
}
} }
} }
// lex creates a new scanner for the input string. // lex creates a new scanner for the input string.
func lex(name, input, left, right string, emitComment bool) *lexer { func lex(name, input, left, right string) *lexer {
if left == "" { if left == "" {
left = leftDelim left = leftDelim
} }
@ -217,27 +246,17 @@ func lex(name, input, left, right string, emitComment bool) *lexer {
right = rightDelim right = rightDelim
} }
l := &lexer{ l := &lexer{
name: name, name: name,
input: input, input: input,
leftDelim: left, leftDelim: left,
rightDelim: right, rightDelim: right,
emitComment: emitComment, line: 1,
items: make(chan item), startLine: 1,
line: 1, insideAction: false,
startLine: 1,
} }
go l.run()
return l return l
} }
// run runs the state machine for the lexer.
func (l *lexer) run() {
for state := lexText; state != nil; {
state = state(l)
}
close(l.items)
}
// state functions // state functions
const ( const (
@ -249,31 +268,33 @@ const (
// lexText scans until an opening action delimiter, "{{". // lexText scans until an opening action delimiter, "{{".
func lexText(l *lexer) stateFn { func lexText(l *lexer) stateFn {
l.width = 0
if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 { if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
ldn := Pos(len(l.leftDelim)) if x > 0 {
l.pos += Pos(x) l.pos += Pos(x)
trimLength := Pos(0) // Do we trim any trailing space?
if hasLeftTrimMarker(l.input[l.pos+ldn:]) { trimLength := Pos(0)
trimLength = rightTrimLength(l.input[l.start:l.pos]) delimEnd := l.pos + Pos(len(l.leftDelim))
} if hasLeftTrimMarker(l.input[delimEnd:]) {
l.pos -= trimLength trimLength = rightTrimLength(l.input[l.start:l.pos])
if l.pos > l.start { }
l.pos -= trimLength
l.line += strings.Count(l.input[l.start:l.pos], "\n") l.line += strings.Count(l.input[l.start:l.pos], "\n")
l.emit(itemText) i := l.thisItem(itemText)
l.pos += trimLength
l.ignore()
if len(i.val) > 0 {
return l.emitItem(i)
}
} }
l.pos += trimLength
l.ignore()
return lexLeftDelim return lexLeftDelim
} }
l.pos = Pos(len(l.input)) l.pos = Pos(len(l.input))
// Correctly reached EOF. // Correctly reached EOF.
if l.pos > l.start { if l.pos > l.start {
l.line += strings.Count(l.input[l.start:l.pos], "\n") l.line += strings.Count(l.input[l.start:l.pos], "\n")
l.emit(itemText) return l.emit(itemText)
} }
l.emit(itemEOF) return l.emit(itemEOF)
return nil
} }
// rightTrimLength returns the length of the spaces at the end of the string. // rightTrimLength returns the length of the spaces at the end of the string.
@ -298,6 +319,7 @@ func leftTrimLength(s string) Pos {
} }
// lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker. // lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker.
// (The text to be trimmed has already been emitted.)
func lexLeftDelim(l *lexer) stateFn { func lexLeftDelim(l *lexer) stateFn {
l.pos += Pos(len(l.leftDelim)) l.pos += Pos(len(l.leftDelim))
trimSpace := hasLeftTrimMarker(l.input[l.pos:]) trimSpace := hasLeftTrimMarker(l.input[l.pos:])
@ -310,28 +332,27 @@ func lexLeftDelim(l *lexer) stateFn {
l.ignore() l.ignore()
return lexComment return lexComment
} }
l.emit(itemLeftDelim) i := l.thisItem(itemLeftDelim)
l.insideAction = true
l.pos += afterMarker l.pos += afterMarker
l.ignore() l.ignore()
l.parenDepth = 0 l.parenDepth = 0
return lexInsideAction return l.emitItem(i)
} }
// lexComment scans a comment. The left comment marker is known to be present. // lexComment scans a comment. The left comment marker is known to be present.
func lexComment(l *lexer) stateFn { func lexComment(l *lexer) stateFn {
l.pos += Pos(len(leftComment)) l.pos += Pos(len(leftComment))
i := strings.Index(l.input[l.pos:], rightComment) x := strings.Index(l.input[l.pos:], rightComment)
if i < 0 { if x < 0 {
return l.errorf("unclosed comment") return l.errorf("unclosed comment")
} }
l.pos += Pos(i + len(rightComment)) l.pos += Pos(x + len(rightComment))
delim, trimSpace := l.atRightDelim() delim, trimSpace := l.atRightDelim()
if !delim { if !delim {
return l.errorf("comment ends before closing delimiter") return l.errorf("comment ends before closing delimiter")
} }
if l.emitComment { i := l.thisItem(itemComment)
l.emit(itemComment)
}
if trimSpace { if trimSpace {
l.pos += trimMarkerLen l.pos += trimMarkerLen
} }
@ -340,23 +361,27 @@ func lexComment(l *lexer) stateFn {
l.pos += leftTrimLength(l.input[l.pos:]) l.pos += leftTrimLength(l.input[l.pos:])
} }
l.ignore() l.ignore()
if l.options.emitComment {
return l.emitItem(i)
}
return lexText return lexText
} }
// lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker. // lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker.
func lexRightDelim(l *lexer) stateFn { func lexRightDelim(l *lexer) stateFn {
trimSpace := hasRightTrimMarker(l.input[l.pos:]) _, trimSpace := l.atRightDelim()
if trimSpace { if trimSpace {
l.pos += trimMarkerLen l.pos += trimMarkerLen
l.ignore() l.ignore()
} }
l.pos += Pos(len(l.rightDelim)) l.pos += Pos(len(l.rightDelim))
l.emit(itemRightDelim) i := l.thisItem(itemRightDelim)
if trimSpace { if trimSpace {
l.pos += leftTrimLength(l.input[l.pos:]) l.pos += leftTrimLength(l.input[l.pos:])
l.ignore() l.ignore()
} }
return lexText l.insideAction = false
return l.emitItem(i)
} }
// lexInsideAction scans the elements inside action delimiters. // lexInsideAction scans the elements inside action delimiters.
@ -378,14 +403,14 @@ func lexInsideAction(l *lexer) stateFn {
l.backup() // Put space back in case we have " -}}". l.backup() // Put space back in case we have " -}}".
return lexSpace return lexSpace
case r == '=': case r == '=':
l.emit(itemAssign) return l.emit(itemAssign)
case r == ':': case r == ':':
if l.next() != '=' { if l.next() != '=' {
return l.errorf("expected :=") return l.errorf("expected :=")
} }
l.emit(itemDeclare) return l.emit(itemDeclare)
case r == '|': case r == '|':
l.emit(itemPipe) return l.emit(itemPipe)
case r == '"': case r == '"':
return lexQuote return lexQuote
case r == '`': case r == '`':
@ -410,20 +435,19 @@ func lexInsideAction(l *lexer) stateFn {
l.backup() l.backup()
return lexIdentifier return lexIdentifier
case r == '(': case r == '(':
l.emit(itemLeftParen)
l.parenDepth++ l.parenDepth++
return l.emit(itemLeftParen)
case r == ')': case r == ')':
l.emit(itemRightParen)
l.parenDepth-- l.parenDepth--
if l.parenDepth < 0 { if l.parenDepth < 0 {
return l.errorf("unexpected right paren %#U", r) return l.errorf("unexpected right paren")
} }
return l.emit(itemRightParen)
case r <= unicode.MaxASCII && unicode.IsPrint(r): case r <= unicode.MaxASCII && unicode.IsPrint(r):
l.emit(itemChar) return l.emit(itemChar)
default: default:
return l.errorf("unrecognized character in action: %#U", r) return l.errorf("unrecognized character in action: %#U", r)
} }
return lexInsideAction
} }
// lexSpace scans a run of space characters. // lexSpace scans a run of space characters.
@ -448,13 +472,11 @@ func lexSpace(l *lexer) stateFn {
return lexRightDelim // On the delim, so go right to that. return lexRightDelim // On the delim, so go right to that.
} }
} }
l.emit(itemSpace) return l.emit(itemSpace)
return lexInsideAction
} }
// lexIdentifier scans an alphanumeric. // lexIdentifier scans an alphanumeric.
func lexIdentifier(l *lexer) stateFn { func lexIdentifier(l *lexer) stateFn {
Loop:
for { for {
switch r := l.next(); { switch r := l.next(); {
case isAlphaNumeric(r): case isAlphaNumeric(r):
@ -468,22 +490,19 @@ Loop:
switch { switch {
case key[word] > itemKeyword: case key[word] > itemKeyword:
item := key[word] item := key[word]
if item == itemBreak && !l.breakOK || item == itemContinue && !l.continueOK { if item == itemBreak && !l.options.breakOK || item == itemContinue && !l.options.continueOK {
l.emit(itemIdentifier) return l.emit(itemIdentifier)
} else {
l.emit(item)
} }
return l.emit(item)
case word[0] == '.': case word[0] == '.':
l.emit(itemField) return l.emit(itemField)
case word == "true", word == "false": case word == "true", word == "false":
l.emit(itemBool) return l.emit(itemBool)
default: default:
l.emit(itemIdentifier) return l.emit(itemIdentifier)
} }
break Loop
} }
} }
return lexInsideAction
} }
// lexField scans a field: .Alphanumeric. // lexField scans a field: .Alphanumeric.
@ -496,8 +515,7 @@ func lexField(l *lexer) stateFn {
// The $ has been scanned. // The $ has been scanned.
func lexVariable(l *lexer) stateFn { func lexVariable(l *lexer) stateFn {
if l.atTerminator() { // Nothing interesting follows -> "$". if l.atTerminator() { // Nothing interesting follows -> "$".
l.emit(itemVariable) return l.emit(itemVariable)
return lexInsideAction
} }
return lexFieldOrVariable(l, itemVariable) return lexFieldOrVariable(l, itemVariable)
} }
@ -507,11 +525,9 @@ func lexVariable(l *lexer) stateFn {
func lexFieldOrVariable(l *lexer, typ itemType) stateFn { func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
if l.atTerminator() { // Nothing interesting follows -> "." or "$". if l.atTerminator() { // Nothing interesting follows -> "." or "$".
if typ == itemVariable { if typ == itemVariable {
l.emit(itemVariable) return l.emit(itemVariable)
} else {
l.emit(itemDot)
} }
return lexInsideAction return l.emit(itemDot)
} }
var r rune var r rune
for { for {
@ -524,8 +540,7 @@ func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
if !l.atTerminator() { if !l.atTerminator() {
return l.errorf("bad character %#U", r) return l.errorf("bad character %#U", r)
} }
l.emit(typ) return l.emit(typ)
return lexInsideAction
} }
// atTerminator reports whether the input is at valid termination character to // atTerminator reports whether the input is at valid termination character to
@ -541,13 +556,7 @@ func (l *lexer) atTerminator() bool {
case eof, '.', ',', '|', ':', ')', '(': case eof, '.', ',', '|', ':', ')', '(':
return true return true
} }
// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will return strings.HasPrefix(l.input[l.pos:], l.rightDelim)
// succeed but should fail) but only in extremely rare cases caused by willfully
// bad choice of delimiter.
if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r {
return true
}
return false
} }
// lexChar scans a character constant. The initial quote is already // lexChar scans a character constant. The initial quote is already
@ -567,8 +576,7 @@ Loop:
break Loop break Loop
} }
} }
l.emit(itemCharConstant) return l.emit(itemCharConstant)
return lexInsideAction
} }
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This // lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
@ -584,11 +592,9 @@ func lexNumber(l *lexer) stateFn {
if !l.scanNumber() || l.input[l.pos-1] != 'i' { if !l.scanNumber() || l.input[l.pos-1] != 'i' {
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos]) return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
} }
l.emit(itemComplex) return l.emit(itemComplex)
} else {
l.emit(itemNumber)
} }
return lexInsideAction return l.emit(itemNumber)
} }
func (l *lexer) scanNumber() bool { func (l *lexer) scanNumber() bool {
@ -644,8 +650,7 @@ Loop:
break Loop break Loop
} }
} }
l.emit(itemString) return l.emit(itemString)
return lexInsideAction
} }
// lexRawQuote scans a raw quoted string. // lexRawQuote scans a raw quoted string.
@ -659,8 +664,7 @@ Loop:
break Loop break Loop
} }
} }
l.emit(itemRawString) return l.emit(itemRawString)
return lexInsideAction
} }
// isSpace reports whether r is a space character. // isSpace reports whether r is a space character.

View file

@ -362,8 +362,7 @@ var lexTests = []lexTest{
{"extra right paren", "{{3)}}", []item{ {"extra right paren", "{{3)}}", []item{
tLeft, tLeft,
mkItem(itemNumber, "3"), mkItem(itemNumber, "3"),
tRpar, mkItem(itemError, "unexpected right paren"),
mkItem(itemError, `unexpected right paren U+0029 ')'`),
}}, }},
// Fixed bugs // Fixed bugs
@ -397,7 +396,12 @@ var lexTests = []lexTest{
// collect gathers the emitted items into a slice. // collect gathers the emitted items into a slice.
func collect(t *lexTest, left, right string) (items []item) { func collect(t *lexTest, left, right string) (items []item) {
l := lex(t.name, t.input, left, right, true) l := lex(t.name, t.input, left, right)
l.options = lexOptions{
emitComment: true,
breakOK: true,
continueOK: true,
}
for { for {
item := l.nextItem() item := l.nextItem()
items = append(items, item) items = append(items, item)
@ -434,7 +438,9 @@ func TestLex(t *testing.T) {
items := collect(&test, "", "") items := collect(&test, "", "")
if !equal(items, test.items, false) { if !equal(items, test.items, false) {
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items) t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
return // TODO
} }
t.Log(test.name, "OK")
} }
} }
@ -472,6 +478,39 @@ func TestDelims(t *testing.T) {
} }
} }
func TestDelimsAlphaNumeric(t *testing.T) {
test := lexTest{"right delimiter with alphanumeric start", "{{hub .host hub}}", []item{
mkItem(itemLeftDelim, "{{hub"),
mkItem(itemSpace, " "),
mkItem(itemField, ".host"),
mkItem(itemSpace, " "),
mkItem(itemRightDelim, "hub}}"),
tEOF,
}}
items := collect(&test, "{{hub", "hub}}")
if !equal(items, test.items, false) {
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
}
}
func TestDelimsAndMarkers(t *testing.T) {
test := lexTest{"delims that look like markers", "{{- .x -}} {{- - .x - -}}", []item{
mkItem(itemLeftDelim, "{{- "),
mkItem(itemField, ".x"),
mkItem(itemRightDelim, " -}}"),
mkItem(itemLeftDelim, "{{- "),
mkItem(itemField, ".x"),
mkItem(itemRightDelim, " -}}"),
tEOF,
}}
items := collect(&test, "{{- ", " -}}")
if !equal(items, test.items, false) {
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
}
}
var lexPosTests = []lexTest{ var lexPosTests = []lexTest{
{"empty", "", []item{{itemEOF, 0, "", 1}}}, {"empty", "", []item{{itemEOF, 0, "", 1}}},
{"punctuation", "{{,@%#}}", []item{ {"punctuation", "{{,@%#}}", []item{
@ -533,22 +572,6 @@ func TestPos(t *testing.T) {
} }
} }
// Test that an error shuts down the lexing goroutine.
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, "{{", "}}", false)
_, err := New("root").parseLexer(lexer)
if err == nil {
t.Fatalf("expected error")
}
// The error should have drained the input. Therefore, the lexer should be shut down.
token, ok := <-lexer.items
if ok {
t.Fatalf("input was not drained; got %v", token)
}
}
// parseLexer is a local version of parse that lets us pass in the lexer instead of building it. // parseLexer is a local version of parse that lets us pass in the lexer instead of building it.
// We expect an error, so the tree set and funcs list are explicitly nil. // We expect an error, so the tree set and funcs list are explicitly nil.
func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) { func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {

View file

@ -210,7 +210,6 @@ func (t *Tree) recover(errp *error) {
panic(e) panic(e)
} }
if t != nil { if t != nil {
t.lex.drain()
t.stopParse() t.stopParse()
} }
*errp = e.(error) *errp = e.(error)
@ -224,8 +223,6 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
t.vars = []string{"$"} t.vars = []string{"$"}
t.funcs = funcs t.funcs = funcs
t.treeSet = treeSet t.treeSet = treeSet
lex.breakOK = !t.hasFunction("break")
lex.continueOK = !t.hasFunction("continue")
} }
// stopParse terminates parsing. // stopParse terminates parsing.
@ -243,8 +240,13 @@ func (t *Tree) stopParse() {
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) { func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) {
defer t.recover(&err) defer t.recover(&err)
t.ParseName = t.Name t.ParseName = t.Name
emitComment := t.Mode&ParseComments != 0 lexer := lex(t.Name, text, leftDelim, rightDelim)
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim, emitComment), treeSet) lexer.options = lexOptions{
emitComment: t.Mode&ParseComments != 0,
breakOK: !t.hasFunction("break"),
continueOK: !t.hasFunction("continue"),
}
t.startParse(funcs, lexer, treeSet)
t.text = text t.text = text
t.parse() t.parse()
t.add() t.add()
@ -341,7 +343,9 @@ func (t *Tree) parseDefinition() {
} }
// itemList: // itemList:
//
// textOrAction* // textOrAction*
//
// Terminates at {{end}} or {{else}}, returned separately. // Terminates at {{end}} or {{else}}, returned separately.
func (t *Tree) itemList() (list *ListNode, next Node) { func (t *Tree) itemList() (list *ListNode, next Node) {
list = t.newList(t.peekNonSpace().pos) list = t.newList(t.peekNonSpace().pos)
@ -358,6 +362,7 @@ func (t *Tree) itemList() (list *ListNode, next Node) {
} }
// textOrAction: // textOrAction:
//
// text | comment | action // text | comment | action
func (t *Tree) textOrAction() Node { func (t *Tree) textOrAction() Node {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
@ -380,8 +385,10 @@ func (t *Tree) clearActionLine() {
} }
// Action: // Action:
//
// control // control
// command ("|" command)* // command ("|" command)*
//
// Left delim is past. Now get actions. // Left delim is past. Now get actions.
// First word could be a keyword such as range. // First word could be a keyword such as range.
func (t *Tree) action() (n Node) { func (t *Tree) action() (n Node) {
@ -412,7 +419,9 @@ func (t *Tree) action() (n Node) {
} }
// Break: // Break:
//
// {{break}} // {{break}}
//
// Break keyword is past. // Break keyword is past.
func (t *Tree) breakControl(pos Pos, line int) Node { func (t *Tree) breakControl(pos Pos, line int) Node {
if token := t.nextNonSpace(); token.typ != itemRightDelim { if token := t.nextNonSpace(); token.typ != itemRightDelim {
@ -425,7 +434,9 @@ func (t *Tree) breakControl(pos Pos, line int) Node {
} }
// Continue: // Continue:
//
// {{continue}} // {{continue}}
//
// Continue keyword is past. // Continue keyword is past.
func (t *Tree) continueControl(pos Pos, line int) Node { func (t *Tree) continueControl(pos Pos, line int) Node {
if token := t.nextNonSpace(); token.typ != itemRightDelim { if token := t.nextNonSpace(); token.typ != itemRightDelim {
@ -438,6 +449,7 @@ func (t *Tree) continueControl(pos Pos, line int) Node {
} }
// Pipeline: // Pipeline:
//
// declarations? command ('|' command)* // declarations? command ('|' command)*
func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) { func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
token := t.peekNonSpace() token := t.peekNonSpace()
@ -549,16 +561,20 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
} }
// If: // If:
//
// {{if pipeline}} itemList {{end}} // {{if pipeline}} itemList {{end}}
// {{if pipeline}} itemList {{else}} itemList {{end}} // {{if pipeline}} itemList {{else}} itemList {{end}}
//
// If keyword is past. // If keyword is past.
func (t *Tree) ifControl() Node { func (t *Tree) ifControl() Node {
return t.newIf(t.parseControl(true, "if")) return t.newIf(t.parseControl(true, "if"))
} }
// Range: // Range:
//
// {{range pipeline}} itemList {{end}} // {{range pipeline}} itemList {{end}}
// {{range pipeline}} itemList {{else}} itemList {{end}} // {{range pipeline}} itemList {{else}} itemList {{end}}
//
// Range keyword is past. // Range keyword is past.
func (t *Tree) rangeControl() Node { func (t *Tree) rangeControl() Node {
r := t.newRange(t.parseControl(false, "range")) r := t.newRange(t.parseControl(false, "range"))
@ -566,22 +582,28 @@ func (t *Tree) rangeControl() Node {
} }
// With: // With:
//
// {{with pipeline}} itemList {{end}} // {{with pipeline}} itemList {{end}}
// {{with pipeline}} itemList {{else}} itemList {{end}} // {{with pipeline}} itemList {{else}} itemList {{end}}
//
// If keyword is past. // If keyword is past.
func (t *Tree) withControl() Node { func (t *Tree) withControl() Node {
return t.newWith(t.parseControl(false, "with")) return t.newWith(t.parseControl(false, "with"))
} }
// End: // End:
//
// {{end}} // {{end}}
//
// End keyword is past. // End keyword is past.
func (t *Tree) endControl() Node { func (t *Tree) endControl() Node {
return t.newEnd(t.expect(itemRightDelim, "end").pos) return t.newEnd(t.expect(itemRightDelim, "end").pos)
} }
// Else: // Else:
//
// {{else}} // {{else}}
//
// Else keyword is past. // Else keyword is past.
func (t *Tree) elseControl() Node { func (t *Tree) elseControl() Node {
// Special case for "else if". // Special case for "else if".
@ -595,7 +617,9 @@ func (t *Tree) elseControl() Node {
} }
// Block: // Block:
//
// {{block stringValue pipeline}} // {{block stringValue pipeline}}
//
// Block keyword is past. // Block keyword is past.
// The name must be something that can evaluate to a string. // The name must be something that can evaluate to a string.
// The pipeline is mandatory. // The pipeline is mandatory.
@ -623,7 +647,9 @@ func (t *Tree) blockControl() Node {
} }
// Template: // Template:
//
// {{template stringValue pipeline}} // {{template stringValue pipeline}}
//
// Template keyword is past. The name must be something that can evaluate // Template keyword is past. The name must be something that can evaluate
// to a string. // to a string.
func (t *Tree) templateControl() Node { func (t *Tree) templateControl() Node {
@ -654,7 +680,9 @@ func (t *Tree) parseTemplateName(token item, context string) (name string) {
} }
// command: // command:
//
// operand (space operand)* // operand (space operand)*
//
// space-separated arguments up to a pipeline character or right delimiter. // space-separated arguments up to a pipeline character or right delimiter.
// we consume the pipe character but leave the right delim to terminate the action. // we consume the pipe character but leave the right delim to terminate the action.
func (t *Tree) command() *CommandNode { func (t *Tree) command() *CommandNode {
@ -684,7 +712,9 @@ func (t *Tree) command() *CommandNode {
} }
// operand: // operand:
//
// term .Field* // term .Field*
//
// An operand is a space-separated component of a command, // An operand is a space-separated component of a command,
// a term possibly followed by field accesses. // a term possibly followed by field accesses.
// A nil return means the next item is not an operand. // A nil return means the next item is not an operand.
@ -718,12 +748,14 @@ func (t *Tree) operand() Node {
} }
// term: // term:
//
// literal (number, string, nil, boolean) // literal (number, string, nil, boolean)
// function (identifier) // function (identifier)
// . // .
// .Field // .Field
// $ // $
// '(' pipeline ')' // '(' pipeline ')'
//
// A term is a simple "expression". // A term is a simple "expression".
// A nil return means the next item is not a term. // A nil return means the next item is not a term.
func (t *Tree) term() Node { func (t *Tree) term() Node {

View file

@ -492,7 +492,7 @@ var errorTests = []parseTest{
hasError, `unclosed left paren`}, hasError, `unclosed left paren`},
{"rparen", {"rparen",
"{{.X 1 2 3 ) }}", "{{.X 1 2 3 ) }}",
hasError, `unexpected ")" in command`}, hasError, "unexpected right paren"},
{"rparen2", {"rparen2",
"{{(.X 1 2 3", "{{(.X 1 2 3",
hasError, `unclosed action`}, hasError, `unclosed action`},
@ -600,7 +600,8 @@ func TestBlock(t *testing.T) {
} }
func TestLineNum(t *testing.T) { func TestLineNum(t *testing.T) {
const count = 100 // const count = 100
const count = 3
text := strings.Repeat("{{printf 1234}}\n", count) text := strings.Repeat("{{printf 1234}}\n", count)
tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins) tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
if err != nil { if err != nil {
@ -614,11 +615,11 @@ func TestLineNum(t *testing.T) {
// Action first. // Action first.
action := nodes[i].(*ActionNode) action := nodes[i].(*ActionNode)
if action.Line != line { if action.Line != line {
t.Fatalf("line %d: action is line %d", line, action.Line) t.Errorf("line %d: action is line %d", line, action.Line)
} }
pipe := action.Pipe pipe := action.Pipe
if pipe.Line != line { if pipe.Line != line {
t.Fatalf("line %d: pipe is line %d", line, pipe.Line) t.Errorf("line %d: pipe is line %d", line, pipe.Line)
} }
} }
} }