mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
parent
58a98c7758
commit
f6ab9553f4
34 changed files with 739 additions and 514 deletions
|
@ -17,7 +17,7 @@ import (
|
|||
)
|
||||
|
||||
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 ...")
|
||||
defer fmt.Println("Done ...")
|
||||
|
||||
|
|
|
@ -36,19 +36,18 @@ func (o *SortedMap) Swap(i, j int) {
|
|||
//
|
||||
// The ordering rules are more general than with Go's < operator:
|
||||
//
|
||||
// - when applicable, nil compares low
|
||||
// - ints, floats, and strings order by <
|
||||
// - NaN compares less than non-NaN floats
|
||||
// - bool compares false before true
|
||||
// - complex compares real, then imag
|
||||
// - pointers compare by machine address
|
||||
// - channel values compare by machine address
|
||||
// - structs compare each field in turn
|
||||
// - arrays compare each element in turn.
|
||||
// Otherwise identical arrays compare by length.
|
||||
// - interface values compare first by reflect.Type describing the concrete type
|
||||
// and then by concrete value as described in the previous rules.
|
||||
//
|
||||
// - when applicable, nil compares low
|
||||
// - ints, floats, and strings order by <
|
||||
// - NaN compares less than non-NaN floats
|
||||
// - bool compares false before true
|
||||
// - complex compares real, then imag
|
||||
// - pointers compare by machine address
|
||||
// - channel values compare by machine address
|
||||
// - structs compare each field in turn
|
||||
// - arrays compare each element in turn.
|
||||
// Otherwise identical arrays compare by length.
|
||||
// - interface values compare first by reflect.Type describing the concrete type
|
||||
// and then by concrete value as described in the previous rules.
|
||||
func Sort(mapValue reflect.Value) *SortedMap {
|
||||
if mapValue.Type().Kind() != reflect.Map {
|
||||
return nil
|
||||
|
|
|
@ -147,7 +147,7 @@ func sprint(data any) string {
|
|||
}
|
||||
b.WriteString(sprintKey(key))
|
||||
b.WriteRune(':')
|
||||
b.WriteString(fmt.Sprint(om.Value[i]))
|
||||
fmt.Fprint(b, om.Value[i])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -26,7 +25,7 @@ func TestAddParseTreeHTML(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
added := Must(root.AddParseTree("b", tree["b"]))
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
err = added.ExecuteTemplate(b, "a", "1>0")
|
||||
if err != nil {
|
||||
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 t3 template, it will be in a CSS context.
|
||||
const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}`
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
|
||||
// Create an incomplete template t0.
|
||||
t0 := Must(New("t0").Parse(tmpl))
|
||||
|
|
|
@ -284,7 +284,7 @@ func TestTypedContent(t *testing.T) {
|
|||
[]string{
|
||||
`#ZgotmplZ`,
|
||||
`#ZgotmplZ`,
|
||||
// Commas are not esacped
|
||||
// Commas are not escaped.
|
||||
`Hello,#ZgotmplZ`,
|
||||
// Leading spaces are not percent escapes.
|
||||
` dir=%22ltr%22`,
|
||||
|
@ -389,7 +389,7 @@ func TestTypedContent(t *testing.T) {
|
|||
tmpl := Must(New("x").Parse(test.input))
|
||||
pre := strings.Index(test.input, "{{.}}")
|
||||
post := len(test.input) - (pre + 5)
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
for i, x := range data {
|
||||
b.Reset()
|
||||
if err := tmpl.Execute(&b, x); err != nil {
|
||||
|
@ -423,7 +423,7 @@ func (s *errorer) Error() string {
|
|||
|
||||
func TestStringer(t *testing.T) {
|
||||
s := &myStringer{3}
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
tmpl := Must(New("x").Parse("{{.}}"))
|
||||
if err := tmpl.Execute(b, s); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -80,7 +80,9 @@ func (c context) mangle(templateName string) string {
|
|||
// HTML5 parsing algorithm because a single token production in the HTML
|
||||
// grammar may contain embedded actions in a template. For instance, the quoted
|
||||
// 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.
|
||||
type state uint8
|
||||
|
||||
|
|
|
@ -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
|
||||
documentation for text/template.
|
||||
|
||||
Introduction
|
||||
# Introduction
|
||||
|
||||
This package wraps package text/template so you can share its template API
|
||||
to parse and execute HTML templates safely.
|
||||
|
||||
tmpl, err := template.New("name").Parse(...)
|
||||
// Error checking elided
|
||||
err = tmpl.Execute(out, data)
|
||||
tmpl, err := template.New("name").Parse(...)
|
||||
// Error checking elided
|
||||
err = tmpl.Execute(out, data)
|
||||
|
||||
If successful, tmpl will now be injection-safe. Otherwise, err is an error
|
||||
defined in the docs for ErrorCode.
|
||||
|
@ -34,38 +34,37 @@ provided below.
|
|||
|
||||
Example
|
||||
|
||||
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
produces
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
but the contextual autoescaping in html/template
|
||||
|
||||
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
import template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
|
||||
|
||||
produces safe, escaped HTML output
|
||||
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
|
||||
Contexts
|
||||
# Contexts
|
||||
|
||||
This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
|
||||
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.
|
||||
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
|
||||
functions.
|
||||
|
@ -73,117 +72,113 @@ functions.
|
|||
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.
|
||||
|
||||
Namespaced and data- attributes
|
||||
# Namespaced and data- attributes
|
||||
|
||||
Attributes with a namespace are treated as if they had no namespace.
|
||||
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".
|
||||
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
|
||||
treated as if they had no "data-" prefix. So given
|
||||
|
||||
<a data-href="{{.}}"></a>
|
||||
<a data-href="{{.}}"></a>
|
||||
|
||||
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
|
||||
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
|
||||
it would be if the "data-" prefix were to be ignored too. Thus at parse
|
||||
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 containing URLs. Given the excerpts
|
||||
|
||||
<a xmlns:title="{{.}}"></a>
|
||||
<a xmlns:href="{{.}}"></a>
|
||||
<a xmlns:onclick="{{.}}"></a>
|
||||
<a xmlns:title="{{.}}"></a>
|
||||
<a xmlns:href="{{.}}"></a>
|
||||
<a xmlns:onclick="{{.}}"></a>
|
||||
|
||||
At parse time they become:
|
||||
|
||||
<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
|
||||
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
|
||||
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
|
||||
<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
|
||||
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
|
||||
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
|
||||
|
||||
Errors
|
||||
# Errors
|
||||
|
||||
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
|
||||
details necessary to understand escaping contexts and error messages. Most users
|
||||
will not need to understand these details.
|
||||
|
||||
|
||||
Contexts
|
||||
# Contexts
|
||||
|
||||
Assuming {{.}} is `O'Reilly: How are <i>you</i>?`, the table below shows
|
||||
how {{.}} appears when used in the context to the left.
|
||||
|
||||
Context {{.}} After
|
||||
{{.}} O'Reilly: How are <i>you</i>?
|
||||
<a title='{{.}}'> O'Reilly: How are you?
|
||||
<a href="/{{.}}"> O'Reilly: How are %3ci%3eyou%3c/i%3e?
|
||||
<a href="?q={{.}}"> O'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='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f
|
||||
Context {{.}} After
|
||||
{{.}} O'Reilly: How are <i>you</i>?
|
||||
<a title='{{.}}'> O'Reilly: How are you?
|
||||
<a href="/{{.}}"> O'Reilly: How are %3ci%3eyou%3c/i%3e?
|
||||
<a href="?q={{.}}"> O'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='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f
|
||||
|
||||
If used in an unsafe context, then the value might be filtered out:
|
||||
|
||||
Context {{.}} After
|
||||
<a href="{{.}}"> #ZgotmplZ
|
||||
Context {{.}} After
|
||||
<a href="{{.}}"> #ZgotmplZ
|
||||
|
||||
since "O'Reilly:" is not an allowed protocol like "http:".
|
||||
|
||||
|
||||
If {{.}} is the innocuous word, `left`, then it can appear more widely,
|
||||
|
||||
Context {{.}} After
|
||||
{{.}} left
|
||||
<a title='{{.}}'> left
|
||||
<a href='{{.}}'> left
|
||||
<a href='/{{.}}'> left
|
||||
<a href='?dir={{.}}'> left
|
||||
<a style="border-{{.}}: 4px"> left
|
||||
<a style="align: {{.}}"> left
|
||||
<a style="background: '{{.}}'> left
|
||||
<a style="background: url('{{.}}')> left
|
||||
<style>p.{{.}} {color:red}</style> left
|
||||
Context {{.}} After
|
||||
{{.}} left
|
||||
<a title='{{.}}'> left
|
||||
<a href='{{.}}'> left
|
||||
<a href='/{{.}}'> left
|
||||
<a href='?dir={{.}}'> left
|
||||
<a style="border-{{.}}: 4px"> left
|
||||
<a style="align: {{.}}"> left
|
||||
<a style="background: '{{.}}'> left
|
||||
<a style="background: url('{{.}}')> left
|
||||
<style>p.{{.}} {color:red}</style> left
|
||||
|
||||
Non-string values can be used in JavaScript contexts.
|
||||
If {{.}} is
|
||||
|
||||
struct{A,B string}{ "foo", "bar" }
|
||||
struct{A,B string}{ "foo", "bar" }
|
||||
|
||||
in the escaped template
|
||||
|
||||
<script>var pair = {{.}};</script>
|
||||
<script>var pair = {{.}};</script>
|
||||
|
||||
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
|
||||
embedding in JavaScript contexts.
|
||||
|
||||
|
||||
Typed Strings
|
||||
# Typed Strings
|
||||
|
||||
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
|
||||
|
@ -197,24 +192,23 @@ exempted from escaping.
|
|||
|
||||
The template
|
||||
|
||||
Hello, {{.}}!
|
||||
Hello, {{.}}!
|
||||
|
||||
can be invoked with
|
||||
|
||||
tmpl.Execute(out, template.HTML(`<b>World</b>`))
|
||||
tmpl.Execute(out, template.HTML(`<b>World</b>`))
|
||||
|
||||
to produce
|
||||
|
||||
Hello, <b>World</b>!
|
||||
Hello, <b>World</b>!
|
||||
|
||||
instead of the
|
||||
|
||||
Hello, <b>World<b>!
|
||||
Hello, <b>World<b>!
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -33,14 +33,17 @@ type ErrorCode int
|
|||
//
|
||||
// Output: "ZgotmplZ"
|
||||
// Example:
|
||||
// <img src="{{.X}}">
|
||||
// where {{.X}} evaluates to `javascript:...`
|
||||
//
|
||||
// <img src="{{.X}}">
|
||||
// where {{.X}} evaluates to `javascript:...`
|
||||
//
|
||||
// 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
|
||||
// <img src="#ZgotmplZ">
|
||||
// If the data comes from a trusted source, use content types to exempt it
|
||||
// from filtering: URL(`javascript:...`).
|
||||
//
|
||||
// "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
|
||||
// <img src="#ZgotmplZ">
|
||||
// If the data comes from a trusted source, use content types to exempt it
|
||||
// from filtering: URL(`javascript:...`).
|
||||
const (
|
||||
// OK indicates the lack of an error.
|
||||
OK ErrorCode = iota
|
||||
|
|
|
@ -412,13 +412,19 @@ func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode {
|
|||
// nudge returns the context that would result from following empty string
|
||||
// transitions from the input context.
|
||||
// For example, parsing:
|
||||
// `<a href=`
|
||||
//
|
||||
// `<a href=`
|
||||
//
|
||||
// will end in context{stateBeforeValue, attrURL}, but parsing one extra rune:
|
||||
// `<a href=x`
|
||||
//
|
||||
// `<a href=x`
|
||||
//
|
||||
// will end in context{stateURL, delimSpaceOrTagEnd, ...}.
|
||||
// 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
|
||||
// consuming any character.
|
||||
//
|
||||
// consuming any character.
|
||||
//
|
||||
// (2) Consume 'x' and transition past the first value character.
|
||||
// In this case, nudging produces the context after (1) happens.
|
||||
func nudge(c context) context {
|
||||
|
|
|
@ -693,7 +693,7 @@ func TestEscape(t *testing.T) {
|
|||
t.Errorf("%s: tree not set properly", test.name)
|
||||
continue
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
if err := tmpl.Execute(b, data); err != nil {
|
||||
t.Errorf("%s: template execution failed: %s", test.name, err)
|
||||
continue
|
||||
|
@ -740,7 +740,7 @@ func TestEscapeMap(t *testing.T) {
|
|||
},
|
||||
} {
|
||||
tmpl := Must(New("").Parse(test.input))
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
if err := tmpl.Execute(b, data); err != nil {
|
||||
t.Errorf("%s: template execution failed: %s", test.desc, err)
|
||||
continue
|
||||
|
@ -882,7 +882,7 @@ func TestEscapeSet(t *testing.T) {
|
|||
t.Errorf("error parsing %q: %v", source, err)
|
||||
continue
|
||||
}
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
|
||||
if err := tmpl.ExecuteTemplate(&b, "main", data); err != nil {
|
||||
t.Errorf("%q executing %v", err.Error(), tmpl.Lookup("main"))
|
||||
|
@ -1833,7 +1833,7 @@ func TestIndirectPrint(t *testing.T) {
|
|||
bp := &b
|
||||
bpp := &bp
|
||||
tmpl := Must(New("t").Parse(`{{.}}`))
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err := tmpl.Execute(&buf, ap)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
|
@ -1876,7 +1876,7 @@ func TestPipeToMethodIsEscaped(t *testing.T) {
|
|||
t.Errorf("panicked: %v\n", panicValue)
|
||||
}
|
||||
}()
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
tmpl.Execute(&b, Issue7379(0))
|
||||
return b.String()
|
||||
}
|
||||
|
@ -1909,7 +1909,7 @@ func TestIdempotentExecute(t *testing.T) {
|
|||
Parse(`{{define "main"}}<body>{{template "hello"}}</body>{{end}}`))
|
||||
Must(tmpl.
|
||||
Parse(`{{define "hello"}}Hello, {{"Ladies & Gentlemen!"}}{{end}}`))
|
||||
got := new(bytes.Buffer)
|
||||
got := new(strings.Builder)
|
||||
var err error
|
||||
// Ensure that "hello" produces the same output when executed twice.
|
||||
want := "Hello, Ladies & Gentlemen!"
|
||||
|
@ -1952,7 +1952,7 @@ func TestOrphanedTemplate(t *testing.T) {
|
|||
t1 := Must(New("foo").Parse(`<a href="{{.}}">link1</a>`))
|
||||
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`
|
||||
if err := t1.Execute(&b, "javascript:alert(1)"); err == nil {
|
||||
t.Fatal("expected error executing t1")
|
||||
|
@ -1981,7 +1981,7 @@ func TestAliasedParseTreeDoesNotOverescape(t *testing.T) {
|
|||
if _, err := tpl.AddParseTree("bar", tpl.Tree); err != nil {
|
||||
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 {
|
||||
t.Fatalf(`ExecuteTemplate failed for "foo": %v`, err)
|
||||
}
|
||||
|
|
|
@ -769,7 +769,7 @@ func mapOfThree() any {
|
|||
}
|
||||
|
||||
func testExecute(execTests []execTest, template *Template, t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
funcs := FuncMap{
|
||||
"add": add,
|
||||
"count": count,
|
||||
|
@ -861,7 +861,7 @@ func TestDelims(t *testing.T) {
|
|||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatalf("delim %q exec err %s", left, err)
|
||||
|
@ -1002,7 +1002,7 @@ func TestTree(t *testing.T) {
|
|||
if err != nil {
|
||||
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]]]]"
|
||||
// First by looking up the template.
|
||||
err = tmpl.Lookup("tree").Execute(&b, tree)
|
||||
|
@ -1196,33 +1196,39 @@ var cmpTests = []cmpTest{
|
|||
{"eq .Iface1 .Iface1", "true", true},
|
||||
{"eq .Iface1 .Iface2", "false", 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
|
||||
{"eq `xy` 1", "", false}, // Different types.
|
||||
{"eq 2 2.0", "", false}, // Different types.
|
||||
{"lt true true", "", false}, // Unordered types.
|
||||
{"lt 1+0i 1+0i", "", false}, // Unordered types.
|
||||
{"eq .Ptr 1", "", false}, // Incompatible types.
|
||||
{"eq .Ptr .NegOne", "", false}, // Incompatible types.
|
||||
{"eq .Map .Map", "", false}, // Uncomparable types.
|
||||
{"eq .Map .V1", "", false}, // Uncomparable types.
|
||||
{"eq `xy` 1", "", false}, // Different types.
|
||||
{"eq 2 2.0", "", false}, // Different types.
|
||||
{"lt true true", "", false}, // Unordered types.
|
||||
{"lt 1+0i 1+0i", "", false}, // Unordered types.
|
||||
{"eq .Ptr 1", "", false}, // Incompatible types.
|
||||
{"eq .Ptr .NegOne", "", false}, // Incompatible types.
|
||||
{"eq .Map .V1", "", false}, // Uncomparable types.
|
||||
{"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types.
|
||||
}
|
||||
|
||||
func TestComparison(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
var cmpStruct = struct {
|
||||
Uthree, Ufour uint
|
||||
NegOne, Three int
|
||||
Ptr, NilPtr *int
|
||||
NonNilMap map[int]int
|
||||
Map map[int]int
|
||||
V1, V2 V
|
||||
Iface1, Iface2 fmt.Stringer
|
||||
}{
|
||||
Uthree: 3,
|
||||
Ufour: 4,
|
||||
NegOne: -1,
|
||||
Three: 3,
|
||||
Ptr: new(int),
|
||||
Iface1: b,
|
||||
Uthree: 3,
|
||||
Ufour: 4,
|
||||
NegOne: -1,
|
||||
Three: 3,
|
||||
Ptr: new(int),
|
||||
NonNilMap: make(map[int]int),
|
||||
Iface1: b,
|
||||
}
|
||||
for _, test := range cmpTests {
|
||||
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
|
||||
|
@ -1254,7 +1260,7 @@ func TestMissingMapKey(t *testing.T) {
|
|||
if err != nil {
|
||||
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
|
||||
err = tmpl.Execute(&b, data)
|
||||
if err != nil {
|
||||
|
@ -1423,7 +1429,7 @@ func TestBlock(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, "hello"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -1529,7 +1535,7 @@ func TestAddrOfIndex(t *testing.T) {
|
|||
}
|
||||
for _, text := range texts {
|
||||
tmpl := Must(New("tmpl").Parse(text))
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}}))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Execute: %v", text, err)
|
||||
|
@ -1585,7 +1591,7 @@ func TestInterfaceValues(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
tmpl := Must(New("tmpl").Parse(tt.text))
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err := tmpl.Execute(&buf, map[string]any{
|
||||
"PlusOne": func(n int) int {
|
||||
return n + 1
|
||||
|
@ -1680,7 +1686,7 @@ func TestIssue31810(t *testing.T) {
|
|||
t.Skip("broken in html/template")
|
||||
|
||||
// A simple value with no arguments is fine.
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
const text = "{{ (.) }}"
|
||||
tmpl, err := New("").Parse(text)
|
||||
if err != nil {
|
||||
|
|
|
@ -84,10 +84,12 @@ var htmlNormReplacementTable = []string{
|
|||
// <script>(function () {
|
||||
// var a = [], d = document.getElementById("d"), i, c, s;
|
||||
// for (i = 0; i < 0x10000; ++i) {
|
||||
// c = String.fromCharCode(i);
|
||||
// d.innerHTML = "<span title=" + c + "lt" + c + "></span>"
|
||||
// s = d.getElementsByTagName("SPAN")[0];
|
||||
// if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); }
|
||||
//
|
||||
// c = String.fromCharCode(i);
|
||||
// d.innerHTML = "<span title=" + c + "lt" + c + "></span>"
|
||||
// s = d.getElementsByTagName("SPAN")[0];
|
||||
// if (!s || s.title !== c + "lt" + c) { a.push(i.toString(16)); }
|
||||
//
|
||||
// }
|
||||
// document.write(a.join(", "));
|
||||
// })()</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.
|
||||
// For example, `<b>¡Hi!</b> <script>...</script>` -> `¡Hi! `.
|
||||
func stripTags(html string) string {
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
s, c, i, allText := []byte(html), context{}, 0, true
|
||||
// Using the transition funcs helps us avoid mangling
|
||||
// `<div title="1>2">` or `I <3 Ponies!`.
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -324,7 +323,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
|
|||
|
||||
// Escape it rune by rune to make sure that any
|
||||
// fast-path checking does not break escaping.
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
for _, c := range input {
|
||||
buf.WriteString(test.escaper(string(c)))
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ package template
|
|||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
|
@ -249,7 +249,7 @@ func TestEmptyTemplate(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
buf := &strings.Builder{}
|
||||
if err := m.Execute(buf, c.in); err != nil {
|
||||
t.Error(i, err)
|
||||
continue
|
||||
|
@ -284,7 +284,7 @@ func TestIssue19294(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
res.Execute(&buf, 0)
|
||||
if buf.String() != "stylesheet" {
|
||||
t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")
|
||||
|
|
|
@ -65,6 +65,7 @@ func (t *Template) Templates() []*Template {
|
|||
//
|
||||
// missingkey: Control the behavior during execution if a map is
|
||||
// indexed with a key that is not present in the map.
|
||||
//
|
||||
// "missingkey=default" or "missingkey=invalid"
|
||||
// The default behavior: Do nothing and continue execution.
|
||||
// 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.
|
||||
// "missingkey=error"
|
||||
// Execution stops immediately with an error.
|
||||
//
|
||||
func (t *Template) Option(opt ...string) *Template {
|
||||
t.text.Option(opt...)
|
||||
return t
|
||||
|
@ -329,14 +329,7 @@ func (t *Template) Name() string {
|
|||
return t.text.Name()
|
||||
}
|
||||
|
||||
// FuncMap is the type of the map defining the mapping from names to
|
||||
// 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
|
||||
type FuncMap = template.FuncMap
|
||||
|
||||
// Funcs adds the elements of the argument map to the template's function map.
|
||||
// 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)
|
||||
// and panics if the error is non-nil. It is intended for use in variable initializations
|
||||
// such as
|
||||
//
|
||||
// var t = template.Must(template.New("name").Parse("html"))
|
||||
func Must(t *Template, err error) *Template {
|
||||
if err != nil {
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestTemplateClone(t *testing.T) {
|
|||
|
||||
const want = "stuff"
|
||||
parsed := Must(clone.Parse(want))
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err = parsed.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
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) {
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err := t.Execute(&buf, val)
|
||||
if err != nil {
|
||||
c.t.Fatalf("execute: %v", err)
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
@ -19,15 +18,15 @@ import (
|
|||
//
|
||||
// This filter conservatively assumes that all schemes other than the following
|
||||
// are unsafe:
|
||||
// * 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
|
||||
// previous website, or closing the window or tab. No irreversible
|
||||
// changes will take place without further user interaction with
|
||||
// the new website.
|
||||
// * https: Same as http.
|
||||
// * mailto: Opens an email program and starts a new draft. This side effect
|
||||
// is not irreversible until the user explicitly clicks send; it
|
||||
// can be undone by closing the email program.
|
||||
// - 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
|
||||
// previous website, or closing the window or tab. No irreversible
|
||||
// changes will take place without further user interaction with
|
||||
// the new website.
|
||||
// - https: Same as http.
|
||||
// - mailto: Opens an email program and starts a new draft. This side effect
|
||||
// is not irreversible until the user explicitly clicks send; it
|
||||
// can be undone by closing the email program.
|
||||
//
|
||||
// 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
|
||||
|
@ -76,7 +75,7 @@ func urlProcessor(norm bool, args ...any) string {
|
|||
if t == contentTypeURL {
|
||||
norm = true
|
||||
}
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
if processURLOnto(s, norm, &b) {
|
||||
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
|
||||
// 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)
|
||||
written := 0
|
||||
// The byte loop below assumes that all URLs use UTF-8 as the
|
||||
|
@ -149,7 +148,7 @@ func srcsetFilterAndEscaper(args ...any) string {
|
|||
case contentTypeURL:
|
||||
// Normalizing gets rid of all HTML whitespace
|
||||
// which separate the image URL from its metadata.
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
if processURLOnto(s, true, &b) {
|
||||
s = b.String()
|
||||
}
|
||||
|
@ -157,7 +156,7 @@ func srcsetFilterAndEscaper(args ...any) string {
|
|||
return strings.ReplaceAll(s, ",", "%2c")
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
written := 0
|
||||
for i := 0; i < len(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)))
|
||||
}
|
||||
|
||||
func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) {
|
||||
func filterSrcsetElement(s string, left int, right int, b *strings.Builder) {
|
||||
start := left
|
||||
for start < right && isHTMLSpace(s[start]) {
|
||||
start++
|
||||
|
|
73
tpl/internal/go_templates/testenv/exec.go
Normal file
73
tpl/internal/go_templates/testenv/exec.go
Normal 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
|
||||
}
|
12
tpl/internal/go_templates/testenv/noopt.go
Normal file
12
tpl/internal/go_templates/testenv/noopt.go
Normal 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
|
||||
}
|
12
tpl/internal/go_templates/testenv/opt.go
Normal file
12
tpl/internal/go_templates/testenv/opt.go
Normal 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
|
||||
}
|
|
@ -11,10 +11,12 @@
|
|||
package testenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -23,7 +25,6 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Builder reports the name of the builder running this test
|
||||
|
@ -34,7 +35,7 @@ func Builder() string {
|
|||
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.
|
||||
func HasGoBuild() bool {
|
||||
if os.Getenv("GO_GCFLAGS") != "" {
|
||||
|
@ -51,7 +52,7 @@ func HasGoBuild() bool {
|
|||
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.
|
||||
// If not, MustHaveGoBuild calls t.Skip with an explanation.
|
||||
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 {
|
||||
// For now, having go run and having go build are the same.
|
||||
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.
|
||||
func MustHaveGoRun(t testing.TB) {
|
||||
if !HasGoRun() {
|
||||
|
@ -96,6 +97,100 @@ func GoToolPath(t testing.TB) string {
|
|||
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.
|
||||
func GoTool() (string, error) {
|
||||
if !HasGoBuild() {
|
||||
|
@ -105,7 +200,11 @@ func GoTool() (string, error) {
|
|||
if runtime.GOOS == "windows" {
|
||||
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 {
|
||||
return path, nil
|
||||
}
|
||||
|
@ -116,16 +215,6 @@ func GoTool() (string, error) {
|
|||
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.
|
||||
func HasSrc() bool {
|
||||
switch runtime.GOOS {
|
||||
|
@ -135,33 +224,6 @@ func HasSrc() bool {
|
|||
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
|
||||
// external (non-localhost) networks.
|
||||
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.
|
||||
func HasSymlink() bool {
|
||||
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.
|
||||
func CPUIsSlow() bool {
|
||||
switch runtime.GOARCH {
|
||||
|
@ -309,58 +323,10 @@ func SkipIfShortAndSlow(t testing.TB) {
|
|||
}
|
||||
}
|
||||
|
||||
// RunWithTimeout runs cmd and returns its combined output. If the
|
||||
// subprocess exits with a non-zero status, it will log that status
|
||||
// and return a non-nil error, but this is not considered fatal.
|
||||
func RunWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) {
|
||||
args := cmd.Args
|
||||
if args == nil {
|
||||
args = []string{cmd.Path}
|
||||
// SkipIfOptimizationOff skips t if optimization is disabled.
|
||||
func SkipIfOptimizationOff(t testing.TB) {
|
||||
if OptimizationOff() {
|
||||
t.Helper()
|
||||
t.Skip("skipping test with optimization disabled")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
54
tpl/internal/go_templates/testenv/testenv_test.go
Normal file
54
tpl/internal/go_templates/testenv/testenv_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
//go:build unix
|
||||
|
||||
package testenv
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ structure as execution proceeds.
|
|||
The input text for a template is UTF-8-encoded text in any format.
|
||||
"Actions"--data evaluations or control structures--are delimited by
|
||||
"{{" 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
|
||||
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
|
||||
constant. Here is a simple example:
|
||||
|
||||
`{{define "T1"}}ONE{{end}}
|
||||
{{define "T1"}}ONE{{end}}
|
||||
{{define "T2"}}TWO{{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
|
||||
when it is executed. Finally it invokes T3. If executed this template will
|
||||
|
|
|
@ -94,6 +94,12 @@ type missingValType struct{}
|
|||
|
||||
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.
|
||||
func (s *state) at(node parse.Node) {
|
||||
s.node = node
|
||||
|
@ -357,11 +363,19 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
|||
oneIteration := func(index, elem reflect.Value) {
|
||||
// Set top var (lexically the second if there are two) to the element.
|
||||
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.
|
||||
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 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) {
|
||||
if len(args) > 1 || final != missingVal {
|
||||
if len(args) > 1 || !isMissing(final) {
|
||||
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() {
|
||||
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.
|
||||
switch receiver.Kind() {
|
||||
case reflect.Struct:
|
||||
|
@ -700,7 +714,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
|||
}
|
||||
typ := fun.Type()
|
||||
numIn := len(args)
|
||||
if final != missingVal {
|
||||
if !isMissing(final) {
|
||||
numIn++
|
||||
}
|
||||
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.
|
||||
if final != missingVal {
|
||||
if !isMissing(final) {
|
||||
t := typ.In(typ.NumIn() - 1)
|
||||
if typ.IsVariadic() {
|
||||
if numIn-1 < numFixed {
|
||||
|
|
|
@ -695,6 +695,8 @@ var execTests = []execTest{
|
|||
{"bug18a", "{{eq . '.'}}", "true", '.', true},
|
||||
{"bug18b", "{{eq . 'e'}}", "true", 'e', true},
|
||||
{"bug18c", "{{eq . 'P'}}", "true", 'P', true},
|
||||
|
||||
{"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true},
|
||||
}
|
||||
|
||||
func zeroArgs() string {
|
||||
|
@ -775,7 +777,7 @@ func mapOfThree() any {
|
|||
}
|
||||
|
||||
func testExecute(execTests []execTest, template *Template, t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
funcs := FuncMap{
|
||||
"add": add,
|
||||
"count": count,
|
||||
|
@ -864,7 +866,7 @@ func TestDelims(t *testing.T) {
|
|||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatalf("delim %q exec err %s", left, err)
|
||||
|
@ -1027,7 +1029,7 @@ func TestTree(t *testing.T) {
|
|||
if err != nil {
|
||||
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]]]]"
|
||||
// First by looking up the template.
|
||||
err = tmpl.Lookup("tree").Execute(&b, tree)
|
||||
|
@ -1223,33 +1225,39 @@ var cmpTests = []cmpTest{
|
|||
{"eq .NilIface .Iface1", "false", true},
|
||||
{"eq .NilIface 0", "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
|
||||
{"eq `xy` 1", "", false}, // Different types.
|
||||
{"eq 2 2.0", "", false}, // Different types.
|
||||
{"lt true true", "", false}, // Unordered types.
|
||||
{"lt 1+0i 1+0i", "", false}, // Unordered types.
|
||||
{"eq .Ptr 1", "", false}, // Incompatible types.
|
||||
{"eq .Ptr .NegOne", "", false}, // Incompatible types.
|
||||
{"eq .Map .Map", "", false}, // Uncomparable types.
|
||||
{"eq .Map .V1", "", false}, // Uncomparable types.
|
||||
{"eq `xy` 1", "", false}, // Different types.
|
||||
{"eq 2 2.0", "", false}, // Different types.
|
||||
{"lt true true", "", false}, // Unordered types.
|
||||
{"lt 1+0i 1+0i", "", false}, // Unordered types.
|
||||
{"eq .Ptr 1", "", false}, // Incompatible types.
|
||||
{"eq .Ptr .NegOne", "", false}, // Incompatible types.
|
||||
{"eq .Map .V1", "", false}, // Uncomparable types.
|
||||
{"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types.
|
||||
}
|
||||
|
||||
func TestComparison(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
b := new(strings.Builder)
|
||||
var cmpStruct = struct {
|
||||
Uthree, Ufour uint
|
||||
NegOne, Three int
|
||||
Ptr, NilPtr *int
|
||||
NonNilMap map[int]int
|
||||
Map map[int]int
|
||||
V1, V2 V
|
||||
Iface1, NilIface fmt.Stringer
|
||||
}{
|
||||
Uthree: 3,
|
||||
Ufour: 4,
|
||||
NegOne: -1,
|
||||
Three: 3,
|
||||
Ptr: new(int),
|
||||
Iface1: b,
|
||||
Uthree: 3,
|
||||
Ufour: 4,
|
||||
NegOne: -1,
|
||||
Three: 3,
|
||||
Ptr: new(int),
|
||||
NonNilMap: make(map[int]int),
|
||||
Iface1: b,
|
||||
}
|
||||
for _, test := range cmpTests {
|
||||
text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr)
|
||||
|
@ -1281,7 +1289,7 @@ func TestMissingMapKey(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
// By default, just get "<no value>"
|
||||
err = tmpl.Execute(&b, data)
|
||||
if err != nil {
|
||||
|
@ -1451,7 +1459,7 @@ func TestBlock(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, "hello"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -1557,7 +1565,7 @@ func TestAddrOfIndex(t *testing.T) {
|
|||
}
|
||||
for _, text := range texts {
|
||||
tmpl := Must(New("tmpl").Parse(text))
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}}))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Execute: %v", text, err)
|
||||
|
@ -1613,7 +1621,7 @@ func TestInterfaceValues(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
tmpl := Must(New("tmpl").Parse(tt.text))
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
err := tmpl.Execute(&buf, map[string]any{
|
||||
"PlusOne": func(n int) int {
|
||||
return n + 1
|
||||
|
@ -1706,7 +1714,7 @@ func TestExecutePanicDuringCall(t *testing.T) {
|
|||
// Issue 31810. Check that a parenthesized first argument behaves properly.
|
||||
func TestIssue31810(t *testing.T) {
|
||||
// A simple value with no arguments is fine.
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
const text = "{{ (.) }}"
|
||||
tmpl, err := New("").Parse(text)
|
||||
if err != nil {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -436,14 +435,33 @@ func basicKind(v reflect.Value) (kind, error) {
|
|||
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 || ...
|
||||
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
|
||||
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 {
|
||||
return false, errNoComparison
|
||||
}
|
||||
|
@ -479,11 +497,14 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
|
|||
case uintKind:
|
||||
truth = arg1.Uint() == arg.Uint()
|
||||
default:
|
||||
if arg == zero || arg1 == zero {
|
||||
truth = arg1 == arg
|
||||
if !canCompare(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 {
|
||||
if t2 := arg.Type(); !t2.Comparable() {
|
||||
return false, fmt.Errorf("uncomparable type %s: %v", t2, arg)
|
||||
if !arg.Type().Comparable() {
|
||||
return false, fmt.Errorf("non-comparable type %s: %v", arg, arg.Type())
|
||||
}
|
||||
truth = arg1.Interface() == arg.Interface()
|
||||
}
|
||||
|
@ -620,7 +641,7 @@ func HTMLEscapeString(s string) string {
|
|||
if !strings.ContainsAny(s, "'\"&<>\000") {
|
||||
return s
|
||||
}
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
HTMLEscape(&b, []byte(s))
|
||||
return b.String()
|
||||
}
|
||||
|
@ -703,7 +724,7 @@ func JSEscapeString(s string) string {
|
|||
if strings.IndexFunc(s, jsIsSpecial) < 0 {
|
||||
return s
|
||||
}
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
JSEscape(&b, []byte(s))
|
||||
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
|
||||
//
|
||||
// fmt.Sprint(args...)
|
||||
//
|
||||
// except that each argument is indirected (if a pointer), as required,
|
||||
// using the same rules as the default string evaluation during template
|
||||
// execution.
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
// 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 such as
|
||||
//
|
||||
// var t = template.Must(template.New("name").Parse("text"))
|
||||
func Must(t *Template, err error) *Template {
|
||||
if err != nil {
|
||||
|
|
|
@ -10,10 +10,10 @@ package template
|
|||
// Tests for multiple-template parsing and execution.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -245,7 +245,7 @@ func TestClone(t *testing.T) {
|
|||
}
|
||||
}
|
||||
// Execute root.
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
err = root.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -284,7 +284,7 @@ func TestAddParseTree(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
// Execute.
|
||||
var b bytes.Buffer
|
||||
var b strings.Builder
|
||||
err = added.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -413,7 +413,7 @@ func TestEmptyTemplate(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
buf := &strings.Builder{}
|
||||
if err := m.Execute(buf, c.in); err != nil {
|
||||
t.Error(i, err)
|
||||
continue
|
||||
|
@ -448,7 +448,7 @@ func TestIssue19294(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
res.Execute(&buf, 0)
|
||||
if buf.String() != "stylesheet" {
|
||||
t.Fatalf("iteration %d: got %q; expected %q", i, buf.String(), "stylesheet")
|
||||
|
|
|
@ -30,6 +30,7 @@ type option struct {
|
|||
//
|
||||
// missingkey: Control the behavior during execution if a map is
|
||||
// indexed with a key that is not present in the map.
|
||||
//
|
||||
// "missingkey=default" or "missingkey=invalid"
|
||||
// The default behavior: Do nothing and continue execution.
|
||||
// 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.
|
||||
// "missingkey=error"
|
||||
// Execution stops immediately with an error.
|
||||
//
|
||||
func (t *Template) Option(opt ...string) *Template {
|
||||
t.init()
|
||||
for _, s := range opt {
|
||||
|
|
|
@ -111,31 +111,36 @@ type stateFn func(*lexer) stateFn
|
|||
|
||||
// lexer holds the state of the scanner.
|
||||
type lexer struct {
|
||||
name string // the name of the input; used only for error reports
|
||||
input string // the string being scanned
|
||||
leftDelim string // start of action
|
||||
rightDelim string // end of action
|
||||
emitComment bool // emit itemComment tokens.
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
width Pos // width of last rune read from input
|
||||
items chan item // channel of scanned items
|
||||
parenDepth int // nesting depth of ( ) exprs
|
||||
line int // 1+number of newlines seen
|
||||
startLine int // start line of this item
|
||||
breakOK bool // break keyword allowed
|
||||
continueOK bool // continue keyword allowed
|
||||
name string // the name of the input; used only for error reports
|
||||
input string // the string being scanned
|
||||
leftDelim string // start of action marker
|
||||
rightDelim string // end of action marker
|
||||
pos Pos // current position in the input
|
||||
start Pos // start position of this item
|
||||
atEOF bool // we have hit the end of input and returned eof
|
||||
parenDepth int // nesting depth of ( ) exprs
|
||||
line int // 1+number of newlines seen
|
||||
startLine int // start line of this item
|
||||
item item // item to return to parser
|
||||
insideAction bool // are we inside an action?
|
||||
options lexOptions
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (l *lexer) next() rune {
|
||||
if int(l.pos) >= len(l.input) {
|
||||
l.width = 0
|
||||
l.atEOF = true
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = Pos(w)
|
||||
l.pos += l.width
|
||||
l.pos += Pos(w)
|
||||
if r == '\n' {
|
||||
l.line++
|
||||
}
|
||||
|
@ -149,23 +154,41 @@ func (l *lexer) peek() rune {
|
|||
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() {
|
||||
l.pos -= l.width
|
||||
// Correct newline count.
|
||||
if l.width == 1 && l.input[l.pos] == '\n' {
|
||||
l.line--
|
||||
if !l.atEOF && l.pos > 0 {
|
||||
r, w := utf8.DecodeLastRuneInString(l.input[:l.pos])
|
||||
l.pos -= Pos(w)
|
||||
// Correct newline count.
|
||||
if r == '\n' {
|
||||
l.line--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// emit passes an item back to the client.
|
||||
func (l *lexer) emit(t itemType) {
|
||||
l.items <- item{t, l.start, l.input[l.start:l.pos], l.startLine}
|
||||
// thisItem returns the item at the current input point with the specified type
|
||||
// and advances the input.
|
||||
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.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.
|
||||
// It tracks newlines in the ignored text, so use it only
|
||||
// for text that is skipped without calling l.next.
|
||||
func (l *lexer) ignore() {
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
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
|
||||
// back a nil pointer that will be the next state, terminating l.nextItem.
|
||||
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
|
||||
}
|
||||
|
||||
// nextItem returns the next item from the input.
|
||||
// Called by the parser, not in the lexing goroutine.
|
||||
func (l *lexer) nextItem() item {
|
||||
return <-l.items
|
||||
}
|
||||
|
||||
// drain drains the output so the lexing goroutine will exit.
|
||||
// Called by the parser, not in the lexing goroutine.
|
||||
func (l *lexer) drain() {
|
||||
for range l.items {
|
||||
l.item = item{itemEOF, l.pos, "EOF", l.startLine}
|
||||
state := lexText
|
||||
if l.insideAction {
|
||||
state = lexInsideAction
|
||||
}
|
||||
for {
|
||||
state = state(l)
|
||||
if state == nil {
|
||||
return l.item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
left = leftDelim
|
||||
}
|
||||
|
@ -217,27 +246,17 @@ func lex(name, input, left, right string, emitComment bool) *lexer {
|
|||
right = rightDelim
|
||||
}
|
||||
l := &lexer{
|
||||
name: name,
|
||||
input: input,
|
||||
leftDelim: left,
|
||||
rightDelim: right,
|
||||
emitComment: emitComment,
|
||||
items: make(chan item),
|
||||
line: 1,
|
||||
startLine: 1,
|
||||
name: name,
|
||||
input: input,
|
||||
leftDelim: left,
|
||||
rightDelim: right,
|
||||
line: 1,
|
||||
startLine: 1,
|
||||
insideAction: false,
|
||||
}
|
||||
go l.run()
|
||||
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
|
||||
|
||||
const (
|
||||
|
@ -249,31 +268,33 @@ const (
|
|||
|
||||
// lexText scans until an opening action delimiter, "{{".
|
||||
func lexText(l *lexer) stateFn {
|
||||
l.width = 0
|
||||
if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
|
||||
ldn := Pos(len(l.leftDelim))
|
||||
l.pos += Pos(x)
|
||||
trimLength := Pos(0)
|
||||
if hasLeftTrimMarker(l.input[l.pos+ldn:]) {
|
||||
trimLength = rightTrimLength(l.input[l.start:l.pos])
|
||||
}
|
||||
l.pos -= trimLength
|
||||
if l.pos > l.start {
|
||||
if x > 0 {
|
||||
l.pos += Pos(x)
|
||||
// Do we trim any trailing space?
|
||||
trimLength := Pos(0)
|
||||
delimEnd := l.pos + Pos(len(l.leftDelim))
|
||||
if hasLeftTrimMarker(l.input[delimEnd:]) {
|
||||
trimLength = rightTrimLength(l.input[l.start:l.pos])
|
||||
}
|
||||
l.pos -= trimLength
|
||||
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
|
||||
}
|
||||
l.pos = Pos(len(l.input))
|
||||
// Correctly reached EOF.
|
||||
if l.pos > l.start {
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
l.emit(itemText)
|
||||
return l.emit(itemText)
|
||||
}
|
||||
l.emit(itemEOF)
|
||||
return nil
|
||||
return l.emit(itemEOF)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// (The text to be trimmed has already been emitted.)
|
||||
func lexLeftDelim(l *lexer) stateFn {
|
||||
l.pos += Pos(len(l.leftDelim))
|
||||
trimSpace := hasLeftTrimMarker(l.input[l.pos:])
|
||||
|
@ -310,28 +332,27 @@ func lexLeftDelim(l *lexer) stateFn {
|
|||
l.ignore()
|
||||
return lexComment
|
||||
}
|
||||
l.emit(itemLeftDelim)
|
||||
i := l.thisItem(itemLeftDelim)
|
||||
l.insideAction = true
|
||||
l.pos += afterMarker
|
||||
l.ignore()
|
||||
l.parenDepth = 0
|
||||
return lexInsideAction
|
||||
return l.emitItem(i)
|
||||
}
|
||||
|
||||
// lexComment scans a comment. The left comment marker is known to be present.
|
||||
func lexComment(l *lexer) stateFn {
|
||||
l.pos += Pos(len(leftComment))
|
||||
i := strings.Index(l.input[l.pos:], rightComment)
|
||||
if i < 0 {
|
||||
x := strings.Index(l.input[l.pos:], rightComment)
|
||||
if x < 0 {
|
||||
return l.errorf("unclosed comment")
|
||||
}
|
||||
l.pos += Pos(i + len(rightComment))
|
||||
l.pos += Pos(x + len(rightComment))
|
||||
delim, trimSpace := l.atRightDelim()
|
||||
if !delim {
|
||||
return l.errorf("comment ends before closing delimiter")
|
||||
}
|
||||
if l.emitComment {
|
||||
l.emit(itemComment)
|
||||
}
|
||||
i := l.thisItem(itemComment)
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
}
|
||||
|
@ -340,23 +361,27 @@ func lexComment(l *lexer) stateFn {
|
|||
l.pos += leftTrimLength(l.input[l.pos:])
|
||||
}
|
||||
l.ignore()
|
||||
if l.options.emitComment {
|
||||
return l.emitItem(i)
|
||||
}
|
||||
return lexText
|
||||
}
|
||||
|
||||
// lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker.
|
||||
func lexRightDelim(l *lexer) stateFn {
|
||||
trimSpace := hasRightTrimMarker(l.input[l.pos:])
|
||||
_, trimSpace := l.atRightDelim()
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
l.ignore()
|
||||
}
|
||||
l.pos += Pos(len(l.rightDelim))
|
||||
l.emit(itemRightDelim)
|
||||
i := l.thisItem(itemRightDelim)
|
||||
if trimSpace {
|
||||
l.pos += leftTrimLength(l.input[l.pos:])
|
||||
l.ignore()
|
||||
}
|
||||
return lexText
|
||||
l.insideAction = false
|
||||
return l.emitItem(i)
|
||||
}
|
||||
|
||||
// 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 " -}}".
|
||||
return lexSpace
|
||||
case r == '=':
|
||||
l.emit(itemAssign)
|
||||
return l.emit(itemAssign)
|
||||
case r == ':':
|
||||
if l.next() != '=' {
|
||||
return l.errorf("expected :=")
|
||||
}
|
||||
l.emit(itemDeclare)
|
||||
return l.emit(itemDeclare)
|
||||
case r == '|':
|
||||
l.emit(itemPipe)
|
||||
return l.emit(itemPipe)
|
||||
case r == '"':
|
||||
return lexQuote
|
||||
case r == '`':
|
||||
|
@ -410,20 +435,19 @@ func lexInsideAction(l *lexer) stateFn {
|
|||
l.backup()
|
||||
return lexIdentifier
|
||||
case r == '(':
|
||||
l.emit(itemLeftParen)
|
||||
l.parenDepth++
|
||||
return l.emit(itemLeftParen)
|
||||
case r == ')':
|
||||
l.emit(itemRightParen)
|
||||
l.parenDepth--
|
||||
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):
|
||||
l.emit(itemChar)
|
||||
return l.emit(itemChar)
|
||||
default:
|
||||
return l.errorf("unrecognized character in action: %#U", r)
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
l.emit(itemSpace)
|
||||
return lexInsideAction
|
||||
return l.emit(itemSpace)
|
||||
}
|
||||
|
||||
// lexIdentifier scans an alphanumeric.
|
||||
func lexIdentifier(l *lexer) stateFn {
|
||||
Loop:
|
||||
for {
|
||||
switch r := l.next(); {
|
||||
case isAlphaNumeric(r):
|
||||
|
@ -468,22 +490,19 @@ Loop:
|
|||
switch {
|
||||
case key[word] > itemKeyword:
|
||||
item := key[word]
|
||||
if item == itemBreak && !l.breakOK || item == itemContinue && !l.continueOK {
|
||||
l.emit(itemIdentifier)
|
||||
} else {
|
||||
l.emit(item)
|
||||
if item == itemBreak && !l.options.breakOK || item == itemContinue && !l.options.continueOK {
|
||||
return l.emit(itemIdentifier)
|
||||
}
|
||||
return l.emit(item)
|
||||
case word[0] == '.':
|
||||
l.emit(itemField)
|
||||
return l.emit(itemField)
|
||||
case word == "true", word == "false":
|
||||
l.emit(itemBool)
|
||||
return l.emit(itemBool)
|
||||
default:
|
||||
l.emit(itemIdentifier)
|
||||
return l.emit(itemIdentifier)
|
||||
}
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
return lexInsideAction
|
||||
}
|
||||
|
||||
// lexField scans a field: .Alphanumeric.
|
||||
|
@ -496,8 +515,7 @@ func lexField(l *lexer) stateFn {
|
|||
// The $ has been scanned.
|
||||
func lexVariable(l *lexer) stateFn {
|
||||
if l.atTerminator() { // Nothing interesting follows -> "$".
|
||||
l.emit(itemVariable)
|
||||
return lexInsideAction
|
||||
return l.emit(itemVariable)
|
||||
}
|
||||
return lexFieldOrVariable(l, itemVariable)
|
||||
}
|
||||
|
@ -507,11 +525,9 @@ func lexVariable(l *lexer) stateFn {
|
|||
func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
|
||||
if l.atTerminator() { // Nothing interesting follows -> "." or "$".
|
||||
if typ == itemVariable {
|
||||
l.emit(itemVariable)
|
||||
} else {
|
||||
l.emit(itemDot)
|
||||
return l.emit(itemVariable)
|
||||
}
|
||||
return lexInsideAction
|
||||
return l.emit(itemDot)
|
||||
}
|
||||
var r rune
|
||||
for {
|
||||
|
@ -524,8 +540,7 @@ func lexFieldOrVariable(l *lexer, typ itemType) stateFn {
|
|||
if !l.atTerminator() {
|
||||
return l.errorf("bad character %#U", r)
|
||||
}
|
||||
l.emit(typ)
|
||||
return lexInsideAction
|
||||
return l.emit(typ)
|
||||
}
|
||||
|
||||
// atTerminator reports whether the input is at valid termination character to
|
||||
|
@ -541,13 +556,7 @@ func (l *lexer) atTerminator() bool {
|
|||
case eof, '.', ',', '|', ':', ')', '(':
|
||||
return true
|
||||
}
|
||||
// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
|
||||
// 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
|
||||
return strings.HasPrefix(l.input[l.pos:], l.rightDelim)
|
||||
}
|
||||
|
||||
// lexChar scans a character constant. The initial quote is already
|
||||
|
@ -567,8 +576,7 @@ Loop:
|
|||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemCharConstant)
|
||||
return lexInsideAction
|
||||
return l.emit(itemCharConstant)
|
||||
}
|
||||
|
||||
// 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' {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
l.emit(itemComplex)
|
||||
} else {
|
||||
l.emit(itemNumber)
|
||||
return l.emit(itemComplex)
|
||||
}
|
||||
return lexInsideAction
|
||||
return l.emit(itemNumber)
|
||||
}
|
||||
|
||||
func (l *lexer) scanNumber() bool {
|
||||
|
@ -644,8 +650,7 @@ Loop:
|
|||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemString)
|
||||
return lexInsideAction
|
||||
return l.emit(itemString)
|
||||
}
|
||||
|
||||
// lexRawQuote scans a raw quoted string.
|
||||
|
@ -659,8 +664,7 @@ Loop:
|
|||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(itemRawString)
|
||||
return lexInsideAction
|
||||
return l.emit(itemRawString)
|
||||
}
|
||||
|
||||
// isSpace reports whether r is a space character.
|
||||
|
|
|
@ -362,8 +362,7 @@ var lexTests = []lexTest{
|
|||
{"extra right paren", "{{3)}}", []item{
|
||||
tLeft,
|
||||
mkItem(itemNumber, "3"),
|
||||
tRpar,
|
||||
mkItem(itemError, `unexpected right paren U+0029 ')'`),
|
||||
mkItem(itemError, "unexpected right paren"),
|
||||
}},
|
||||
|
||||
// Fixed bugs
|
||||
|
@ -397,7 +396,12 @@ var lexTests = []lexTest{
|
|||
|
||||
// collect gathers the emitted items into a slice.
|
||||
func collect(t *lexTest, left, right string) (items []item) {
|
||||
l := lex(t.name, t.input, left, right, true)
|
||||
l := lex(t.name, t.input, left, right)
|
||||
l.options = lexOptions{
|
||||
emitComment: true,
|
||||
breakOK: true,
|
||||
continueOK: true,
|
||||
}
|
||||
for {
|
||||
item := l.nextItem()
|
||||
items = append(items, item)
|
||||
|
@ -434,7 +438,9 @@ func TestLex(t *testing.T) {
|
|||
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)
|
||||
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{
|
||||
{"empty", "", []item{{itemEOF, 0, "", 1}}},
|
||||
{"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.
|
||||
// We expect an error, so the tree set and funcs list are explicitly nil.
|
||||
func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {
|
||||
|
|
|
@ -210,7 +210,6 @@ func (t *Tree) recover(errp *error) {
|
|||
panic(e)
|
||||
}
|
||||
if t != nil {
|
||||
t.lex.drain()
|
||||
t.stopParse()
|
||||
}
|
||||
*errp = e.(error)
|
||||
|
@ -224,8 +223,6 @@ func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string
|
|||
t.vars = []string{"$"}
|
||||
t.funcs = funcs
|
||||
t.treeSet = treeSet
|
||||
lex.breakOK = !t.hasFunction("break")
|
||||
lex.continueOK = !t.hasFunction("continue")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
defer t.recover(&err)
|
||||
t.ParseName = t.Name
|
||||
emitComment := t.Mode&ParseComments != 0
|
||||
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim, emitComment), treeSet)
|
||||
lexer := lex(t.Name, text, leftDelim, rightDelim)
|
||||
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.parse()
|
||||
t.add()
|
||||
|
@ -341,7 +343,9 @@ func (t *Tree) parseDefinition() {
|
|||
}
|
||||
|
||||
// itemList:
|
||||
//
|
||||
// textOrAction*
|
||||
//
|
||||
// Terminates at {{end}} or {{else}}, returned separately.
|
||||
func (t *Tree) itemList() (list *ListNode, next Node) {
|
||||
list = t.newList(t.peekNonSpace().pos)
|
||||
|
@ -358,6 +362,7 @@ func (t *Tree) itemList() (list *ListNode, next Node) {
|
|||
}
|
||||
|
||||
// textOrAction:
|
||||
//
|
||||
// text | comment | action
|
||||
func (t *Tree) textOrAction() Node {
|
||||
switch token := t.nextNonSpace(); token.typ {
|
||||
|
@ -380,8 +385,10 @@ func (t *Tree) clearActionLine() {
|
|||
}
|
||||
|
||||
// Action:
|
||||
//
|
||||
// control
|
||||
// command ("|" command)*
|
||||
//
|
||||
// Left delim is past. Now get actions.
|
||||
// First word could be a keyword such as range.
|
||||
func (t *Tree) action() (n Node) {
|
||||
|
@ -412,7 +419,9 @@ func (t *Tree) action() (n Node) {
|
|||
}
|
||||
|
||||
// Break:
|
||||
//
|
||||
// {{break}}
|
||||
//
|
||||
// Break keyword is past.
|
||||
func (t *Tree) breakControl(pos Pos, line int) Node {
|
||||
if token := t.nextNonSpace(); token.typ != itemRightDelim {
|
||||
|
@ -425,7 +434,9 @@ func (t *Tree) breakControl(pos Pos, line int) Node {
|
|||
}
|
||||
|
||||
// Continue:
|
||||
//
|
||||
// {{continue}}
|
||||
//
|
||||
// Continue keyword is past.
|
||||
func (t *Tree) continueControl(pos Pos, line int) Node {
|
||||
if token := t.nextNonSpace(); token.typ != itemRightDelim {
|
||||
|
@ -438,6 +449,7 @@ func (t *Tree) continueControl(pos Pos, line int) Node {
|
|||
}
|
||||
|
||||
// Pipeline:
|
||||
//
|
||||
// declarations? command ('|' command)*
|
||||
func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
|
||||
token := t.peekNonSpace()
|
||||
|
@ -549,16 +561,20 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
|
|||
}
|
||||
|
||||
// If:
|
||||
//
|
||||
// {{if pipeline}} itemList {{end}}
|
||||
// {{if pipeline}} itemList {{else}} itemList {{end}}
|
||||
//
|
||||
// If keyword is past.
|
||||
func (t *Tree) ifControl() Node {
|
||||
return t.newIf(t.parseControl(true, "if"))
|
||||
}
|
||||
|
||||
// Range:
|
||||
//
|
||||
// {{range pipeline}} itemList {{end}}
|
||||
// {{range pipeline}} itemList {{else}} itemList {{end}}
|
||||
//
|
||||
// Range keyword is past.
|
||||
func (t *Tree) rangeControl() Node {
|
||||
r := t.newRange(t.parseControl(false, "range"))
|
||||
|
@ -566,22 +582,28 @@ func (t *Tree) rangeControl() Node {
|
|||
}
|
||||
|
||||
// With:
|
||||
//
|
||||
// {{with pipeline}} itemList {{end}}
|
||||
// {{with pipeline}} itemList {{else}} itemList {{end}}
|
||||
//
|
||||
// If keyword is past.
|
||||
func (t *Tree) withControl() Node {
|
||||
return t.newWith(t.parseControl(false, "with"))
|
||||
}
|
||||
|
||||
// End:
|
||||
//
|
||||
// {{end}}
|
||||
//
|
||||
// End keyword is past.
|
||||
func (t *Tree) endControl() Node {
|
||||
return t.newEnd(t.expect(itemRightDelim, "end").pos)
|
||||
}
|
||||
|
||||
// Else:
|
||||
//
|
||||
// {{else}}
|
||||
//
|
||||
// Else keyword is past.
|
||||
func (t *Tree) elseControl() Node {
|
||||
// Special case for "else if".
|
||||
|
@ -595,7 +617,9 @@ func (t *Tree) elseControl() Node {
|
|||
}
|
||||
|
||||
// Block:
|
||||
//
|
||||
// {{block stringValue pipeline}}
|
||||
//
|
||||
// Block keyword is past.
|
||||
// The name must be something that can evaluate to a string.
|
||||
// The pipeline is mandatory.
|
||||
|
@ -623,7 +647,9 @@ func (t *Tree) blockControl() Node {
|
|||
}
|
||||
|
||||
// Template:
|
||||
//
|
||||
// {{template stringValue pipeline}}
|
||||
//
|
||||
// Template keyword is past. The name must be something that can evaluate
|
||||
// to a string.
|
||||
func (t *Tree) templateControl() Node {
|
||||
|
@ -654,7 +680,9 @@ func (t *Tree) parseTemplateName(token item, context string) (name string) {
|
|||
}
|
||||
|
||||
// command:
|
||||
//
|
||||
// operand (space operand)*
|
||||
//
|
||||
// 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.
|
||||
func (t *Tree) command() *CommandNode {
|
||||
|
@ -684,7 +712,9 @@ func (t *Tree) command() *CommandNode {
|
|||
}
|
||||
|
||||
// operand:
|
||||
//
|
||||
// term .Field*
|
||||
//
|
||||
// An operand is a space-separated component of a command,
|
||||
// a term possibly followed by field accesses.
|
||||
// A nil return means the next item is not an operand.
|
||||
|
@ -718,12 +748,14 @@ func (t *Tree) operand() Node {
|
|||
}
|
||||
|
||||
// term:
|
||||
//
|
||||
// literal (number, string, nil, boolean)
|
||||
// function (identifier)
|
||||
// .
|
||||
// .Field
|
||||
// $
|
||||
// '(' pipeline ')'
|
||||
//
|
||||
// A term is a simple "expression".
|
||||
// A nil return means the next item is not a term.
|
||||
func (t *Tree) term() Node {
|
||||
|
|
|
@ -492,7 +492,7 @@ var errorTests = []parseTest{
|
|||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
"{{.X 1 2 3 ) }}",
|
||||
hasError, `unexpected ")" in command`},
|
||||
hasError, "unexpected right paren"},
|
||||
{"rparen2",
|
||||
"{{(.X 1 2 3",
|
||||
hasError, `unclosed action`},
|
||||
|
@ -600,7 +600,8 @@ func TestBlock(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLineNum(t *testing.T) {
|
||||
const count = 100
|
||||
// const count = 100
|
||||
const count = 3
|
||||
text := strings.Repeat("{{printf 1234}}\n", count)
|
||||
tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
|
||||
if err != nil {
|
||||
|
@ -614,11 +615,11 @@ func TestLineNum(t *testing.T) {
|
|||
// Action first.
|
||||
action := nodes[i].(*ActionNode)
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue