Skip to content

Commit ae99146

Browse files
authoredAug 18, 2024··
refactor: improve void element parsing perf, fixes #886 and #857 (#887)
1 parent c24c8e4 commit ae99146

12 files changed

+171
-67
lines changed
 

‎generator/test-html/template.templ

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ templ render(p person) {
1010
<hr noshade?={ true }/>
1111
<hr optionA optionB?={ true } optionC="other" optionD?={ false }/>
1212
<hr noshade/>
13-
<input name="test">Text</input>
13+
<input name="test"/>Text
1414
}

‎generator/test-html/template_templ.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎parser/v2/benchmarks_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package parser
2+
3+
import (
4+
_ "embed"
5+
"strings"
6+
"testing"
7+
)
8+
9+
//go:embed benchmarktestdata/benchmark.txt
10+
var benchmarkTemplate string
11+
12+
func BenchmarkParse(b *testing.B) {
13+
b.ReportAllocs()
14+
for i := 0; i < b.N; i++ {
15+
if _, err := ParseString(benchmarkTemplate); err != nil {
16+
b.Fatal(err)
17+
}
18+
}
19+
}
20+
21+
func BenchmarkFormat(b *testing.B) {
22+
b.ReportAllocs()
23+
sb := new(strings.Builder)
24+
for i := 0; i < b.N; i++ {
25+
tf, err := ParseString(benchmarkTemplate)
26+
if err != nil {
27+
b.Fatal(err)
28+
}
29+
if err = tf.Write(sb); err != nil {
30+
b.Fatal(err)
31+
}
32+
sb.Reset()
33+
}
34+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package benchmarktestdata
2+
3+
templ Benchmark() {
4+
<div>Hello</div>
5+
<br>
6+
<br>
7+
<br>
8+
</br>
9+
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
10+
<br>
11+
<br>
12+
<ul>
13+
<li>Item 1</li>
14+
<li>Item 2</li>
15+
<li>Item 3</li>
16+
</ul>
17+
}
18+

‎parser/v2/diagnostics.go

-19
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package parser
22

33
import (
44
"errors"
5-
"fmt"
65
)
76

87
type diagnoser func(Node) ([]Diagnostic, error)
@@ -35,7 +34,6 @@ func walkNodes(t []Node, f func(Node) bool) {
3534

3635
var diagnosers = []diagnoser{
3736
useOfLegacyCallSyntaxDiagnoser,
38-
voidElementWithChildrenDiagnoser,
3937
}
4038

4139
func Diagnose(t TemplateFile) ([]Diagnostic, error) {
@@ -64,20 +62,3 @@ func useOfLegacyCallSyntaxDiagnoser(n Node) ([]Diagnostic, error) {
6462
}
6563
return nil, nil
6664
}
67-
68-
func voidElementWithChildrenDiagnoser(n Node) (d []Diagnostic, err error) {
69-
e, ok := n.(Element)
70-
if !ok {
71-
return
72-
}
73-
if !e.IsVoidElement() {
74-
return
75-
}
76-
if len(e.Children) == 0 {
77-
return
78-
}
79-
return []Diagnostic{{
80-
Message: fmt.Sprintf("void element <%s> should not have child content", e.Name),
81-
Range: e.NameRange,
82-
}}, nil
83-
}

‎parser/v2/diagnostics_test.go

-15
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,6 @@ templ template () {
134134
}`,
135135
want: nil,
136136
},
137-
{
138-
name: "voidElementWithChildrenDiagnoser: with diagnostics",
139-
template: `
140-
package main
141-
142-
templ template () {
143-
<div>
144-
<input>Child content</input>
145-
</div>
146-
}`,
147-
want: []Diagnostic{{
148-
Message: "void element <input> should not have child content",
149-
Range: Range{Position{46, 5, 4}, Position{51, 5, 9}},
150-
}},
151-
},
152137
}
153138
for _, tt := range tests {
154139
t.Run(tt.name, func(t *testing.T) {

‎parser/v2/elementparser.go

+34-11
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,34 @@ var (
375375
})
376376
)
377377

378+
// Void element closer.
379+
var voidElementCloser voidElementCloserParser
380+
381+
type voidElementCloserParser struct{}
382+
383+
var voidElementCloseTags = []string{"</area>", "</base>", "</br>", "</col>", "</command>", "</embed>", "</hr>", "</img>", "</input>", "</keygen>", "</link>", "</meta>", "</param>", "</source>", "</track>", "</wbr>"}
384+
385+
func (voidElementCloserParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
386+
var ve string
387+
for _, ve = range voidElementCloseTags {
388+
s, canPeekLen := pi.Peek(len(ve))
389+
if !canPeekLen {
390+
continue
391+
}
392+
if !strings.EqualFold(s, ve) {
393+
continue
394+
}
395+
// Found a match.
396+
ok = true
397+
break
398+
}
399+
if !ok {
400+
return nil, false, nil
401+
}
402+
pi.Take(len(ve))
403+
return nil, true, nil
404+
}
405+
378406
// Element.
379407
var element elementParser
380408

@@ -396,28 +424,19 @@ func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
396424

397425
// Once we've got an open tag, the rest must be present.
398426
l := pi.Position().Line
399-
endOfOpenTag := pi.Index()
400427

401428
// If the element is self-closing, even if it's not really a void element (br, hr etc.), we can return early.
402-
if ot.Void {
429+
if ot.Void || r.IsVoidElement() {
403430
// Escape early, no need to try to parse children for self-closing elements.
404431
return addTrailingSpaceAndValidate(start, r, pi)
405432
}
406433

407-
// Void elements _might_ have children, even though it's invalid.
408-
// We want to allow this to be parsed.
434+
// Parse children.
409435
closer := StripType(parse.All(parse.String("</"), parse.String(ot.Name), parse.Rune('>')))
410436
tnp := newTemplateNodeParser(closer, fmt.Sprintf("<%s>: close tag", ot.Name))
411437
nodes, _, err := tnp.Parse(pi)
412438
if err != nil {
413439
notFoundErr, isNotFoundError := err.(UntilNotFoundError)
414-
if r.IsVoidElement() && isNotFoundError {
415-
// Void elements shouldn't have children, or a close tag, so this is expected.
416-
// When the template is reformatted, we won't reach here, because <hr> will be converted to <hr/>.
417-
// Return the element as we have it.
418-
pi.Seek(endOfOpenTag)
419-
return addTrailingSpaceAndValidate(start, r, pi)
420-
}
421440
if isNotFoundError {
422441
err = notFoundErr.ParseError
423442
}
@@ -443,6 +462,10 @@ func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
443462
}
444463

445464
func addTrailingSpaceAndValidate(start parse.Position, e Element, pi *parse.Input) (n Node, ok bool, err error) {
465+
// Elide any void close tags.
466+
if _, _, err = voidElementCloser.Parse(pi); err != nil {
467+
return e, false, err
468+
}
446469
// Add trailing space.
447470
ws, _, err := parse.Whitespace.Parse(pi)
448471
if err != nil {

‎parser/v2/elementparser_test.go

+21-19
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,20 @@ if test {
550550
}
551551
}
552552

553+
func TestVoidElementCloserParser(t *testing.T) {
554+
t.Run("all void elements are parsed", func(t *testing.T) {
555+
for _, input := range voidElementCloseTags {
556+
_, ok, err := voidElementCloser.Parse(parse.NewInput(input))
557+
if err != nil {
558+
t.Fatalf("unexpected error: %v", err)
559+
}
560+
if !ok {
561+
t.Fatalf("failed to parse %q", input)
562+
}
563+
}
564+
})
565+
}
566+
553567
func TestElementParser(t *testing.T) {
554568
tests := []struct {
555569
name string
@@ -662,15 +676,9 @@ func TestElementParser(t *testing.T) {
662676
From: Position{Index: 1, Line: 0, Col: 1},
663677
To: Position{Index: 6, Line: 0, Col: 6},
664678
},
665-
Children: []Node{
666-
Text{
667-
Value: "Text",
668-
Range: Range{
669-
From: Position{Index: 7, Line: 0, Col: 7},
670-
To: Position{Index: 11, Line: 0, Col: 11},
671-
},
672-
},
673-
},
679+
// <input> is a void element, so text is not a child of the input.
680+
// </input> is ignored.
681+
Children: nil,
674682
},
675683
},
676684
{
@@ -736,23 +744,17 @@ func TestElementParser(t *testing.T) {
736744
},
737745
},
738746
{
739-
name: "element: void nesting others is OK (br/hr)",
747+
name: "element: void nesting is ignored",
740748
input: `<br><hr></br>`,
741749
expected: Element{
742750
Name: "br",
743751
NameRange: Range{
744752
From: Position{Index: 1, Line: 0, Col: 1},
745753
To: Position{Index: 3, Line: 0, Col: 3},
746754
},
747-
Children: []Node{
748-
Element{
749-
Name: "hr",
750-
NameRange: Range{
751-
From: Position{Index: 5, Line: 0, Col: 5},
752-
To: Position{Index: 7, Line: 0, Col: 7},
753-
},
754-
},
755-
},
755+
// <br> is a void element, so <hr> is not a child of the <br>.
756+
// </br> is ignored.
757+
Children: nil,
756758
},
757759
},
758760
{

‎parser/v2/formattestdata/void_elements_are_converted_to_self_closing_elements.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ templ input(value, validation string) {
2020
<img></img>
2121
<input>
2222
<input></input>
23+
<input>Text
2324
<input>Text</input>
2425
<keygen>
2526
<keygen></keygen>
@@ -59,7 +60,8 @@ templ input(value, validation string) {
5960
<img/>
6061
<input/>
6162
<input/>
62-
<input>Text</input>
63+
<input/>Text
64+
<input/>Text
6365
<keygen/>
6466
<keygen/>
6567
<link/>

‎parser/v2/templateparser.go

+16
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ type templateNodeParser[TUntil any] struct {
6161

6262
var rawElements = parse.Any(styleElement, scriptElement)
6363

64+
var templateNodeSkipParsers = []parse.Parser[Node]{
65+
voidElementCloser, // </br>, </img> etc. - should be ignored.
66+
}
67+
6468
var templateNodeParsers = []parse.Parser[Node]{
6569
docType, // <!DOCTYPE html>
6670
htmlComment, // <!--
@@ -80,6 +84,7 @@ var templateNodeParsers = []parse.Parser[Node]{
8084
}
8185

8286
func (p templateNodeParser[T]) Parse(pi *parse.Input) (op Nodes, ok bool, err error) {
87+
outer:
8388
for {
8489
// Check if we've reached the end.
8590
if p.until != nil {
@@ -94,6 +99,17 @@ func (p templateNodeParser[T]) Parse(pi *parse.Input) (op Nodes, ok bool, err er
9499
}
95100
}
96101

102+
// Skip any nodes that we don't care about.
103+
for _, p := range templateNodeSkipParsers {
104+
_, matched, err := p.Parse(pi)
105+
if err != nil {
106+
return Nodes{}, false, err
107+
}
108+
if matched {
109+
continue outer
110+
}
111+
}
112+
97113
// Attempt to parse a node.
98114
// Loop through the parsers and try to parse a node.
99115
var matched bool

‎parser/v2/templateparser_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,44 @@ func TestTemplateParser(t *testing.T) {
806806
},
807807
},
808808
},
809+
{
810+
name: "template: void element closers are ignored",
811+
input: `templ Name() {
812+
<br></br><br>
813+
}`,
814+
expected: HTMLTemplate{
815+
Range: Range{
816+
From: Position{Index: 0, Line: 0, Col: 0},
817+
To: Position{Index: 31, Line: 2, Col: 1},
818+
},
819+
Expression: Expression{
820+
Value: "Name()",
821+
Range: Range{
822+
From: Position{Index: 6, Line: 0, Col: 6},
823+
To: Position{Index: 12, Line: 0, Col: 12},
824+
},
825+
},
826+
Children: []Node{
827+
Whitespace{Value: "\t"},
828+
Element{
829+
Name: "br",
830+
NameRange: Range{
831+
From: Position{Index: 17, Line: 1, Col: 2},
832+
To: Position{Index: 19, Line: 1, Col: 4},
833+
},
834+
TrailingSpace: SpaceNone,
835+
},
836+
Element{
837+
Name: "br",
838+
NameRange: Range{
839+
From: Position{Index: 26, Line: 1, Col: 11},
840+
To: Position{Index: 28, Line: 1, Col: 13},
841+
},
842+
TrailingSpace: SpaceVertical,
843+
},
844+
},
845+
},
846+
},
809847
}
810848
for _, tt := range tests {
811849
tt := tt

‎parser/v2/textparser.go

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ var textParser = parse.Func(func(pi *parse.Input) (n Node, ok bool, err error) {
2525
}
2626
t.Range = NewRange(from, pi.Position())
2727

28+
// Elide any void element closing tags.
29+
if _, _, err = voidElementCloser.Parse(pi); err != nil {
30+
return
31+
}
32+
2833
// Parse trailing whitespace.
2934
ws, _, err := parse.Whitespace.Parse(pi)
3035
if err != nil {

0 commit comments

Comments
 (0)
Please sign in to comment.