package parser import ( "strings" "testing" "github.com/a-h/parse" "github.com/google/go-cmp/cmp" ) type attributeTest[T any] struct { name string input string parser parse.Parser[T] expected T } func TestAttributeParser(t *testing.T) { tests := []attributeTest[any]{ { name: "element: open", input: ``, parser: StripType(elementOpenTagParser), expected: elementOpenTag{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, }, }, { name: "element: hyphen in name", input: ``, parser: StripType(elementOpenTagParser), expected: elementOpenTag{ Name: "turbo-frame", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 12, Line: 0, Col: 12}, }, }, }, { name: "element: open with hyperscript attribute", input: `
`, parser: StripType(elementOpenTagParser), expected: elementOpenTag{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "_", Value: "show = true", NameRange: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 6, Line: 0, Col: 6}, }, }, }, }, }, { name: "element: open with complex attributes", input: `
`, parser: StripType(elementOpenTagParser), expected: elementOpenTag{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "@click", Value: "show = true", NameRange: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 11, Line: 0, Col: 11}, }, }, ConstantAttribute{ Name: ":class", Value: "{'foo': true}", NameRange: Range{ From: Position{Index: 26, Line: 0, Col: 26}, To: Position{Index: 32, Line: 0, Col: 32}, }, }, }, }, }, { name: "element: open with attributes", input: `
`, parser: StripType(elementOpenTagParser), expected: elementOpenTag{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "id", Value: "123", NameRange: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 7, Line: 0, Col: 7}, }, }, ConstantAttribute{ Name: "style", Value: "padding: 10px", NameRange: Range{ From: Position{Index: 14, Line: 0, Col: 14}, To: Position{Index: 19, Line: 0, Col: 19}, }, }, }, }, }, { name: "conditional expression attribute - single", input: ` if p.important { class="important" } "`, parser: StripType(conditionalAttribute), expected: ConditionalAttribute{ Expression: Expression{ Value: "p.important", Range: Range{ From: Position{ Index: 6, Line: 1, Col: 5, }, To: Position{ Index: 17, Line: 1, Col: 16, }, }, }, Then: []Attribute{ ConstantAttribute{ Name: "class", Value: "important", NameRange: Range{ From: Position{Index: 23, Line: 2, Col: 3}, To: Position{Index: 28, Line: 2, Col: 8}, }, }, }, }, }, { name: "conditional expression attribute - multiple", input: ` if test { class="itIsTrue" noshade name={ "other" } } "`, parser: StripType(conditionalAttribute), expected: ConditionalAttribute{ Expression: Expression{ Value: "test", Range: Range{ From: Position{ Index: 4, Line: 1, Col: 3, }, To: Position{ Index: 8, Line: 1, Col: 7, }, }, }, Then: []Attribute{ ConstantAttribute{ Name: "class", Value: "itIsTrue", NameRange: Range{ From: Position{Index: 13, Line: 2, Col: 1}, To: Position{Index: 18, Line: 2, Col: 6}, }, }, BoolConstantAttribute{ Name: "noshade", NameRange: Range{ From: Position{Index: 31, Line: 3, Col: 1}, To: Position{Index: 38, Line: 3, Col: 8}, }, }, ExpressionAttribute{ Name: "name", NameRange: Range{ From: Position{Index: 40, Line: 4, Col: 1}, To: Position{Index: 44, Line: 4, Col: 5}, }, Expression: Expression{ Value: `"other"`, Range: Range{ From: Position{ Index: 47, Line: 4, Col: 8, }, To: Position{ Index: 54, Line: 4, Col: 15, }, }, }, }, }, }, }, { name: "boolean expression attribute", input: ` noshade?={ true }"`, parser: StripType(boolExpressionAttributeParser), expected: BoolExpressionAttribute{ Name: "noshade", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 8, Line: 0, Col: 8}, }, Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 12, Line: 0, Col: 12, }, To: Position{ Index: 16, Line: 0, Col: 16, }, }, }, }, }, { name: "boolean expression attribute without spaces", input: ` noshade?={true}"`, parser: StripType(boolExpressionAttributeParser), expected: BoolExpressionAttribute{ Name: "noshade", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 8, Line: 0, Col: 8}, }, Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 11, Line: 0, Col: 11, }, To: Position{ Index: 15, Line: 0, Col: 15, }, }, }, }, }, { name: "attribute parsing handles boolean expression attributes", input: ` noshade?={ true }`, parser: StripType(attributeParser{}), expected: BoolExpressionAttribute{ Name: "noshade", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 8, Line: 0, Col: 8}, }, Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 12, Line: 0, Col: 12, }, To: Position{ Index: 16, Line: 0, Col: 16, }, }, }, }, }, { name: "boolean expression with excess spaces", input: ` noshade?={ true }"`, parser: StripType(boolExpressionAttributeParser), expected: BoolExpressionAttribute{ Name: "noshade", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 8, Line: 0, Col: 8}, }, Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 12, Line: 0, Col: 12, }, To: Position{ Index: 16, Line: 0, Col: 16, }, }, }, }, }, { name: "spread attributes", input: ` { spread... }"`, parser: StripType(spreadAttributesParser), expected: SpreadAttributes{ Expression{ Value: "spread", Range: Range{ From: Position{ Index: 3, Line: 0, Col: 3, }, To: Position{ Index: 9, Line: 0, Col: 9, }, }, }, }, }, { name: "constant attribute", input: ` href="test"`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "href", Value: "test", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, { name: "single quote not required constant attribute", input: ` href='no double quote in value'`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "href", Value: `no double quote in value`, SingleQuote: false, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, { name: "single quote required constant attribute", input: ` href='"test"'`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "href", Value: `"test"`, SingleQuote: true, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, { name: "attribute name with hyphens", input: ` data-turbo-permanent="value"`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "data-turbo-permanent", Value: "value", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 21, Line: 0, Col: 21}, }, }, }, { name: "empty attribute", input: ` data=""`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "data", Value: "", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, { name: "multiline attribute", input: ` data-script="on click do something end" `, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "data-script", Value: "on click\n do something\n end", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 12, Line: 0, Col: 12}, }, }, }, { name: "bool constant attribute", input: `
`, parser: StripType(elementOpenTagParser), expected: elementOpenTag{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Attributes: []Attribute{ BoolConstantAttribute{ Name: "data", NameRange: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 9, Line: 0, Col: 9}, }, }, }, }, }, { name: "bool constant attributes can end with a Unix newline", input: "", parser: StripType(element), expected: Element{ Name: "input", IndentAttrs: true, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 6, Line: 0, Col: 6}, }, Attributes: []Attribute{ BoolConstantAttribute{ Name: "required", NameRange: Range{ From: Position{Index: 9, Line: 1, Col: 2}, To: Position{Index: 17, Line: 1, Col: 10}, }, }, }, }, }, { name: "bool constant attributes can end with a Windows newline", input: "", parser: StripType(element), expected: Element{ Name: "input", IndentAttrs: true, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 6, Line: 0, Col: 6}, }, Attributes: []Attribute{ BoolConstantAttribute{ Name: "required", NameRange: Range{ From: Position{Index: 10, Line: 1, Col: 2}, To: Position{Index: 18, Line: 1, Col: 10}, }, }, }, }, }, { name: "attribute containing escaped text", input: ` href="<">"`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "href", Value: `<">`, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, { name: "HTMX wildcard attribute names are supported", input: ` hx-target-*="#errors"`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "hx-target-*", Value: `#errors`, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 12, Line: 0, Col: 12}, }, }, }, { name: "unquoted attributes are supported", input: ` data=123`, parser: StripType(constantAttributeParser), expected: ConstantAttribute{ Name: "data", Value: "123", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { input := parse.NewInput(tt.input) result, ok, err := tt.parser.Parse(input) if err != nil { t.Error(err) } if !ok { t.Errorf("failed to parse at %v", input.Position()) } if diff := cmp.Diff(tt.expected, result); diff != "" { t.Error(diff) } }) } } func TestVoidElementCloserParser(t *testing.T) { t.Run("all void elements are parsed", func(t *testing.T) { for _, input := range voidElementCloseTags { _, ok, err := voidElementCloser.Parse(parse.NewInput(input)) if err != nil { t.Fatalf("unexpected error: %v", err) } if !ok { t.Fatalf("failed to parse %q", input) } } }) } func TestElementParser(t *testing.T) { tests := []struct { name string input string expected Element }{ { name: "element: self-closing with single constant attribute", input: ``, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "href", Value: "test", NameRange: Range{ From: Position{Index: 3, Line: 0, Col: 3}, To: Position{Index: 7, Line: 0, Col: 7}, }, }, }, }, }, { 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", Range: Range{ From: Position{Index: 10, Line: 0, Col: 10}, To: Position{Index: 17, Line: 0, Col: 17}, }, }, }, }, }, { 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}, }, // is a void element, so text is not a child of the input. // is ignored. Children: nil, }, }, { name: "element: self-closing with single bool expression attribute", 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{ BoolExpressionAttribute{ Name: "noshade", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 11, Line: 0, Col: 11}, }, Expression: Expression{ Value: `true`, Range: Range{ From: Position{ Index: 15, Line: 0, Col: 15, }, To: Position{ Index: 19, Line: 0, Col: 19, }, }, }, }, }, }, }, { 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 is ignored", input: `


`, expected: Element{ Name: "br", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 3, Line: 0, Col: 3}, }, //
is a void element, so
is not a child of the
. //
is ignored. Children: nil, }, }, { name: "element: self-closing with single expression attribute", input: `
`, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Attributes: []Attribute{ ExpressionAttribute{ Name: "href", NameRange: Range{ From: Position{Index: 3, Line: 0, Col: 3}, To: Position{Index: 7, Line: 0, Col: 7}, }, Expression: Expression{ Value: `"test"`, Range: Range{ From: Position{ Index: 10, Line: 0, Col: 10, }, To: Position{ Index: 16, Line: 0, Col: 16, }, }, }, }, }, }, }, { name: "element: self-closing with multiple constant attributes", input: ``, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "href", Value: "test", NameRange: Range{ From: Position{Index: 3, Line: 0, Col: 3}, To: Position{Index: 7, Line: 0, Col: 7}, }, }, ConstantAttribute{ Name: "style", Value: "text-underline: auto", NameRange: Range{ From: Position{Index: 15, Line: 0, Col: 15}, To: Position{Index: 20, Line: 0, Col: 20}, }, }, }, }, }, { name: "element: self-closing with multiple spreads attributes", input: ``, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Attributes: []Attribute{ SpreadAttributes{ Expression: Expression{ Value: "firstSpread", Range: Range{ From: Position{ Index: 5, Line: 0, Col: 5, }, To: Position{ Index: 16, Line: 0, Col: 16, }, }, }, }, SpreadAttributes{ Expression: Expression{ Value: "children", Range: Range{ From: Position{ Index: 24, Line: 0, Col: 24, }, To: Position{ Index: 32, Line: 0, Col: 32, }, }, }, }, }, }, }, { name: "element: self-closing with multiple boolean attributes", 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: "optionA", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 11, Line: 0, Col: 11}, }, }, BoolExpressionAttribute{ Name: "optionB", NameRange: Range{ From: Position{Index: 12, Line: 0, Col: 12}, To: Position{Index: 19, Line: 0, Col: 19}, }, Expression: Expression{ Value: `true`, Range: Range{ From: Position{ Index: 23, Line: 0, Col: 23, }, To: Position{ Index: 27, Line: 0, Col: 27, }, }, }, }, ConstantAttribute{ Name: "optionC", Value: "other", NameRange: Range{ From: Position{Index: 30, Line: 0, Col: 30}, To: Position{Index: 37, Line: 0, Col: 37}, }, }, }, }, }, { name: "element: self-closing with multiple constant and expr attributes", input: `
`, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "href", Value: "test", NameRange: Range{ From: Position{Index: 3, Line: 0, Col: 3}, To: Position{Index: 7, Line: 0, Col: 7}, }, }, ExpressionAttribute{ Name: "title", NameRange: Range{ From: Position{Index: 15, Line: 0, Col: 15}, To: Position{Index: 20, Line: 0, Col: 20}, }, Expression: Expression{ Value: `localisation.Get("a_title")`, Range: Range{ From: Position{ Index: 23, Line: 0, Col: 23, }, To: Position{ Index: 50, Line: 0, Col: 50, }, }, }, }, ConstantAttribute{ Name: "style", Value: "text-underline: auto", NameRange: Range{ From: Position{Index: 53, Line: 0, Col: 53}, To: Position{Index: 58, Line: 0, Col: 58}, }, }, }, }, }, { name: "element: self-closing with multiple constant, conditional and expr attributes", input: `
Test
} `, expected: Element{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "style", Value: "width: 100;", NameRange: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 10, Line: 0, Col: 10}, }, }, ConditionalAttribute{ Expression: Expression{ Value: `p.important`, Range: Range{ From: Position{ Index: 30, Line: 1, Col: 5, }, To: Position{ Index: 41, Line: 1, Col: 16, }, }, }, Then: []Attribute{ ConstantAttribute{ Name: "class", Value: "important", NameRange: Range{ From: Position{Index: 47, Line: 2, Col: 3}, To: Position{Index: 52, Line: 2, Col: 8}, }, }, }, }, }, IndentAttrs: true, Children: []Node{ Text{ Value: "Test", Range: Range{ From: Position{Index: 70, Line: 4, Col: 1}, To: Position{Index: 74, Line: 4, Col: 5}, }, }, }, TrailingSpace: SpaceVertical, }, }, { name: "element: self-closing with no attributes", input: `
`, expected: Element{ Name: "hr", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 3, Line: 0, Col: 3}, }, }, }, { name: "element: self-closing with attribute", 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{ ConstantAttribute{ Name: "style", Value: "padding: 10px", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 9, Line: 0, Col: 9}, }, }, }, }, }, { name: "element: self-closing with conditional attribute", 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{ ConstantAttribute{ Name: "style", Value: "padding: 10px", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 9, Line: 0, Col: 9}, }, }, ConditionalAttribute{ Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 33, Line: 1, Col: 6, }, To: Position{ Index: 37, Line: 1, Col: 10, }, }, }, Then: []Attribute{ ConstantAttribute{ Name: "class", Value: "itIsTrue", NameRange: Range{ From: Position{Index: 44, Line: 2, Col: 4}, To: Position{Index: 49, Line: 2, Col: 9}, }, }, }, }, }, IndentAttrs: true, }, }, { name: "element: self-closing with conditional attribute with else block", 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{ ConstantAttribute{ Name: "style", Value: "padding: 10px", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 9, Line: 0, Col: 9}, }, }, ConditionalAttribute{ Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 33, Line: 1, Col: 6, }, To: Position{ Index: 37, Line: 1, Col: 10, }, }, }, Then: []Attribute{ ConstantAttribute{ Name: "class", Value: "itIsTrue", NameRange: Range{ From: Position{Index: 44, Line: 2, Col: 4}, To: Position{Index: 49, Line: 2, Col: 9}, }, }, }, Else: []Attribute{ ConstantAttribute{ Name: "class", Value: "itIsNotTrue", NameRange: Range{ From: Position{Index: 77, Line: 4, Col: 4}, To: Position{Index: 82, Line: 4, Col: 9}, }, }, }, }, }, IndentAttrs: true, }, }, { name: "element: open and close with conditional attribute", input: `

Test

`, expected: Element{ Name: "p", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "style", Value: "padding: 10px", NameRange: Range{ From: Position{Index: 3, Line: 0, Col: 3}, To: Position{Index: 8, Line: 0, Col: 8}, }, }, ConditionalAttribute{ Expression: Expression{ Value: "true", Range: Range{ From: Position{ Index: 32, Line: 1, Col: 6, }, To: Position{ Index: 36, Line: 1, Col: 10, }, }, }, Then: []Attribute{ ConstantAttribute{ Name: "class", Value: "itIsTrue", NameRange: Range{ From: Position{Index: 43, Line: 2, Col: 4}, To: Position{Index: 48, Line: 2, Col: 9}, }, }, }, }, }, IndentAttrs: true, Children: []Node{ Text{ Value: "Test", Range: Range{ From: Position{Index: 66, Line: 4, Col: 1}, To: Position{Index: 70, Line: 4, Col: 5}, }, }, }, }, }, { name: "element: open and close", input: `
`, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, }, }, { name: "element: open and close with text", input: `The text`, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Children: []Node{ Text{ Value: "The text", Range: Range{ From: Position{Index: 3, Line: 0, Col: 3}, To: Position{Index: 11, Line: 0, Col: 11}, }, }, }, }, }, { name: "element: with self-closing child element", input: ``, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Children: []Node{ Element{ Name: "b", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, }, }, { name: "element: with non-self-closing child element", input: ``, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Children: []Node{ Element{ Name: "b", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, }, }, }, { name: "element: containing space", input: ` `, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Children: []Node{ Whitespace{Value: " "}, Element{ Name: "b", NameRange: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 6, Line: 0, Col: 6}, }, Children: []Node{ Whitespace{Value: " "}, }, TrailingSpace: SpaceHorizontal, }, }, }, }, { name: "element: with multiple child elements", input: ``, expected: Element{ Name: "a", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 2, Line: 0, Col: 2}, }, Children: []Node{ Element{ Name: "b", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 5, Line: 0, Col: 5}, }, }, Element{ Name: "c", NameRange: Range{ From: Position{Index: 11, Line: 0, Col: 11}, To: Position{Index: 12, Line: 0, Col: 12}, }, Children: []Node{ Element{ Name: "d", NameRange: Range{ From: Position{Index: 14, Line: 0, Col: 14}, To: Position{Index: 15, Line: 0, Col: 15}, }, }, }, }, }, }, }, { name: "element: empty", input: `
`, expected: Element{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, }, }, { name: "element: containing string expression", input: `
{ "test" }
`, expected: Element{ Name: "div", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Children: []Node{ StringExpression{ Expression: Expression{ Value: `"test"`, Range: Range{ From: Position{ Index: 7, Line: 0, Col: 7, }, To: Position{ Index: 13, Line: 0, Col: 13, }, }, }, }, }, }, }, { name: "element: inputs can contain class attributes", input: ``, expected: Element{ Name: "input", NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 6, Line: 0, Col: 6}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "type", Value: "email", NameRange: Range{ From: Position{Index: 8, Line: 0, Col: 8}, To: Position{Index: 12, Line: 0, Col: 12}, }, }, ConstantAttribute{ Name: "id", Value: "email", NameRange: Range{ From: Position{Index: 21, Line: 0, Col: 21}, To: Position{Index: 23, Line: 0, Col: 23}, }, }, ConstantAttribute{ Name: "name", Value: "email", NameRange: Range{ From: Position{Index: 32, Line: 0, Col: 32}, To: Position{Index: 36, Line: 0, Col: 36}, }, }, ExpressionAttribute{ Name: "class", NameRange: Range{ From: Position{Index: 45, Line: 0, Col: 45}, To: Position{Index: 50, Line: 0, Col: 50}, }, Expression: Expression{ Value: `"a", "b", "c", templ.KV("c", false)`, Range: Range{ From: Position{ Index: 53, Line: 0, Col: 53, }, To: Position{ Index: 89, Line: 0, Col: 89, }, }, }, }, ConstantAttribute{ Name: "placeholder", Value: "your@email.com", NameRange: Range{ From: Position{Index: 91, Line: 0, Col: 91}, To: Position{Index: 102, Line: 0, Col: 102}, }, }, ConstantAttribute{ Name: "autocomplete", Value: "off", NameRange: Range{ From: Position{Index: 120, Line: 0, Col: 120}, To: Position{Index: 132, Line: 0, Col: 132}, }, }, }, }, }, { name: "element: with multi-line attributes", input: ``, expected: Element{ Name: "input", IndentAttrs: true, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 6, Line: 0, Col: 6}, }, Attributes: []Attribute{ ConstantAttribute{ Name: "type", Value: "email", NameRange: Range{ From: Position{Index: 8, Line: 1, Col: 1}, To: Position{Index: 12, Line: 1, Col: 5}, }, }, ConstantAttribute{ Name: "id", Value: "email", NameRange: Range{ From: Position{Index: 23, Line: 2, Col: 1}, To: Position{Index: 25, Line: 2, Col: 3}, }, }, ConstantAttribute{ Name: "name", Value: "email", NameRange: Range{ From: Position{Index: 36, Line: 3, Col: 1}, To: Position{Index: 40, Line: 3, Col: 5}, }, }, }, }, }, { name: "element: can contain text that starts with for", input: `
for which any amount is charged
`, expected: Element{ Name: "div", IndentChildren: true, NameRange: Range{ From: Position{Index: 1, Line: 0, Col: 1}, To: Position{Index: 4, Line: 0, Col: 4}, }, Children: []Node{ Text{ Value: "for which any ", Range: Range{ From: Position{Index: 5, Line: 0, Col: 5}, To: Position{Index: 19, Line: 0, Col: 19}, }, TrailingSpace: SpaceVertical, }, Text{ Value: "amount is charged", Range: Range{ From: Position{Index: 20, Line: 1, Col: 0}, To: Position{Index: 37, Line: 1, Col: 17}, }, TrailingSpace: SpaceNone, }, }, }, }, { name: "element: self-closing with unquoted attribute", 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{ ConstantAttribute{ Name: "noshade", Value: "noshade", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 11, Line: 0, Col: 11}, }, }, }, }, }, { name: "element: self-closing with unquoted and other attributes", 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{ ConstantAttribute{ Name: "noshade", Value: "noshade", NameRange: Range{ From: Position{Index: 4, Line: 0, Col: 4}, To: Position{Index: 11, Line: 0, Col: 11}, }, }, BoolConstantAttribute{ Name: "disabled", NameRange: Range{ From: Position{Index: 20, Line: 0, Col: 20}, To: Position{Index: 28, Line: 0, Col: 28}, }, }, ExpressionAttribute{ Name: "other-attribute", NameRange: Range{ From: Position{Index: 29, Line: 0, Col: 29}, To: Position{Index: 44, Line: 0, Col: 44}, }, Expression: Expression{ Value: "false", Range: Range{ From: Position{ Index: 47, Line: 0, Col: 47, }, To: Position{ Index: 52, Line: 0, Col: 52, }, }, }, }, }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { input := parse.NewInput(tt.input) result, ok, err := element.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 TestElementParserErrors(t *testing.T) { tests := []struct { name string input string expected error }{ { name: "element: mismatched end tag", input: `
`, expected: parse.Error(": close tag not found", parse.Position{ Index: 3, Line: 0, Col: 3, }), }, { name: "element: style must only contain text", input: ``, expected: parse.Error("`, expected: parse.Error("