Changed: DB Params
This commit is contained in:
26
templ/parser/v2/allocs_test.go
Normal file
26
templ/parser/v2/allocs_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
func RunParserAllocTest[T any](t *testing.T, p parse.Parser[T], expectOK bool, maxAllocs int64, input string) {
|
||||
pi := parse.NewInput(input)
|
||||
actual := testing.AllocsPerRun(4, func() {
|
||||
pi.Seek(0)
|
||||
_, ok, err := p.Parse(pi)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing %T: %v", p, err)
|
||||
}
|
||||
if ok != expectOK {
|
||||
t.Fatalf("failed to parse %T", p)
|
||||
}
|
||||
})
|
||||
|
||||
// Run the benchmark.
|
||||
if int64(actual) > maxAllocs {
|
||||
t.Fatalf("Expected allocs <= %d, got %d", maxAllocs, int64(actual))
|
||||
}
|
||||
}
|
34
templ/parser/v2/benchmarks_test.go
Normal file
34
templ/parser/v2/benchmarks_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed benchmarktestdata/benchmark.txt
|
||||
var benchmarkTemplate string
|
||||
|
||||
func BenchmarkParse(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := ParseString(benchmarkTemplate); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFormat(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
sb := new(strings.Builder)
|
||||
for i := 0; i < b.N; i++ {
|
||||
tf, err := ParseString(benchmarkTemplate)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if err = tf.Write(sb); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
sb.Reset()
|
||||
}
|
||||
}
|
18
templ/parser/v2/benchmarktestdata/benchmark.txt
Normal file
18
templ/parser/v2/benchmarktestdata/benchmark.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
package benchmarktestdata
|
||||
|
||||
templ Benchmark() {
|
||||
<div>Hello</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</br>
|
||||
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
|
||||
<br>
|
||||
<br>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
</ul>
|
||||
}
|
||||
|
33
templ/parser/v2/calltemplateparser.go
Normal file
33
templ/parser/v2/calltemplateparser.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
var callTemplateExpression callTemplateExpressionParser
|
||||
|
||||
var callTemplateExpressionStart = parse.Or(parse.String("{! "), parse.String("{!"))
|
||||
|
||||
type callTemplateExpressionParser struct{}
|
||||
|
||||
func (p callTemplateExpressionParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
// Check the prefix first.
|
||||
if _, ok, err = callTemplateExpressionStart.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once we have a prefix, we must have an expression that returns a template.
|
||||
var r CallTemplateExpression
|
||||
if r.Expression, err = parseGo("call template expression", pi, goexpression.Expression); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Eat the final brace.
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("call template expression: missing closing brace", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
}
|
100
templ/parser/v2/calltemplateparser_test.go
Normal file
100
templ/parser/v2/calltemplateparser_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestCallTemplateExpressionParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected CallTemplateExpression
|
||||
}{
|
||||
{
|
||||
name: "call: simple",
|
||||
input: `{! Other(p.Test) }`,
|
||||
expected: CallTemplateExpression{
|
||||
Expression: Expression{
|
||||
Value: "Other(p.Test)",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 16,
|
||||
Line: 0,
|
||||
Col: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call: simple, missing start space",
|
||||
input: `{!Other(p.Test) }`,
|
||||
expected: CallTemplateExpression{
|
||||
Expression: Expression{
|
||||
Value: "Other(p.Test)",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 2,
|
||||
Line: 0,
|
||||
Col: 2,
|
||||
},
|
||||
To: Position{
|
||||
Index: 15,
|
||||
Line: 0,
|
||||
Col: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call: simple, missing start and end space",
|
||||
input: `{!Other(p.Test)}`,
|
||||
expected: CallTemplateExpression{
|
||||
Expression: Expression{
|
||||
Value: "Other(p.Test)",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 2,
|
||||
Line: 0,
|
||||
Col: 2,
|
||||
},
|
||||
To: Position{
|
||||
Index: 15,
|
||||
Line: 0,
|
||||
Col: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := callTemplateExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallTemplateParserAllocsSkip(t *testing.T) {
|
||||
RunParserAllocTest(t, callTemplateExpression, false, 0, ``)
|
||||
}
|
21
templ/parser/v2/childrenparser.go
Normal file
21
templ/parser/v2/childrenparser.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
var childrenExpressionParser = parse.StringFrom(
|
||||
openBraceWithOptionalPadding,
|
||||
parse.OptionalWhitespace,
|
||||
parse.String("children..."),
|
||||
parse.OptionalWhitespace,
|
||||
closeBraceWithOptionalPadding,
|
||||
)
|
||||
|
||||
var childrenExpression = parse.Func(func(in *parse.Input) (n Node, ok bool, err error) {
|
||||
_, ok, err = childrenExpressionParser.Parse(in)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
return ChildrenExpression{}, true, nil
|
||||
})
|
56
templ/parser/v2/childrenparser_test.go
Normal file
56
templ/parser/v2/childrenparser_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestChildrenExpressionParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected ChildrenExpression
|
||||
}{
|
||||
{
|
||||
name: "standard",
|
||||
input: `{ children...}`,
|
||||
expected: ChildrenExpression{},
|
||||
},
|
||||
{
|
||||
name: "condensed",
|
||||
input: `{children...}`,
|
||||
expected: ChildrenExpression{},
|
||||
},
|
||||
{
|
||||
name: "extra spaces",
|
||||
input: `{ children... }`,
|
||||
expected: ChildrenExpression{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := childrenExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChildrenExpressionParserAllocsOK(t *testing.T) {
|
||||
RunParserAllocTest(t, childrenExpression, true, 2, `{ children... }`)
|
||||
}
|
||||
|
||||
func TestChildrenExpressionParserAllocsSkip(t *testing.T) {
|
||||
RunParserAllocTest(t, childrenExpression, false, 2, ``)
|
||||
}
|
100
templ/parser/v2/conditionalattributeparser.go
Normal file
100
templ/parser/v2/conditionalattributeparser.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
var conditionalAttribute parse.Parser[ConditionalAttribute] = conditionalAttributeParser{}
|
||||
|
||||
type conditionalAttributeParser struct{}
|
||||
|
||||
func (conditionalAttributeParser) Parse(pi *parse.Input) (r ConditionalAttribute, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Strip leading whitespace and look for `if `.
|
||||
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if !peekPrefix(pi, "if ") {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the Go if expression.
|
||||
if r.Expression, err = parseGo("if attribute", pi, goexpression.If); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Eat " {\n".
|
||||
if _, ok, err = openBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("attribute if: unterminated (missing closing '{\n')", pi.PositionAt(start))
|
||||
return
|
||||
}
|
||||
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Read the 'Then' attributes.
|
||||
// If there's no match, there's a problem reading the attributes.
|
||||
if r.Then, ok, err = (attributesParser{}).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("attribute if: expected attributes in block, but none were found", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
if len(r.Then) == 0 {
|
||||
err = parse.Error("attribute if: invalid content or no attributes were found in the if block", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Read the optional 'Else' Nodes.
|
||||
if r.Else, ok, err = attributeElseExpression.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok && len(r.Else) == 0 {
|
||||
err = parse.Error("attribute if: invalid content or no attributes were found in the else block", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Clear any optional whitespace.
|
||||
_, _, _ = parse.OptionalWhitespace.Parse(pi)
|
||||
|
||||
// Read the required closing brace.
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("attribute if: missing end (expected '}')", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
var attributeElseExpression parse.Parser[[]Attribute] = attributeElseExpressionParser{}
|
||||
|
||||
type attributeElseExpressionParser struct{}
|
||||
|
||||
func (attributeElseExpressionParser) Parse(in *parse.Input) (r []Attribute, ok bool, err error) {
|
||||
start := in.Index()
|
||||
|
||||
// Strip any initial whitespace.
|
||||
_, _, _ = parse.OptionalWhitespace.Parse(in)
|
||||
|
||||
// } else {
|
||||
var endElseParser = parse.All(
|
||||
parse.Rune('}'),
|
||||
parse.OptionalWhitespace,
|
||||
parse.String("else"),
|
||||
parse.OptionalWhitespace,
|
||||
parse.Rune('{'))
|
||||
if _, ok, err = endElseParser.Parse(in); err != nil || !ok {
|
||||
in.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Else contents
|
||||
if r, ok, err = (attributesParser{}).Parse(in); err != nil || !ok {
|
||||
err = parse.Error("attribute if: expected attributes in else block, but none were found", in.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
}
|
197
templ/parser/v2/cssparser.go
Normal file
197
templ/parser/v2/cssparser.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
// CSS.
|
||||
|
||||
// CSS Parser.
|
||||
var cssParser = parse.Func(func(pi *parse.Input) (r CSSTemplate, ok bool, err error) {
|
||||
from := pi.Position()
|
||||
|
||||
r = CSSTemplate{
|
||||
Properties: []CSSProperty{},
|
||||
}
|
||||
|
||||
// Parse the name.
|
||||
var exp cssExpression
|
||||
if exp, ok, err = cssExpressionParser.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
r.Name = exp.Name
|
||||
r.Expression = exp.Expression
|
||||
|
||||
for {
|
||||
var cssProperty CSSProperty
|
||||
|
||||
// Try for an expression CSS declaration.
|
||||
// background-color: { constants.BackgroundColor };
|
||||
cssProperty, ok, err = expressionCSSPropertyParser.Parse(pi)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
r.Properties = append(r.Properties, cssProperty)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try for a constant CSS declaration.
|
||||
// color: #ffffff;
|
||||
cssProperty, ok, err = constantCSSPropertyParser.Parse(pi)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
r.Properties = append(r.Properties, cssProperty)
|
||||
continue
|
||||
}
|
||||
|
||||
// Eat any whitespace.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Try for }
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("css property expression: missing closing brace", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
r.Range = NewRange(from, pi.Position())
|
||||
|
||||
return r, true, nil
|
||||
}
|
||||
})
|
||||
|
||||
// css Func() {
|
||||
type cssExpression struct {
|
||||
Expression Expression
|
||||
Name string
|
||||
}
|
||||
|
||||
var cssExpressionParser = parse.Func(func(pi *parse.Input) (r cssExpression, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
if !peekPrefix(pi, "css ") {
|
||||
return r, false, nil
|
||||
}
|
||||
|
||||
// Once we have the prefix, everything to the brace is Go.
|
||||
// e.g.
|
||||
// css (x []string) Test() {
|
||||
// becomes:
|
||||
// func (x []string) Test() templ.CSSComponent {
|
||||
if r.Name, r.Expression, err = parseCSSFuncDecl(pi); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
// Eat " {\n".
|
||||
if _, ok, err = parse.All(openBraceWithOptionalPadding, parse.NewLine).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("css expression: parameters missing open bracket", pi.PositionAt(start))
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
||||
|
||||
// CSS property name parser.
|
||||
var cssPropertyNameFirst = "abcdefghijklmnopqrstuvwxyz-"
|
||||
var cssPropertyNameSubsequent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
|
||||
var cssPropertyNameParser = parse.Func(func(in *parse.Input) (name string, ok bool, err error) {
|
||||
start := in.Position()
|
||||
var prefix, suffix string
|
||||
if prefix, ok, err = parse.RuneIn(cssPropertyNameFirst).Parse(in); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
if suffix, ok, err = parse.StringUntil(parse.RuneNotIn(cssPropertyNameSubsequent)).Parse(in); err != nil || !ok {
|
||||
in.Seek(start.Index)
|
||||
return
|
||||
}
|
||||
if len(suffix)+1 > 128 {
|
||||
ok = false
|
||||
err = parse.Error("css property names must be < 128 characters long", in.Position())
|
||||
return
|
||||
}
|
||||
return prefix + suffix, true, nil
|
||||
})
|
||||
|
||||
// background-color: {%= constants.BackgroundColor %};
|
||||
var expressionCSSPropertyParser = parse.Func(func(pi *parse.Input) (r ExpressionCSSProperty, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
// Property name.
|
||||
if r.Name, ok, err = cssPropertyNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
// <space>:<space>
|
||||
if _, ok, err = parse.All(parse.OptionalWhitespace, parse.Rune(':'), parse.OptionalWhitespace).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// { string }
|
||||
var se Node
|
||||
if se, ok, err = stringExpression.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
r.Value = se.(StringExpression)
|
||||
|
||||
// ;
|
||||
if _, ok, err = parse.String(";").Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("missing expected semicolon (;)", pi.Position())
|
||||
return
|
||||
}
|
||||
// \n
|
||||
if _, ok, err = parse.NewLine.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("missing expected linebreak", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
||||
|
||||
// background-color: #ffffff;
|
||||
var constantCSSPropertyParser = parse.Func(func(pi *parse.Input) (r ConstantCSSProperty, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
// Property name.
|
||||
if r.Name, ok, err = cssPropertyNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
// <space>:<space>
|
||||
if _, ok, err = parse.All(parse.OptionalWhitespace, parse.Rune(':'), parse.OptionalWhitespace).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Everything until ';\n'
|
||||
untilEnd := parse.All(
|
||||
parse.OptionalWhitespace,
|
||||
parse.Rune(';'),
|
||||
parse.NewLine,
|
||||
)
|
||||
if r.Value, ok, err = parse.StringUntil(untilEnd).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("missing expected semicolon and linebreak (;\\n", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Chomp the ;\n
|
||||
if _, ok, err = untilEnd.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("failed to chomp semicolon and linebreak (;\\n)", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
337
templ/parser/v2/cssparser_test.go
Normal file
337
templ/parser/v2/cssparser_test.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestExpressionCSSPropertyParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected ExpressionCSSProperty
|
||||
}{
|
||||
{
|
||||
name: "css: single constant property",
|
||||
input: `background-color: { constants.BackgroundColor };`,
|
||||
expected: ExpressionCSSProperty{
|
||||
Name: "background-color",
|
||||
Value: StringExpression{
|
||||
Expression: Expression{
|
||||
Value: "constants.BackgroundColor",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 20,
|
||||
Line: 0,
|
||||
Col: 20,
|
||||
},
|
||||
To: Position{
|
||||
Index: 45,
|
||||
Line: 0,
|
||||
Col: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css: single constant property with windows newlines",
|
||||
input: "background-color:\r\n{ constants.BackgroundColor };\r\n",
|
||||
expected: ExpressionCSSProperty{
|
||||
Name: "background-color",
|
||||
Value: StringExpression{
|
||||
Expression: Expression{
|
||||
Value: "constants.BackgroundColor",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 21,
|
||||
Line: 1,
|
||||
Col: 2,
|
||||
},
|
||||
To: Position{
|
||||
Index: 46,
|
||||
Line: 1,
|
||||
Col: 27,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input + "\n")
|
||||
result, ok, err := expressionCSSPropertyParser.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantCSSPropertyParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected ConstantCSSProperty
|
||||
}{
|
||||
{
|
||||
name: "css: single constant property",
|
||||
input: `background-color: #ffffff;`,
|
||||
expected: ConstantCSSProperty{
|
||||
Name: "background-color",
|
||||
Value: "#ffffff",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css: single constant webkit property",
|
||||
input: `-webkit-text-stroke-color: #ffffff;`,
|
||||
expected: ConstantCSSProperty{
|
||||
Name: "-webkit-text-stroke-color",
|
||||
Value: "#ffffff",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input + "\n")
|
||||
result, ok, err := constantCSSPropertyParser.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSSParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected CSSTemplate
|
||||
}{
|
||||
{
|
||||
name: "css: no parameters, no content",
|
||||
input: `css Name() {
|
||||
}`,
|
||||
expected: CSSTemplate{
|
||||
Name: "Name",
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 14, Line: 1, Col: 1},
|
||||
},
|
||||
Expression: Expression{
|
||||
Value: "Name()",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 10,
|
||||
Line: 0,
|
||||
Col: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: []CSSProperty{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css: without spaces",
|
||||
input: `css Name() {
|
||||
}`,
|
||||
expected: CSSTemplate{
|
||||
Name: "Name",
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 14, Line: 1, Col: 1},
|
||||
},
|
||||
Expression: Expression{
|
||||
Value: "Name()",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 10,
|
||||
Line: 0,
|
||||
Col: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: []CSSProperty{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css: single constant property",
|
||||
input: `css Name() {
|
||||
background-color: #ffffff;
|
||||
}`,
|
||||
expected: CSSTemplate{
|
||||
Name: "Name",
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 41, Line: 2, Col: 1},
|
||||
},
|
||||
Expression: Expression{
|
||||
Value: "Name()",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 10,
|
||||
Line: 0,
|
||||
Col: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: []CSSProperty{
|
||||
ConstantCSSProperty{
|
||||
Name: "background-color",
|
||||
Value: "#ffffff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css: single expression property",
|
||||
input: `css Name() {
|
||||
background-color: { constants.BackgroundColor };
|
||||
}`,
|
||||
expected: CSSTemplate{
|
||||
Name: "Name",
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 63, Line: 2, Col: 1},
|
||||
},
|
||||
Expression: Expression{
|
||||
Value: "Name()",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 10,
|
||||
Line: 0,
|
||||
Col: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: []CSSProperty{
|
||||
ExpressionCSSProperty{
|
||||
Name: "background-color",
|
||||
Value: StringExpression{
|
||||
Expression: Expression{
|
||||
Value: "constants.BackgroundColor",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 33,
|
||||
Line: 1,
|
||||
Col: 20,
|
||||
},
|
||||
To: Position{
|
||||
Index: 58,
|
||||
Line: 1,
|
||||
Col: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css: single expression with parameter",
|
||||
input: `css Name(prop string) {
|
||||
background-color: { prop };
|
||||
}`,
|
||||
expected: CSSTemplate{
|
||||
Name: "Name",
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 53, Line: 2, Col: 1},
|
||||
},
|
||||
Expression: Expression{
|
||||
Value: "Name(prop string)",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 21,
|
||||
Line: 0,
|
||||
Col: 21,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: []CSSProperty{
|
||||
ExpressionCSSProperty{
|
||||
Name: "background-color",
|
||||
Value: StringExpression{
|
||||
Expression: Expression{
|
||||
Value: "prop",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 44,
|
||||
Line: 1,
|
||||
Col: 20,
|
||||
},
|
||||
To: Position{
|
||||
Index: 48,
|
||||
Line: 1,
|
||||
Col: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := cssParser.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
64
templ/parser/v2/diagnostics.go
Normal file
64
templ/parser/v2/diagnostics.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type diagnoser func(Node) ([]Diagnostic, error)
|
||||
|
||||
// Diagnostic for template file.
|
||||
type Diagnostic struct {
|
||||
Message string
|
||||
Range Range
|
||||
}
|
||||
|
||||
func walkTemplate(t TemplateFile, f func(Node) bool) {
|
||||
for _, n := range t.Nodes {
|
||||
hn, ok := n.(HTMLTemplate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
walkNodes(hn.Children, f)
|
||||
}
|
||||
}
|
||||
func walkNodes(t []Node, f func(Node) bool) {
|
||||
for _, n := range t {
|
||||
if !f(n) {
|
||||
continue
|
||||
}
|
||||
if h, ok := n.(CompositeNode); ok {
|
||||
walkNodes(h.ChildNodes(), f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var diagnosers = []diagnoser{
|
||||
useOfLegacyCallSyntaxDiagnoser,
|
||||
}
|
||||
|
||||
func Diagnose(t TemplateFile) ([]Diagnostic, error) {
|
||||
var diags []Diagnostic
|
||||
var errs error
|
||||
walkTemplate(t, func(n Node) bool {
|
||||
for _, d := range diagnosers {
|
||||
diag, err := d(n)
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
return false
|
||||
}
|
||||
diags = append(diags, diag...)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return diags, errs
|
||||
}
|
||||
|
||||
func useOfLegacyCallSyntaxDiagnoser(n Node) ([]Diagnostic, error) {
|
||||
if c, ok := n.(CallTemplateExpression); ok {
|
||||
return []Diagnostic{{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: c.Expression.Range,
|
||||
}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
153
templ/parser/v2/diagnostics_test.go
Normal file
153
templ/parser/v2/diagnostics_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestDiagnose(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
template string
|
||||
want []Diagnostic
|
||||
}{
|
||||
{
|
||||
name: "no diagnostics",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
<p>Hello, World!</p>
|
||||
}`,
|
||||
want: nil,
|
||||
},
|
||||
|
||||
// useOfLegacyCallSyntaxDiagnoser
|
||||
|
||||
{
|
||||
name: "useOfLegacyCallSyntaxDiagnoser: template root",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
{! templ.Raw("foo") }
|
||||
}`,
|
||||
want: []Diagnostic{{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{39, 4, 4}, Position{55, 4, 20}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "useOfLegacyCallSyntaxDiagnoser: in div",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
<div>
|
||||
{! templ.Raw("foo") }
|
||||
</div>
|
||||
}`,
|
||||
want: []Diagnostic{{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{47, 5, 5}, Position{63, 5, 21}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "useOfLegacyCallSyntaxDiagnoser: in if",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
if true {
|
||||
{! templ.Raw("foo") }
|
||||
}
|
||||
}`,
|
||||
want: []Diagnostic{{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{51, 5, 5}, Position{67, 5, 21}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "useOfLegacyCallSyntaxDiagnoser: in for",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
for i := range x {
|
||||
{! templ.Raw("foo") }
|
||||
}
|
||||
}`,
|
||||
want: []Diagnostic{{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{60, 5, 5}, Position{76, 5, 21}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "useOfLegacyCallSyntaxDiagnoser: in switch",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
switch x {
|
||||
case 1:
|
||||
{! templ.Raw("foo") }
|
||||
default:
|
||||
{! x }
|
||||
}
|
||||
}`,
|
||||
want: []Diagnostic{
|
||||
{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{61, 6, 5}, Position{77, 6, 21}},
|
||||
},
|
||||
{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{95, 8, 5}, Position{96, 8, 6}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "useOfLegacyCallSyntaxDiagnoser: in block",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
@layout("Home") {
|
||||
{! templ.Raw("foo") }
|
||||
}
|
||||
}`,
|
||||
want: []Diagnostic{{
|
||||
Message: "`{! foo }` syntax is deprecated. Use `@foo` syntax instead. Run `templ fmt .` to fix all instances.",
|
||||
Range: Range{Position{59, 5, 5}, Position{75, 5, 21}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "voidElementWithChildrenDiagnoser: no diagnostics",
|
||||
template: `
|
||||
package main
|
||||
|
||||
templ template () {
|
||||
<div>
|
||||
<input/>
|
||||
</div>
|
||||
}`,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tf, err := ParseString(tt.template)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTemplateFile() error = %v", err)
|
||||
}
|
||||
got, err := Diagnose(tf)
|
||||
if err != nil {
|
||||
t.Fatalf("Diagnose() error = %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("Diagnose() mismatch (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
32
templ/parser/v2/doctypeparser.go
Normal file
32
templ/parser/v2/doctypeparser.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
var doctypeStartParser = parse.StringInsensitive("<!doctype ")
|
||||
|
||||
var untilLtOrGt = parse.Or(lt, gt)
|
||||
var stringUntilLtOrGt = parse.StringUntil(untilLtOrGt)
|
||||
|
||||
var docType = parse.Func(func(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
start := pi.Position()
|
||||
var r DocType
|
||||
if _, ok, err = doctypeStartParser.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once a doctype has started, take everything until the end.
|
||||
if r.Value, ok, err = stringUntilLtOrGt.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("unclosed DOCTYPE", start)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear the final '>'.
|
||||
if _, ok, err = gt.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("unclosed DOCTYPE", start)
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
101
templ/parser/v2/doctypeparser_test.go
Normal file
101
templ/parser/v2/doctypeparser_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestDocTypeParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected DocType
|
||||
}{
|
||||
{
|
||||
name: "HTML 5 doctype - uppercase",
|
||||
input: `<!DOCTYPE html>`,
|
||||
expected: DocType{
|
||||
Value: "html",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTML 5 doctype - lowercase",
|
||||
input: `<!doctype html>`,
|
||||
expected: DocType{
|
||||
Value: "html",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTML 4.01 doctype",
|
||||
input: `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">`,
|
||||
expected: DocType{
|
||||
Value: `HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "XHTML 1.1",
|
||||
input: `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">`,
|
||||
expected: DocType{
|
||||
Value: `html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := docType.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocTypeParserErrors(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "doctype unclosed",
|
||||
input: `<!DOCTYPE html`,
|
||||
expected: parse.Error("unclosed DOCTYPE",
|
||||
parse.Position{
|
||||
Index: 0,
|
||||
Line: 0,
|
||||
Col: 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "doctype new tag started",
|
||||
input: `<!DOCTYPE html
|
||||
<div>`,
|
||||
expected: parse.Error("unclosed DOCTYPE",
|
||||
parse.Position{
|
||||
Index: 0,
|
||||
Line: 0,
|
||||
Col: 0,
|
||||
}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
_, _, err := docType.Parse(input)
|
||||
if diff := cmp.Diff(tt.expected, err); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
509
templ/parser/v2/elementparser.go
Normal file
509
templ/parser/v2/elementparser.go
Normal file
@@ -0,0 +1,509 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
// Element.
|
||||
|
||||
// Element open tag.
|
||||
type elementOpenTag struct {
|
||||
Name string
|
||||
Attributes []Attribute
|
||||
IndentAttrs bool
|
||||
NameRange Range
|
||||
Void bool
|
||||
}
|
||||
|
||||
var elementOpenTagParser = parse.Func(func(pi *parse.Input) (e elementOpenTag, ok bool, err error) {
|
||||
start := pi.Position()
|
||||
|
||||
// <
|
||||
if _, ok, err = lt.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Element name.
|
||||
l := pi.Position().Line
|
||||
if e.Name, ok, err = elementNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start.Index)
|
||||
return
|
||||
}
|
||||
e.NameRange = NewRange(pi.PositionAt(pi.Index()-len(e.Name)), pi.Position())
|
||||
|
||||
if e.Attributes, ok, err = (attributesParser{}).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start.Index)
|
||||
return
|
||||
}
|
||||
|
||||
// If any attribute is not on the same line as the element name, indent them.
|
||||
if pi.Position().Line != l {
|
||||
e.IndentAttrs = true
|
||||
}
|
||||
|
||||
// Optional whitespace.
|
||||
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
|
||||
pi.Seek(start.Index)
|
||||
return
|
||||
}
|
||||
|
||||
// />
|
||||
if _, ok, err = parse.String("/>").Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
e.Void = true
|
||||
return
|
||||
}
|
||||
|
||||
// >
|
||||
if _, ok, err = gt.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If it's not a self-closing or complete open element, we have an error.
|
||||
if !ok {
|
||||
err = parse.Error(fmt.Sprintf("<%s>: malformed open element", e.Name), pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return e, true, nil
|
||||
})
|
||||
|
||||
// Attribute name.
|
||||
var (
|
||||
attributeNameFirst = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:_@"
|
||||
attributeNameSubsequent = attributeNameFirst + "-.0123456789*"
|
||||
attributeNameParser = parse.Func(func(in *parse.Input) (name string, ok bool, err error) {
|
||||
start := in.Index()
|
||||
var prefix, suffix string
|
||||
if prefix, ok, err = parse.RuneIn(attributeNameFirst).Parse(in); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
if suffix, ok, err = parse.StringUntil(parse.RuneNotIn(attributeNameSubsequent)).Parse(in); err != nil {
|
||||
in.Seek(start)
|
||||
return
|
||||
}
|
||||
if len(suffix)+1 > 128 {
|
||||
ok = false
|
||||
err = parse.Error("attribute names must be < 128 characters long", in.Position())
|
||||
return
|
||||
}
|
||||
return prefix + suffix, true, nil
|
||||
})
|
||||
)
|
||||
|
||||
type attributeValueParser struct {
|
||||
EqualsAndQuote parse.Parser[string]
|
||||
Suffix parse.Parser[string]
|
||||
UseSingleQuote bool
|
||||
}
|
||||
|
||||
func (avp attributeValueParser) Parse(pi *parse.Input) (value string, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
if _, ok, err = avp.EqualsAndQuote.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
if value, ok, err = parse.StringUntil(avp.Suffix).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
if _, ok, err = avp.Suffix.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// Constant attribute.
|
||||
var (
|
||||
attributeValueParsers = []attributeValueParser{
|
||||
// Double quoted.
|
||||
{EqualsAndQuote: parse.String(`="`), Suffix: parse.String(`"`), UseSingleQuote: false},
|
||||
// Single quoted.
|
||||
{EqualsAndQuote: parse.String(`='`), Suffix: parse.String(`'`), UseSingleQuote: true},
|
||||
// Unquoted.
|
||||
// A valid unquoted attribute value in HTML is any string of text that is not an empty string,
|
||||
// and that doesn’t contain spaces, tabs, line feeds, form feeds, carriage returns, ", ', `, =, <, or >.
|
||||
{EqualsAndQuote: parse.String("="), Suffix: parse.Any(parse.RuneIn(" \t\n\r\"'`=<>/"), parse.EOF[string]()), UseSingleQuote: false},
|
||||
}
|
||||
constantAttributeParser = parse.Func(func(pi *parse.Input) (attr ConstantAttribute, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace leader.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Attribute name.
|
||||
if attr.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position())
|
||||
|
||||
for _, p := range attributeValueParsers {
|
||||
attr.Value, ok, err = p.Parse(pi)
|
||||
if err != nil {
|
||||
pos := pi.Position()
|
||||
if pErr, isParseError := err.(parse.ParseError); isParseError {
|
||||
pos = pErr.Pos
|
||||
}
|
||||
return attr, false, parse.Error(fmt.Sprintf("%s: %v", attr.Name, err), pos)
|
||||
}
|
||||
if ok {
|
||||
attr.SingleQuote = p.UseSingleQuote
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
pi.Seek(start)
|
||||
return attr, false, nil
|
||||
}
|
||||
|
||||
attr.Value = html.UnescapeString(attr.Value)
|
||||
|
||||
// Only use single quotes if actually required, due to double quote in the value (prefer double quotes).
|
||||
attr.SingleQuote = attr.SingleQuote && strings.Contains(attr.Value, "\"")
|
||||
|
||||
return attr, true, nil
|
||||
})
|
||||
)
|
||||
|
||||
// BoolConstantAttribute.
|
||||
var boolConstantAttributeParser = parse.Func(func(pi *parse.Input) (attr BoolConstantAttribute, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace leader.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Attribute name.
|
||||
if attr.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position())
|
||||
|
||||
// We have a name, but if we have an equals sign, it's not a constant boolean attribute.
|
||||
next, ok := pi.Peek(1)
|
||||
if !ok {
|
||||
err = parse.Error("boolConstantAttributeParser: unexpected EOF after attribute name", pi.Position())
|
||||
return
|
||||
}
|
||||
if next == "=" || next == "?" {
|
||||
// It's one of the other attribute types.
|
||||
pi.Seek(start)
|
||||
return attr, false, nil
|
||||
}
|
||||
if !(next == " " || next == "\t" || next == "\r" || next == "\n" || next == "/" || next == ">") {
|
||||
err = parse.Error(fmt.Sprintf("boolConstantAttributeParser: expected attribute name to end with space, newline, '/>' or '>', but got %q", next), pi.Position())
|
||||
return attr, false, err
|
||||
}
|
||||
|
||||
return attr, true, nil
|
||||
})
|
||||
|
||||
// BoolExpressionAttribute.
|
||||
var boolExpressionStart = parse.Or(parse.String("?={ "), parse.String("?={"))
|
||||
|
||||
var boolExpressionAttributeParser = parse.Func(func(pi *parse.Input) (r BoolExpressionAttribute, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace leader.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Attribute name.
|
||||
if r.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
r.NameRange = NewRange(pi.PositionAt(pi.Index()-len(r.Name)), pi.Position())
|
||||
|
||||
// Check whether this is a boolean expression attribute.
|
||||
if _, ok, err = boolExpressionStart.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Once we have a prefix, we must have an expression that returns a boolean.
|
||||
if r.Expression, err = parseGo("boolean attribute", pi, goexpression.Expression); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
// Eat the Final brace.
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("boolean expression: missing closing brace", pi.Position())
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
||||
|
||||
var expressionAttributeParser = parse.Func(func(pi *parse.Input) (attr ExpressionAttribute, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace leader.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Attribute name.
|
||||
if attr.Name, ok, err = attributeNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
attr.NameRange = NewRange(pi.PositionAt(pi.Index()-len(attr.Name)), pi.Position())
|
||||
|
||||
// ={
|
||||
if _, ok, err = parse.Or(parse.String("={ "), parse.String("={")).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Expression.
|
||||
if attr.Expression, err = parseGoSliceArgs(pi); err != nil {
|
||||
return attr, false, err
|
||||
}
|
||||
|
||||
// Eat whitespace, plus the final brace.
|
||||
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
|
||||
return attr, false, err
|
||||
}
|
||||
if _, ok, err = closeBrace.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("string expression attribute: missing closing brace", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return attr, true, nil
|
||||
})
|
||||
|
||||
var spreadAttributesParser = parse.Func(func(pi *parse.Input) (attr SpreadAttributes, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Optional whitespace leader.
|
||||
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Eat the first brace.
|
||||
if _, ok, err = openBraceWithOptionalPadding.Parse(pi); err != nil ||
|
||||
!ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Expression.
|
||||
if attr.Expression, err = parseGo("spread attributes", pi, goexpression.Expression); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if end of expression has "..." for spread.
|
||||
if !strings.HasSuffix(attr.Expression.Value, "...") {
|
||||
pi.Seek(start)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
// Remove extra spread characters from expression.
|
||||
attr.Expression.Value = strings.TrimSuffix(attr.Expression.Value, "...")
|
||||
attr.Expression.Range.To.Col -= 3
|
||||
attr.Expression.Range.To.Index -= 3
|
||||
|
||||
// Eat the final brace.
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("attribute spread expression: missing closing brace", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return attr, true, nil
|
||||
})
|
||||
|
||||
// Attributes.
|
||||
type attributeParser struct{}
|
||||
|
||||
func (attributeParser) Parse(in *parse.Input) (out Attribute, ok bool, err error) {
|
||||
if out, ok, err = boolExpressionAttributeParser.Parse(in); err != nil || ok {
|
||||
return
|
||||
}
|
||||
if out, ok, err = expressionAttributeParser.Parse(in); err != nil || ok {
|
||||
return
|
||||
}
|
||||
if out, ok, err = conditionalAttribute.Parse(in); err != nil || ok {
|
||||
return
|
||||
}
|
||||
if out, ok, err = boolConstantAttributeParser.Parse(in); err != nil || ok {
|
||||
return
|
||||
}
|
||||
if out, ok, err = spreadAttributesParser.Parse(in); err != nil || ok {
|
||||
return
|
||||
}
|
||||
if out, ok, err = constantAttributeParser.Parse(in); err != nil || ok {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var attribute attributeParser
|
||||
|
||||
type attributesParser struct{}
|
||||
|
||||
func (attributesParser) Parse(in *parse.Input) (attributes []Attribute, ok bool, err error) {
|
||||
for {
|
||||
var attr Attribute
|
||||
attr, ok, err = attribute.Parse(in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
attributes = append(attributes, attr)
|
||||
}
|
||||
return attributes, true, nil
|
||||
}
|
||||
|
||||
// Element name.
|
||||
var (
|
||||
elementNameFirst = "abcdefghijklmnopqrstuvwxyz"
|
||||
elementNameSubsequent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-:"
|
||||
elementNameParser = parse.Func(func(in *parse.Input) (name string, ok bool, err error) {
|
||||
start := in.Index()
|
||||
var prefix, suffix string
|
||||
if prefix, ok, err = parse.RuneIn(elementNameFirst).Parse(in); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
if suffix, ok, err = parse.StringUntil(parse.RuneNotIn(elementNameSubsequent)).Parse(in); err != nil || !ok {
|
||||
in.Seek(start)
|
||||
return
|
||||
}
|
||||
if len(suffix)+1 > 128 {
|
||||
ok = false
|
||||
err = parse.Error("element names must be < 128 characters long", in.Position())
|
||||
return
|
||||
}
|
||||
return prefix + suffix, true, nil
|
||||
})
|
||||
)
|
||||
|
||||
// Void element closer.
|
||||
var voidElementCloser voidElementCloserParser
|
||||
|
||||
type voidElementCloserParser struct{}
|
||||
|
||||
var voidElementCloseTags = []string{"</area>", "</base>", "</br>", "</col>", "</command>", "</embed>", "</hr>", "</img>", "</input>", "</keygen>", "</link>", "</meta>", "</param>", "</source>", "</track>", "</wbr>"}
|
||||
|
||||
func (voidElementCloserParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
var ve string
|
||||
for _, ve = range voidElementCloseTags {
|
||||
s, canPeekLen := pi.Peek(len(ve))
|
||||
if !canPeekLen {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(s, ve) {
|
||||
continue
|
||||
}
|
||||
// Found a match.
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
pi.Take(len(ve))
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
// Element.
|
||||
var element elementParser
|
||||
|
||||
type elementParser struct{}
|
||||
|
||||
func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
var r Element
|
||||
start := pi.Position()
|
||||
|
||||
// Check the open tag.
|
||||
var ot elementOpenTag
|
||||
if ot, ok, err = elementOpenTagParser.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
r.Name = ot.Name
|
||||
r.Attributes = ot.Attributes
|
||||
r.IndentAttrs = ot.IndentAttrs
|
||||
r.NameRange = ot.NameRange
|
||||
|
||||
// Once we've got an open tag, the rest must be present.
|
||||
l := pi.Position().Line
|
||||
|
||||
// If the element is self-closing, even if it's not really a void element (br, hr etc.), we can return early.
|
||||
if ot.Void || r.IsVoidElement() {
|
||||
// Escape early, no need to try to parse children for self-closing elements.
|
||||
return addTrailingSpaceAndValidate(start, r, pi)
|
||||
}
|
||||
|
||||
// Parse children.
|
||||
closer := StripType(parse.All(parse.String("</"), parse.String(ot.Name), parse.Rune('>')))
|
||||
tnp := newTemplateNodeParser(closer, fmt.Sprintf("<%s>: close tag", ot.Name))
|
||||
nodes, _, err := tnp.Parse(pi)
|
||||
if err != nil {
|
||||
notFoundErr, isNotFoundError := err.(UntilNotFoundError)
|
||||
if isNotFoundError {
|
||||
err = notFoundErr.ParseError
|
||||
}
|
||||
return r, false, err
|
||||
}
|
||||
r.Children = nodes.Nodes
|
||||
// If the children are not all on the same line, indent them.
|
||||
if l != pi.Position().Line {
|
||||
r.IndentChildren = true
|
||||
}
|
||||
|
||||
// Close tag.
|
||||
_, ok, err = closer.Parse(pi)
|
||||
if err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
if !ok {
|
||||
err = parse.Error(fmt.Sprintf("<%s>: expected end tag not present or invalid tag contents", r.Name), pi.Position())
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
return addTrailingSpaceAndValidate(start, r, pi)
|
||||
}
|
||||
|
||||
func addTrailingSpaceAndValidate(start parse.Position, e Element, pi *parse.Input) (n Node, ok bool, err error) {
|
||||
// Elide any void close tags.
|
||||
if _, _, err = voidElementCloser.Parse(pi); err != nil {
|
||||
return e, false, err
|
||||
}
|
||||
// Add trailing space.
|
||||
ws, _, err := parse.Whitespace.Parse(pi)
|
||||
if err != nil {
|
||||
return e, false, err
|
||||
}
|
||||
e.TrailingSpace, err = NewTrailingSpace(ws)
|
||||
if err != nil {
|
||||
return e, false, err
|
||||
}
|
||||
|
||||
// Validate.
|
||||
var msgs []string
|
||||
if msgs, ok = e.Validate(); !ok {
|
||||
err = parse.Error(fmt.Sprintf("<%s>: %s", e.Name, strings.Join(msgs, ", ")), start)
|
||||
return e, false, err
|
||||
}
|
||||
|
||||
return e, true, nil
|
||||
}
|
1786
templ/parser/v2/elementparser_test.go
Normal file
1786
templ/parser/v2/elementparser_test.go
Normal file
File diff suppressed because it is too large
Load Diff
239
templ/parser/v2/expressionparser.go
Normal file
239
templ/parser/v2/expressionparser.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
// StripType takes the parser and throws away the return value.
|
||||
func StripType[T any](p parse.Parser[T]) parse.Parser[any] {
|
||||
return parse.Func(func(in *parse.Input) (out any, ok bool, err error) {
|
||||
return p.Parse(in)
|
||||
})
|
||||
}
|
||||
|
||||
func ExpressionOf(p parse.Parser[string]) parse.Parser[Expression] {
|
||||
return parse.Func(func(in *parse.Input) (out Expression, ok bool, err error) {
|
||||
from := in.Position()
|
||||
|
||||
var exp string
|
||||
if exp, ok, err = p.Parse(in); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return NewExpression(exp, from, in.Position()), true, nil
|
||||
})
|
||||
}
|
||||
|
||||
var lt = parse.Rune('<')
|
||||
var gt = parse.Rune('>')
|
||||
var openBrace = parse.String("{")
|
||||
var optionalSpaces = parse.StringFrom(parse.Optional(
|
||||
parse.AtLeast(1, parse.Rune(' '))))
|
||||
var openBraceWithPadding = parse.StringFrom(optionalSpaces,
|
||||
openBrace,
|
||||
optionalSpaces)
|
||||
var openBraceWithOptionalPadding = parse.Any(openBraceWithPadding, openBrace)
|
||||
|
||||
var closeBrace = parse.String("}")
|
||||
var closeBraceWithOptionalPadding = parse.StringFrom(optionalSpaces, closeBrace)
|
||||
|
||||
var dblCloseBrace = parse.String("}}")
|
||||
var dblCloseBraceWithOptionalPadding = parse.StringFrom(optionalSpaces, dblCloseBrace)
|
||||
|
||||
var openBracket = parse.String("(")
|
||||
var closeBracket = parse.String(")")
|
||||
|
||||
var stringUntilNewLine = parse.StringUntil(parse.NewLine)
|
||||
var newLineOrEOF = parse.Or(parse.NewLine, parse.EOF[string]())
|
||||
var stringUntilNewLineOrEOF = parse.StringUntil(newLineOrEOF)
|
||||
|
||||
var jsOrGoSingleLineComment = parse.StringFrom(parse.String("//"), parse.StringUntil(parse.Any(parse.NewLine, parse.EOF[string]())))
|
||||
var jsOrGoMultiLineComment = parse.StringFrom(parse.String("/*"), parse.StringUntil(parse.String("*/")))
|
||||
|
||||
var exp = expressionParser{
|
||||
startBraceCount: 1,
|
||||
}
|
||||
|
||||
type expressionParser struct {
|
||||
startBraceCount int
|
||||
}
|
||||
|
||||
func (p expressionParser) Parse(pi *parse.Input) (s Expression, ok bool, err error) {
|
||||
from := pi.Position()
|
||||
|
||||
braceCount := p.startBraceCount
|
||||
|
||||
sb := new(strings.Builder)
|
||||
loop:
|
||||
for {
|
||||
var result string
|
||||
|
||||
// Try to parse a single line comment.
|
||||
if result, ok, err = jsOrGoSingleLineComment.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
sb.WriteString(result)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to parse a multi-line comment.
|
||||
if result, ok, err = jsOrGoMultiLineComment.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
sb.WriteString(result)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to read a string literal.
|
||||
if result, ok, err = string_lit.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
sb.WriteString(result)
|
||||
continue
|
||||
}
|
||||
// Also try for a rune literal.
|
||||
if result, ok, err = rune_lit.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
sb.WriteString(result)
|
||||
continue
|
||||
}
|
||||
// Try opener.
|
||||
if result, ok, err = openBrace.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
braceCount++
|
||||
sb.WriteString(result)
|
||||
continue
|
||||
}
|
||||
// Try closer.
|
||||
startOfCloseBrace := pi.Index()
|
||||
if result, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
braceCount--
|
||||
if braceCount < 0 {
|
||||
err = parse.Error("expression: too many closing braces", pi.Position())
|
||||
return
|
||||
}
|
||||
if braceCount == 0 {
|
||||
pi.Seek(startOfCloseBrace)
|
||||
break loop
|
||||
}
|
||||
sb.WriteString(result)
|
||||
continue
|
||||
}
|
||||
|
||||
// Read anything else.
|
||||
var c string
|
||||
c, ok = pi.Take(1)
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
if rune(c[0]) == 65533 { // Invalid Unicode.
|
||||
break loop
|
||||
}
|
||||
sb.WriteString(c)
|
||||
}
|
||||
if braceCount != 0 {
|
||||
err = parse.Error("expression: unexpected brace count", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return NewExpression(sb.String(), from, pi.Position()), true, nil
|
||||
}
|
||||
|
||||
// Letters and digits
|
||||
|
||||
var octal_digit = parse.RuneIn("01234567")
|
||||
var hex_digit = parse.RuneIn("0123456789ABCDEFabcdef")
|
||||
|
||||
// https://go.dev/ref/spec#Rune_literals
|
||||
|
||||
var rune_lit = parse.StringFrom(
|
||||
parse.Rune('\''),
|
||||
parse.StringFrom(parse.Until(
|
||||
parse.Any(unicode_value_rune, byte_value),
|
||||
parse.Rune('\''),
|
||||
)),
|
||||
parse.Rune('\''),
|
||||
)
|
||||
var unicode_value_rune = parse.Any(little_u_value, big_u_value, escaped_char, parse.RuneNotIn("'"))
|
||||
|
||||
// byte_value = octal_byte_value | hex_byte_value .
|
||||
var byte_value = parse.Any(octal_byte_value, hex_byte_value)
|
||||
|
||||
// octal_byte_value = `\` octal_digit octal_digit octal_digit .
|
||||
var octal_byte_value = parse.StringFrom(
|
||||
parse.String(`\`),
|
||||
octal_digit, octal_digit, octal_digit,
|
||||
)
|
||||
|
||||
// hex_byte_value = `\` "x" hex_digit hex_digit .
|
||||
var hex_byte_value = parse.StringFrom(
|
||||
parse.String(`\x`),
|
||||
hex_digit, hex_digit,
|
||||
)
|
||||
|
||||
// little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit .
|
||||
var little_u_value = parse.StringFrom(
|
||||
parse.String(`\u`),
|
||||
hex_digit, hex_digit,
|
||||
hex_digit, hex_digit,
|
||||
)
|
||||
|
||||
// big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit
|
||||
var big_u_value = parse.StringFrom(
|
||||
parse.String(`\U`),
|
||||
hex_digit, hex_digit, hex_digit, hex_digit,
|
||||
hex_digit, hex_digit, hex_digit, hex_digit,
|
||||
)
|
||||
|
||||
// escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
|
||||
var escaped_char = parse.StringFrom(
|
||||
parse.Rune('\\'),
|
||||
parse.Any(
|
||||
parse.Rune('a'),
|
||||
parse.Rune('b'),
|
||||
parse.Rune('f'),
|
||||
parse.Rune('n'),
|
||||
parse.Rune('r'),
|
||||
parse.Rune('t'),
|
||||
parse.Rune('v'),
|
||||
parse.Rune('\\'),
|
||||
parse.Rune('\''),
|
||||
parse.Rune('"'),
|
||||
),
|
||||
)
|
||||
|
||||
// https://go.dev/ref/spec#String_literals
|
||||
|
||||
var string_lit = parse.Any(parse.String(`""`), parse.String(`''`), interpreted_string_lit, raw_string_lit)
|
||||
|
||||
var interpreted_string_lit = parse.StringFrom(
|
||||
parse.Rune('"'),
|
||||
parse.StringFrom(parse.Until(
|
||||
parse.Any(unicode_value_interpreted, byte_value),
|
||||
parse.Rune('"'),
|
||||
)),
|
||||
parse.Rune('"'),
|
||||
)
|
||||
var unicode_value_interpreted = parse.Any(little_u_value, big_u_value, escaped_char, parse.RuneNotIn("\n\""))
|
||||
|
||||
var raw_string_lit = parse.StringFrom(
|
||||
parse.Rune('`'),
|
||||
parse.StringFrom(parse.Until(
|
||||
unicode_value_raw,
|
||||
parse.Rune('`'),
|
||||
)),
|
||||
parse.Rune('`'),
|
||||
)
|
||||
var unicode_value_raw = parse.Any(little_u_value, big_u_value, escaped_char, parse.RuneNotIn("`"))
|
252
templ/parser/v2/expressionparser_test.go
Normal file
252
templ/parser/v2/expressionparser_test.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// # List of situations where a templ file could contain braces.
|
||||
|
||||
// Inside a HTML attribute.
|
||||
// <a style="font-family: { arial }">That does not make sense, but still...</a>
|
||||
|
||||
// Inside a script tag.
|
||||
// <script>var value = { test: 123 };</script>
|
||||
|
||||
// Inside a templ definition expression.
|
||||
// { templ Name(data map[string]any) }
|
||||
|
||||
// Inside a templ script.
|
||||
// { script Name(data map[string]any) }
|
||||
// { something }
|
||||
// { endscript }
|
||||
|
||||
// Inside a call to a template, passing some data.
|
||||
// {! localisations(map[string]any { "key": 123 }) }
|
||||
|
||||
// Inside a string.
|
||||
// {! localisations("\"value{'data'}") }
|
||||
|
||||
// Inside a tick string.
|
||||
// {! localisations(`value{'data'}`) }
|
||||
|
||||
// Parser logic...
|
||||
// Read until ( ` | " | { | } | EOL/EOF )
|
||||
// If " handle any escaped quotes or ticks until the end of the string.
|
||||
// If ` read until the closing tick.
|
||||
// If { increment the brace count up
|
||||
// If } increment the brace count down
|
||||
// If brace count == 0, break
|
||||
// If EOL, break
|
||||
// If EOF, break
|
||||
// If brace count != 0 throw an error
|
||||
|
||||
func TestRuneLiterals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "rune literal with escaped newline",
|
||||
input: `'\n' `,
|
||||
expected: `'\n'`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, ok, err := rune_lit.Parse(parse.NewInput(tt.input))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringLiterals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "string literal with escaped newline",
|
||||
input: `"\n" `,
|
||||
expected: `"\n"`,
|
||||
},
|
||||
{
|
||||
name: "raw literal with \n",
|
||||
input: "`\\n` ",
|
||||
expected: "`\\n`",
|
||||
},
|
||||
{
|
||||
name: "empty single quote string",
|
||||
input: `'' `,
|
||||
expected: `''`,
|
||||
},
|
||||
{
|
||||
name: "empty double quote string",
|
||||
input: `"" `,
|
||||
expected: `""`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, ok, err := string_lit.Parse(parse.NewInput(tt.input))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpressions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
prefix string
|
||||
startBraceCount int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "templ: no parameters",
|
||||
input: "{ templ TemplName() }\n",
|
||||
prefix: "{ templ ",
|
||||
startBraceCount: 1,
|
||||
expected: "TemplName()",
|
||||
},
|
||||
{
|
||||
name: "templ: string parameter",
|
||||
input: `{ templ TemplName(a string) }`,
|
||||
prefix: "{ templ ",
|
||||
startBraceCount: 1,
|
||||
expected: `TemplName(a string)`,
|
||||
},
|
||||
{
|
||||
name: "templ: map parameter",
|
||||
input: `{ templ TemplName(data map[string]any) }`,
|
||||
prefix: "{ templ ",
|
||||
startBraceCount: 1,
|
||||
expected: `TemplName(data map[string]any)`,
|
||||
},
|
||||
{
|
||||
name: "call: string parameter",
|
||||
input: `{! Header("test") }`,
|
||||
prefix: "{! ",
|
||||
startBraceCount: 1,
|
||||
expected: `Header("test")`,
|
||||
},
|
||||
{
|
||||
name: "call: string parameter with escaped values and mismatched braces",
|
||||
input: `{! Header("\"}}") }`,
|
||||
prefix: "{! ",
|
||||
startBraceCount: 1,
|
||||
expected: `Header("\"}}")`,
|
||||
},
|
||||
{
|
||||
name: "call: string parameter, with rune literals",
|
||||
input: `{! Header('\"') }`,
|
||||
prefix: "{! ",
|
||||
startBraceCount: 1,
|
||||
expected: `Header('\"')`,
|
||||
},
|
||||
{
|
||||
name: "call: map literal",
|
||||
input: `{! Header(map[string]any{ "test": 123 }) }`,
|
||||
prefix: "{! ",
|
||||
startBraceCount: 1,
|
||||
expected: `Header(map[string]any{ "test": 123 })`,
|
||||
},
|
||||
{
|
||||
name: "call: rune and map literal",
|
||||
input: `{! Header('\"', map[string]any{ "test": 123 }) }`,
|
||||
prefix: "{! ",
|
||||
startBraceCount: 1,
|
||||
expected: `Header('\"', map[string]any{ "test": 123 })`,
|
||||
},
|
||||
{
|
||||
name: "if: function call",
|
||||
input: `{ if findOut("}") }`,
|
||||
prefix: "{ if ",
|
||||
startBraceCount: 1,
|
||||
expected: `findOut("}")`,
|
||||
},
|
||||
{
|
||||
name: "if: function call, tricky string/rune params",
|
||||
input: `{ if findOut("}", '}', '\'') }`,
|
||||
prefix: "{ if ",
|
||||
startBraceCount: 1,
|
||||
expected: `findOut("}", '}', '\'')`,
|
||||
},
|
||||
{
|
||||
name: "if: function call, function param",
|
||||
input: `{ if findOut(func() bool { return true }) }`,
|
||||
prefix: "{ if ",
|
||||
startBraceCount: 1,
|
||||
expected: `findOut(func() bool { return true })`,
|
||||
},
|
||||
{
|
||||
name: "attribute value: simple string",
|
||||
// Used to be {%= "data" %}, but can be simplified, since the position
|
||||
// of the node in the document defines how it can be used.
|
||||
// As an attribute value, it must be a Go expression that returns a string.
|
||||
input: `{ "data" }`,
|
||||
prefix: "{ ",
|
||||
startBraceCount: 1,
|
||||
expected: `"data"`,
|
||||
},
|
||||
{
|
||||
name: "javascript expression",
|
||||
input: "var x = 123;",
|
||||
prefix: "",
|
||||
startBraceCount: 0,
|
||||
expected: "var x = 123;",
|
||||
},
|
||||
{
|
||||
name: "javascript expression",
|
||||
input: `var x = "}";`,
|
||||
prefix: "",
|
||||
startBraceCount: 0,
|
||||
expected: `var x = "}";`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ep := &expressionParser{
|
||||
startBraceCount: tt.startBraceCount,
|
||||
}
|
||||
expr := tt.input[len(tt.prefix):]
|
||||
actual, ok, err := ep.Parse(parse.NewInput(expr))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
expected := Expression{
|
||||
Value: tt.expected,
|
||||
Range: Range{
|
||||
From: Position{0, 0, 0},
|
||||
To: Position{int64(len(tt.expected)), 0, uint32(len(tt.expected))},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
52
templ/parser/v2/forexpressionparser.go
Normal file
52
templ/parser/v2/forexpressionparser.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
var forExpression parse.Parser[Node] = forExpressionParser{}
|
||||
|
||||
type forExpressionParser struct{}
|
||||
|
||||
func (forExpressionParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
var r ForExpression
|
||||
start := pi.Index()
|
||||
|
||||
// Strip leading whitespace and look for `for `.
|
||||
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
if !peekPrefix(pi, "for ") {
|
||||
pi.Seek(start)
|
||||
return r, false, nil
|
||||
}
|
||||
|
||||
// Parse the Go for expression.
|
||||
if r.Expression, err = parseGo("for", pi, goexpression.For); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
// Eat " {\n".
|
||||
if _, ok, err = parse.All(openBraceWithOptionalPadding, parse.NewLine).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
// Node contents.
|
||||
tnp := newTemplateNodeParser(closeBraceWithOptionalPadding, "for expression closing brace")
|
||||
var nodes Nodes
|
||||
if nodes, ok, err = tnp.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("for: expected nodes, but none were found", pi.Position())
|
||||
return
|
||||
}
|
||||
r.Children = nodes.Nodes
|
||||
|
||||
// Read the required closing brace.
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("for: "+unterminatedMissingEnd, pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
}
|
162
templ/parser/v2/forexpressionparser_test.go
Normal file
162
templ/parser/v2/forexpressionparser_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestForExpressionParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "for: simple",
|
||||
input: `for _, item := range p.Items {
|
||||
<div>{ item }</div>
|
||||
}`,
|
||||
expected: ForExpression{
|
||||
Expression: Expression{
|
||||
Value: `_, item := range p.Items`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 28,
|
||||
Line: 0,
|
||||
Col: 28,
|
||||
},
|
||||
},
|
||||
},
|
||||
Children: []Node{
|
||||
Whitespace{Value: "\t\t\t\t\t"},
|
||||
Element{
|
||||
Name: "div",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 37, Line: 1, Col: 6},
|
||||
To: Position{Index: 40, Line: 1, Col: 9},
|
||||
},
|
||||
Children: []Node{
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `item`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 43,
|
||||
Line: 1,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 47,
|
||||
Line: 1,
|
||||
Col: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "for: simple, without spaces",
|
||||
input: `for _, item := range p.Items{
|
||||
<div>{ item }</div>
|
||||
}`,
|
||||
expected: ForExpression{
|
||||
Expression: Expression{
|
||||
Value: `_, item := range p.Items`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 4,
|
||||
Line: 0,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 28,
|
||||
Line: 0,
|
||||
Col: 28,
|
||||
},
|
||||
},
|
||||
},
|
||||
Children: []Node{
|
||||
Whitespace{Value: "\t\t\t\t\t"},
|
||||
Element{
|
||||
Name: "div",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 36, Line: 1, Col: 6},
|
||||
To: Position{Index: 39, Line: 1, Col: 9},
|
||||
},
|
||||
Children: []Node{
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `item`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 42,
|
||||
Line: 1,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 46,
|
||||
Line: 1,
|
||||
Col: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
actual, ok, err := forExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncompleteFor(t *testing.T) {
|
||||
t.Run("no opening brace", func(t *testing.T) {
|
||||
input := parse.NewInput(`for with no brace is ignored`)
|
||||
_, ok, err := forExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected a non match, but got a match")
|
||||
}
|
||||
})
|
||||
t.Run("capitalised For", func(t *testing.T) {
|
||||
input := parse.NewInput(`For with no brace`)
|
||||
_, ok, err := forExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected a non match, but got a match")
|
||||
}
|
||||
})
|
||||
}
|
45
templ/parser/v2/format_test.go
Normal file
45
templ/parser/v2/format_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
func TestFormatting(t *testing.T) {
|
||||
files, _ := filepath.Glob("formattestdata/*.txt")
|
||||
if len(files) == 0 {
|
||||
t.Errorf("no test files found")
|
||||
}
|
||||
for _, file := range files {
|
||||
t.Run(filepath.Base(file), func(t *testing.T) {
|
||||
a, err := txtar.ParseFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(a.Files) != 2 {
|
||||
t.Fatalf("expected 2 files, got %d", len(a.Files))
|
||||
}
|
||||
tem, err := ParseString(clean(a.Files[0].Data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var actual bytes.Buffer
|
||||
if err := tem.Write(&actual); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(string(a.Files[1].Data), actual.String()); diff != "" {
|
||||
t.Fatalf("%s:\n%s", file, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func clean(b []byte) string {
|
||||
b = bytes.ReplaceAll(b, []byte("$\n"), []byte("\n"))
|
||||
b = bytes.TrimSuffix(b, []byte("\n"))
|
||||
return string(b)
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div><p>{ "the" }<a href="http://example.com">{ "data" }
|
||||
</a></p></div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<p>
|
||||
{ "the" }
|
||||
<a href="http://example.com">
|
||||
{ "data" }
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div><p>{ "the" }<a href="http://example.com">{ "data" } </a></p></div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div><p>{ "the" }<a href="http://example.com">{ "data" } </a></p></div>
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<div>Linebreaks<br/>used<br/>for<br/>spacing</div>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<div>Linebreaks<br/>used<br/>for<br/>spacing</div>
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<div>
|
||||
Linebreaks<br/>and<hr/>rules<br/>for<br/>spacing
|
||||
</div>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<div>
|
||||
Linebreaks
|
||||
<br/>
|
||||
and
|
||||
<hr/>
|
||||
rules
|
||||
<br/>
|
||||
for
|
||||
<br/>
|
||||
spacing
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div><p>{ "the" }<a href="http://example.com">{ "data" }</a></p>
|
||||
</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<p>{ "the" }<a href="http://example.com">{ "data" }</a></p>
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<p>{ "the" }<a href="http://example.com">{ "data" }</a></p></div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<p>{ "the" }<a href="http://example.com">{ "data" }</a></p>
|
||||
</div>
|
||||
}
|
24
templ/parser/v2/formattestdata/comments_are_preserved.txt
Normal file
24
templ/parser/v2/formattestdata/comments_are_preserved.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<!-- This is a comment -->
|
||||
// This is not included in the output.
|
||||
<div>Some standard templ</div>
|
||||
/* This is not included in the output too. */
|
||||
/*
|
||||
Leave this alone.
|
||||
*/
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<!-- This is a comment -->
|
||||
// This is not included in the output.
|
||||
<div>Some standard templ</div>
|
||||
/* This is not included in the output too. */
|
||||
/*
|
||||
Leave this alone.
|
||||
*/
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
}
|
||||
>
|
||||
Content</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div
|
||||
id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
}
|
||||
>
|
||||
Content
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div id="conditional" if addClass {
|
||||
class="itWasTrue"
|
||||
}
|
||||
width="300">Content</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div
|
||||
id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
}
|
||||
width="300"
|
||||
>Content</div>
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
}
|
||||
width="300">Content</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div
|
||||
id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
}
|
||||
width="300"
|
||||
>Content</div>
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
} else {
|
||||
class="itWasNotTrue"
|
||||
}
|
||||
width="300">Content</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ conditionalAttributes(addClass bool) {
|
||||
<div
|
||||
id="conditional"
|
||||
if addClass {
|
||||
class="itWasTrue"
|
||||
} else {
|
||||
class="itWasNotTrue"
|
||||
}
|
||||
width="300"
|
||||
>Content</div>
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ nested() {
|
||||
<div class="double">double</div>
|
||||
<div class='single-not-required'>single-not-required</div>
|
||||
<div data-value='{"data":"value"}'>single-required</div>
|
||||
}
|
||||
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ nested() {
|
||||
<div class="double">double</div>
|
||||
<div class="single-not-required">single-not-required</div>
|
||||
<div data-value='{"data":"value"}'>single-required</div>
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
css ClassName() {
|
||||
background-color: #ffffff;
|
||||
color: { constants.White };
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
css ClassName() {
|
||||
background-color: #ffffff;
|
||||
color: { constants.White };
|
||||
}
|
15
templ/parser/v2/formattestdata/css_whitespace_is_tidied.txt
Normal file
15
templ/parser/v2/formattestdata/css_whitespace_is_tidied.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
css ClassName() {
|
||||
background-color : #ffffff ;
|
||||
color : { constants.White };
|
||||
}
|
||||
|
||||
-- out --
|
||||
package test
|
||||
|
||||
css ClassName() {
|
||||
background-color: #ffffff;
|
||||
color: { constants.White };
|
||||
}
|
24
templ/parser/v2/formattestdata/cssarguments_multiline.txt
Normal file
24
templ/parser/v2/formattestdata/cssarguments_multiline.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
css Style(
|
||||
a string,
|
||||
b string,
|
||||
c string,
|
||||
) {
|
||||
color: {a};
|
||||
background-color: {b};
|
||||
border-color: {c};
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
css Style(
|
||||
a string,
|
||||
b string,
|
||||
c string,
|
||||
) {
|
||||
color: { a };
|
||||
background-color: { b };
|
||||
border-color: { c };
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<p>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<p></p>
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(items []string) {
|
||||
<div>{ "the" }<div>{ "other" }</div>for _, item := range items {
|
||||
<div>{ item }</div>
|
||||
}</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(items []string) {
|
||||
<div>
|
||||
{ "the" }
|
||||
<div>{ "other" }</div>
|
||||
for _, item := range items {
|
||||
<div>{ item }</div>
|
||||
}
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ nested() {
|
||||
<div>{ "the" }<div>{ "other" }</div></div>
|
||||
}
|
||||
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ nested() {
|
||||
<div>{ "the" }<div>{ "other" }</div></div>
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
type Link struct {
|
||||
Name string
|
||||
Url string
|
||||
}
|
||||
|
||||
var a = false;
|
||||
|
||||
func test() {
|
||||
log.Print("hoi")
|
||||
|
||||
if (a) {
|
||||
log.Fatal("OH NO !")
|
||||
}
|
||||
}
|
||||
|
||||
templ x() {
|
||||
<div>Hello World</div>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
type Link struct {
|
||||
Name string
|
||||
Url string
|
||||
}
|
||||
|
||||
var a = false
|
||||
|
||||
func test() {
|
||||
log.Print("hoi")
|
||||
|
||||
if a {
|
||||
log.Fatal("OH NO !")
|
||||
}
|
||||
}
|
||||
|
||||
templ x() {
|
||||
<div>Hello World</div>
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
// test the comment handling.
|
||||
templ test() {
|
||||
Test
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
// test the comment handling.
|
||||
templ test() {
|
||||
Test
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(items []string) {
|
||||
<div>{ "the" }<div>{ "other" }</div>if items != nil {
|
||||
<div>{ items[0] }</div>
|
||||
} else {
|
||||
<div>{ items[1] }</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(items []string) {
|
||||
<div>
|
||||
{ "the" }
|
||||
<div>{ "other" }</div>
|
||||
if items != nil {
|
||||
<div>{ items[0] }</div>
|
||||
} else {
|
||||
<div>{ items[1] }</div>
|
||||
}
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<p>
|
||||
In a flowing <strong>paragraph</strong>, you can use inline elements.
|
||||
These <strong>inline elements</strong> can be <strong>styled</strong>
|
||||
and are not placed on new lines.
|
||||
</p>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ test() {
|
||||
<p>
|
||||
In a flowing <strong>paragraph</strong>, you can use inline elements.
|
||||
These <strong>inline elements</strong> can be <strong>styled</strong>
|
||||
and are not placed on new lines.
|
||||
</p>
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
@something(`Hi
|
||||
some cool text
|
||||
|
||||
foo
|
||||
|
||||
bar
|
||||
|
||||
`)
|
||||
@something(`
|
||||
something
|
||||
`,
|
||||
`
|
||||
something
|
||||
`,
|
||||
)
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
@something(`Hi
|
||||
some cool text
|
||||
|
||||
foo
|
||||
|
||||
bar
|
||||
|
||||
`)
|
||||
@something(`
|
||||
something
|
||||
`,
|
||||
`
|
||||
something
|
||||
`,
|
||||
)
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div><div><b>Text</b></div></div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div><div><b>Text</b></div></div>
|
||||
}
|
12
templ/parser/v2/formattestdata/raw_go_is_formatted.txt
Normal file
12
templ/parser/v2/formattestdata/raw_go_is_formatted.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ nameList(items []Item) {
|
||||
{{ first := items[0] }}
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ nameList(items []Item) {
|
||||
{{ first := items[0] }}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<script src="https://example.com/myscript.js"></script>
|
||||
}
|
||||
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<script src="https://example.com/myscript.js"></script>
|
||||
}
|
20
templ/parser/v2/formattestdata/scriptarguments_multiline.txt
Normal file
20
templ/parser/v2/formattestdata/scriptarguments_multiline.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
script Style(
|
||||
a string,
|
||||
b string,
|
||||
c string,
|
||||
) {
|
||||
console.log(a, b, c);
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
script Style(
|
||||
a string,
|
||||
b string,
|
||||
c string,
|
||||
) {
|
||||
console.log(a, b, c);
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<div>{firstName} {lastName}</div>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<div>{ firstName } { lastName }</div>
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<div>{pt1}{pt2}</div>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<div>{ pt1 }{ pt2 }</div>
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<div>{firstName...} {lastName...}</div>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<div>{ firstName... } { lastName... }</div>
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(items []string) {
|
||||
<div>{ "the" }<div>{ "other" }</div>switch items[0] {
|
||||
case "a":
|
||||
<div>{ items[0] }</div>
|
||||
case "b":
|
||||
<div>{ items[1] }</div>
|
||||
}</div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(items []string) {
|
||||
<div>
|
||||
{ "the" }
|
||||
<div>{ "other" }</div>
|
||||
switch items[0] {
|
||||
case "a":
|
||||
<div>{ items[0] }</div>
|
||||
case "b":
|
||||
<div>{ items[1] }</div>
|
||||
}
|
||||
</div>
|
||||
}
|
30
templ/parser/v2/formattestdata/tables_are_formatted_well.txt
Normal file
30
templ/parser/v2/formattestdata/tables_are_formatted_well.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ table(accountNumber string, registration string) {
|
||||
<table>
|
||||
<tr>
|
||||
<th width="20%">Your account number</th>
|
||||
<td width="80%">{ accountNumber }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Registration</td>
|
||||
<td>{ strings.ToUpper(registration) }</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ table(accountNumber string, registration string) {
|
||||
<table>
|
||||
<tr>
|
||||
<th width="20%">Your account number</th>
|
||||
<td width="80%">{ accountNumber }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Registration</td>
|
||||
<td>{ strings.ToUpper(registration) }</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ x(id string, class string) {
|
||||
<button
|
||||
id={id}
|
||||
name={
|
||||
"name"
|
||||
}
|
||||
class={
|
||||
"blue",
|
||||
class,
|
||||
map[string]bool{
|
||||
"a": true,
|
||||
},
|
||||
}
|
||||
></button>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ x(id string, class string) {
|
||||
<button
|
||||
id={ id }
|
||||
name={ "name" }
|
||||
class={
|
||||
"blue",
|
||||
class,
|
||||
map[string]bool{
|
||||
"a": true,
|
||||
},
|
||||
}
|
||||
></button>
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
-- in --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<li>
|
||||
<a href="/">
|
||||
Home
|
||||
@hello("home") {
|
||||
data
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
-- out --
|
||||
package main
|
||||
|
||||
templ x() {
|
||||
<li>
|
||||
<a href="/">
|
||||
Home
|
||||
@hello("home") {
|
||||
data
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ List(
|
||||
list1 list[item],
|
||||
list2 list[item],
|
||||
list3 list[item],
|
||||
) {
|
||||
<div></div>
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ List(
|
||||
list1 list[item],
|
||||
list2 list[item],
|
||||
list3 list[item],
|
||||
) {
|
||||
<div></div>
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
-- in --
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
||||
|
||||
-- out --
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
-- in --
|
||||
// Go comment
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
||||
-- out --
|
||||
// Go comment
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
-- in --
|
||||
// Go comment
|
||||
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
||||
-- out --
|
||||
// Go comment
|
||||
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
-- in --
|
||||
/********************
|
||||
* multiline message *
|
||||
********************/
|
||||
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
||||
-- out --
|
||||
/********************
|
||||
* multiline message *
|
||||
********************/
|
||||
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
-- in --
|
||||
// Go comment
|
||||
|
||||
/* Multiline comment on a single line */
|
||||
|
||||
/*
|
||||
|
||||
Multi-line comment on multiple lines
|
||||
|
||||
*/
|
||||
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
||||
-- out --
|
||||
// Go comment
|
||||
|
||||
/* Multiline comment on a single line */
|
||||
|
||||
/*
|
||||
|
||||
Multi-line comment on multiple lines
|
||||
|
||||
*/
|
||||
|
||||
package goof
|
||||
|
||||
templ Hello() {
|
||||
Hello
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
-- in --
|
||||
//go:build dev
|
||||
|
||||
package p
|
||||
|
||||
-- out --
|
||||
//go:build dev
|
||||
|
||||
package p
|
||||
|
@@ -0,0 +1,34 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(
|
||||
p.Test,
|
||||
"s",
|
||||
){
|
||||
@another.Component(
|
||||
p.Test,
|
||||
3,
|
||||
"s",
|
||||
){
|
||||
<p>hello</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(
|
||||
p.Test,
|
||||
"s",
|
||||
) {
|
||||
@another.Component(
|
||||
p.Test,
|
||||
3,
|
||||
"s",
|
||||
) {
|
||||
<p>hello</p>
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(
|
||||
p.Test,
|
||||
"s",
|
||||
){
|
||||
<p>hello</p>
|
||||
}
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(
|
||||
p.Test,
|
||||
"s",
|
||||
) {
|
||||
<p>hello</p>
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
<div>
|
||||
@Other(
|
||||
p.Test,
|
||||
"s",
|
||||
)
|
||||
</div>
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
<div>
|
||||
@Other(
|
||||
p.Test,
|
||||
"s",
|
||||
)
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(p.Test,"s")
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(p.Test, "s")
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(p.Test, "s"){
|
||||
<p>hello</p>
|
||||
}
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(p.Test, "s") {
|
||||
<p>hello</p>
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
<div>
|
||||
@Other(p.Test, "s")
|
||||
</div>
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
<div>
|
||||
@Other(p.Test, "s")
|
||||
</div>
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
-- in --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(p.Test)
|
||||
}
|
||||
-- out --
|
||||
package p
|
||||
|
||||
templ f() {
|
||||
@Other(p.Test)
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<area>
|
||||
<area></area>
|
||||
<base>
|
||||
<base></base>
|
||||
<br>
|
||||
<br></br>
|
||||
<col>
|
||||
<col></col>
|
||||
<command>
|
||||
<command></command>
|
||||
<embed>
|
||||
<embed></embed>
|
||||
<hr>
|
||||
<hr></hr>
|
||||
<img>
|
||||
<img></img>
|
||||
<input>
|
||||
<input></input>
|
||||
<input>Text
|
||||
<input>Text</input>
|
||||
<keygen>
|
||||
<keygen></keygen>
|
||||
<link>
|
||||
<link></link>
|
||||
<meta>
|
||||
<meta></meta>
|
||||
<param>
|
||||
<param></param>
|
||||
<source>
|
||||
<source></source>
|
||||
<track>
|
||||
<track></track>
|
||||
<wbr>
|
||||
<wbr></wbr>
|
||||
}
|
||||
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<area/>
|
||||
<area/>
|
||||
<base/>
|
||||
<base/>
|
||||
<br/>
|
||||
<br/>
|
||||
<col/>
|
||||
<col/>
|
||||
<command/>
|
||||
<command/>
|
||||
<embed/>
|
||||
<embed/>
|
||||
<hr/>
|
||||
<hr/>
|
||||
<img/>
|
||||
<img/>
|
||||
<input/>
|
||||
<input/>
|
||||
<input/>Text
|
||||
<input/>Text
|
||||
<keygen/>
|
||||
<keygen/>
|
||||
<link/>
|
||||
<link/>
|
||||
<meta/>
|
||||
<meta/>
|
||||
<param/>
|
||||
<param/>
|
||||
<source/>
|
||||
<source/>
|
||||
<track/>
|
||||
<track/>
|
||||
<wbr/>
|
||||
<wbr/>
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
-- in --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<div><b>Text</b></div></div>
|
||||
}
|
||||
-- out --
|
||||
package test
|
||||
|
||||
templ input(value, validation string) {
|
||||
<div>
|
||||
<div><b>Text</b></div>
|
||||
</div>
|
||||
}
|
2
templ/parser/v2/fuzz.sh
Executable file
2
templ/parser/v2/fuzz.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
echo Element
|
||||
go test -fuzz=FuzzElement -fuzztime=120s
|
45
templ/parser/v2/gocodeparser.go
Normal file
45
templ/parser/v2/gocodeparser.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
var goCode = parse.Func(func(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
// Check the prefix first.
|
||||
if _, ok, err = parse.Or(parse.String("{{ "), parse.String("{{")).Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once we have a prefix, we must have an expression that returns a string, with optional err.
|
||||
l := pi.Position().Line
|
||||
var r GoCode
|
||||
if r.Expression, err = parseGo("go code", pi, goexpression.Expression); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
if l != pi.Position().Line {
|
||||
r.Multiline = true
|
||||
}
|
||||
|
||||
// Clear any optional whitespace.
|
||||
_, _, _ = parse.OptionalWhitespace.Parse(pi)
|
||||
|
||||
// }}
|
||||
if _, ok, err = dblCloseBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("go code: missing close braces", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Parse trailing whitespace.
|
||||
ws, _, err := parse.Whitespace.Parse(pi)
|
||||
if err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
r.TrailingSpace, err = NewTrailingSpace(ws)
|
||||
if err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
137
templ/parser/v2/gocodeparser_test.go
Normal file
137
templ/parser/v2/gocodeparser_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestGoCodeParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected GoCode
|
||||
}{
|
||||
{
|
||||
name: "basic expression",
|
||||
input: `{{ p := "this" }}`,
|
||||
expected: GoCode{
|
||||
Expression: Expression{
|
||||
Value: `p := "this"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 14,
|
||||
Line: 0,
|
||||
Col: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic expression, no space",
|
||||
input: `{{p:="this"}}`,
|
||||
expected: GoCode{
|
||||
Expression: Expression{
|
||||
Value: `p:="this"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 2,
|
||||
Line: 0,
|
||||
Col: 2,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiline function decl",
|
||||
input: `{{
|
||||
p := func() {
|
||||
dosomething()
|
||||
}
|
||||
}}`,
|
||||
expected: GoCode{
|
||||
Expression: Expression{
|
||||
Value: `
|
||||
p := func() {
|
||||
dosomething()
|
||||
}`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 2,
|
||||
Line: 0,
|
||||
Col: 2,
|
||||
},
|
||||
To: Position{
|
||||
Index: 45,
|
||||
Line: 3,
|
||||
Col: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
Multiline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comments in expression",
|
||||
input: `{{
|
||||
one := "one"
|
||||
two := "two"
|
||||
// Comment in middle of expression.
|
||||
four := "four"
|
||||
// Comment at end of expression.
|
||||
}}`,
|
||||
expected: GoCode{
|
||||
Expression: Expression{
|
||||
Value: `
|
||||
one := "one"
|
||||
two := "two"
|
||||
// Comment in middle of expression.
|
||||
four := "four"
|
||||
// Comment at end of expression.`,
|
||||
Range: Range{
|
||||
From: Position{Index: 2, Line: 0, Col: 2},
|
||||
To: Position{Index: 117, Line: 5, Col: 33},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceNone,
|
||||
Multiline: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
an, ok, err := goCode.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
actual := an.(GoCode)
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
|
||||
// Check the index.
|
||||
cut := tt.input[actual.Expression.Range.From.Index:actual.Expression.Range.To.Index]
|
||||
if tt.expected.Expression.Value != cut {
|
||||
t.Errorf("range, expected %q, got %q", tt.expected.Expression.Value, cut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
63
templ/parser/v2/gocommentparser.go
Normal file
63
templ/parser/v2/gocommentparser.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
var goSingleLineCommentStart = parse.String("//")
|
||||
var goSingleLineCommentEnd = parse.Any(parse.NewLine, parse.EOF[string]())
|
||||
|
||||
type goSingleLineCommentParser struct {
|
||||
}
|
||||
|
||||
var goSingleLineComment = goSingleLineCommentParser{}
|
||||
|
||||
func (p goSingleLineCommentParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
// Comment start.
|
||||
var c GoComment
|
||||
if _, ok, err = goSingleLineCommentStart.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
// Once we've got the comment start sequence, parse anything until the end
|
||||
// sequence as the comment contents.
|
||||
if c.Contents, ok, err = parse.StringUntil(goSingleLineCommentEnd).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("expected end comment literal '\n' not found", pi.Position())
|
||||
return
|
||||
}
|
||||
// Move past the end element.
|
||||
_, _, _ = goSingleLineCommentEnd.Parse(pi)
|
||||
// Return the comment.
|
||||
c.Multiline = false
|
||||
return c, true, nil
|
||||
}
|
||||
|
||||
var goMultiLineCommentStart = parse.String("/*")
|
||||
var goMultiLineCommentEnd = parse.String("*/")
|
||||
|
||||
type goMultiLineCommentParser struct {
|
||||
}
|
||||
|
||||
var goMultiLineComment = goMultiLineCommentParser{}
|
||||
|
||||
func (p goMultiLineCommentParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
// Comment start.
|
||||
start := pi.Position()
|
||||
var c GoComment
|
||||
if _, ok, err = goMultiLineCommentStart.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once we've got the comment start sequence, parse anything until the end
|
||||
// sequence as the comment contents.
|
||||
if c.Contents, ok, err = parse.StringUntil(goMultiLineCommentEnd).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("expected end comment literal '*/' not found", start)
|
||||
return
|
||||
}
|
||||
// Move past the end element.
|
||||
_, _, _ = goMultiLineCommentEnd.Parse(pi)
|
||||
// Return the comment.
|
||||
c.Multiline = true
|
||||
return c, true, nil
|
||||
}
|
||||
|
||||
var goComment = parse.Any(goSingleLineComment, goMultiLineComment)
|
101
templ/parser/v2/gocommentparser_test.go
Normal file
101
templ/parser/v2/gocommentparser_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestGoCommentParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected GoComment
|
||||
}{
|
||||
{
|
||||
name: "single line can have a newline at the end",
|
||||
input: `// single line comment
|
||||
`,
|
||||
expected: GoComment{
|
||||
Contents: " single line comment",
|
||||
Multiline: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single line comments can terminate the file",
|
||||
input: `// single line comment`,
|
||||
expected: GoComment{
|
||||
Contents: " single line comment",
|
||||
Multiline: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiline comments can be on one line",
|
||||
input: `/* multiline comment, on one line */`,
|
||||
expected: GoComment{
|
||||
Contents: " multiline comment, on one line ",
|
||||
Multiline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiline comments can span lines",
|
||||
input: `/* multiline comment,
|
||||
on multiple lines */`,
|
||||
expected: GoComment{
|
||||
Contents: " multiline comment,\non multiple lines ",
|
||||
Multiline: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := goComment.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentParserErrors(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "unclosed multi-line Go comments result in an error",
|
||||
input: `/* unclosed Go comment`,
|
||||
expected: parse.Error("expected end comment literal '*/' not found",
|
||||
parse.Position{
|
||||
Index: 0,
|
||||
Line: 0,
|
||||
Col: 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "single-line Go comment with no newline is allowed",
|
||||
input: `// Comment with no newline`,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
_, _, err := goComment.Parse(input)
|
||||
if diff := cmp.Diff(tt.expected, err); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
18
templ/parser/v2/goexpression/fuzz.sh
Executable file
18
templ/parser/v2/goexpression/fuzz.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
echo If
|
||||
go test -fuzz=FuzzIf -fuzztime=120s
|
||||
echo For
|
||||
go test -fuzz=FuzzFor -fuzztime=120s
|
||||
echo Switch
|
||||
go test -fuzz=FuzzSwitch -fuzztime=120s
|
||||
echo Case
|
||||
go test -fuzz=FuzzCaseStandard -fuzztime=120s
|
||||
echo Default
|
||||
go test -fuzz=FuzzCaseDefault -fuzztime=120s
|
||||
echo TemplExpression
|
||||
go test -fuzz=FuzzTemplExpression -fuzztime=120s
|
||||
echo Expression
|
||||
go test -fuzz=FuzzExpression -fuzztime=120s
|
||||
echo SliceArgs
|
||||
go test -fuzz=FuzzSliceArgs -fuzztime=120s
|
||||
echo Funcs
|
||||
go test -fuzz=FuzzFuncs -fuzztime=120s
|
343
templ/parser/v2/goexpression/parse.go
Normal file
343
templ/parser/v2/goexpression/parse.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package goexpression
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrContainerFuncNotFound = errors.New("parser error: templ container function not found")
|
||||
ErrExpectedNodeNotFound = errors.New("parser error: expected node not found")
|
||||
)
|
||||
|
||||
var defaultRegexp = regexp.MustCompile(`^default\s*:`)
|
||||
|
||||
func Case(content string) (start, end int, err error) {
|
||||
if !(strings.HasPrefix(content, "case ") || defaultRegexp.MatchString(content)) {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
prefix := "switch {\n"
|
||||
src := prefix + content
|
||||
start, end, err = extract(src, func(body []ast.Stmt) (start, end int, err error) {
|
||||
sw, ok := body[0].(*ast.SwitchStmt)
|
||||
if !ok {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
if sw.Body == nil || len(sw.Body.List) == 0 {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
stmt, ok := sw.Body.List[0].(*ast.CaseClause)
|
||||
if !ok {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
start = int(stmt.Case) - 1
|
||||
end = int(stmt.Colon)
|
||||
return start, end, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
// Since we added a `switch {` prefix, we need to remove it.
|
||||
start -= len(prefix)
|
||||
end -= len(prefix)
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func If(content string) (start, end int, err error) {
|
||||
if !strings.HasPrefix(content, "if") {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
return extract(content, func(body []ast.Stmt) (start, end int, err error) {
|
||||
stmt, ok := body[0].(*ast.IfStmt)
|
||||
if !ok {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
start = int(stmt.If) + len("if")
|
||||
end = latestEnd(start, stmt.Init, stmt.Cond)
|
||||
return start, end, nil
|
||||
})
|
||||
}
|
||||
|
||||
func For(content string) (start, end int, err error) {
|
||||
if !strings.HasPrefix(content, "for") {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
return extract(content, func(body []ast.Stmt) (start, end int, err error) {
|
||||
stmt := body[0]
|
||||
switch stmt := stmt.(type) {
|
||||
case *ast.ForStmt:
|
||||
start = int(stmt.For) + len("for")
|
||||
end = latestEnd(start, stmt.Init, stmt.Cond, stmt.Post)
|
||||
return start, end, nil
|
||||
case *ast.RangeStmt:
|
||||
start = int(stmt.For) + len("for")
|
||||
end = latestEnd(start, stmt.Key, stmt.Value, stmt.X)
|
||||
return start, end, nil
|
||||
}
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
})
|
||||
}
|
||||
|
||||
func Switch(content string) (start, end int, err error) {
|
||||
if !strings.HasPrefix(content, "switch") {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
return extract(content, func(body []ast.Stmt) (start, end int, err error) {
|
||||
stmt := body[0]
|
||||
switch stmt := stmt.(type) {
|
||||
case *ast.SwitchStmt:
|
||||
start = int(stmt.Switch) + len("switch")
|
||||
end = latestEnd(start, stmt.Init, stmt.Tag)
|
||||
return start, end, nil
|
||||
case *ast.TypeSwitchStmt:
|
||||
start = int(stmt.Switch) + len("switch")
|
||||
end = latestEnd(start, stmt.Init, stmt.Assign)
|
||||
return start, end, nil
|
||||
}
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
})
|
||||
}
|
||||
|
||||
func TemplExpression(src string) (start, end int, err error) {
|
||||
var s scanner.Scanner
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
errorHandler := func(pos token.Position, msg string) {
|
||||
err = fmt.Errorf("error parsing expression: %v", msg)
|
||||
}
|
||||
s.Init(file, []byte(src), errorHandler, scanner.ScanComments)
|
||||
|
||||
// Read chains of identifiers, e.g.:
|
||||
// components.Variable
|
||||
// components[0].Variable
|
||||
// components["name"].Function()
|
||||
// functionCall(withLots(), func() { return true })
|
||||
ep := NewExpressionParser()
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
stop, err := ep.Insert(pos, tok, lit)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, ep.End, nil
|
||||
}
|
||||
|
||||
func Expression(src string) (start, end int, err error) {
|
||||
var s scanner.Scanner
|
||||
fset := token.NewFileSet()
|
||||
file := fset.AddFile("", fset.Base(), len(src))
|
||||
errorHandler := func(pos token.Position, msg string) {
|
||||
err = fmt.Errorf("error parsing expression: %v", msg)
|
||||
}
|
||||
s.Init(file, []byte(src), errorHandler, scanner.ScanComments)
|
||||
|
||||
// Read chains of identifiers and constants up until RBRACE, e.g.:
|
||||
// true
|
||||
// 123.45 == true
|
||||
// components.Variable
|
||||
// components[0].Variable
|
||||
// components["name"].Function()
|
||||
// functionCall(withLots(), func() { return true })
|
||||
// !true
|
||||
parenDepth := 0
|
||||
bracketDepth := 0
|
||||
braceDepth := 0
|
||||
loop:
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break loop
|
||||
}
|
||||
switch tok {
|
||||
case token.LPAREN: // (
|
||||
parenDepth++
|
||||
case token.RPAREN: // )
|
||||
end = int(pos)
|
||||
parenDepth--
|
||||
case token.LBRACK: // [
|
||||
bracketDepth++
|
||||
case token.RBRACK: // ]
|
||||
end = int(pos)
|
||||
bracketDepth--
|
||||
case token.LBRACE: // {
|
||||
braceDepth++
|
||||
case token.RBRACE: // }
|
||||
braceDepth--
|
||||
if braceDepth < 0 {
|
||||
// We've hit the end of the expression.
|
||||
break loop
|
||||
}
|
||||
end = int(pos)
|
||||
case token.IDENT, token.INT, token.FLOAT, token.IMAG, token.CHAR, token.STRING:
|
||||
end = int(pos) + len(lit) - 1
|
||||
case token.SEMICOLON:
|
||||
continue
|
||||
case token.COMMENT:
|
||||
end = int(pos) + len(lit) - 1
|
||||
case token.ILLEGAL:
|
||||
return 0, 0, fmt.Errorf("illegal token: %v", lit)
|
||||
default:
|
||||
end = int(pos) + len(tok.String()) - 1
|
||||
}
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func SliceArgs(content string) (expr string, err error) {
|
||||
prefix := "package main\nvar templ_args = []any{"
|
||||
src := prefix + content + "}"
|
||||
|
||||
node, parseErr := parser.ParseFile(token.NewFileSet(), "", src, parser.AllErrors)
|
||||
if node == nil {
|
||||
return expr, parseErr
|
||||
}
|
||||
|
||||
var from, to int
|
||||
inspectFirstNode(node, func(n ast.Node) bool {
|
||||
decl, ok := n.(*ast.CompositeLit)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
from = int(decl.Lbrace)
|
||||
to = int(decl.Rbrace) - 1
|
||||
for _, e := range decl.Elts {
|
||||
to = int(e.End()) - 1
|
||||
}
|
||||
if to > int(decl.Rbrace)-1 {
|
||||
to = int(decl.Rbrace) - 1
|
||||
}
|
||||
betweenEndAndBrace := src[to : decl.Rbrace-1]
|
||||
var hasCodeBetweenEndAndBrace bool
|
||||
for _, r := range betweenEndAndBrace {
|
||||
if !unicode.IsSpace(r) {
|
||||
hasCodeBetweenEndAndBrace = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasCodeBetweenEndAndBrace {
|
||||
to = int(decl.Rbrace) - 1
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return src[from:to], err
|
||||
}
|
||||
|
||||
// Func returns the Go code up to the opening brace of the function body.
|
||||
func Func(content string) (name, expr string, err error) {
|
||||
prefix := "package main\n"
|
||||
src := prefix + content
|
||||
|
||||
node, parseErr := parser.ParseFile(token.NewFileSet(), "", src, parser.AllErrors)
|
||||
if node == nil {
|
||||
return name, expr, parseErr
|
||||
}
|
||||
|
||||
inspectFirstNode(node, func(n ast.Node) bool {
|
||||
// Find the first function declaration.
|
||||
fn, ok := n.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
start := int(fn.Pos()) + len("func")
|
||||
end := fn.Type.Params.End() - 1
|
||||
if len(src) < int(end) {
|
||||
err = errors.New("parser error: function identifier")
|
||||
return false
|
||||
}
|
||||
expr = strings.Clone(src[start:end])
|
||||
name = fn.Name.Name
|
||||
return false
|
||||
})
|
||||
|
||||
return name, expr, err
|
||||
}
|
||||
|
||||
func latestEnd(start int, nodes ...ast.Node) (end int) {
|
||||
end = start
|
||||
for _, n := range nodes {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
if int(n.End())-1 > end {
|
||||
end = int(n.End()) - 1
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func inspectFirstNode(node ast.Node, f func(ast.Node) bool) {
|
||||
var stop bool
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
if stop {
|
||||
return true
|
||||
}
|
||||
if f(n) {
|
||||
return true
|
||||
}
|
||||
stop = true
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// Extract a Go expression from the content.
|
||||
// The Go expression starts at "start" and ends at "end".
|
||||
// The reader should skip until "length" to pass over the expression and into the next
|
||||
// logical block.
|
||||
type Extractor func(body []ast.Stmt) (start, end int, err error)
|
||||
|
||||
func extract(content string, extractor Extractor) (start, end int, err error) {
|
||||
prefix := "package main\nfunc templ_container() {\n"
|
||||
src := prefix + content
|
||||
|
||||
node, parseErr := parser.ParseFile(token.NewFileSet(), "", src, parser.AllErrors)
|
||||
if node == nil {
|
||||
return 0, 0, parseErr
|
||||
}
|
||||
|
||||
var found bool
|
||||
inspectFirstNode(node, func(n ast.Node) bool {
|
||||
// Find the "templ_container" function.
|
||||
fn, ok := n.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if fn.Name == nil || fn.Name.Name != "templ_container" {
|
||||
err = ErrContainerFuncNotFound
|
||||
return false
|
||||
}
|
||||
if fn.Body == nil || len(fn.Body.List) == 0 {
|
||||
err = ErrExpectedNodeNotFound
|
||||
return false
|
||||
}
|
||||
found = true
|
||||
start, end, err = extractor(fn.Body.List)
|
||||
return false
|
||||
})
|
||||
if !found {
|
||||
return 0, 0, ErrExpectedNodeNotFound
|
||||
}
|
||||
|
||||
start -= len(prefix)
|
||||
end -= len(prefix)
|
||||
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
if start > end {
|
||||
start = end
|
||||
}
|
||||
|
||||
return start, end, err
|
||||
}
|
781
templ/parser/v2/goexpression/parse_test.go
Normal file
781
templ/parser/v2/goexpression/parse_test.go
Normal file
@@ -0,0 +1,781 @@
|
||||
package goexpression
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var ifTests = []testInput{
|
||||
{
|
||||
name: "basic if",
|
||||
input: `true`,
|
||||
},
|
||||
{
|
||||
name: "if function call",
|
||||
input: `pkg.Func()`,
|
||||
},
|
||||
{
|
||||
name: "compound",
|
||||
input: "x := val(); x > 3",
|
||||
},
|
||||
{
|
||||
name: "if multiple",
|
||||
input: `x && y && (!z)`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestIf(t *testing.T) {
|
||||
prefix := "if "
|
||||
suffixes := []string{
|
||||
"{\n<div>\nif true content\n\t</div>}",
|
||||
" {\n<div>\nif true content\n\t</div>}",
|
||||
}
|
||||
for _, test := range ifTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, prefix, suffix, If))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzIf(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"{\n<div>\nif true content\n\t</div>}",
|
||||
" {\n<div>\nif true content\n\t</div>}",
|
||||
}
|
||||
for _, test := range ifTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add("if " + test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
start, end, err := If(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
func panicIfInvalid(src string, start, end int) {
|
||||
_ = src[start:end]
|
||||
}
|
||||
|
||||
var forTests = []testInput{
|
||||
{
|
||||
name: "three component",
|
||||
input: `i := 0; i < 100; i++`,
|
||||
},
|
||||
{
|
||||
name: "three component, empty",
|
||||
input: `; ; i++`,
|
||||
},
|
||||
{
|
||||
name: "while",
|
||||
input: `n < 5`,
|
||||
},
|
||||
{
|
||||
name: "infinite",
|
||||
input: ``,
|
||||
},
|
||||
{
|
||||
name: "range with index",
|
||||
input: `k, v := range m`,
|
||||
},
|
||||
{
|
||||
name: "range with key only",
|
||||
input: `k := range m`,
|
||||
},
|
||||
{
|
||||
name: "channel receive",
|
||||
input: `x := range channel`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestFor(t *testing.T) {
|
||||
prefix := "for "
|
||||
suffixes := []string{
|
||||
" {\n<div>\nloop content\n\t</div>}",
|
||||
}
|
||||
for _, test := range forTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, prefix, suffix, For))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzFor(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
" {",
|
||||
" {}",
|
||||
" {\n<div>\nloop content\n\t</div>}",
|
||||
}
|
||||
for _, test := range forTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add("for " + test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
start, end, err := For(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
var switchTests = []testInput{
|
||||
{
|
||||
name: "switch",
|
||||
input: ``,
|
||||
},
|
||||
{
|
||||
name: "switch with expression",
|
||||
input: `x`,
|
||||
},
|
||||
{
|
||||
name: "switch with function call",
|
||||
input: `pkg.Func()`,
|
||||
},
|
||||
{
|
||||
name: "type switch",
|
||||
input: `x := x.(type)`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestSwitch(t *testing.T) {
|
||||
prefix := "switch "
|
||||
suffixes := []string{
|
||||
" {\ncase 1:\n\t<div>\n\tcase 2:\n\t\t<div>\n\tdefault:\n\t\t<div>\n\t</div>}",
|
||||
" {\ndefault:\n\t<div>\n\t</div>}",
|
||||
" {\n}",
|
||||
}
|
||||
for _, test := range switchTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, prefix, suffix, Switch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzSwitch(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
" {",
|
||||
" {}",
|
||||
" {\n<div>\nloop content\n\t</div>}",
|
||||
}
|
||||
for _, test := range switchTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add(test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
src := "switch " + s
|
||||
start, end, err := For(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
var caseTests = []testInput{
|
||||
{
|
||||
name: "case",
|
||||
input: `case 1:`,
|
||||
},
|
||||
{
|
||||
name: "case with expression",
|
||||
input: `case x > 3:`,
|
||||
},
|
||||
{
|
||||
name: "case with function call",
|
||||
input: `case pkg.Func():`,
|
||||
},
|
||||
{
|
||||
name: "case with multiple expressions",
|
||||
input: `case x > 3, x < 4:`,
|
||||
},
|
||||
{
|
||||
name: "case with multiple expressions and default",
|
||||
input: `case x > 3, x < 4, x == 5:`,
|
||||
},
|
||||
{
|
||||
name: "case with type switch",
|
||||
input: `case bool:`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCase(t *testing.T) {
|
||||
suffixes := []string{
|
||||
"\n<div>\ncase 1 content\n\t</div>\n\tcase 3:",
|
||||
"\ndefault:\n\t<div>\n\t</div>}",
|
||||
"\n}",
|
||||
}
|
||||
for _, test := range caseTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, "", suffix, Case))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzCaseStandard(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
"\n<div>\ncase 1 content\n\t</div>\n\tcase 3:",
|
||||
"\ndefault:\n\t<div>\n\t</div>}",
|
||||
"\n}",
|
||||
}
|
||||
for _, test := range caseTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add(test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
start, end, err := Case(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCaseDefault(t *testing.T) {
|
||||
suffixes := []string{
|
||||
"\n<div>\ncase 1 content\n\t</div>\n\tcase 3:",
|
||||
"\ncase:\n\t<div>\n\t</div>}",
|
||||
"\n}",
|
||||
}
|
||||
tests := []testInput{
|
||||
{
|
||||
name: "default",
|
||||
input: `default:`,
|
||||
},
|
||||
{
|
||||
name: "default with padding",
|
||||
input: `default :`,
|
||||
},
|
||||
{
|
||||
name: "default with padding",
|
||||
input: `default :`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, "", suffix, Case))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzCaseDefault(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
" ",
|
||||
"\n<div>\ncase 1 content\n\t</div>\n\tcase 3:",
|
||||
"\ncase:\n\t<div>\n\t</div>}",
|
||||
"\n}",
|
||||
}
|
||||
for _, suffix := range suffixes {
|
||||
f.Add("default:" + suffix)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
start, end, err := Case(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
var expressionTests = []testInput{
|
||||
{
|
||||
name: "string literal",
|
||||
input: `"hello"`,
|
||||
},
|
||||
{
|
||||
name: "string literal with escape",
|
||||
input: `"hello\n"`,
|
||||
},
|
||||
{
|
||||
name: "backtick string literal",
|
||||
input: "`hello`",
|
||||
},
|
||||
{
|
||||
name: "backtick string literal containing double quote",
|
||||
input: "`hello" + `"` + `world` + "`",
|
||||
},
|
||||
{
|
||||
name: "function call in package",
|
||||
input: `components.Other()`,
|
||||
},
|
||||
{
|
||||
name: "slice index call",
|
||||
input: `components[0].Other()`,
|
||||
},
|
||||
{
|
||||
name: "map index function call",
|
||||
input: `components["name"].Other()`,
|
||||
},
|
||||
{
|
||||
name: "function literal",
|
||||
input: `components["name"].Other(func() bool { return true })`,
|
||||
},
|
||||
{
|
||||
name: "multiline function call",
|
||||
input: `component(map[string]string{
|
||||
"namea": "name_a",
|
||||
"nameb": "name_b",
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "call with braces and brackets",
|
||||
input: `templates.New(test{}, other())`,
|
||||
},
|
||||
{
|
||||
name: "bare variable",
|
||||
input: `component`,
|
||||
},
|
||||
{
|
||||
name: "boolean expression",
|
||||
input: `direction == "newest"`,
|
||||
},
|
||||
{
|
||||
name: "boolean expression with parens",
|
||||
input: `len(data.previousPageUrl) == 0`,
|
||||
},
|
||||
{
|
||||
name: "string concat",
|
||||
input: `direction + "newest"`,
|
||||
},
|
||||
{
|
||||
name: "function call",
|
||||
input: `SplitRule(types.GroupMember{
|
||||
UserID: uuid.NewString(),
|
||||
Username: "user me",
|
||||
}, []types.GroupMember{
|
||||
{
|
||||
UserID: uuid.NewString(),
|
||||
Username: "user 1",
|
||||
},
|
||||
})`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestExpression(t *testing.T) {
|
||||
prefix := ""
|
||||
suffixes := []string{
|
||||
"",
|
||||
"}",
|
||||
"\t}",
|
||||
" }",
|
||||
}
|
||||
for _, test := range expressionTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, prefix, suffix, Expression))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var templExpressionTests = []testInput{
|
||||
{
|
||||
name: "function call in package",
|
||||
input: `components.Other()`,
|
||||
},
|
||||
{
|
||||
name: "slice index call",
|
||||
input: `components[0].Other()`,
|
||||
},
|
||||
{
|
||||
name: "map index function call",
|
||||
input: `components["name"].Other()`,
|
||||
},
|
||||
{
|
||||
name: "multiline chain call",
|
||||
input: `components.
|
||||
Other()`,
|
||||
},
|
||||
{
|
||||
name: "map index function call backtick literal",
|
||||
input: "components[`name" + `"` + "`].Other()",
|
||||
},
|
||||
{
|
||||
name: "function literal",
|
||||
input: `components["name"].Other(func() bool { return true })`,
|
||||
},
|
||||
{
|
||||
name: "multiline function call",
|
||||
input: `component(map[string]string{
|
||||
"namea": "name_a",
|
||||
"nameb": "name_b",
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with slice of complex types",
|
||||
input: `tabs([]TabData{
|
||||
{Name: "A"},
|
||||
{Name: "B"},
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with slice of explicitly named complex types",
|
||||
input: `tabs([]TabData{
|
||||
TabData{Name: "A"},
|
||||
TabData{Name: "B"},
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with empty slice of strings",
|
||||
input: `Inner([]string{})`,
|
||||
},
|
||||
{
|
||||
name: "function call with empty slice of maps",
|
||||
input: `Inner([]map[string]any{})`,
|
||||
},
|
||||
{
|
||||
name: "function call with empty slice of anon structs",
|
||||
input: `Inner([]map[string]struct{}{})`,
|
||||
},
|
||||
{
|
||||
name: "function call with slice of pointers to complex types",
|
||||
input: `tabs([]*TabData{
|
||||
&{Name: "A"},
|
||||
&{Name: "B"},
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with slice of pointers to explicitly named complex types",
|
||||
input: `tabs([]*TabData{
|
||||
&TabData{Name: "A"},
|
||||
&TabData{Name: "B"},
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with array of explicit length",
|
||||
input: `tabs([2]TabData{
|
||||
{Name: "A"},
|
||||
{Name: "B"},
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with array of inferred length",
|
||||
input: `tabs([...]TabData{
|
||||
{Name: "A"},
|
||||
{Name: "B"},
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with function arg",
|
||||
input: `componentA(func(y []int) string {
|
||||
return "hi"
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "function call with function called arg",
|
||||
input: `componentA(func(y []int) string {
|
||||
return "hi"
|
||||
}())`,
|
||||
},
|
||||
{
|
||||
name: "call with braces and brackets",
|
||||
input: `templates.New(test{}, other())`,
|
||||
},
|
||||
{
|
||||
name: "generic call",
|
||||
input: `templates.New(toString[[]int](data))`,
|
||||
},
|
||||
{
|
||||
name: "struct method call",
|
||||
input: `typeName{}.Method()`,
|
||||
},
|
||||
{
|
||||
name: "struct method call in other package",
|
||||
input: "layout.DefaultLayout{}.Compile()",
|
||||
},
|
||||
{
|
||||
name: "bare variable",
|
||||
input: `component`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestTemplExpression(t *testing.T) {
|
||||
prefix := ""
|
||||
suffixes := []string{
|
||||
"",
|
||||
"}",
|
||||
"\t}",
|
||||
" }",
|
||||
"</div>",
|
||||
"<p>/</p>",
|
||||
" just some text",
|
||||
" { <div>Child content</div> }",
|
||||
}
|
||||
for _, test := range templExpressionTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, prefix, suffix, TemplExpression))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzTemplExpression(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
" }",
|
||||
" }}</a>\n}",
|
||||
"...",
|
||||
}
|
||||
for _, test := range expressionTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add(test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
src := "switch " + s
|
||||
start, end, err := TemplExpression(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzExpression(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
" }",
|
||||
" }}</a>\n}",
|
||||
"...",
|
||||
}
|
||||
for _, test := range expressionTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add(test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
src := "switch " + s
|
||||
start, end, err := Expression(src)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
panicIfInvalid(src, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
var sliceArgsTests = []testInput{
|
||||
{
|
||||
name: "no input",
|
||||
input: ``,
|
||||
},
|
||||
{
|
||||
name: "single input",
|
||||
input: `nil`,
|
||||
},
|
||||
{
|
||||
name: "inputs to function call",
|
||||
input: `a, b, "c"`,
|
||||
},
|
||||
{
|
||||
name: "function call in package",
|
||||
input: `components.Other()`,
|
||||
},
|
||||
{
|
||||
name: "slice index call",
|
||||
input: `components[0].Other()`,
|
||||
},
|
||||
{
|
||||
name: "map index function call",
|
||||
input: `components["name"].Other()`,
|
||||
},
|
||||
{
|
||||
name: "function literal",
|
||||
input: `components["name"].Other(func() bool { return true })`,
|
||||
},
|
||||
{
|
||||
name: "multiline function call",
|
||||
input: `component(map[string]string{
|
||||
"namea": "name_a",
|
||||
"nameb": "name_b",
|
||||
})`,
|
||||
},
|
||||
{
|
||||
name: "package name, but no variable or function",
|
||||
input: `fmt.`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestSliceArgs(t *testing.T) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
"}",
|
||||
"}</a>\n}\nvar x = []struct {}{}",
|
||||
}
|
||||
for _, test := range sliceArgsTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), func(t *testing.T) {
|
||||
expr, err := SliceArgs(test.input + suffix)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse slice args: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(test.input, expr); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzSliceArgs(f *testing.F) {
|
||||
suffixes := []string{
|
||||
"",
|
||||
"}",
|
||||
" }",
|
||||
"}</a>\n}\nvar x = []struct {}{}",
|
||||
}
|
||||
for _, test := range sliceArgsTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add(test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
_, err := SliceArgs(s)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestChildren(t *testing.T) {
|
||||
prefix := ""
|
||||
suffixes := []string{
|
||||
" }",
|
||||
" } <div>Other content</div>",
|
||||
"", // End of file.
|
||||
}
|
||||
tests := []testInput{
|
||||
{
|
||||
name: "children",
|
||||
input: `children...`,
|
||||
},
|
||||
{
|
||||
name: "function",
|
||||
input: `components.Spread()...`,
|
||||
},
|
||||
{
|
||||
name: "alternative variable",
|
||||
input: `components...`,
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
input: `groups[0]...`,
|
||||
},
|
||||
{
|
||||
name: "map",
|
||||
input: `components["name"]...`,
|
||||
},
|
||||
{
|
||||
name: "map func key",
|
||||
input: `components[getKey(ctx)]...`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), run(test, prefix, suffix, Expression))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var funcTests = []testInput{
|
||||
{
|
||||
name: "void func",
|
||||
input: `myfunc()`,
|
||||
},
|
||||
{
|
||||
name: "receiver func",
|
||||
input: `(r recv) myfunc()`,
|
||||
},
|
||||
}
|
||||
|
||||
func FuzzFuncs(f *testing.F) {
|
||||
prefix := "func "
|
||||
suffixes := []string{
|
||||
"",
|
||||
"}",
|
||||
" }",
|
||||
"}</a>\n}\nvar x = []struct {}{}",
|
||||
}
|
||||
for _, test := range funcTests {
|
||||
for _, suffix := range suffixes {
|
||||
f.Add(prefix + test.input + suffix)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
_, _, err := Func(s)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFunc(t *testing.T) {
|
||||
prefix := "func "
|
||||
suffixes := []string{
|
||||
"",
|
||||
"}",
|
||||
"}\nvar x = []struct {}{}",
|
||||
"}\nfunc secondFunc() {}",
|
||||
}
|
||||
for _, test := range funcTests {
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", test.name, i), func(t *testing.T) {
|
||||
name, expr, err := Func(prefix + test.input + suffix)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse slice args: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(test.input, expr); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff("myfunc", name); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testInput struct {
|
||||
name string
|
||||
input string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
type extractor func(content string) (start, end int, err error)
|
||||
|
||||
func run(test testInput, prefix, suffix string, e extractor) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
src := prefix + test.input + suffix
|
||||
start, end, err := e(src)
|
||||
if test.expectedErr == nil && err != nil {
|
||||
t.Fatalf("expected nil error got error type %T: %v", err, err)
|
||||
}
|
||||
if test.expectedErr != nil && err == nil {
|
||||
t.Fatalf("expected err %q, got %v", test.expectedErr.Error(), err)
|
||||
}
|
||||
if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("expected err %q, got %q", test.expectedErr.Error(), err.Error())
|
||||
}
|
||||
actual := src[start:end]
|
||||
if diff := cmp.Diff(test.input, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
105
templ/parser/v2/goexpression/parsebench_test.go
Normal file
105
templ/parser/v2/goexpression/parsebench_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package goexpression
|
||||
|
||||
import "testing"
|
||||
|
||||
var testStringExpression = `"this string expression" }
|
||||
<div>
|
||||
But afterwards, it keeps searching.
|
||||
<div>
|
||||
|
||||
<div>
|
||||
But that's not right, we can stop searching. It won't find anything valid.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Certainly not later in the file.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
`
|
||||
|
||||
func BenchmarkExpression(b *testing.B) {
|
||||
// Baseline...
|
||||
// BenchmarkExpression-10 6484 184862 ns/op
|
||||
// Updated...
|
||||
// BenchmarkExpression-10 3942538 279.6 ns/op
|
||||
for n := 0; n < b.N; n++ {
|
||||
start, end, err := Expression(testStringExpression)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if start != 0 || end != 24 {
|
||||
b.Fatalf("expected 0, 24, got %d, %d", start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testTemplExpression = `templates.CallMethod(map[string]any{
|
||||
"name": "this string expression",
|
||||
})
|
||||
|
||||
<div>
|
||||
But afterwards, it keeps searching.
|
||||
<div>
|
||||
|
||||
<div>
|
||||
But that's not right, we can stop searching. It won't find anything valid.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Certainly not later in the file.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
It's going to try all the tokens.
|
||||
)}]@<+.
|
||||
</div>
|
||||
`
|
||||
|
||||
func BenchmarkTemplExpression(b *testing.B) {
|
||||
// BenchmarkTemplExpression-10 2694 431934 ns/op
|
||||
// Updated...
|
||||
// BenchmarkTemplExpression-10 1339399 897.6 ns/op
|
||||
for n := 0; n < b.N; n++ {
|
||||
start, end, err := TemplExpression(testTemplExpression)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if start != 0 || end != 74 {
|
||||
b.Fatalf("expected 0, 74, got %d, %d", start, end)
|
||||
}
|
||||
}
|
||||
}
|
180
templ/parser/v2/goexpression/scanner.go
Normal file
180
templ/parser/v2/goexpression/scanner.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package goexpression
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
type Stack[T any] []T
|
||||
|
||||
func (s *Stack[T]) Push(v T) {
|
||||
*s = append(*s, v)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (v T) {
|
||||
if len(*s) == 0 {
|
||||
return v
|
||||
}
|
||||
v = (*s)[len(*s)-1]
|
||||
*s = (*s)[:len(*s)-1]
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (v T) {
|
||||
if len(*s) == 0 {
|
||||
return v
|
||||
}
|
||||
return (*s)[len(*s)-1]
|
||||
}
|
||||
|
||||
var goTokenOpenToClose = map[token.Token]token.Token{
|
||||
token.LPAREN: token.RPAREN,
|
||||
token.LBRACE: token.RBRACE,
|
||||
token.LBRACK: token.RBRACK,
|
||||
}
|
||||
|
||||
var goTokenCloseToOpen = map[token.Token]token.Token{
|
||||
token.RPAREN: token.LPAREN,
|
||||
token.RBRACE: token.LBRACE,
|
||||
token.RBRACK: token.LBRACK,
|
||||
}
|
||||
|
||||
type ErrUnbalanced struct {
|
||||
Token token.Token
|
||||
}
|
||||
|
||||
func (e ErrUnbalanced) Error() string {
|
||||
return fmt.Sprintf("unbalanced '%s'", e.Token)
|
||||
}
|
||||
|
||||
func NewExpressionParser() *ExpressionParser {
|
||||
return &ExpressionParser{
|
||||
Stack: make(Stack[token.Token], 0),
|
||||
Previous: token.PERIOD,
|
||||
Fns: make(Stack[int], 0),
|
||||
}
|
||||
}
|
||||
|
||||
type ExpressionParser struct {
|
||||
Stack Stack[token.Token]
|
||||
End int
|
||||
Previous token.Token
|
||||
Fns Stack[int] // Stack of function depths.
|
||||
}
|
||||
|
||||
func (ep *ExpressionParser) setEnd(pos token.Pos, tok token.Token, lit string) {
|
||||
ep.End = int(pos) + len(tokenString(tok, lit)) - 1
|
||||
}
|
||||
|
||||
func (ep *ExpressionParser) hasSpaceBeforeCurrentToken(pos token.Pos) bool {
|
||||
return (int(pos) - 1) > ep.End
|
||||
}
|
||||
|
||||
func (ep *ExpressionParser) isTopLevel() bool {
|
||||
return len(ep.Fns) == 0 && len(ep.Stack) == 0
|
||||
}
|
||||
|
||||
func (ep *ExpressionParser) Insert(
|
||||
pos token.Pos,
|
||||
tok token.Token,
|
||||
lit string,
|
||||
) (stop bool, err error) {
|
||||
defer func() {
|
||||
ep.Previous = tok
|
||||
}()
|
||||
|
||||
// If we've reach the end of the file, terminate reading.
|
||||
if tok == token.EOF {
|
||||
// If the EOF was reached, but we're not at the top level, we must have an unbalanced expression.
|
||||
if !ep.isTopLevel() {
|
||||
return true, ErrUnbalanced{ep.Stack.Pop()}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Handle function literals e.g. func() { fmt.Println("Hello") }
|
||||
// By pushing the current depth onto the stack, we prevent stopping
|
||||
// until we've closed the function.
|
||||
if tok == token.FUNC {
|
||||
ep.Fns.Push(len(ep.Stack))
|
||||
ep.setEnd(pos, tok, lit)
|
||||
return false, nil
|
||||
}
|
||||
// If we're opening a pair, we don't stop until we've closed it.
|
||||
if _, isOpener := goTokenOpenToClose[tok]; isOpener {
|
||||
// If we're at an open brace, at the top level, where a space has been used, stop.
|
||||
if tok == token.LBRACE && ep.isTopLevel() {
|
||||
// Previous was paren, e.g. () {
|
||||
if ep.Previous == token.RPAREN {
|
||||
return true, nil
|
||||
}
|
||||
// Previous was ident that isn't a type.
|
||||
// In `name {`, `name` is considered to be a variable.
|
||||
// In `name{`, `name` is considered to be a type name.
|
||||
if ep.Previous == token.IDENT && ep.hasSpaceBeforeCurrentToken(pos) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
ep.Stack.Push(tok)
|
||||
ep.setEnd(pos, tok, lit)
|
||||
return false, nil
|
||||
}
|
||||
if opener, isCloser := goTokenCloseToOpen[tok]; isCloser {
|
||||
if len(ep.Stack) == 0 {
|
||||
// We've got a close token, but there's nothing to close, so we must be done.
|
||||
return true, nil
|
||||
}
|
||||
actual := ep.Stack.Pop()
|
||||
if !isCloser {
|
||||
return false, ErrUnbalanced{tok}
|
||||
}
|
||||
if actual != opener {
|
||||
return false, ErrUnbalanced{tok}
|
||||
}
|
||||
if tok == token.RBRACE {
|
||||
// If we're closing a function, pop the function depth.
|
||||
if len(ep.Stack) == ep.Fns.Peek() {
|
||||
ep.Fns.Pop()
|
||||
}
|
||||
}
|
||||
ep.setEnd(pos, tok, lit)
|
||||
return false, nil
|
||||
}
|
||||
// If we're in a function literal slice, or pair, we allow anything until we close it.
|
||||
if len(ep.Fns) > 0 || len(ep.Stack) > 0 {
|
||||
ep.setEnd(pos, tok, lit)
|
||||
return false, nil
|
||||
}
|
||||
// We allow an ident to follow a period or a closer.
|
||||
// e.g. "package.name", "typeName{field: value}.name()".
|
||||
// or "call().name", "call().name()".
|
||||
// But not "package .name" or "typeName{field: value} .name()".
|
||||
if tok == token.IDENT && (ep.Previous == token.PERIOD || isCloser(ep.Previous)) {
|
||||
if isCloser(ep.Previous) && ep.hasSpaceBeforeCurrentToken(pos) {
|
||||
// This token starts later than the last ending, which means
|
||||
// there's a space.
|
||||
return true, nil
|
||||
}
|
||||
ep.setEnd(pos, tok, lit)
|
||||
return false, nil
|
||||
}
|
||||
if tok == token.PERIOD && (ep.Previous == token.IDENT || isCloser(ep.Previous)) {
|
||||
ep.setEnd(pos, tok, lit)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// No match, so stop.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func tokenString(tok token.Token, lit string) string {
|
||||
if tok.IsKeyword() || tok.IsOperator() {
|
||||
return tok.String()
|
||||
}
|
||||
return lit
|
||||
}
|
||||
|
||||
func isCloser(tok token.Token) bool {
|
||||
_, ok := goTokenCloseToOpen[tok]
|
||||
return ok
|
||||
}
|
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzCaseDefault/3c6f43d3ec8a900b
vendored
Normal file
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzCaseDefault/3c6f43d3ec8a900b
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("default0")
|
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzCaseDefault/986e7bc325c7890c
vendored
Normal file
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzCaseDefault/986e7bc325c7890c
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("default:{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{`0\r000000")
|
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzCaseDefault/d8a9a4cd9fc8cb11
vendored
Normal file
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzCaseDefault/d8a9a4cd9fc8cb11
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("default")
|
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzExpression/ac5d99902f5e7914
vendored
Normal file
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzExpression/ac5d99902f5e7914
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("#")
|
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzFuncs/46c9ed6c9d427bd2
vendored
Normal file
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzFuncs/46c9ed6c9d427bd2
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("func")
|
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzIf/7a174efc13e3fdd6
vendored
Normal file
2
templ/parser/v2/goexpression/testdata/fuzz/FuzzIf/7a174efc13e3fdd6
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("(")
|
70
templ/parser/v2/goparser.go
Normal file
70
templ/parser/v2/goparser.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
func parseGoFuncDecl(prefix string, pi *parse.Input) (name string, expression Expression, err error) {
|
||||
prefix = prefix + " "
|
||||
from := pi.Index()
|
||||
src, _ := pi.Peek(-1)
|
||||
src = strings.TrimPrefix(src, prefix)
|
||||
name, expr, err := goexpression.Func("func " + src)
|
||||
if err != nil {
|
||||
return name, expression, parse.Error(fmt.Sprintf("invalid %s declaration: %v", prefix, err.Error()), pi.Position())
|
||||
}
|
||||
pi.Take(len(prefix) + len(expr))
|
||||
to := pi.Position()
|
||||
return name, NewExpression(expr, pi.PositionAt(from+len(prefix)), to), nil
|
||||
}
|
||||
|
||||
func parseTemplFuncDecl(pi *parse.Input) (name string, expression Expression, err error) {
|
||||
return parseGoFuncDecl("templ", pi)
|
||||
}
|
||||
|
||||
func parseCSSFuncDecl(pi *parse.Input) (name string, expression Expression, err error) {
|
||||
return parseGoFuncDecl("css", pi)
|
||||
}
|
||||
|
||||
func parseGoSliceArgs(pi *parse.Input) (r Expression, err error) {
|
||||
from := pi.Position()
|
||||
src, _ := pi.Peek(-1)
|
||||
expr, err := goexpression.SliceArgs(src)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
pi.Take(len(expr))
|
||||
to := pi.Position()
|
||||
return NewExpression(expr, from, to), nil
|
||||
}
|
||||
|
||||
func peekPrefix(pi *parse.Input, prefixes ...string) bool {
|
||||
for _, prefix := range prefixes {
|
||||
pp, ok := pi.Peek(len(prefix))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if prefix == pp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type extractor func(content string) (start, end int, err error)
|
||||
|
||||
func parseGo(name string, pi *parse.Input, e extractor) (r Expression, err error) {
|
||||
from := pi.Index()
|
||||
src, _ := pi.Peek(-1)
|
||||
start, end, err := e(src)
|
||||
if err != nil {
|
||||
return r, parse.Error(fmt.Sprintf("%s: invalid go expression: %v", name, err.Error()), pi.Position())
|
||||
}
|
||||
expr := src[start:end]
|
||||
pi.Take(end)
|
||||
return NewExpression(expr, pi.PositionAt(from+start), pi.PositionAt(from+end)), nil
|
||||
}
|
39
templ/parser/v2/htmlcommentparser.go
Normal file
39
templ/parser/v2/htmlcommentparser.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
var htmlCommentStart = parse.String("<!--")
|
||||
var htmlCommentEnd = parse.String("--")
|
||||
|
||||
type htmlCommentParser struct {
|
||||
}
|
||||
|
||||
var htmlComment = htmlCommentParser{}
|
||||
|
||||
func (p htmlCommentParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
// Comment start.
|
||||
start := pi.Position()
|
||||
var c HTMLComment
|
||||
if _, ok, err = htmlCommentStart.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once we've got the comment start sequence, parse anything until the end
|
||||
// sequence as the comment contents.
|
||||
if c.Contents, ok, err = parse.StringUntil(htmlCommentEnd).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("expected end comment literal '-->' not found", start)
|
||||
return
|
||||
}
|
||||
// Cut the end element.
|
||||
_, _, _ = htmlCommentEnd.Parse(pi)
|
||||
|
||||
// Cut the gt.
|
||||
if _, ok, err = gt.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("comment contains invalid sequence '--'", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return c, true, nil
|
||||
}
|
110
templ/parser/v2/htmlcommentparser_test.go
Normal file
110
templ/parser/v2/htmlcommentparser_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHTMLCommentParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected HTMLComment
|
||||
}{
|
||||
{
|
||||
name: "comment - single line",
|
||||
input: `<!-- single line comment -->`,
|
||||
expected: HTMLComment{
|
||||
Contents: " single line comment ",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment - no whitespace",
|
||||
input: `<!--no whitespace between sequence open and close-->`,
|
||||
expected: HTMLComment{
|
||||
Contents: "no whitespace between sequence open and close",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment - multiline",
|
||||
input: `<!-- multiline
|
||||
comment
|
||||
-->`,
|
||||
expected: HTMLComment{
|
||||
Contents: ` multiline
|
||||
comment
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment - with tag",
|
||||
input: `<!-- <p class="test">tag</p> -->`,
|
||||
expected: HTMLComment{
|
||||
Contents: ` <p class="test">tag</p> `,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comments can contain tags",
|
||||
input: `<!-- <div> hello world </div> -->`,
|
||||
expected: HTMLComment{
|
||||
Contents: ` <div> hello world </div> `,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := htmlComment.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLCommentParserErrors(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "unclosed HTML comment",
|
||||
input: `<!-- unclosed HTML comment`,
|
||||
expected: parse.Error("expected end comment literal '-->' not found",
|
||||
parse.Position{
|
||||
Index: 0,
|
||||
Line: 0,
|
||||
Col: 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "comment in comment",
|
||||
input: `<!-- <-- other --> -->`,
|
||||
expected: parse.Error("comment contains invalid sequence '--'", parse.Position{
|
||||
Index: 8,
|
||||
Line: 0,
|
||||
Col: 8,
|
||||
}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
_, _, err := htmlComment.Parse(input)
|
||||
if diff := cmp.Diff(tt.expected, err); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
135
templ/parser/v2/ifexpressionparser.go
Normal file
135
templ/parser/v2/ifexpressionparser.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
"github.com/a-h/templ/parser/v2/goexpression"
|
||||
)
|
||||
|
||||
var ifExpression ifExpressionParser
|
||||
|
||||
var untilElseIfElseOrEnd = parse.Any(StripType(elseIfExpression), StripType(elseExpression), StripType(closeBraceWithOptionalPadding))
|
||||
|
||||
type ifExpressionParser struct{}
|
||||
|
||||
func (ifExpressionParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
var r IfExpression
|
||||
start := pi.Index()
|
||||
|
||||
if !peekPrefix(pi, "if ") {
|
||||
return r, false, nil
|
||||
}
|
||||
|
||||
// Parse the Go if expression.
|
||||
if r.Expression, err = parseGo("if", pi, goexpression.If); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
// Eat " {\n".
|
||||
if _, ok, err = parse.All(openBraceWithOptionalPadding, parse.NewLine).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("if: "+unterminatedMissingCurly, pi.PositionAt(start))
|
||||
return
|
||||
}
|
||||
|
||||
// Once we've had the start of an if block, we must conclude the block.
|
||||
|
||||
// Read the 'Then' nodes.
|
||||
// If there's no match, there's a problem in the template nodes.
|
||||
np := newTemplateNodeParser(untilElseIfElseOrEnd, "else expression or closing brace")
|
||||
var thenNodes Nodes
|
||||
if thenNodes, ok, err = np.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("if: expected nodes, but none were found", pi.Position())
|
||||
return
|
||||
}
|
||||
r.Then = thenNodes.Nodes
|
||||
|
||||
// Read the optional 'ElseIf' Nodes.
|
||||
if r.ElseIfs, _, err = parse.ZeroOrMore(elseIfExpression).Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Read the optional 'Else' Nodes.
|
||||
var elseNodes Nodes
|
||||
if elseNodes, _, err = elseExpression.Parse(pi); err != nil {
|
||||
return
|
||||
}
|
||||
r.Else = elseNodes.Nodes
|
||||
|
||||
// Read the required closing brace.
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("if: "+unterminatedMissingEnd, pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
var elseIfExpression parse.Parser[ElseIfExpression] = elseIfExpressionParser{}
|
||||
|
||||
type elseIfExpressionParser struct{}
|
||||
|
||||
func (elseIfExpressionParser) Parse(pi *parse.Input) (r ElseIfExpression, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// Check the prefix first.
|
||||
if _, ok, err = parse.All(parse.OptionalWhitespace, closeBrace, parse.OptionalWhitespace, parse.String("else if")).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Rewind to the start of the `if` statement.
|
||||
pi.Seek(pi.Index() - 2)
|
||||
// Parse the Go if expression.
|
||||
if r.Expression, err = parseGo("else if", pi, goexpression.If); err != nil {
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
// Eat " {\n".
|
||||
if _, ok, err = parse.All(openBraceWithOptionalPadding, parse.NewLine).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("else if: "+unterminatedMissingCurly, pi.PositionAt(start))
|
||||
return
|
||||
}
|
||||
|
||||
// Once we've had the start of an if block, we must conclude the block.
|
||||
|
||||
// Read the 'Then' nodes.
|
||||
// If there's no match, there's a problem in the template nodes.
|
||||
np := newTemplateNodeParser(untilElseIfElseOrEnd, "else expression or closing brace")
|
||||
var thenNodes Nodes
|
||||
if thenNodes, ok, err = np.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("if: expected nodes, but none were found", pi.Position())
|
||||
return
|
||||
}
|
||||
r.Then = thenNodes.Nodes
|
||||
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
var endElseParser = parse.All(
|
||||
parse.Rune('}'),
|
||||
parse.OptionalWhitespace,
|
||||
parse.String("else"),
|
||||
parse.OptionalWhitespace,
|
||||
parse.Rune('{'),
|
||||
parse.OptionalWhitespace)
|
||||
|
||||
var elseExpression parse.Parser[Nodes] = elseExpressionParser{}
|
||||
|
||||
type elseExpressionParser struct{}
|
||||
|
||||
func (elseExpressionParser) Parse(in *parse.Input) (r Nodes, ok bool, err error) {
|
||||
start := in.Index()
|
||||
|
||||
// } else {
|
||||
if _, ok, err = endElseParser.Parse(in); err != nil || !ok {
|
||||
in.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Else contents
|
||||
if r, ok, err = newTemplateNodeParser(closeBraceWithOptionalPadding, "else expression closing brace").Parse(in); err != nil || !ok {
|
||||
in.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
}
|
649
templ/parser/v2/ifexpressionparser_test.go
Normal file
649
templ/parser/v2/ifexpressionparser_test.go
Normal file
@@ -0,0 +1,649 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestIfExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected IfExpression
|
||||
}{
|
||||
{
|
||||
name: "if: simple expression",
|
||||
input: `if p.Test {
|
||||
<span>
|
||||
{ "span content" }
|
||||
</span>
|
||||
}
|
||||
`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.Test`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 9,
|
||||
Line: 0,
|
||||
Col: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Element{
|
||||
Name: "span",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 13, Line: 1, Col: 1},
|
||||
To: Position{Index: 17, Line: 1, Col: 5},
|
||||
},
|
||||
|
||||
Children: []Node{
|
||||
Whitespace{Value: "\n "},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"span content"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 23,
|
||||
Line: 2,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 37,
|
||||
Line: 2,
|
||||
Col: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
IndentChildren: true,
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: else",
|
||||
input: `if p.A {
|
||||
{ "A" }
|
||||
} else {
|
||||
{ "B" }
|
||||
}`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.A`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 6,
|
||||
Line: 0,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"A"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 1,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 15,
|
||||
Line: 1,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
Else: []Node{
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"B"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 30,
|
||||
Line: 3,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 33,
|
||||
Line: 3,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: expressions can have a space after the opening brace",
|
||||
input: `if p.Test {
|
||||
text
|
||||
}
|
||||
`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.Test`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 9,
|
||||
Line: 0,
|
||||
Col: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: " "},
|
||||
Text{
|
||||
Value: "text",
|
||||
Range: Range{
|
||||
From: Position{Index: 15, Line: 1, Col: 2},
|
||||
To: Position{Index: 19, Line: 1, Col: 6},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: simple expression, without spaces",
|
||||
input: `if p.Test {
|
||||
<span>
|
||||
{ "span content" }
|
||||
</span>
|
||||
}
|
||||
`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.Test`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 9,
|
||||
Line: 0,
|
||||
Col: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Element{
|
||||
Name: "span",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 13, Line: 1, Col: 1},
|
||||
To: Position{Index: 17, Line: 1, Col: 5},
|
||||
},
|
||||
|
||||
Children: []Node{
|
||||
Whitespace{Value: "\n "},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"span content"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 23,
|
||||
Line: 2,
|
||||
Col: 4,
|
||||
},
|
||||
To: Position{
|
||||
Index: 37,
|
||||
Line: 2,
|
||||
Col: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
IndentChildren: true,
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: else, without spaces",
|
||||
input: `if p.A{
|
||||
{ "A" }
|
||||
} else {
|
||||
{ "B" }
|
||||
}`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.A`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 6,
|
||||
Line: 0,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"A"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 11,
|
||||
Line: 1,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 14,
|
||||
Line: 1,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
Else: []Node{
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"B"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 29,
|
||||
Line: 3,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 32,
|
||||
Line: 3,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: nested",
|
||||
input: `if p.A {
|
||||
if p.B {
|
||||
<div>{ "B" }</div>
|
||||
}
|
||||
}`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.A`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 3,
|
||||
Line: 0,
|
||||
Col: 3,
|
||||
},
|
||||
To: Position{
|
||||
Index: 6,
|
||||
Line: 0,
|
||||
Col: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t\t\t\t\t"},
|
||||
IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.B`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 17,
|
||||
Line: 1,
|
||||
Col: 8,
|
||||
},
|
||||
To: Position{
|
||||
Index: 20,
|
||||
Line: 1,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t\t\t\t\t\t"},
|
||||
Element{
|
||||
Name: "div",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 30, Line: 2, Col: 7},
|
||||
To: Position{Index: 33, Line: 2, Col: 10},
|
||||
},
|
||||
|
||||
Children: []Node{
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"B"`,
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 36,
|
||||
Line: 2,
|
||||
Col: 13,
|
||||
},
|
||||
To: Position{
|
||||
Index: 39,
|
||||
Line: 2,
|
||||
Col: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
Whitespace{Value: "\n\t\t\t\t"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: else if",
|
||||
input: `if p.A {
|
||||
{ "A" }
|
||||
} else if p.B {
|
||||
{ "B" }
|
||||
}`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.A`,
|
||||
Range: Range{
|
||||
From: Position{Index: 3, Line: 0, Col: 3},
|
||||
To: Position{Index: 6, Line: 0, Col: 6},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"A"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 12, Line: 1, Col: 3},
|
||||
To: Position{Index: 15, Line: 1, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
ElseIfs: []ElseIfExpression{
|
||||
{
|
||||
Expression: Expression{
|
||||
Value: `p.B`,
|
||||
Range: Range{
|
||||
From: Position{Index: 28, Line: 2, Col: 10},
|
||||
To: Position{Index: 31, Line: 2, Col: 13},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"B"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 37, Line: 3, Col: 3},
|
||||
To: Position{Index: 40, Line: 3, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: else if, else if",
|
||||
input: `if p.A {
|
||||
{ "A" }
|
||||
} else if p.B {
|
||||
{ "B" }
|
||||
} else if p.C {
|
||||
{ "C" }
|
||||
}`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.A`,
|
||||
Range: Range{
|
||||
From: Position{Index: 3, Line: 0, Col: 3},
|
||||
To: Position{Index: 6, Line: 0, Col: 6},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"A"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 12, Line: 1, Col: 3},
|
||||
To: Position{Index: 15, Line: 1, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
ElseIfs: []ElseIfExpression{
|
||||
{
|
||||
Expression: Expression{
|
||||
Value: `p.B`,
|
||||
Range: Range{
|
||||
From: Position{Index: 28, Line: 2, Col: 10},
|
||||
To: Position{Index: 31, Line: 2, Col: 13},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"B"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 37, Line: 3, Col: 3},
|
||||
To: Position{Index: 40, Line: 3, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Expression: Expression{
|
||||
Value: `p.C`,
|
||||
Range: Range{
|
||||
From: Position{Index: 53, Line: 4, Col: 10},
|
||||
To: Position{Index: 56, Line: 4, Col: 13},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"C"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 62, Line: 5, Col: 3},
|
||||
To: Position{Index: 65, Line: 5, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "if: else if, else if, else",
|
||||
input: `if p.A {
|
||||
{ "A" }
|
||||
} else if p.B {
|
||||
{ "B" }
|
||||
} else if p.C {
|
||||
{ "C" }
|
||||
} else {
|
||||
{ "D" }
|
||||
}`,
|
||||
expected: IfExpression{
|
||||
Expression: Expression{
|
||||
Value: `p.A`,
|
||||
Range: Range{
|
||||
From: Position{Index: 3, Line: 0, Col: 3},
|
||||
To: Position{Index: 6, Line: 0, Col: 6},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"A"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 12, Line: 1, Col: 3},
|
||||
To: Position{Index: 15, Line: 1, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
ElseIfs: []ElseIfExpression{
|
||||
{
|
||||
Expression: Expression{
|
||||
Value: `p.B`,
|
||||
Range: Range{
|
||||
From: Position{Index: 28, Line: 2, Col: 10},
|
||||
To: Position{Index: 31, Line: 2, Col: 13},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"B"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 37, Line: 3, Col: 3},
|
||||
To: Position{Index: 40, Line: 3, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Expression: Expression{
|
||||
Value: `p.C`,
|
||||
Range: Range{
|
||||
From: Position{Index: 53, Line: 4, Col: 10},
|
||||
To: Position{Index: 56, Line: 4, Col: 13},
|
||||
},
|
||||
},
|
||||
Then: []Node{
|
||||
Whitespace{Value: "\t"},
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"C"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 62, Line: 5, Col: 3},
|
||||
To: Position{Index: 65, Line: 5, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Else: []Node{
|
||||
StringExpression{
|
||||
Expression: Expression{
|
||||
Value: `"D"`,
|
||||
Range: Range{
|
||||
From: Position{Index: 80, Line: 7, Col: 3},
|
||||
To: Position{Index: 83, Line: 7, Col: 6},
|
||||
},
|
||||
},
|
||||
TrailingSpace: SpaceVertical,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
actual, ok, err := ifExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncompleteIf(t *testing.T) {
|
||||
t.Run("no opening brace", func(t *testing.T) {
|
||||
input := parse.NewInput(`if a tree falls in the woods`)
|
||||
_, _, err := ifExpression.Parse(input)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
pe, isParseError := err.(parse.ParseError)
|
||||
if !isParseError {
|
||||
t.Fatalf("expected a parse error, got %T", err)
|
||||
}
|
||||
if pe.Msg != "if: unterminated (missing closing '{\\n') - https://templ.guide/syntax-and-usage/statements#incomplete-statements" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if pe.Pos.Line != 0 {
|
||||
t.Fatalf("unexpected line: %d", pe.Pos.Line)
|
||||
}
|
||||
})
|
||||
t.Run("capitalised If", func(t *testing.T) {
|
||||
input := parse.NewInput(`If a tree falls in the woods`)
|
||||
_, ok, err := ifExpression.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("expected a non match")
|
||||
}
|
||||
})
|
||||
}
|
32
templ/parser/v2/packageparser.go
Normal file
32
templ/parser/v2/packageparser.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
// Package.
|
||||
var pkg = parse.Func(func(pi *parse.Input) (pkg Package, ok bool, err error) {
|
||||
start := pi.Position()
|
||||
|
||||
// Package prefix.
|
||||
if _, ok, err = parse.String("package ").Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once we have the prefix, it's an expression until the end of the line.
|
||||
var exp string
|
||||
if exp, ok, err = stringUntilNewLine.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("package literal not terminated", pi.Position())
|
||||
return
|
||||
}
|
||||
if len(exp) == 0 {
|
||||
ok = false
|
||||
err = parse.Error("package literal not terminated", start)
|
||||
return
|
||||
}
|
||||
|
||||
// Success!
|
||||
pkg.Expression = NewExpression("package "+exp, start, pi.Position())
|
||||
|
||||
return pkg, true, nil
|
||||
})
|
100
templ/parser/v2/packageparser_test.go
Normal file
100
templ/parser/v2/packageparser_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestPackageParserErrors(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected parse.ParseError
|
||||
}{
|
||||
{
|
||||
name: "unterminated package",
|
||||
input: "package ",
|
||||
expected: parse.Error(
|
||||
"package literal not terminated",
|
||||
parse.Position{
|
||||
Index: 8,
|
||||
Line: 0,
|
||||
Col: 8,
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unterminated package, new line",
|
||||
input: "package \n",
|
||||
expected: parse.Error(
|
||||
"package literal not terminated",
|
||||
parse.Position{
|
||||
Index: 0,
|
||||
Line: 0,
|
||||
Col: 0,
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pi := parse.NewInput(tt.input)
|
||||
_, ok, err := pkg.Parse(pi)
|
||||
if ok {
|
||||
t.Errorf("expected parsing to fail, but it succeeded")
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, err); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "package: standard",
|
||||
input: "package parser\n",
|
||||
expected: Package{
|
||||
Expression: Expression{
|
||||
Value: "package parser",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 0,
|
||||
Line: 0,
|
||||
Col: 0,
|
||||
},
|
||||
To: Position{
|
||||
Index: 14,
|
||||
Line: 0,
|
||||
Col: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
actual, ok, err := pkg.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
50
templ/parser/v2/parser.go
Normal file
50
templ/parser/v2/parser.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
// ) {
|
||||
var expressionFuncEnd = parse.All(parse.Rune(')'), openBraceWithOptionalPadding)
|
||||
|
||||
// Template
|
||||
|
||||
var template = parse.Func(func(pi *parse.Input) (r HTMLTemplate, ok bool, err error) {
|
||||
start := pi.Position()
|
||||
|
||||
// templ FuncName(p Person, other Other) {
|
||||
var te templateExpression
|
||||
if te, ok, err = templateExpressionParser.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
r.Expression = te.Expression
|
||||
|
||||
// Once we're in a template, we should expect some template whitespace, if/switch/for,
|
||||
// or node string expressions etc.
|
||||
var nodes Nodes
|
||||
nodes, ok, err = newTemplateNodeParser(closeBraceWithOptionalPadding, "template closing brace").Parse(pi)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
err = parse.Error("templ: expected nodes in templ body, but found none", pi.Position())
|
||||
return
|
||||
}
|
||||
r.Children = nodes.Nodes
|
||||
|
||||
// Eat any whitespace.
|
||||
_, _, err = parse.OptionalWhitespace.Parse(pi)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Try for }
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("template: missing closing brace", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
r.Range = NewRange(start, pi.Position())
|
||||
|
||||
return r, true, nil
|
||||
})
|
70
templ/parser/v2/raw.go
Normal file
70
templ/parser/v2/raw.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
var styleElement = rawElementParser{
|
||||
name: "style",
|
||||
}
|
||||
|
||||
var scriptElement = rawElementParser{
|
||||
name: "script",
|
||||
}
|
||||
|
||||
type rawElementParser struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (p rawElementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
|
||||
start := pi.Index()
|
||||
|
||||
// <
|
||||
if _, ok, err = lt.Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Element name.
|
||||
var e RawElement
|
||||
if e.Name, ok, err = elementNameParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
if e.Name != p.name {
|
||||
pi.Seek(start)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if e.Attributes, ok, err = (attributesParser{}).Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Optional whitespace.
|
||||
if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// >
|
||||
if _, ok, err = gt.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start)
|
||||
return
|
||||
}
|
||||
|
||||
// Once we've got an open tag, parse anything until the end tag as the tag contents.
|
||||
// It's going to be rendered out raw.
|
||||
end := parse.All(parse.String("</"), parse.String(p.name), parse.String(">"))
|
||||
if e.Contents, ok, err = parse.StringUntil(end).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error(fmt.Sprintf("<%s>: expected end tag not present", e.Name), pi.Position())
|
||||
return
|
||||
}
|
||||
// Cut the end element.
|
||||
_, _, _ = end.Parse(pi)
|
||||
|
||||
return e, true, nil
|
||||
}
|
125
templ/parser/v2/raw_test.go
Normal file
125
templ/parser/v2/raw_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var ignoredContent = `{
|
||||
fjkjkl: 123,
|
||||
{{
|
||||
}`
|
||||
|
||||
func TestRawElementParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected RawElement
|
||||
}{
|
||||
{
|
||||
name: "style tag",
|
||||
input: `<style type="text/css">contents</style>`,
|
||||
expected: RawElement{
|
||||
Name: "style",
|
||||
Attributes: []Attribute{
|
||||
ConstantAttribute{
|
||||
Name: "type",
|
||||
Value: "text/css",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 7, Line: 0, Col: 7},
|
||||
To: Position{Index: 11, Line: 0, Col: 11},
|
||||
},
|
||||
},
|
||||
},
|
||||
Contents: "contents",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "style tag containing mismatched braces",
|
||||
input: `<style type="text/css">` + ignoredContent + "</style>",
|
||||
expected: RawElement{
|
||||
Name: "style",
|
||||
Attributes: []Attribute{
|
||||
ConstantAttribute{
|
||||
Name: "type",
|
||||
Value: "text/css",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 7, Line: 0, Col: 7},
|
||||
To: Position{Index: 11, Line: 0, Col: 11},
|
||||
},
|
||||
},
|
||||
},
|
||||
Contents: ignoredContent,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script tag",
|
||||
input: `<script type="vbscript">dim x = 1</script>`,
|
||||
expected: RawElement{
|
||||
Name: "script",
|
||||
Attributes: []Attribute{
|
||||
ConstantAttribute{
|
||||
Name: "type",
|
||||
Value: "vbscript",
|
||||
NameRange: Range{
|
||||
From: Position{Index: 8, Line: 0, Col: 8},
|
||||
To: Position{Index: 12, Line: 0, Col: 12},
|
||||
},
|
||||
},
|
||||
},
|
||||
Contents: "dim x = 1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
actual, ok, err := rawElements.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawElementParserIsNotGreedy(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected RawElement
|
||||
}{
|
||||
{
|
||||
name: "styles tag",
|
||||
input: `<styles></styles>`,
|
||||
},
|
||||
{
|
||||
name: "scripts tag",
|
||||
input: `<scripts></scripts>`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
actual, ok, err := rawElements.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("unexpected success for input %q", tt.input)
|
||||
}
|
||||
if actual != nil {
|
||||
t.Fatalf("expected nil Node got %v", actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
88
templ/parser/v2/scripttemplateparser.go
Normal file
88
templ/parser/v2/scripttemplateparser.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/a-h/parse"
|
||||
)
|
||||
|
||||
var scriptTemplateParser = parse.Func(func(pi *parse.Input) (r ScriptTemplate, ok bool, err error) {
|
||||
start := pi.Position()
|
||||
|
||||
// Parse the name.
|
||||
var se scriptExpression
|
||||
if se, ok, err = scriptExpressionParser.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start.Index)
|
||||
return
|
||||
}
|
||||
r.Name = se.Name
|
||||
r.Parameters = se.Parameters
|
||||
|
||||
// Read code expression.
|
||||
var e Expression
|
||||
if e, ok, err = exp.Parse(pi); err != nil || !ok {
|
||||
pi.Seek(start.Index)
|
||||
return
|
||||
}
|
||||
r.Value = e.Value
|
||||
|
||||
// Try for }
|
||||
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("script template: missing closing brace", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
r.Range = NewRange(start, pi.Position())
|
||||
|
||||
return r, true, nil
|
||||
})
|
||||
|
||||
// script Func() {
|
||||
type scriptExpression struct {
|
||||
Name Expression
|
||||
Parameters Expression
|
||||
}
|
||||
|
||||
var scriptExpressionNameParser = ExpressionOf(parse.StringFrom(
|
||||
parse.Letter,
|
||||
parse.StringFrom(parse.AtMost(1000, parse.Any(parse.Letter, parse.ZeroToNine))),
|
||||
))
|
||||
|
||||
var scriptExpressionParser = parse.Func(func(pi *parse.Input) (r scriptExpression, ok bool, err error) {
|
||||
// Check the prefix first.
|
||||
if _, ok, err = parse.String("script ").Parse(pi); err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Once we have the prefix, we must have a name and parameters.
|
||||
// Read the name of the function.
|
||||
if r.Name, ok, err = scriptExpressionNameParser.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("script expression: invalid name", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Eat the open bracket.
|
||||
if _, ok, err = openBracket.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("script expression: parameters missing open bracket", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Read the parameters.
|
||||
// p Person, other Other, t thing.Thing)
|
||||
if r.Parameters, ok, err = ExpressionOf(parse.StringUntil(closeBracket)).Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("script expression: parameters missing close bracket", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Eat ") {".
|
||||
if _, ok, err = expressionFuncEnd.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("script expression: unterminated (missing ') {')", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
// Expect a newline.
|
||||
if _, ok, err = parse.NewLine.Parse(pi); err != nil || !ok {
|
||||
err = parse.Error("script expression: missing terminating newline", pi.Position())
|
||||
return
|
||||
}
|
||||
|
||||
return r, true, nil
|
||||
})
|
296
templ/parser/v2/scripttemplateparser_test.go
Normal file
296
templ/parser/v2/scripttemplateparser_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/parse"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestScriptTemplateParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected ScriptTemplate
|
||||
}{
|
||||
{
|
||||
name: "script: no parameters, no content",
|
||||
input: `script Name() {
|
||||
}`,
|
||||
expected: ScriptTemplate{
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 17, Line: 1, Col: 1},
|
||||
},
|
||||
Name: Expression{
|
||||
Value: "Name",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 7,
|
||||
Line: 0,
|
||||
Col: 7,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Parameters: Expression{
|
||||
Value: "",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script: no spaces",
|
||||
input: `script Name(){
|
||||
}`,
|
||||
expected: ScriptTemplate{
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 16, Line: 1, Col: 1},
|
||||
},
|
||||
Name: Expression{
|
||||
Value: "Name",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 7,
|
||||
Line: 0,
|
||||
Col: 7,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Parameters: Expression{
|
||||
Value: "",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script: containing a JS variable",
|
||||
input: `script Name() {
|
||||
var x = "x";
|
||||
}`,
|
||||
expected: ScriptTemplate{
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 30, Line: 2, Col: 1},
|
||||
},
|
||||
Name: Expression{
|
||||
Value: "Name",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 7,
|
||||
Line: 0,
|
||||
Col: 7,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Parameters: Expression{
|
||||
Value: "",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: `var x = "x";` + "\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script: single argument",
|
||||
input: `script Name(value string) {
|
||||
console.log(value);
|
||||
}`,
|
||||
expected: ScriptTemplate{
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 49, Line: 2, Col: 1},
|
||||
},
|
||||
Name: Expression{
|
||||
Value: "Name",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 7,
|
||||
Line: 0,
|
||||
Col: 7,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Parameters: Expression{
|
||||
Value: "value string",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 24,
|
||||
Line: 0,
|
||||
Col: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: `console.log(value);` + "\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script: comment with single quote",
|
||||
input: `script Name() {
|
||||
//'
|
||||
}`,
|
||||
expected: ScriptTemplate{
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 22, Line: 2, Col: 1},
|
||||
},
|
||||
Name: Expression{
|
||||
Value: "Name",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 7,
|
||||
Line: 0,
|
||||
Col: 7,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Parameters: Expression{
|
||||
Value: "",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: ` //'` + "\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script: empty assignment",
|
||||
input: `script Name() {
|
||||
let x = '';
|
||||
}`,
|
||||
expected: ScriptTemplate{
|
||||
Range: Range{
|
||||
From: Position{Index: 0, Line: 0, Col: 0},
|
||||
To: Position{Index: 31, Line: 2, Col: 1},
|
||||
},
|
||||
Name: Expression{
|
||||
Value: "Name",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 7,
|
||||
Line: 0,
|
||||
Col: 7,
|
||||
},
|
||||
To: Position{
|
||||
Index: 11,
|
||||
Line: 0,
|
||||
Col: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: ` let x = '';` + "\n",
|
||||
Parameters: Expression{
|
||||
Value: "",
|
||||
Range: Range{
|
||||
From: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
To: Position{
|
||||
Index: 12,
|
||||
Line: 0,
|
||||
Col: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
suffixes := []string{"", " Trailing '", ` Trailing "`, "\n// More content."}
|
||||
for i, suffix := range suffixes {
|
||||
t.Run(fmt.Sprintf("%s_%d", tt.name, i), func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input + suffix)
|
||||
actual, ok, err := scriptTemplateParser.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("unexpected failure for input %q", tt.input)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
actualSuffix, _ := input.Peek(-1)
|
||||
if diff := cmp.Diff(suffix, actualSuffix); diff != "" {
|
||||
t.Error("unexpected suffix")
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
134
templ/parser/v2/sourcemap.go
Normal file
134
templ/parser/v2/sourcemap.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// NewSourceMap creates a new lookup to map templ source code to items in the
|
||||
// parsed template.
|
||||
func NewSourceMap() *SourceMap {
|
||||
return &SourceMap{
|
||||
SourceLinesToTarget: make(map[uint32]map[uint32]Position),
|
||||
TargetLinesToSource: make(map[uint32]map[uint32]Position),
|
||||
SourceSymbolRangeToTarget: make(map[uint32]map[uint32]Range),
|
||||
TargetSymbolRangeToSource: make(map[uint32]map[uint32]Range),
|
||||
}
|
||||
}
|
||||
|
||||
type SourceMap struct {
|
||||
Expressions []string
|
||||
SourceLinesToTarget map[uint32]map[uint32]Position
|
||||
TargetLinesToSource map[uint32]map[uint32]Position
|
||||
SourceSymbolRangeToTarget map[uint32]map[uint32]Range
|
||||
TargetSymbolRangeToSource map[uint32]map[uint32]Range
|
||||
}
|
||||
|
||||
func (sm *SourceMap) AddSymbolRange(src Range, tgt Range) {
|
||||
sm.SourceSymbolRangeToTarget[src.From.Line] = make(map[uint32]Range)
|
||||
sm.SourceSymbolRangeToTarget[src.From.Line][src.From.Col] = tgt
|
||||
sm.TargetSymbolRangeToSource[tgt.From.Line] = make(map[uint32]Range)
|
||||
sm.TargetSymbolRangeToSource[tgt.From.Line][tgt.From.Col] = src
|
||||
}
|
||||
|
||||
func (sm *SourceMap) SymbolTargetRangeFromSource(line, col uint32) (tgt Range, ok bool) {
|
||||
lm, ok := sm.SourceSymbolRangeToTarget[line]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tgt, ok = lm[col]
|
||||
return
|
||||
}
|
||||
|
||||
func (sm *SourceMap) SymbolSourceRangeFromTarget(line, col uint32) (src Range, ok bool) {
|
||||
lm, ok := sm.TargetSymbolRangeToSource[line]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
src, ok = lm[col]
|
||||
return
|
||||
}
|
||||
|
||||
// Add an item to the lookup.
|
||||
func (sm *SourceMap) Add(src Expression, tgt Range) (updatedFrom Position) {
|
||||
sm.Expressions = append(sm.Expressions, src.Value)
|
||||
srcIndex := src.Range.From.Index
|
||||
tgtIndex := tgt.From.Index
|
||||
|
||||
lines := strings.Split(src.Value, "\n")
|
||||
for lineIndex, line := range lines {
|
||||
srcLine := src.Range.From.Line + uint32(lineIndex)
|
||||
tgtLine := tgt.From.Line + uint32(lineIndex)
|
||||
|
||||
var srcCol, tgtCol uint32
|
||||
if lineIndex == 0 {
|
||||
// First line can have an offset.
|
||||
srcCol += src.Range.From.Col
|
||||
tgtCol += tgt.From.Col
|
||||
}
|
||||
|
||||
// Process the cols.
|
||||
for _, r := range line {
|
||||
if _, ok := sm.SourceLinesToTarget[srcLine]; !ok {
|
||||
sm.SourceLinesToTarget[srcLine] = make(map[uint32]Position)
|
||||
}
|
||||
sm.SourceLinesToTarget[srcLine][srcCol] = NewPosition(tgtIndex, tgtLine, tgtCol)
|
||||
|
||||
if _, ok := sm.TargetLinesToSource[tgtLine]; !ok {
|
||||
sm.TargetLinesToSource[tgtLine] = make(map[uint32]Position)
|
||||
}
|
||||
sm.TargetLinesToSource[tgtLine][tgtCol] = NewPosition(srcIndex, srcLine, srcCol)
|
||||
|
||||
// Ignore invalid runes.
|
||||
rlen := utf8.RuneLen(r)
|
||||
if rlen < 0 {
|
||||
rlen = 1
|
||||
}
|
||||
srcCol += uint32(rlen)
|
||||
tgtCol += uint32(rlen)
|
||||
srcIndex += int64(rlen)
|
||||
tgtIndex += int64(rlen)
|
||||
}
|
||||
|
||||
// LSPs include the newline char as a col.
|
||||
if _, ok := sm.SourceLinesToTarget[srcLine]; !ok {
|
||||
sm.SourceLinesToTarget[srcLine] = make(map[uint32]Position)
|
||||
}
|
||||
sm.SourceLinesToTarget[srcLine][srcCol] = NewPosition(tgtIndex, tgtLine, tgtCol)
|
||||
|
||||
if _, ok := sm.TargetLinesToSource[tgtLine]; !ok {
|
||||
sm.TargetLinesToSource[tgtLine] = make(map[uint32]Position)
|
||||
}
|
||||
sm.TargetLinesToSource[tgtLine][tgtCol] = NewPosition(srcIndex, srcLine, srcCol)
|
||||
|
||||
srcIndex++
|
||||
tgtIndex++
|
||||
}
|
||||
return src.Range.From
|
||||
}
|
||||
|
||||
// TargetPositionFromSource looks up the target position using the source position.
|
||||
func (sm *SourceMap) TargetPositionFromSource(line, col uint32) (tgt Position, ok bool) {
|
||||
lm, ok := sm.SourceLinesToTarget[line]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tgt, ok = lm[col]
|
||||
return
|
||||
}
|
||||
|
||||
// SourcePositionFromTarget looks the source position using the target position.
|
||||
// If a source exists on the line but not the col, the function will search backwards.
|
||||
func (sm *SourceMap) SourcePositionFromTarget(line, col uint32) (src Position, ok bool) {
|
||||
lm, ok := sm.TargetLinesToSource[line]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for {
|
||||
src, ok = lm[col]
|
||||
if ok || col == 0 {
|
||||
return
|
||||
}
|
||||
col--
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user