Changed: DB Params

This commit is contained in:
2025-03-20 12:35:13 +01:00
parent 8640a12439
commit b71b3d12ca
822 changed files with 134218 additions and 0 deletions

View 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))
}
}

View 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()
}
}

View 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>
}

View 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
}

View 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, ``)
}

View 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
})

View 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, ``)
}

View 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
}

View 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
})

View 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)
}
})
}
}

View 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
}

View 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)
}
})
}
}

View 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
})

View 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)
}
})
}
}

View 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 doesnt 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
}

File diff suppressed because it is too large Load Diff

View 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("`"))

View 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)
}
})
}
}

View 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
}

View 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")
}
})
}

View 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)
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View 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.
*/
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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 };
}

View 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 };
}

View 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 };
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -0,0 +1,14 @@
-- in --
package main
// test the comment handling.
templ test() {
Test
}
-- out --
package main
// test the comment handling.
templ test() {
Test
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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
`,
)
}

View File

@@ -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>
}

View 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] }}
}

View File

@@ -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>
}

View 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);
}

View File

@@ -0,0 +1,12 @@
-- in --
package main
templ x() {
<div>{firstName} {lastName}</div>
}
-- out --
package main
templ x() {
<div>{ firstName } { lastName }</div>
}

View File

@@ -0,0 +1,12 @@
-- in --
package main
templ x() {
<div>{pt1}{pt2}</div>
}
-- out --
package main
templ x() {
<div>{ pt1 }{ pt2 }</div>
}

View File

@@ -0,0 +1,12 @@
-- in --
package main
templ x() {
<div>{firstName...} {lastName...}</div>
}
-- out --
package main
templ x() {
<div>{ firstName... } { lastName... }</div>
}

View File

@@ -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>
}

View 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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -0,0 +1,13 @@
-- in --
package goof
templ Hello() {
Hello
}
-- out --
package goof
templ Hello() {
Hello
}

View File

@@ -0,0 +1,14 @@
-- in --
// Go comment
package goof
templ Hello() {
Hello
}
-- out --
// Go comment
package goof
templ Hello() {
Hello
}

View File

@@ -0,0 +1,16 @@
-- in --
// Go comment
package goof
templ Hello() {
Hello
}
-- out --
// Go comment
package goof
templ Hello() {
Hello
}

View File

@@ -0,0 +1,20 @@
-- in --
/********************
* multiline message *
********************/
package goof
templ Hello() {
Hello
}
-- out --
/********************
* multiline message *
********************/
package goof
templ Hello() {
Hello
}

View File

@@ -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
}

View File

@@ -0,0 +1,10 @@
-- in --
//go:build dev
package p
-- out --
//go:build dev
package p

View File

@@ -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>
}
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}

View File

@@ -0,0 +1,12 @@
-- in --
package p
templ f() {
@Other(p.Test,"s")
}
-- out --
package p
templ f() {
@Other(p.Test, "s")
}

View File

@@ -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>
}
}

View File

@@ -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>
}

View File

@@ -0,0 +1,12 @@
-- in --
package p
templ f() {
@Other(p.Test)
}
-- out --
package p
templ f() {
@Other(p.Test)
}

View File

@@ -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/>
}

View File

@@ -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
View File

@@ -0,0 +1,2 @@
echo Element
go test -fuzz=FuzzElement -fuzztime=120s

View 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
})

View 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)
}
})
}
}

View 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)

View 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)
}
})
}
}

View 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

View 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
}

View 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)
}
}
}

View 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)
}
}
}

View 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
}

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("default0")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("default:{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{`0\r000000")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("default")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("#")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("func")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("(")

View 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
}

View 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
}

View 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)
}
})
}
}

View 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
}

View 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")
}
})
}

View 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
})

View 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
View 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
View 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
View 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)
}
})
}
}

View 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
})

View 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)
}
})
}
}
}

View 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