mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Fix upstream Go templates bug with reversed key/value assignment
The template packages are based on go1.20.5 with the patch in befec5ddbbfbd81ec84e74e15a38044d67f8785b added. This also includes a security fix that now disallows Go template actions in JS literals (inside backticks). This will throw an error saying "... appears in a JS template literal". If you're really sure this isn't a security risk in your case, you can revert to the old behaviour: ```toml [security] [security.gotemplates] allowActionJSTmpl = true ``` See https://github.com/golang/go/issues/59234 Fixes #11112
This commit is contained in:
parent
0f989d5e21
commit
ee359df172
24 changed files with 276 additions and 143 deletions
|
@ -34,6 +34,7 @@ import (
|
||||||
hglob "github.com/gohugoio/hugo/hugofs/glob"
|
hglob "github.com/gohugoio/hugo/hugofs/glob"
|
||||||
"github.com/gohugoio/hugo/modules"
|
"github.com/gohugoio/hugo/modules"
|
||||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||||
|
"github.com/gohugoio/hugo/tpl"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,6 +90,9 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||||
return nil, fmt.Errorf("failed to init config: %w", err)
|
return nil, fmt.Errorf("failed to init config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is unfortunate, but this is a global setting.
|
||||||
|
tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl)
|
||||||
|
|
||||||
return configs, nil
|
return configs, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,9 @@ type Config struct {
|
||||||
|
|
||||||
// Allow inline shortcodes
|
// Allow inline shortcodes
|
||||||
EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
|
EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
|
||||||
|
|
||||||
|
// Go templates related security config.
|
||||||
|
GoTemplates GoTemplates `json:"goTemplates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec holds os/exec policies.
|
// Exec holds os/exec policies.
|
||||||
|
@ -93,6 +96,15 @@ type HTTP struct {
|
||||||
MediaTypes Whitelist `json:"mediaTypes"`
|
MediaTypes Whitelist `json:"mediaTypes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GoTemplates struct {
|
||||||
|
|
||||||
|
// Enable to allow template actions inside bakcticks in ES6 template literals.
|
||||||
|
// This was blocked in Hugo 0.114.0 for security reasons and you now get errors on the form
|
||||||
|
// "... appears in a JS template literal" if you have this in your templates.
|
||||||
|
// See https://github.com/golang/go/issues/59234
|
||||||
|
AllowActionJSTmpl bool
|
||||||
|
}
|
||||||
|
|
||||||
// ToTOML converts c to TOML with [security] as the root.
|
// ToTOML converts c to TOML with [security] as the root.
|
||||||
func (c Config) ToTOML() string {
|
func (c Config) ToTOML() string {
|
||||||
sec := c.ToSecurityMap()
|
sec := c.ToSecurityMap()
|
||||||
|
|
|
@ -140,7 +140,7 @@ func TestToTOML(t *testing.T) {
|
||||||
got := DefaultConfig.ToTOML()
|
got := DefaultConfig.ToTOML()
|
||||||
|
|
||||||
c.Assert(got, qt.Equals,
|
c.Assert(got, qt.Equals,
|
||||||
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
|
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.goTemplates]\n AllowActionJSTmpl = false\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,6 +121,8 @@ const (
|
||||||
stateJSDqStr
|
stateJSDqStr
|
||||||
// stateJSSqStr occurs inside a JavaScript single quoted string.
|
// stateJSSqStr occurs inside a JavaScript single quoted string.
|
||||||
stateJSSqStr
|
stateJSSqStr
|
||||||
|
// stateJSBqStr occurs inside a JavaScript back quoted string.
|
||||||
|
stateJSBqStr
|
||||||
// stateJSRegexp occurs inside a JavaScript regexp literal.
|
// stateJSRegexp occurs inside a JavaScript regexp literal.
|
||||||
stateJSRegexp
|
stateJSRegexp
|
||||||
// stateJSBlockCmt occurs inside a JavaScript /* block comment */.
|
// stateJSBlockCmt occurs inside a JavaScript /* block comment */.
|
||||||
|
|
|
@ -238,7 +238,7 @@ func cssValueFilter(args ...any) string {
|
||||||
// inside a string that might embed JavaScript source.
|
// inside a string that might embed JavaScript source.
|
||||||
for i, c := range b {
|
for i, c := range b {
|
||||||
switch c {
|
switch c {
|
||||||
case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}':
|
case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}', '<', '>':
|
||||||
return filterFailsafe
|
return filterFailsafe
|
||||||
case '-':
|
case '-':
|
||||||
// Disallow <!-- or -->.
|
// Disallow <!-- or -->.
|
||||||
|
|
|
@ -234,6 +234,8 @@ func TestCSSValueFilter(t *testing.T) {
|
||||||
{`-exp\000052 ession(alert(1337))`, "ZgotmplZ"},
|
{`-exp\000052 ession(alert(1337))`, "ZgotmplZ"},
|
||||||
{`-expre\0000073sion`, "-expre\x073sion"},
|
{`-expre\0000073sion`, "-expre\x073sion"},
|
||||||
{`@import url evil.css`, "ZgotmplZ"},
|
{`@import url evil.css`, "ZgotmplZ"},
|
||||||
|
{"<", "ZgotmplZ"},
|
||||||
|
{">", "ZgotmplZ"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
got := cssValueFilter(test.css)
|
got := cssValueFilter(test.css)
|
||||||
|
|
|
@ -231,5 +231,12 @@ Least Surprise Property:
|
||||||
"A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who
|
"A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who
|
||||||
knows that contextual autoescaping happens should be able to look at a {{.}}
|
knows that contextual autoescaping happens should be able to look at a {{.}}
|
||||||
and correctly infer what sanitization happens."
|
and correctly infer what sanitization happens."
|
||||||
|
|
||||||
|
As a consequence of the Least Surprise Property, template actions within an
|
||||||
|
ECMAScript 6 template literal are disabled by default.
|
||||||
|
Handling string interpolation within these literals is rather complex resulting
|
||||||
|
in no clear safe way to support it.
|
||||||
|
To re-enable template actions within ECMAScript 6 template literals, use the
|
||||||
|
GODEBUG=jstmpllitinterp=1 environment variable.
|
||||||
*/
|
*/
|
||||||
package template
|
package template
|
||||||
|
|
|
@ -215,6 +215,19 @@ const (
|
||||||
// pipeline occurs in an unquoted attribute value context, "html" is
|
// pipeline occurs in an unquoted attribute value context, "html" is
|
||||||
// disallowed. Avoid using "html" and "urlquery" entirely in new templates.
|
// disallowed. Avoid using "html" and "urlquery" entirely in new templates.
|
||||||
ErrPredefinedEscaper
|
ErrPredefinedEscaper
|
||||||
|
|
||||||
|
// errJSTmplLit: "... appears in a JS template literal"
|
||||||
|
// Example:
|
||||||
|
// <script>var tmpl = `{{.Interp}`</script>
|
||||||
|
// Discussion:
|
||||||
|
// Package html/template does not support actions inside of JS template
|
||||||
|
// literals.
|
||||||
|
//
|
||||||
|
// TODO(rolandshoemaker): we cannot add this as an exported error in a minor
|
||||||
|
// release, since it is backwards incompatible with the other minor
|
||||||
|
// releases. As such we need to leave it unexported, and then we'll add it
|
||||||
|
// in the next major release.
|
||||||
|
errJSTmplLit
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
|
|
|
@ -161,6 +161,8 @@ func (e *escaper) escape(c context, n parse.Node) context {
|
||||||
panic("escaping " + n.String() + " is unimplemented")
|
panic("escaping " + n.String() + " is unimplemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
|
||||||
|
|
||||||
// escapeAction escapes an action template node.
|
// escapeAction escapes an action template node.
|
||||||
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
|
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
|
||||||
if len(n.Pipe.Decl) != 0 {
|
if len(n.Pipe.Decl) != 0 {
|
||||||
|
@ -224,6 +226,15 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
|
||||||
c.jsCtx = jsCtxDivOp
|
c.jsCtx = jsCtxDivOp
|
||||||
case stateJSDqStr, stateJSSqStr:
|
case stateJSDqStr, stateJSSqStr:
|
||||||
s = append(s, "_html_template_jsstrescaper")
|
s = append(s, "_html_template_jsstrescaper")
|
||||||
|
case stateJSBqStr:
|
||||||
|
if SecurityAllowActionJSTmpl.Load() { // .Value() == "1" {
|
||||||
|
s = append(s, "_html_template_jsstrescaper")
|
||||||
|
} else {
|
||||||
|
return context{
|
||||||
|
state: stateError,
|
||||||
|
err: errorf(errJSTmplLit, n, n.Line, "%s appears in a JS template literal", n),
|
||||||
|
}
|
||||||
|
}
|
||||||
case stateJSRegexp:
|
case stateJSRegexp:
|
||||||
s = append(s, "_html_template_jsregexpescaper")
|
s = append(s, "_html_template_jsregexpescaper")
|
||||||
case stateCSS:
|
case stateCSS:
|
||||||
|
@ -370,9 +381,8 @@ func normalizeEscFn(e string) string {
|
||||||
// for all x.
|
// for all x.
|
||||||
var redundantFuncs = map[string]map[string]bool{
|
var redundantFuncs = map[string]map[string]bool{
|
||||||
"_html_template_commentescaper": {
|
"_html_template_commentescaper": {
|
||||||
"_html_template_attrescaper": true,
|
"_html_template_attrescaper": true,
|
||||||
"_html_template_nospaceescaper": true,
|
"_html_template_htmlescaper": true,
|
||||||
"_html_template_htmlescaper": true,
|
|
||||||
},
|
},
|
||||||
"_html_template_cssescaper": {
|
"_html_template_cssescaper": {
|
||||||
"_html_template_attrescaper": true,
|
"_html_template_attrescaper": true,
|
||||||
|
|
|
@ -683,38 +683,49 @@ func TestEscape(t *testing.T) {
|
||||||
`<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`,
|
`<img srcset={{",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"}}>`,
|
||||||
`<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`,
|
`<img srcset=,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"unquoted empty attribute value (plaintext)",
|
||||||
|
"<p name={{.U}}>",
|
||||||
|
"<p name=ZgotmplZ>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unquoted empty attribute value (url)",
|
||||||
|
"<p href={{.U}}>",
|
||||||
|
"<p href=ZgotmplZ>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"quoted empty attribute value",
|
||||||
|
"<p name=\"{{.U}}\">",
|
||||||
|
"<p name=\"\">",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
tmpl := New(test.name)
|
t.Run(test.name, func(t *testing.T) {
|
||||||
tmpl = Must(tmpl.Parse(test.input))
|
tmpl := New(test.name)
|
||||||
// Check for bug 6459: Tree field was not set in Parse.
|
tmpl = Must(tmpl.Parse(test.input))
|
||||||
if tmpl.Tree != tmpl.text.Tree {
|
// Check for bug 6459: Tree field was not set in Parse.
|
||||||
t.Errorf("%s: tree not set properly", test.name)
|
if tmpl.Tree != tmpl.text.Tree {
|
||||||
continue
|
t.Fatalf("%s: tree not set properly", test.name)
|
||||||
}
|
}
|
||||||
b := new(strings.Builder)
|
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.Fatalf("%s: template execution failed: %s", test.name, err)
|
||||||
continue
|
}
|
||||||
}
|
if w, g := test.output, b.String(); w != g {
|
||||||
if w, g := test.output, b.String(); w != g {
|
t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
|
||||||
t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g)
|
}
|
||||||
continue
|
b.Reset()
|
||||||
}
|
if err := tmpl.Execute(b, pdata); err != nil {
|
||||||
b.Reset()
|
t.Fatalf("%s: template execution failed for pointer: %s", test.name, err)
|
||||||
if err := tmpl.Execute(b, pdata); err != nil {
|
}
|
||||||
t.Errorf("%s: template execution failed for pointer: %s", test.name, err)
|
if w, g := test.output, b.String(); w != g {
|
||||||
continue
|
t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
|
||||||
}
|
}
|
||||||
if w, g := test.output, b.String(); w != g {
|
if tmpl.Tree != tmpl.text.Tree {
|
||||||
t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g)
|
t.Fatalf("%s: tree mismatch", test.name)
|
||||||
continue
|
}
|
||||||
}
|
})
|
||||||
if tmpl.Tree != tmpl.text.Tree {
|
|
||||||
t.Errorf("%s: tree mismatch", test.name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -941,6 +952,10 @@ func TestErrors(t *testing.T) {
|
||||||
"{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
|
"{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}",
|
||||||
"",
|
"",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"<script>var a = `${a+b}`</script>`",
|
||||||
|
"",
|
||||||
|
},
|
||||||
// Error cases.
|
// Error cases.
|
||||||
{
|
{
|
||||||
"{{if .Cond}}<a{{end}}",
|
"{{if .Cond}}<a{{end}}",
|
||||||
|
@ -1087,6 +1102,10 @@ func TestErrors(t *testing.T) {
|
||||||
// html is allowed since it is the last command in the pipeline, but urlquery is not.
|
// html is allowed since it is the last command in the pipeline, but urlquery is not.
|
||||||
`predefined escaper "urlquery" disallowed in template`,
|
`predefined escaper "urlquery" disallowed in template`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"<script>var tmpl = `asd {{.}}`;</script>",
|
||||||
|
`{{.}} appears in a JS template literal`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
@ -1308,6 +1327,10 @@ func TestEscapeText(t *testing.T) {
|
||||||
`<a onclick="'foo"`,
|
`<a onclick="'foo"`,
|
||||||
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
|
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"<a onclick=\"`foo",
|
||||||
|
context{state: stateJSBqStr, delim: delimDoubleQuote, attr: attrScript},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
`<A ONCLICK="'`,
|
`<A ONCLICK="'`,
|
||||||
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
|
context{state: stateJSSqStr, delim: delimDoubleQuote, attr: attrScript},
|
||||||
|
|
|
@ -14,6 +14,9 @@ import (
|
||||||
// htmlNospaceEscaper escapes for inclusion in unquoted attribute values.
|
// htmlNospaceEscaper escapes for inclusion in unquoted attribute values.
|
||||||
func htmlNospaceEscaper(args ...any) string {
|
func htmlNospaceEscaper(args ...any) string {
|
||||||
s, t := stringify(args...)
|
s, t := stringify(args...)
|
||||||
|
if s == "" {
|
||||||
|
return filterFailsafe
|
||||||
|
}
|
||||||
if t == contentTypeHTML {
|
if t == contentTypeHTML {
|
||||||
return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false)
|
return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,15 @@
|
||||||
package template
|
package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// See https://github.com/golang/go/issues/59234
|
||||||
|
// Moved here to avoid dependency on Go's internal/debug package.
|
||||||
|
var SecurityAllowActionJSTmpl atomic.Bool
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
This files contains the Hugo related addons. All the other files in this
|
This files contains the Hugo related addons. All the other files in this
|
||||||
|
|
|
@ -14,6 +14,11 @@ import (
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// jsWhitespace contains all of the JS whitespace characters, as defined
|
||||||
|
// by the \s character class.
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes.
|
||||||
|
const jsWhitespace = "\f\n\r\t\v\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
||||||
|
|
||||||
// nextJSCtx returns the context that determines whether a slash after the
|
// nextJSCtx returns the context that determines whether a slash after the
|
||||||
// given run of tokens starts a regular expression instead of a division
|
// given run of tokens starts a regular expression instead of a division
|
||||||
// operator: / or /=.
|
// operator: / or /=.
|
||||||
|
@ -27,7 +32,8 @@ import (
|
||||||
// JavaScript 2.0 lexical grammar and requires one token of lookbehind:
|
// JavaScript 2.0 lexical grammar and requires one token of lookbehind:
|
||||||
// https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html
|
// https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html
|
||||||
func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
|
func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
|
||||||
s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029")
|
// Trim all JS whitespace characters
|
||||||
|
s = bytes.TrimRight(s, jsWhitespace)
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
return preceding
|
return preceding
|
||||||
}
|
}
|
||||||
|
@ -309,6 +315,7 @@ var jsStrReplacementTable = []string{
|
||||||
// Encode HTML specials as hex so the output can be embedded
|
// Encode HTML specials as hex so the output can be embedded
|
||||||
// in HTML attributes without further encoding.
|
// in HTML attributes without further encoding.
|
||||||
'"': `\u0022`,
|
'"': `\u0022`,
|
||||||
|
'`': `\u0060`,
|
||||||
'&': `\u0026`,
|
'&': `\u0026`,
|
||||||
'\'': `\u0027`,
|
'\'': `\u0027`,
|
||||||
'+': `\u002b`,
|
'+': `\u002b`,
|
||||||
|
@ -332,6 +339,7 @@ var jsStrNormReplacementTable = []string{
|
||||||
'"': `\u0022`,
|
'"': `\u0022`,
|
||||||
'&': `\u0026`,
|
'&': `\u0026`,
|
||||||
'\'': `\u0027`,
|
'\'': `\u0027`,
|
||||||
|
'`': `\u0060`,
|
||||||
'+': `\u002b`,
|
'+': `\u002b`,
|
||||||
'/': `\/`,
|
'/': `\/`,
|
||||||
'<': `\u003c`,
|
'<': `\u003c`,
|
||||||
|
|
|
@ -83,14 +83,17 @@ func TestNextJsCtx(t *testing.T) {
|
||||||
{jsCtxDivOp, "0"},
|
{jsCtxDivOp, "0"},
|
||||||
// Dots that are part of a number are div preceders.
|
// Dots that are part of a number are div preceders.
|
||||||
{jsCtxDivOp, "0."},
|
{jsCtxDivOp, "0."},
|
||||||
|
// Some JS interpreters treat NBSP as a normal space, so
|
||||||
|
// we must too in order to properly escape things.
|
||||||
|
{jsCtxRegexp, "=\u00A0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx {
|
if ctx := nextJSCtx([]byte(test.s), jsCtxRegexp); ctx != test.jsCtx {
|
||||||
t.Errorf("want %s got %q", test.jsCtx, test.s)
|
t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
|
||||||
}
|
}
|
||||||
if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx {
|
if ctx := nextJSCtx([]byte(test.s), jsCtxDivOp); ctx != test.jsCtx {
|
||||||
t.Errorf("want %s got %q", test.jsCtx, test.s)
|
t.Errorf("%q: want %s got %s", test.s, test.jsCtx, ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,7 +297,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
|
||||||
`0123456789:;\u003c=\u003e?` +
|
`0123456789:;\u003c=\u003e?` +
|
||||||
`@ABCDEFGHIJKLMNO` +
|
`@ABCDEFGHIJKLMNO` +
|
||||||
`PQRSTUVWXYZ[\\]^_` +
|
`PQRSTUVWXYZ[\\]^_` +
|
||||||
"`abcdefghijklmno" +
|
"\\u0060abcdefghijklmno" +
|
||||||
"pqrstuvwxyz{|}~\u007f" +
|
"pqrstuvwxyz{|}~\u007f" +
|
||||||
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
|
"\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,15 @@ package template
|
||||||
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[jsCtxRegexp-0]
|
||||||
|
_ = x[jsCtxDivOp-1]
|
||||||
|
_ = x[jsCtxUnknown-2]
|
||||||
|
}
|
||||||
|
|
||||||
const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
|
const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
|
||||||
|
|
||||||
var _jsCtx_index = [...]uint8{0, 11, 21, 33}
|
var _jsCtx_index = [...]uint8{0, 11, 21, 33}
|
||||||
|
|
|
@ -4,9 +4,42 @@ package template
|
||||||
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateError"
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[stateText-0]
|
||||||
|
_ = x[stateTag-1]
|
||||||
|
_ = x[stateAttrName-2]
|
||||||
|
_ = x[stateAfterName-3]
|
||||||
|
_ = x[stateBeforeValue-4]
|
||||||
|
_ = x[stateHTMLCmt-5]
|
||||||
|
_ = x[stateRCDATA-6]
|
||||||
|
_ = x[stateAttr-7]
|
||||||
|
_ = x[stateURL-8]
|
||||||
|
_ = x[stateSrcset-9]
|
||||||
|
_ = x[stateJS-10]
|
||||||
|
_ = x[stateJSDqStr-11]
|
||||||
|
_ = x[stateJSSqStr-12]
|
||||||
|
_ = x[stateJSBqStr-13]
|
||||||
|
_ = x[stateJSRegexp-14]
|
||||||
|
_ = x[stateJSBlockCmt-15]
|
||||||
|
_ = x[stateJSLineCmt-16]
|
||||||
|
_ = x[stateCSS-17]
|
||||||
|
_ = x[stateCSSDqStr-18]
|
||||||
|
_ = x[stateCSSSqStr-19]
|
||||||
|
_ = x[stateCSSDqURL-20]
|
||||||
|
_ = x[stateCSSSqURL-21]
|
||||||
|
_ = x[stateCSSURL-22]
|
||||||
|
_ = x[stateCSSBlockCmt-23]
|
||||||
|
_ = x[stateCSSLineCmt-24]
|
||||||
|
_ = x[stateError-25]
|
||||||
|
_ = x[stateDead-26]
|
||||||
|
}
|
||||||
|
|
||||||
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 155, 170, 184, 192, 205, 218, 231, 244, 255, 271, 286, 296}
|
const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValuestateHTMLCmtstateRCDATAstateAttrstateURLstateSrcsetstateJSstateJSDqStrstateJSSqStrstateJSBqStrstateJSRegexpstateJSBlockCmtstateJSLineCmtstateCSSstateCSSDqStrstateCSSSqStrstateCSSDqURLstateCSSSqURLstateCSSURLstateCSSBlockCmtstateCSSLineCmtstateErrorstateDead"
|
||||||
|
|
||||||
|
var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 154, 167, 182, 196, 204, 217, 230, 243, 256, 267, 283, 298, 308, 317}
|
||||||
|
|
||||||
func (i state) String() string {
|
func (i state) String() string {
|
||||||
if i >= state(len(_state_index)-1) {
|
if i >= state(len(_state_index)-1) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){
|
||||||
stateJS: tJS,
|
stateJS: tJS,
|
||||||
stateJSDqStr: tJSDelimited,
|
stateJSDqStr: tJSDelimited,
|
||||||
stateJSSqStr: tJSDelimited,
|
stateJSSqStr: tJSDelimited,
|
||||||
|
stateJSBqStr: tJSDelimited,
|
||||||
stateJSRegexp: tJSDelimited,
|
stateJSRegexp: tJSDelimited,
|
||||||
stateJSBlockCmt: tBlockCmt,
|
stateJSBlockCmt: tBlockCmt,
|
||||||
stateJSLineCmt: tLineCmt,
|
stateJSLineCmt: tLineCmt,
|
||||||
|
@ -262,7 +263,7 @@ func tURL(c context, s []byte) (context, int) {
|
||||||
|
|
||||||
// tJS is the context transition function for the JS state.
|
// tJS is the context transition function for the JS state.
|
||||||
func tJS(c context, s []byte) (context, int) {
|
func tJS(c context, s []byte) (context, int) {
|
||||||
i := bytes.IndexAny(s, `"'/`)
|
i := bytes.IndexAny(s, "\"`'/")
|
||||||
if i == -1 {
|
if i == -1 {
|
||||||
// Entire input is non string, comment, regexp tokens.
|
// Entire input is non string, comment, regexp tokens.
|
||||||
c.jsCtx = nextJSCtx(s, c.jsCtx)
|
c.jsCtx = nextJSCtx(s, c.jsCtx)
|
||||||
|
@ -274,6 +275,8 @@ func tJS(c context, s []byte) (context, int) {
|
||||||
c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
|
c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
|
||||||
case '\'':
|
case '\'':
|
||||||
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
|
c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
|
||||||
|
case '`':
|
||||||
|
c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp
|
||||||
case '/':
|
case '/':
|
||||||
switch {
|
switch {
|
||||||
case i+1 < len(s) && s[i+1] == '/':
|
case i+1 < len(s) && s[i+1] == '/':
|
||||||
|
@ -303,6 +306,8 @@ func tJSDelimited(c context, s []byte) (context, int) {
|
||||||
switch c.state {
|
switch c.state {
|
||||||
case stateJSSqStr:
|
case stateJSSqStr:
|
||||||
specials = `\'`
|
specials = `\'`
|
||||||
|
case stateJSBqStr:
|
||||||
|
specials = "`\\"
|
||||||
case stateJSRegexp:
|
case stateJSRegexp:
|
||||||
specials = `\/[]`
|
specials = `\/[]`
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HasExec reports whether the current system can start new processes
|
// HasExec reports whether the current system can start new processes
|
||||||
|
@ -84,87 +82,7 @@ func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd {
|
||||||
// - fails the test if the command does not complete before the test's deadline, and
|
// - fails the test if the command does not complete before the test's deadline, and
|
||||||
// - sets a Cleanup function that verifies that the test did not leak a subprocess.
|
// - sets a Cleanup function that verifies that the test did not leak a subprocess.
|
||||||
func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd {
|
func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||||
t.Helper()
|
panic("Not implemented, Hugo is not using this")
|
||||||
MustHaveExec(t)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cancelCtx context.CancelFunc
|
|
||||||
gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging)
|
|
||||||
)
|
|
||||||
|
|
||||||
if t, ok := t.(interface {
|
|
||||||
testing.TB
|
|
||||||
Deadline() (time.Time, bool)
|
|
||||||
}); ok {
|
|
||||||
if td, ok := t.Deadline(); ok {
|
|
||||||
// Start with a minimum grace period, just long enough to consume the
|
|
||||||
// output of a reasonable program after it terminates.
|
|
||||||
gracePeriod = 100 * time.Millisecond
|
|
||||||
if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" {
|
|
||||||
scale, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err)
|
|
||||||
}
|
|
||||||
gracePeriod *= time.Duration(scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If time allows, increase the termination grace period to 5% of the
|
|
||||||
// test's remaining time.
|
|
||||||
testTimeout := time.Until(td)
|
|
||||||
if gp := testTimeout / 20; gp > gracePeriod {
|
|
||||||
gracePeriod = gp
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we run commands that execute subprocesses, we want to reserve two
|
|
||||||
// grace periods to clean up: one for the delay between the first
|
|
||||||
// termination signal being sent (via the Cancel callback when the Context
|
|
||||||
// expires) and the process being forcibly terminated (via the WaitDelay
|
|
||||||
// field), and a second one for the delay becween the process being
|
|
||||||
// terminated and and the test logging its output for debugging.
|
|
||||||
//
|
|
||||||
// (We want to ensure that the test process itself has enough time to
|
|
||||||
// log the output before it is also terminated.)
|
|
||||||
cmdTimeout := testTimeout - 2*gracePeriod
|
|
||||||
|
|
||||||
if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout {
|
|
||||||
// Either ctx doesn't have a deadline, or its deadline would expire
|
|
||||||
// after (or too close before) the test has already timed out.
|
|
||||||
// Add a shorter timeout so that the test will produce useful output.
|
|
||||||
ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, name, args...)
|
|
||||||
/*cmd.Cancel = func() error {
|
|
||||||
if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded {
|
|
||||||
// The command timed out due to running too close to the test's deadline.
|
|
||||||
// There is no way the test did that intentionally — it's too close to the
|
|
||||||
// wire! — so mark it as a test failure. That way, if the test expects the
|
|
||||||
// command to fail for some other reason, it doesn't have to distinguish
|
|
||||||
// between that reason and a timeout.
|
|
||||||
t.Errorf("test timed out while running command: %v", cmd)
|
|
||||||
} else {
|
|
||||||
// The command is being terminated due to ctx being canceled, but
|
|
||||||
// apparently not due to an explicit test deadline that we added.
|
|
||||||
// Log that information in case it is useful for diagnosing a failure,
|
|
||||||
// but don't actually fail the test because of it.
|
|
||||||
t.Logf("%v: terminating command: %v", ctx.Err(), cmd)
|
|
||||||
}
|
|
||||||
return cmd.Process.Signal(Sigquit)
|
|
||||||
}
|
|
||||||
cmd.WaitDelay = gracePeriod*/
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if cancelCtx != nil {
|
|
||||||
cancelCtx()
|
|
||||||
}
|
|
||||||
if cmd.Process != nil && cmd.ProcessState == nil {
|
|
||||||
t.Errorf("command was started, but test did not wait for it to complete: %v", cmd)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command is like exec.Command, but applies the same changes as
|
// Command is like exec.Command, but applies the same changes as
|
||||||
|
|
|
@ -258,7 +258,7 @@ func MustHaveCGO(t testing.TB) {
|
||||||
// CanInternalLink reports whether the current system can link programs with
|
// CanInternalLink reports whether the current system can link programs with
|
||||||
// internal linking.
|
// internal linking.
|
||||||
func CanInternalLink() bool {
|
func CanInternalLink() bool {
|
||||||
return false
|
panic("not implemented, not needed by Hugo")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustInternalLink checks that the current system can link programs with internal
|
// MustInternalLink checks that the current system can link programs with internal
|
||||||
|
@ -349,15 +349,5 @@ func SkipIfOptimizationOff(t testing.TB) {
|
||||||
// dstPath containing entries for the packages in std and cmd in addition
|
// dstPath containing entries for the packages in std and cmd in addition
|
||||||
// to the package to package file mappings in additionalPackageFiles.
|
// to the package to package file mappings in additionalPackageFiles.
|
||||||
func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) {
|
func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) {
|
||||||
/*importcfg, err := goroot.Importcfg()
|
panic("not implemented, not needed by Hugo")
|
||||||
for k, v := range additionalPackageFiles {
|
|
||||||
importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("preparing the importcfg failed: %s", err)
|
|
||||||
}
|
|
||||||
err = os.WriteFile(dstPath, []byte(importcfg), 0655)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing the importcfg failed: %s", err)
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ import (
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func _TestGoToolLocation(t *testing.T) {
|
func TestGoToolLocation(t *testing.T) {
|
||||||
|
t.Skip("skipping test that requires go command")
|
||||||
testenv.MustHaveGoBuild(t)
|
testenv.MustHaveGoBuild(t)
|
||||||
|
|
||||||
var exeSuffix string
|
var exeSuffix string
|
||||||
|
|
|
@ -361,19 +361,27 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||||
// mark top of stack before any variables in the body are pushed.
|
// mark top of stack before any variables in the body are pushed.
|
||||||
mark := s.mark()
|
mark := s.mark()
|
||||||
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.
|
|
||||||
if len(r.Pipe.Decl) > 0 {
|
if len(r.Pipe.Decl) > 0 {
|
||||||
if r.Pipe.IsAssign {
|
if r.Pipe.IsAssign {
|
||||||
s.setVar(r.Pipe.Decl[0].Ident[0], elem)
|
// With two variables, index comes first.
|
||||||
|
// With one, we use the element.
|
||||||
|
if len(r.Pipe.Decl) > 1 {
|
||||||
|
s.setVar(r.Pipe.Decl[0].Ident[0], index)
|
||||||
|
} else {
|
||||||
|
s.setVar(r.Pipe.Decl[0].Ident[0], elem)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Set top var (lexically the second if there
|
||||||
|
// are two) to the element.
|
||||||
s.setTopVar(1, elem)
|
s.setTopVar(1, elem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 {
|
||||||
if r.Pipe.IsAssign {
|
if r.Pipe.IsAssign {
|
||||||
s.setVar(r.Pipe.Decl[1].Ident[0], index)
|
s.setVar(r.Pipe.Decl[1].Ident[0], elem)
|
||||||
} else {
|
} else {
|
||||||
|
// Set next var (lexically the first if there
|
||||||
|
// are two) to the index.
|
||||||
s.setTopVar(2, index)
|
s.setTopVar(2, index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -697,6 +697,7 @@ var execTests = []execTest{
|
||||||
{"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},
|
{"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true},
|
||||||
|
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
func zeroArgs() string {
|
func zeroArgs() string {
|
||||||
|
|
|
@ -169,6 +169,13 @@ func SetPageInContext(ctx context.Context, p page) context.Context {
|
||||||
return context.WithValue(ctx, texttemplate.PageContextKey, p)
|
return context.WithValue(ctx, texttemplate.PageContextKey, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSecurityAllowActionJSTmpl sets the global setting for allowing tempalte actions in JS template literals.
|
||||||
|
// This was added in Hugo 0.114.0.
|
||||||
|
// See https://github.com/golang/go/issues/59234
|
||||||
|
func SetSecurityAllowActionJSTmpl(b bool) {
|
||||||
|
htmltemplate.SecurityAllowActionJSTmpl.Store(b)
|
||||||
|
}
|
||||||
|
|
||||||
type page interface {
|
type page interface {
|
||||||
IsNode() bool
|
IsNode() bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package tplimpl_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
qt "github.com/frankban/quicktest"
|
qt "github.com/frankban/quicktest"
|
||||||
|
@ -160,3 +161,70 @@ title: "S3P1"
|
||||||
b.AssertFileContent("public/s2/p1/index.html", `S2P1`)
|
b.AssertFileContent("public/s2/p1/index.html", `S2P1`)
|
||||||
b.AssertFileContent("public/s3/p1/index.html", `S3P1`)
|
b.AssertFileContent("public/s3/p1/index.html", `S3P1`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGoTemplateBugs(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("Issue 11112", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files := `
|
||||||
|
-- config.toml --
|
||||||
|
-- layouts/index.html --
|
||||||
|
{{ $m := dict "key" "value" }}
|
||||||
|
{{ $k := "" }}
|
||||||
|
{{ $v := "" }}
|
||||||
|
{{ range $k, $v = $m }}
|
||||||
|
{{ $k }} = {{ $v }}
|
||||||
|
{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
b := hugolib.NewIntegrationTestBuilder(
|
||||||
|
hugolib.IntegrationTestConfig{
|
||||||
|
T: t,
|
||||||
|
TxtarString: files,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
b.Build()
|
||||||
|
|
||||||
|
b.AssertFileContent("public/index.html", `key = value`)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecurityAllowActionJSTmpl(t *testing.T) {
|
||||||
|
|
||||||
|
filesTemplate := `
|
||||||
|
-- config.toml --
|
||||||
|
SECURITYCONFIG
|
||||||
|
-- layouts/index.html --
|
||||||
|
<script>
|
||||||
|
var a = §§{{.Title }}§§;
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
|
||||||
|
files := strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", "")
|
||||||
|
|
||||||
|
b, err := hugolib.NewIntegrationTestBuilder(
|
||||||
|
hugolib.IntegrationTestConfig{
|
||||||
|
T: t,
|
||||||
|
TxtarString: files,
|
||||||
|
},
|
||||||
|
).BuildE()
|
||||||
|
|
||||||
|
b.Assert(err, qt.Not(qt.IsNil))
|
||||||
|
b.Assert(err.Error(), qt.Contains, "{{.Title}} appears in a JS template literal")
|
||||||
|
|
||||||
|
files = strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", `
|
||||||
|
[security]
|
||||||
|
[security.gotemplates]
|
||||||
|
allowActionJSTmpl = true
|
||||||
|
`)
|
||||||
|
|
||||||
|
b = hugolib.NewIntegrationTestBuilder(
|
||||||
|
hugolib.IntegrationTestConfig{
|
||||||
|
T: t,
|
||||||
|
TxtarString: files,
|
||||||
|
},
|
||||||
|
).Build()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue