From ad707cbb81bdae77bed1a3ac0ffb95f832bdb3dd Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Fri, 10 May 2024 16:50:00 +0100 Subject: [PATCH] feat: support parsing (and converting) void elements, fixes #637 (#732) --- .version | 2 +- README.md | 10 + generator/generator.go | 44 +- generator/test-html/expected.html | 2 +- generator/test-html/template.templ | 1 + generator/test-html/template_templ.go | 2 +- parser/v2/diagnostics.go | 32 +- parser/v2/diagnostics_test.go | 43 +- parser/v2/elementparser.go | 158 ++-- parser/v2/elementparser_test.go | 154 +++- parser/v2/format_test.go | 2 +- parser/v2/templateparser.go | 8 +- ...ntation__when_close_tag_is_on_new_line.txt | 20 + ...tation__when_close_tag_is_on_same_line.txt | 12 + ...n_one_line_are_not_placed_on_new_lines.txt | 12 + .../br_elements_are_placed_on_new_lines.txt | 24 + .../children_indented__closing_elm.txt | 15 + .../children_indented__first_child.txt | 15 + parser/v2/testdata/comments_are_preserved.txt | 24 + ..._indentation_rules_as_regular_elements.txt | 24 + ...pressions_result_in_all_attrs_indented.txt | 21 + ...ssions_result_in_all_attrs_indented__2.txt | 22 + ...ns_with_else_blocks_are_also_formatted.txt | 26 + ...tes__but_use_single_quotes_if_required.txt | 17 + .../testdata/css_is_indented_by_one_level.txt | 14 + .../v2/testdata/css_whitespace_is_tidied.txt | 15 + .../empty_elements_stay_on_the_same_line.txt | 17 + .../for_loops_are_placed_on_a_new_line.txt | 20 + .../formatting_does_not_alter_whitespace.txt | 13 + ...ions_are_formatted_by_the_go_formatter.txt | 42 + .../testdata/godoc_comments_are_preserved.txt | 14 + ...if_statements_are_placed_on_a_new_line.txt | 25 + ..._elements_are_not_placed_on_a_new_line.txt | 20 + ...line_are_not_split_into_multiple_lines.txt | 12 + ...not_converted_to_self_closing_elements.txt | 13 + ...ing_between_string_expressions_is_kept.txt | 12 + ...ing_expressions_is_not_magically_added.txt | 12 + ...ween_string_spreads_attributes_is_kept.txt | 12 + ...ch_statements_are_placed_on_a_new_line.txt | 26 + .../v2/testdata/tables_are_formatted_well.txt | 30 + ...are_formatted_correctly_when_multiline.txt | 34 + ...e_formatted_the_same_as_other_elements.txt | 26 + ...are_converted_to_self_closing_elements.txt | 77 ++ ...n_new_lines__the_children_are_indented.txt | 15 + parser/v2/types_test.go | 856 ------------------ 45 files changed, 953 insertions(+), 1042 deletions(-) create mode 100644 parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_new_line.txt create mode 100644 parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_same_line.txt create mode 100644 parser/v2/testdata/br_and_hr_all_on_one_line_are_not_placed_on_new_lines.txt create mode 100644 parser/v2/testdata/br_elements_are_placed_on_new_lines.txt create mode 100644 parser/v2/testdata/children_indented__closing_elm.txt create mode 100644 parser/v2/testdata/children_indented__first_child.txt create mode 100644 parser/v2/testdata/comments_are_preserved.txt create mode 100644 parser/v2/testdata/conditional_expressions_have_the_same_child_indentation_rules_as_regular_elements.txt create mode 100644 parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented.txt create mode 100644 parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented__2.txt create mode 100644 parser/v2/testdata/conditional_expressions_with_else_blocks_are_also_formatted.txt create mode 100644 parser/v2/testdata/constant_attributes_prerfer_double_quotes__but_use_single_quotes_if_required.txt create mode 100644 parser/v2/testdata/css_is_indented_by_one_level.txt create mode 100644 parser/v2/testdata/css_whitespace_is_tidied.txt create mode 100644 parser/v2/testdata/empty_elements_stay_on_the_same_line.txt create mode 100644 parser/v2/testdata/for_loops_are_placed_on_a_new_line.txt create mode 100644 parser/v2/testdata/formatting_does_not_alter_whitespace.txt create mode 100644 parser/v2/testdata/go_expressions_are_formatted_by_the_go_formatter.txt create mode 100644 parser/v2/testdata/godoc_comments_are_preserved.txt create mode 100644 parser/v2/testdata/if_statements_are_placed_on_a_new_line.txt create mode 100644 parser/v2/testdata/inline_elements_are_not_placed_on_a_new_line.txt create mode 100644 parser/v2/testdata/non_empty_elements_with_children_that_are_all_on_the_same_line_are_not_split_into_multiple_lines.txt create mode 100644 parser/v2/testdata/script_tags_are_not_converted_to_self_closing_elements.txt create mode 100644 parser/v2/testdata/spacing_between_string_expressions_is_kept.txt create mode 100644 parser/v2/testdata/spacing_between_string_expressions_is_not_magically_added.txt create mode 100644 parser/v2/testdata/spacing_between_string_spreads_attributes_is_kept.txt create mode 100644 parser/v2/testdata/switch_statements_are_placed_on_a_new_line.txt create mode 100644 parser/v2/testdata/tables_are_formatted_well.txt create mode 100644 parser/v2/testdata/templ_expression_attributes_are_formatted_correctly_when_multiline.txt create mode 100644 parser/v2/testdata/templ_expression_elements_are_formatted_the_same_as_other_elements.txt create mode 100644 parser/v2/testdata/void_elements_are_converted_to_self_closing_elements.txt create mode 100644 parser/v2/testdata/when_an_element_contains_children_that_are_on_new_lines__the_children_are_indented.txt delete mode 100644 parser/v2/types_test.go diff --git a/.version b/.version index 91dbe0903..2b4e4ea41 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.682 \ No newline at end of file +0.2.683 \ No newline at end of file diff --git a/README.md b/README.md index 79161ddb7..b888fd54d 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,16 @@ go run ./cmd/templ generate -include-version=false go test ./... ``` +### test-short + +Run Go tests. + +```sh +go run ./get-version > .version +go run ./cmd/templ generate -include-version=false +go test ./... -short +``` + ### test-cover Run Go tests. diff --git a/generator/generator.go b/generator/generator.go index 4b954973d..8254b0f9a 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -880,46 +880,6 @@ func (g *generator) writeExpressionErrorHandler(indentLevel int, expression pars } func (g *generator) writeElement(indentLevel int, n parser.Element) (err error) { - if n.IsVoidElement() { - return g.writeVoidElement(indentLevel, n) - } - return g.writeStandardElement(indentLevel, n) -} - -func (g *generator) writeVoidElement(indentLevel int, n parser.Element) (err error) { - if len(n.Children) > 0 { - return fmt.Errorf("writeVoidElement: void element %q must not have child elements", n.Name) - } - if len(n.Attributes) == 0 { - //
- if _, err = g.w.WriteStringLiteral(indentLevel, fmt.Sprintf(`<%s>`, html.EscapeString(n.Name))); err != nil { - return err - } - } else { - // - if err = g.writeElementCSS(indentLevel, n); err != nil { - return err - } - // - if err = g.writeElementScript(indentLevel, n); err != nil { - return err - } - //
- if _, err = g.w.WriteStringLiteral(indentLevel, `>`); err != nil { - return err - } - } - return err -} - -func (g *generator) writeStandardElement(indentLevel int, n parser.Element) (err error) { if len(n.Attributes) == 0 { //
if _, err = g.w.WriteStringLiteral(indentLevel, fmt.Sprintf(`<%s>`, html.EscapeString(n.Name))); err != nil { @@ -946,6 +906,10 @@ func (g *generator) writeStandardElement(indentLevel int, n parser.Element) (err return err } } + // Skip children and close tag for void elements. + if n.IsVoidElement() && len(n.Children) == 0 { + return nil + } // Children. if err = g.writeNodes(indentLevel, stripWhitespace(n.Children), nil); err != nil { return err diff --git a/generator/test-html/expected.html b/generator/test-html/expected.html index 6b26cd275..a04251dba 100644 --- a/generator/test-html/expected.html +++ b/generator/test-html/expected.html @@ -8,4 +8,4 @@

Luiz Bonfa




- +Text diff --git a/generator/test-html/template.templ b/generator/test-html/template.templ index 85749016e..8afe58f9b 100644 --- a/generator/test-html/template.templ +++ b/generator/test-html/template.templ @@ -10,4 +10,5 @@ templ render(p person) {


+ Text } diff --git a/generator/test-html/template_templ.go b/generator/test-html/template_templ.go index 3d70d0187..dc2a5b87b 100644 --- a/generator/test-html/template_templ.go +++ b/generator/test-html/template_templ.go @@ -100,7 +100,7 @@ func render(p person) templ.Component { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">
Text") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/parser/v2/diagnostics.go b/parser/v2/diagnostics.go index 7c0e59fd2..a8ee5e985 100644 --- a/parser/v2/diagnostics.go +++ b/parser/v2/diagnostics.go @@ -1,6 +1,9 @@ package parser -import "errors" +import ( + "errors" + "fmt" +) type diagnoser func(Node) ([]Diagnostic, error) @@ -30,10 +33,12 @@ func walkNodes(t []Node, f func(Node) bool) { } } +var diagnosers = []diagnoser{ + useOfLegacyCallSyntaxDiagnoser, + voidElementWithChildrenDiagnoser, +} + func Diagnose(t TemplateFile) ([]Diagnostic, error) { - diagnosers := []diagnoser{ - legacyCallSyntaxDiagnoser, - } var diags []Diagnostic var errs error walkTemplate(t, func(n Node) bool { @@ -50,7 +55,7 @@ func Diagnose(t TemplateFile) ([]Diagnostic, error) { return diags, errs } -func legacyCallSyntaxDiagnoser(n Node) ([]Diagnostic, error) { +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.", @@ -59,3 +64,20 @@ func legacyCallSyntaxDiagnoser(n Node) ([]Diagnostic, error) { } return nil, nil } + +func voidElementWithChildrenDiagnoser(n Node) (d []Diagnostic, err error) { + e, ok := n.(Element) + if !ok { + return + } + if !e.IsVoidElement() { + return + } + if len(e.Children) == 0 { + return + } + return []Diagnostic{{ + Message: fmt.Sprintf("void element <%s> should not have child content", e.Name), + Range: e.NameRange, + }}, nil +} diff --git a/parser/v2/diagnostics_test.go b/parser/v2/diagnostics_test.go index fbe9871b6..3a7cea5be 100644 --- a/parser/v2/diagnostics_test.go +++ b/parser/v2/diagnostics_test.go @@ -23,10 +23,10 @@ templ template () { want: nil, }, - // legacyCallSyntaxDiagnoser + // useOfLegacyCallSyntaxDiagnoser { - name: "legacyCallSyntaxDiagnoser: template root", + name: "useOfLegacyCallSyntaxDiagnoser: template root", template: ` package main @@ -39,7 +39,7 @@ templ template () { }}, }, { - name: "legacyCallSyntaxDiagnoser: in div", + name: "useOfLegacyCallSyntaxDiagnoser: in div", template: ` package main @@ -54,7 +54,7 @@ templ template () { }}, }, { - name: "legacyCallSyntaxDiagnoser: in if", + name: "useOfLegacyCallSyntaxDiagnoser: in if", template: ` package main @@ -69,7 +69,7 @@ templ template () { }}, }, { - name: "legacyCallSyntaxDiagnoser: in for", + name: "useOfLegacyCallSyntaxDiagnoser: in for", template: ` package main @@ -84,7 +84,7 @@ templ template () { }}, }, { - name: "legacyCallSyntaxDiagnoser: in switch", + name: "useOfLegacyCallSyntaxDiagnoser: in switch", template: ` package main @@ -108,7 +108,7 @@ templ template () { }, }, { - name: "legacyCallSyntaxDiagnoser: in block", + name: "useOfLegacyCallSyntaxDiagnoser: in block", template: ` package main @@ -122,6 +122,33 @@ templ template () { Range: Range{Position{59, 5, 5}, Position{75, 5, 21}}, }}, }, + { + name: "voidElementWithChildrenDiagnoser: no diagnostics", + template: ` +package main + +templ template () { +
+ +
+}`, + want: nil, + }, + { + name: "voidElementWithChildrenDiagnoser: with diagnostics", + template: ` +package main + +templ template () { +
+ Child content +
+}`, + want: []Diagnostic{{ + Message: "void element should not have child content", + Range: Range{Position{46, 5, 4}, Position{51, 5, 9}}, + }}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -133,7 +160,7 @@ templ template () { if err != nil { t.Fatalf("Diagnose() error = %v", err) } - if diff := cmp.Diff(got, tt.want); diff != "" { + if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Diagnose() mismatch (-got +want):\n%s", diff) } }) diff --git a/parser/v2/elementparser.go b/parser/v2/elementparser.go index f64990087..478d17621 100644 --- a/parser/v2/elementparser.go +++ b/parser/v2/elementparser.go @@ -17,6 +17,7 @@ type elementOpenTag struct { Attributes []Attribute IndentAttrs bool NameRange Range + Void bool } var elementOpenTagParser = parse.Func(func(pi *parse.Input) (e elementOpenTag, ok bool, err error) { @@ -51,36 +52,29 @@ var elementOpenTagParser = parse.Func(func(pi *parse.Input) (e elementOpenTag, o 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 e, false, err + return } return e, true, nil }) -// Element close tag. -type elementCloseTag struct { - Name string -} - -var elementCloseTagParser = parse.Func(func(in *parse.Input) (ct elementCloseTag, ok bool, err error) { - var parts []string - parts, ok, err = parse.All( - parse.String("')).Parse(in) - if err != nil || !ok { - return - } - ct.Name = parts[1] - return ct, true, nil -}) - // Attribute name. var ( attributeNameFirst = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:_@" @@ -382,11 +376,14 @@ var ( ) // Element. -var elementOpenClose elementOpenCloseParser +var element elementParser -type elementOpenCloseParser struct{} +type elementParser struct{} + +func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) { + var r Element + start := pi.Position() -func (elementOpenCloseParser) Parse(pi *parse.Input) (r Element, ok bool, err error) { // Check the open tag. var ot elementOpenTag if ot, ok, err = elementOpenTagParser.Parse(pi); err != nil || !ok { @@ -399,84 +396,54 @@ func (elementOpenCloseParser) Parse(pi *parse.Input) (r Element, ok bool, err er // Once we've got an open tag, the rest must be present. l := pi.Position().Line - var nodes Nodes - if nodes, ok, err = newTemplateNodeParser[any](nil, "").Parse(pi); err != nil || !ok { - return + endOfOpenTag := pi.Index() + + // 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 { + // Escape early, no need to try to parse children for self-closing elements. + return addTrailingSpaceAndValidate(start, r, pi) + } + + // Void elements _might_ have children, even though it's invalid. + // We want to allow this to be parsed. + closer := StripType(parse.All(parse.String("'))) + tnp := newTemplateNodeParser[any](closer, fmt.Sprintf("<%s>: close tag", ot.Name)) + nodes, _, err := tnp.Parse(pi) + if err != nil { + notFoundErr, isNotFoundError := err.(UntilNotFoundError) + if r.IsVoidElement() && isNotFoundError { + // Void elements shouldn't have children, or a close tag, so this is expected. + // When the template is reformatted, we won't reach here, because
will be converted to
. + // Return the element as we have it. + pi.Seek(endOfOpenTag) + return addTrailingSpaceAndValidate(start, r, pi) + } + 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 the children are not all on the same line, indent them. if l != pi.Position().Line { r.IndentChildren = true } // Close tag. - pos := pi.Position() - var ct elementCloseTag - ct, ok, err = elementCloseTagParser.Parse(pi) + _, ok, err = closer.Parse(pi) if err != nil { - return + 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 - } - if ct.Name != r.Name { - err = parse.Error(fmt.Sprintf("<%s>: mismatched end tag, expected '', got ''", r.Name, r.Name, ct.Name), pos) - 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 + return addTrailingSpaceAndValidate(start, r, pi) } -// Element self-closing tag. -var selfClosingElement = parse.Func(func(pi *parse.Input) (e Element, ok bool, err error) { - start := pi.Index() - - // lt - 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) - 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) - return - } - - // Optional whitespace. - if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil { - pi.Seek(start) - return - } - - // If any attribute is not on the same line as the element name, indent them. - if pi.Position().Line != l { - e.IndentAttrs = true - } - - if _, ok, err = parse.String("/>").Parse(pi); err != nil || !ok { - pi.Seek(start) - return - } - - // Parse trailing whitespace. +func addTrailingSpaceAndValidate(start parse.Position, e Element, pi *parse.Input) (n Node, ok bool, err error) { + // Add trailing space. ws, _, err := parse.Whitespace.Parse(pi) if err != nil { return e, false, err @@ -486,25 +453,12 @@ var selfClosingElement = parse.Func(func(pi *parse.Input) (e Element, ok bool, e return e, false, err } - return e, true, nil -}) - -// Element -var element elementParser - -type elementParser struct{} - -func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) { - start := pi.Position() - - var r Element - if r, ok, err = parse.Any[Element](selfClosingElement, elementOpenClose).Parse(pi); err != nil || !ok { - return - } + // Validate. var msgs []string - if msgs, ok = r.Validate(); !ok { - err = parse.Error(fmt.Sprintf("<%s>: %s", r.Name, strings.Join(msgs, ", ")), start) + if msgs, ok = e.Validate(); !ok { + err = parse.Error(fmt.Sprintf("<%s>: %s", e.Name, strings.Join(msgs, ", ")), start) + return e, false, err } - return r, ok, err + return e, true, nil } diff --git a/parser/v2/elementparser_test.go b/parser/v2/elementparser_test.go index 1399ae9a3..2ca10c1d4 100644 --- a/parser/v2/elementparser_test.go +++ b/parser/v2/elementparser_test.go @@ -41,31 +41,6 @@ func TestAttributeParser(t *testing.T) { }, }, }, - { - name: "element: colon in name", - input: ``, - parser: StripType(elementOpenTagParser), - expected: elementOpenTag{ - Name: "maps:map", - NameRange: Range{ - From: Position{Index: 1, Line: 0, Col: 1}, - To: Position{Index: 9, Line: 0, Col: 9}, - }, - }, - }, - { - name: "element: colon in name, closing", - input: `Content`, - parser: StripType(element), - expected: Element{ - Name: "maps:map", - NameRange: Range{ - From: Position{Index: 1, Line: 0, Col: 1}, - To: Position{Index: 9, Line: 0, Col: 9}, - }, - Children: []Node{Text{Value: "Content"}}, - }, - }, { name: "element: open with hyperscript attribute", input: `
`, @@ -602,6 +577,86 @@ func TestElementParser(t *testing.T) { }, }, }, + { + name: "element: colon in name, empty", + input: ``, + expected: Element{ + Name: "maps:map", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 9, Line: 0, Col: 9}, + }, + }, + }, + { + name: "element: colon in name, with content", + input: `Content`, + expected: Element{ + Name: "maps:map", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 9, Line: 0, Col: 9}, + }, + Children: []Node{Text{Value: "Content"}}, + }, + }, + { + name: "element: void (input)", + input: ``, + expected: Element{ + Name: "input", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 6, Line: 0, Col: 6}, + }, + Children: nil, + }, + }, + { + name: "element: void (br)", + input: `
`, + expected: Element{ + Name: "br", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 3, Line: 0, Col: 3}, + }, + Children: nil, + }, + }, + { + name: "element: void (hr)", + input: `
`, + expected: Element{ + Name: "hr", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 3, Line: 0, Col: 3}, + }, + Attributes: []Attribute{ + BoolConstantAttribute{ + Name: "noshade", + NameRange: Range{ + From: Position{Index: 4, Line: 0, Col: 4}, + To: Position{Index: 11, Line: 0, Col: 11}, + }, + }, + }, + Children: nil, + }, + }, + { + name: "element: void with content", + input: `Text`, + expected: Element{ + Name: "input", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 6, Line: 0, Col: 6}, + }, + Children: []Node{Text{Value: "Text"}}, + }, + }, { name: "element: self-closing with single bool expression attribute", input: `
`, @@ -637,6 +692,53 @@ func TestElementParser(t *testing.T) { }, }, }, + { + name: "element: void nesting same is OK", + input: `



`, + expected: Element{ + Name: "div", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 4, Line: 0, Col: 4}, + }, + Children: []Node{ + Element{ + Name: "br", // The
one. + NameRange: Range{ + From: Position{Index: 6, Line: 0, Col: 6}, + To: Position{Index: 8, Line: 0, Col: 8}, + }, + }, + Element{ + Name: "br", // The

one. + NameRange: Range{ + From: Position{Index: 10, Line: 0, Col: 10}, + To: Position{Index: 12, Line: 0, Col: 12}, + }, + }, + }, + }, + }, + { + name: "element: void nesting others is OK (br/hr)", + input: `


`, + expected: Element{ + Name: "br", + NameRange: Range{ + From: Position{Index: 1, Line: 0, Col: 1}, + To: Position{Index: 3, Line: 0, Col: 3}, + }, + Children: []Node{ + Element{ + Name: "hr", + NameRange: Range{ + From: Position{Index: 5, Line: 0, Col: 5}, + To: Position{Index: 7, Line: 0, Col: 7}, + }, + }, + }, + }, + }, { name: "element: self-closing with single expression attribute", input: ``, @@ -1432,7 +1534,7 @@ func TestElementParserErrors(t *testing.T) { { name: "element: mismatched end tag", input: ``, - expected: parse.Error(": mismatched end tag, expected '', got ''", + expected: parse.Error(": close tag not found", parse.Position{ Index: 3, Line: 0, diff --git a/parser/v2/format_test.go b/parser/v2/format_test.go index 64b69f1c3..9fb574529 100644 --- a/parser/v2/format_test.go +++ b/parser/v2/format_test.go @@ -9,7 +9,7 @@ import ( "golang.org/x/tools/txtar" ) -func TestFormat(t *testing.T) { +func TestFormatting(t *testing.T) { files, _ := filepath.Glob("testdata/*.txt") if len(files) == 0 { t.Errorf("no test files found") diff --git a/parser/v2/templateparser.go b/parser/v2/templateparser.go index 054e2ec47..ea0176fab 100644 --- a/parser/v2/templateparser.go +++ b/parser/v2/templateparser.go @@ -118,9 +118,15 @@ func (p templateNodeParser[T]) Parse(pi *parse.Input) (op Nodes, ok bool, err er break } - err = fmt.Errorf("%v not found", p.untilName) + err = UntilNotFoundError{ + ParseError: parse.Error(fmt.Sprintf("%v not found", p.untilName), pi.Position()), + } return } return op, true, nil } + +type UntilNotFoundError struct { + parse.ParseError +} diff --git a/parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_new_line.txt b/parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_new_line.txt new file mode 100644 index 000000000..128829537 --- /dev/null +++ b/parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_new_line.txt @@ -0,0 +1,20 @@ +-- in -- +package test + +templ input(value, validation string) { +

{ "the" }{ "data" } +

+} +-- out -- +package test + +templ input(value, validation string) { +
+

+ { "the" } + + { "data" } + +

+
+} diff --git a/parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_same_line.txt b/parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_same_line.txt new file mode 100644 index 000000000..9cdc11ce5 --- /dev/null +++ b/parser/v2/testdata/all_children_indented__with_nested_indentation__when_close_tag_is_on_same_line.txt @@ -0,0 +1,12 @@ +-- in -- +package test + +templ input(value, validation string) { +

{ "the" }{ "data" }

+} +-- out -- +package test + +templ input(value, validation string) { +

{ "the" }{ "data" }

+} diff --git a/parser/v2/testdata/br_and_hr_all_on_one_line_are_not_placed_on_new_lines.txt b/parser/v2/testdata/br_and_hr_all_on_one_line_are_not_placed_on_new_lines.txt new file mode 100644 index 000000000..f5e6d0969 --- /dev/null +++ b/parser/v2/testdata/br_and_hr_all_on_one_line_are_not_placed_on_new_lines.txt @@ -0,0 +1,12 @@ +-- in -- +package main + +templ test() { +
Linebreaks
used
for
spacing
+} +-- out -- +package main + +templ test() { +
Linebreaks
used
for
spacing
+} diff --git a/parser/v2/testdata/br_elements_are_placed_on_new_lines.txt b/parser/v2/testdata/br_elements_are_placed_on_new_lines.txt new file mode 100644 index 000000000..17ffe897a --- /dev/null +++ b/parser/v2/testdata/br_elements_are_placed_on_new_lines.txt @@ -0,0 +1,24 @@ +-- in -- +package main + +templ test() { +
+ Linebreaks
and
rules
for
spacing +
+} +-- out -- +package main + +templ test() { +
+ Linebreaks +
+ and +
+ rules +
+ for +
+ spacing +
+} diff --git a/parser/v2/testdata/children_indented__closing_elm.txt b/parser/v2/testdata/children_indented__closing_elm.txt new file mode 100644 index 000000000..4776955b5 --- /dev/null +++ b/parser/v2/testdata/children_indented__closing_elm.txt @@ -0,0 +1,15 @@ +-- in -- +package test + +templ input(value, validation string) { +

{ "the" }{ "data" }

+
+} +-- out -- +package test + +templ input(value, validation string) { +
+

{ "the" }{ "data" }

+
+} diff --git a/parser/v2/testdata/children_indented__first_child.txt b/parser/v2/testdata/children_indented__first_child.txt new file mode 100644 index 000000000..42feba691 --- /dev/null +++ b/parser/v2/testdata/children_indented__first_child.txt @@ -0,0 +1,15 @@ +-- in -- +package test + +templ input(value, validation string) { +
+

{ "the" }{ "data" }

+} +-- out -- +package test + +templ input(value, validation string) { +
+

{ "the" }{ "data" }

+
+} diff --git a/parser/v2/testdata/comments_are_preserved.txt b/parser/v2/testdata/comments_are_preserved.txt new file mode 100644 index 000000000..0da95634f --- /dev/null +++ b/parser/v2/testdata/comments_are_preserved.txt @@ -0,0 +1,24 @@ +-- in -- +package main + +templ test() { + + // This is not included in the output. +
Some standard templ
+ /* This is not included in the output too. */ + /* + Leave this alone. + */ +} +-- out -- +package main + +templ test() { + + // This is not included in the output. +
Some standard templ
+ /* This is not included in the output too. */ + /* + Leave this alone. + */ +} diff --git a/parser/v2/testdata/conditional_expressions_have_the_same_child_indentation_rules_as_regular_elements.txt b/parser/v2/testdata/conditional_expressions_have_the_same_child_indentation_rules_as_regular_elements.txt new file mode 100644 index 000000000..195558cfc --- /dev/null +++ b/parser/v2/testdata/conditional_expressions_have_the_same_child_indentation_rules_as_regular_elements.txt @@ -0,0 +1,24 @@ +-- in -- +package test + +templ conditionalAttributes(addClass bool) { +
+Content
+} +-- out -- +package test + +templ conditionalAttributes(addClass bool) { +
+ Content +
+} diff --git a/parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented.txt b/parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented.txt new file mode 100644 index 000000000..a29d2037f --- /dev/null +++ b/parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented.txt @@ -0,0 +1,21 @@ +-- in -- +package test + +templ conditionalAttributes(addClass bool) { +
Content
+} +-- out -- +package test + +templ conditionalAttributes(addClass bool) { +
Content
+} diff --git a/parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented__2.txt b/parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented__2.txt new file mode 100644 index 000000000..4f60cc526 --- /dev/null +++ b/parser/v2/testdata/conditional_expressions_result_in_all_attrs_indented__2.txt @@ -0,0 +1,22 @@ +-- in -- +package test + +templ conditionalAttributes(addClass bool) { +
Content
+} +-- out -- +package test + +templ conditionalAttributes(addClass bool) { +
Content
+} diff --git a/parser/v2/testdata/conditional_expressions_with_else_blocks_are_also_formatted.txt b/parser/v2/testdata/conditional_expressions_with_else_blocks_are_also_formatted.txt new file mode 100644 index 000000000..4ff95d375 --- /dev/null +++ b/parser/v2/testdata/conditional_expressions_with_else_blocks_are_also_formatted.txt @@ -0,0 +1,26 @@ +-- in -- +package test + +templ conditionalAttributes(addClass bool) { +
Content
+} +-- out -- +package test + +templ conditionalAttributes(addClass bool) { +
Content
+} diff --git a/parser/v2/testdata/constant_attributes_prerfer_double_quotes__but_use_single_quotes_if_required.txt b/parser/v2/testdata/constant_attributes_prerfer_double_quotes__but_use_single_quotes_if_required.txt new file mode 100644 index 000000000..5c785630a --- /dev/null +++ b/parser/v2/testdata/constant_attributes_prerfer_double_quotes__but_use_single_quotes_if_required.txt @@ -0,0 +1,17 @@ +-- in -- +package test + +templ nested() { +
double
+
single-not-required
+
single-required
+} + +-- out -- +package test + +templ nested() { +
double
+
single-not-required
+
single-required
+} diff --git a/parser/v2/testdata/css_is_indented_by_one_level.txt b/parser/v2/testdata/css_is_indented_by_one_level.txt new file mode 100644 index 000000000..403e40367 --- /dev/null +++ b/parser/v2/testdata/css_is_indented_by_one_level.txt @@ -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 }; +} diff --git a/parser/v2/testdata/css_whitespace_is_tidied.txt b/parser/v2/testdata/css_whitespace_is_tidied.txt new file mode 100644 index 000000000..f5217dd45 --- /dev/null +++ b/parser/v2/testdata/css_whitespace_is_tidied.txt @@ -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 }; +} diff --git a/parser/v2/testdata/empty_elements_stay_on_the_same_line.txt b/parser/v2/testdata/empty_elements_stay_on_the_same_line.txt new file mode 100644 index 000000000..f34bb2433 --- /dev/null +++ b/parser/v2/testdata/empty_elements_stay_on_the_same_line.txt @@ -0,0 +1,17 @@ +-- in -- +package test + +templ input(value, validation string) { +
+

+

+
+} +-- out -- +package test + +templ input(value, validation string) { +
+

+
+} diff --git a/parser/v2/testdata/for_loops_are_placed_on_a_new_line.txt b/parser/v2/testdata/for_loops_are_placed_on_a_new_line.txt new file mode 100644 index 000000000..97f7d586f --- /dev/null +++ b/parser/v2/testdata/for_loops_are_placed_on_a_new_line.txt @@ -0,0 +1,20 @@ +-- in -- +package test + +templ input(items []string) { +
{ "the" }
{ "other" }
for _, item := range items { +
{ item }
+}
+} +-- out -- +package test + +templ input(items []string) { +
+ { "the" } +
{ "other" }
+ for _, item := range items { +
{ item }
+ } +
+} diff --git a/parser/v2/testdata/formatting_does_not_alter_whitespace.txt b/parser/v2/testdata/formatting_does_not_alter_whitespace.txt new file mode 100644 index 000000000..5db030a0e --- /dev/null +++ b/parser/v2/testdata/formatting_does_not_alter_whitespace.txt @@ -0,0 +1,13 @@ +-- in -- +package test + +templ nested() { +
{ "the" }
{ "other" }
+} + +-- out -- +package test + +templ nested() { +
{ "the" }
{ "other" }
+} diff --git a/parser/v2/testdata/go_expressions_are_formatted_by_the_go_formatter.txt b/parser/v2/testdata/go_expressions_are_formatted_by_the_go_formatter.txt new file mode 100644 index 000000000..43e4e2812 --- /dev/null +++ b/parser/v2/testdata/go_expressions_are_formatted_by_the_go_formatter.txt @@ -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() { +
Hello World
+} +-- 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() { +
Hello World
+} diff --git a/parser/v2/testdata/godoc_comments_are_preserved.txt b/parser/v2/testdata/godoc_comments_are_preserved.txt new file mode 100644 index 000000000..c7f457a5a --- /dev/null +++ b/parser/v2/testdata/godoc_comments_are_preserved.txt @@ -0,0 +1,14 @@ +-- in -- +package main + +// test the comment handling. +templ test() { + Test +} +-- out -- +package main + +// test the comment handling. +templ test() { + Test +} diff --git a/parser/v2/testdata/if_statements_are_placed_on_a_new_line.txt b/parser/v2/testdata/if_statements_are_placed_on_a_new_line.txt new file mode 100644 index 000000000..77a40ba98 --- /dev/null +++ b/parser/v2/testdata/if_statements_are_placed_on_a_new_line.txt @@ -0,0 +1,25 @@ +-- in -- +package test + +templ input(items []string) { +
{ "the" }
{ "other" }
if items != nil { +
{ items[0] }
+ } else { +
{ items[1] }
+ } +
+} +-- out -- +package test + +templ input(items []string) { +
+ { "the" } +
{ "other" }
+ if items != nil { +
{ items[0] }
+ } else { +
{ items[1] }
+ } +
+} diff --git a/parser/v2/testdata/inline_elements_are_not_placed_on_a_new_line.txt b/parser/v2/testdata/inline_elements_are_not_placed_on_a_new_line.txt new file mode 100644 index 000000000..b28bc2fa0 --- /dev/null +++ b/parser/v2/testdata/inline_elements_are_not_placed_on_a_new_line.txt @@ -0,0 +1,20 @@ +-- in -- +package main + +templ test() { +

+ In a flowing paragraph, you can use inline elements. + These inline elements can be styled + and are not placed on new lines. +

+} +-- out -- +package main + +templ test() { +

+ In a flowing paragraph, you can use inline elements. + These inline elements can be styled + and are not placed on new lines. +

+} diff --git a/parser/v2/testdata/non_empty_elements_with_children_that_are_all_on_the_same_line_are_not_split_into_multiple_lines.txt b/parser/v2/testdata/non_empty_elements_with_children_that_are_all_on_the_same_line_are_not_split_into_multiple_lines.txt new file mode 100644 index 000000000..87d566247 --- /dev/null +++ b/parser/v2/testdata/non_empty_elements_with_children_that_are_all_on_the_same_line_are_not_split_into_multiple_lines.txt @@ -0,0 +1,12 @@ +-- in -- +package test + +templ input(value, validation string) { +
Text
+} +-- out -- +package test + +templ input(value, validation string) { +
Text
+} diff --git a/parser/v2/testdata/script_tags_are_not_converted_to_self_closing_elements.txt b/parser/v2/testdata/script_tags_are_not_converted_to_self_closing_elements.txt new file mode 100644 index 000000000..9cd06031b --- /dev/null +++ b/parser/v2/testdata/script_tags_are_not_converted_to_self_closing_elements.txt @@ -0,0 +1,13 @@ +-- in -- +package test + +templ input(value, validation string) { + +} + +-- out -- +package test + +templ input(value, validation string) { + +} diff --git a/parser/v2/testdata/spacing_between_string_expressions_is_kept.txt b/parser/v2/testdata/spacing_between_string_expressions_is_kept.txt new file mode 100644 index 000000000..a982c0aa7 --- /dev/null +++ b/parser/v2/testdata/spacing_between_string_expressions_is_kept.txt @@ -0,0 +1,12 @@ +-- in -- +package main + +templ x() { +
{firstName} {lastName}
+} +-- out -- +package main + +templ x() { +
{ firstName } { lastName }
+} diff --git a/parser/v2/testdata/spacing_between_string_expressions_is_not_magically_added.txt b/parser/v2/testdata/spacing_between_string_expressions_is_not_magically_added.txt new file mode 100644 index 000000000..06ac7b072 --- /dev/null +++ b/parser/v2/testdata/spacing_between_string_expressions_is_not_magically_added.txt @@ -0,0 +1,12 @@ +-- in -- +package main + +templ x() { +
{pt1}{pt2}
+} +-- out -- +package main + +templ x() { +
{ pt1 }{ pt2 }
+} diff --git a/parser/v2/testdata/spacing_between_string_spreads_attributes_is_kept.txt b/parser/v2/testdata/spacing_between_string_spreads_attributes_is_kept.txt new file mode 100644 index 000000000..ed604f009 --- /dev/null +++ b/parser/v2/testdata/spacing_between_string_spreads_attributes_is_kept.txt @@ -0,0 +1,12 @@ +-- in -- +package main + +templ x() { +
{firstName...} {lastName...}
+} +-- out -- +package main + +templ x() { +
{ firstName... } { lastName... }
+} diff --git a/parser/v2/testdata/switch_statements_are_placed_on_a_new_line.txt b/parser/v2/testdata/switch_statements_are_placed_on_a_new_line.txt new file mode 100644 index 000000000..bcee5f9cf --- /dev/null +++ b/parser/v2/testdata/switch_statements_are_placed_on_a_new_line.txt @@ -0,0 +1,26 @@ +-- in -- +package test + +templ input(items []string) { +
{ "the" }
{ "other" }
switch items[0] { + case "a": +
{ items[0] }
+ case "b": +
{ items[1] }
+}
+} +-- out -- +package test + +templ input(items []string) { +
+ { "the" } +
{ "other" }
+ switch items[0] { + case "a": +
{ items[0] }
+ case "b": +
{ items[1] }
+ } +
+} diff --git a/parser/v2/testdata/tables_are_formatted_well.txt b/parser/v2/testdata/tables_are_formatted_well.txt new file mode 100644 index 000000000..950d1cf92 --- /dev/null +++ b/parser/v2/testdata/tables_are_formatted_well.txt @@ -0,0 +1,30 @@ +-- in -- +package test + +templ table(accountNumber string, registration string) { + + + + + + + + + +
Your account number{ accountNumber }
Registration{ strings.ToUpper(registration) }
+} +-- out -- +package test + +templ table(accountNumber string, registration string) { + + + + + + + + + +
Your account number{ accountNumber }
Registration{ strings.ToUpper(registration) }
+} diff --git a/parser/v2/testdata/templ_expression_attributes_are_formatted_correctly_when_multiline.txt b/parser/v2/testdata/templ_expression_attributes_are_formatted_correctly_when_multiline.txt new file mode 100644 index 000000000..b499007aa --- /dev/null +++ b/parser/v2/testdata/templ_expression_attributes_are_formatted_correctly_when_multiline.txt @@ -0,0 +1,34 @@ +-- in -- +package main + +templ x(id string, class string) { + +} +-- out -- +package main + +templ x(id string, class string) { + +} diff --git a/parser/v2/testdata/templ_expression_elements_are_formatted_the_same_as_other_elements.txt b/parser/v2/testdata/templ_expression_elements_are_formatted_the_same_as_other_elements.txt new file mode 100644 index 000000000..cc1ccfaea --- /dev/null +++ b/parser/v2/testdata/templ_expression_elements_are_formatted_the_same_as_other_elements.txt @@ -0,0 +1,26 @@ +-- in -- +package main + +templ x() { +
  • + + Home + @hello("home") { + data + } + +
  • +} +-- out -- +package main + +templ x() { +
  • + + Home + @hello("home") { + data + } + +
  • +} diff --git a/parser/v2/testdata/void_elements_are_converted_to_self_closing_elements.txt b/parser/v2/testdata/void_elements_are_converted_to_self_closing_elements.txt new file mode 100644 index 000000000..16cf47d2f --- /dev/null +++ b/parser/v2/testdata/void_elements_are_converted_to_self_closing_elements.txt @@ -0,0 +1,77 @@ +-- in -- +package test + +templ input(value, validation string) { + + + + +
    +

    + + + + + + +
    +
    + + + + + Text + + + + + + + + + + + + + + +} + +-- out -- +package test + +templ input(value, validation string) { + + + + +
    +
    + + + + + + +
    +
    + + + + + Text + + + + + + + + + + + + + + +} diff --git a/parser/v2/testdata/when_an_element_contains_children_that_are_on_new_lines__the_children_are_indented.txt b/parser/v2/testdata/when_an_element_contains_children_that_are_on_new_lines__the_children_are_indented.txt new file mode 100644 index 000000000..1ef5678fa --- /dev/null +++ b/parser/v2/testdata/when_an_element_contains_children_that_are_on_new_lines__the_children_are_indented.txt @@ -0,0 +1,15 @@ +-- in -- +package test + +templ input(value, validation string) { +
    +
    Text
    +} +-- out -- +package test + +templ input(value, validation string) { +
    +
    Text
    +
    +} diff --git a/parser/v2/types_test.go b/parser/v2/types_test.go deleted file mode 100644 index 312aa007a..000000000 --- a/parser/v2/types_test.go +++ /dev/null @@ -1,856 +0,0 @@ -package parser - -import ( - "strings" - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestFormatting(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "void elements are converted to self-closing elements", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { - - -

    - - - -
    - - - - - - - - - -} - -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { - - -
    - - - -
    - - - - - - - - - -} -`, - }, - { - name: "script tags are not converted to self-closing elements", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { - -} - -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { - -} -`, - }, - { - name: "empty elements stay on the same line", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -

    -

    -
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -

    -
    -} -`, - }, - { - name: "non-empty elements with children that are all on the same line are not split into multiple lines", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    Text
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    Text
    -} -`, - }, - { - name: "when an element contains children that are on new lines, the children are indented", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -
    Text
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -
    Text
    -
    -} -`, - }, - { - name: "children indented, closing elm", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -

    { "the" }{ "data" }

    -
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -

    { "the" }{ "data" }

    -
    -} -`, - }, - { - name: "children indented, first child", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -

    { "the" }{ "data" }

    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -

    { "the" }{ "data" }

    -
    -} -`, - }, - { - name: "all children indented, with nested indentation, when close tag is on new line", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -

    { "the" }{ "data" } -

    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -
    -

    - { "the" } - - { "data" } - -

    -
    -} -`, - }, - { - name: "all children indented, with nested indentation, when close tag is on same line", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -

    { "the" }{ "data" }

    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(value, validation string) { -

    { "the" }{ "data" }

    -} -`, - }, - { - name: "formatting does not alter whitespace", - input: ` // first line removed to make indentation clear in Go code -package test - -templ nested() { -
    { "the" }
    { "other" }
    -} - -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ nested() { -
    { "the" }
    { "other" }
    -} -`, - }, - { - name: "constant attributes prerfer double quotes, but use single quotes if required", - input: ` // first line removed to make indentation clear in Go code -package test - -templ nested() { -
    double
    -
    single-not-required
    -
    single-required
    -} - -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ nested() { -
    double
    -
    single-not-required
    -
    single-required
    -} -`, - }, - { - name: "for loops are placed on a new line", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(items []string) { -
    { "the" }
    { "other" }
    for _, item := range items { -
    { item }
    -}
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(items []string) { -
    - { "the" } -
    { "other" }
    - for _, item := range items { -
    { item }
    - } -
    -} -`, - }, - { - name: "if statements are placed on a new line", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(items []string) { -
    { "the" }
    { "other" }
    if items != nil { -
    { items[0] }
    - } else { -
    { items[1] }
    - } -
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(items []string) { -
    - { "the" } -
    { "other" }
    - if items != nil { -
    { items[0] }
    - } else { -
    { items[1] }
    - } -
    -} -`, - }, - { - name: "switch statements are placed on a new line", - input: ` // first line removed to make indentation clear in Go code -package test - -templ input(items []string) { -
    { "the" }
    { "other" }
    switch items[0] { - case "a": -
    { items[0] }
    - case "b": -
    { items[1] }
    -}
    -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -templ input(items []string) { -
    - { "the" } -
    { "other" }
    - switch items[0] { - case "a": -
    { items[0] }
    - case "b": -
    { items[1] }
    - } -
    -} -`, - }, - { - name: "css is indented by one level", - input: ` // first line removed to make indentation clear in Go code -package test - -css ClassName() { -background-color: #ffffff; -color: { constants.White }; -} -`, - expected: `// first line removed to make indentation clear in Go code -package test - -css ClassName() { - background-color: #ffffff; - color: { constants.White }; -} -`, - }, - { - name: "css whitespace is tidied", - input: ` // first line removed to make indentation clear in Go code -package test - -css ClassName() { -background-color : #ffffff ; - color : { constants.White }; - } - `, - expected: `// first line removed to make indentation clear in Go code -package test - -css ClassName() { - background-color: #ffffff; - color: { constants.White }; -} -`, - }, - { - name: "tables are formatted well", - input: ` // first line removed to make indentation clear -package test - -templ table(accountNumber string, registration string) { - - - - - - - - - -
    Your account number{ accountNumber }
    Registration{ strings.ToUpper(registration) }
    -} -`, - expected: ` // first line removed to make indentation clear -package test - -templ table(accountNumber string, registration string) { - - - - - - - - - -
    Your account number{ accountNumber }
    Registration{ strings.ToUpper(registration) }
    -} -`, - }, - { - name: "conditional expressions result in all attrs indented", - input: ` // first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    Content
    -} -`, - expected: `// first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    Content
    -} -`, - }, - { - name: "conditional expressions result in all attrs indented, 2", - input: ` // first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    Content
    -} -`, - expected: `// first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    Content
    -} -`, - }, - { - name: "conditional expressions have the same child indentation rules as regular elements", - input: ` // first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    -Content
    -} -`, - expected: ` // first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    - Content -
    -} -`, - }, - { - name: "conditional expressions with else blocks are also formatted", - input: ` // first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    Content
    -} -`, - expected: ` // first line removed to make indentation clear -package test - -templ conditionalAttributes(addClass bool) { -
    Content
    -} -`, - }, - { - name: "templ expression elements are formatted the same as other elements", - input: ` // first line removed to make indentation clear -package main - -templ x() { -
  • - - Home - @hello("home") { - data - } - -
  • -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ x() { -
  • - - Home - @hello("home") { - data - } - -
  • -} -`, - }, - { - name: "templ expression attributes are formatted correctly when multiline", - input: ` // first line removed to make indentation clear -package main - -templ x(id string, class string) { - -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ x(id string, class string) { - -} -`, - }, - { - name: "spacing between string expressions is kept", - input: ` // first line removed to make indentation clear -package main - -templ x() { -
    {firstName} {lastName}
    -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ x() { -
    { firstName } { lastName }
    -} -`, - }, - { - name: "spacing between string expressions is not magically added", - input: ` // first line removed to make indentation clear -package main - -templ x() { -
    {pt1}{pt2}
    -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ x() { -
    { pt1 }{ pt2 }
    -} -`, - }, - { - name: "spacing between string spreads attributes is kept", - input: ` // first line removed to make indentation clear -package main - -templ x() { -
    {firstName...} {lastName...}
    -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ x() { -
    { firstName... } { lastName... }
    -} -`, - }, - { - name: "go expressions are formatted by the go formatter", - input: ` // first line removed to make indentation clear -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() { -
    Hello World
    -} -`, - expected: ` // first line removed to make indentation clear -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() { -
    Hello World
    -} -`, - }, - { - name: "inline elements are not placed on a new line", - input: ` // first line removed to make indentation clear -package main - -templ test() { -

    - In a flowing paragraph, you can use inline elements. - These inline elements can be styled - and are not placed on new lines. -

    -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ test() { -

    - In a flowing paragraph, you can use inline elements. - These inline elements can be styled - and are not placed on new lines. -

    -} -`, - }, - { - name: "br elements are placed on new lines", - input: ` // first line removed to make indentation clear -package main - -templ test() { -
    - Linebreaks
    and
    rules
    for
    spacing -
    -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ test() { -
    - Linebreaks -
    - and -
    - rules -
    - for -
    - spacing -
    -} -`, - }, - { - name: "br and hr all on one line are not placed on new lines", - input: ` // first line removed to make indentation clear -package main - -templ test() { -
    Linebreaks
    used
    for
    spacing
    -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ test() { -
    Linebreaks
    used
    for
    spacing
    -} -`, - }, - { - name: "comments are preserved", - input: ` // first line removed to make indentation clear -package main - -templ test() { - - // This is not included in the output. -
    Some standard templ
    - /* This is not included in the output too. */ - /* - Leave this alone. - */ -} -`, - expected: ` // first line removed to make indentation clear -package main - -templ test() { - - // This is not included in the output. -
    Some standard templ
    - /* This is not included in the output too. */ - /* - Leave this alone. - */ -} -`, - }, - { - name: "godoc comments are preserved", - input: ` // first line removed to make indentation clear -package main - -// test the comment handling. -templ test() { - Test -} -`, - expected: ` // first line removed to make indentation clear -package main - -// test the comment handling. -templ test() { - Test -} -`, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - // Remove the first line of the test data. - input := strings.SplitN(tt.input, "\n", 2)[1] - expected := strings.SplitN(tt.expected, "\n", 2)[1] - - // Execute the test. - template, err := ParseString(input) - if err != nil { - t.Fatalf("failed to parse template: %v", err) - } - w := new(strings.Builder) - err = template.Write(w) - if err != nil { - t.Fatalf("failed to write template: %v", err) - } - if diff := cmp.Diff(expected, w.String()); diff != "" { - t.Error(diff) - - t.Errorf("input:\n%s", displayWhitespaceChars(input)) - t.Errorf("expected:\n%s", displayWhitespaceChars(expected)) - t.Errorf("got:\n%s", displayWhitespaceChars(w.String())) - } - }) - } -} - -func displayWhitespaceChars(s string) (output string) { - s = strings.ReplaceAll(s, " ", ".") - s = strings.ReplaceAll(s, "\t", "→") - s = strings.ReplaceAll(s, "\n", "↵\n") - return s -}