diff --git a/generator/generator.go b/generator/generator.go index b3e080529..da641244d 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -1113,6 +1113,12 @@ func (g *generator) writeAttributesCSS(indentLevel int, attrs []parser.Attribute if err != nil { return err } + for _, elseIf := range cattr.ElseIfs { + err = g.writeAttributesCSS(indentLevel, elseIf.Then) + if err != nil { + return err + } + } err = g.writeAttributesCSS(indentLevel, cattr.Else) if err != nil { return err @@ -1160,6 +1166,11 @@ func getAttributeScripts(attr parser.Attribute) (scripts []string) { for _, attr := range attr.Then { scripts = append(scripts, getAttributeScripts(attr)...) } + for _, elseIf := range attr.ElseIfs { + for _, attr := range elseIf.Then { + scripts = append(scripts, getAttributeScripts(attr)...) + } + } for _, attr := range attr.Else { scripts = append(scripts, getAttributeScripts(attr)...) } @@ -1468,6 +1479,26 @@ func (g *generator) writeConditionalAttribute(indentLevel int, elementName strin } indentLevel-- } + for _, elseIf := range attr.ElseIfs { + // } else if x == y { + if _, err = g.w.WriteIndent(indentLevel, `} else if `); err != nil { + return err + } + if r, err = g.w.Write(elseIf.Expression.Value); err != nil { + return err + } + g.sourceMap.Add(elseIf.Expression, r) + if _, err = g.w.Write(` {` + "\n"); err != nil { + return err + } + { + indentLevel++ + if err = g.writeElementAttributes(indentLevel, elementName, elseIf.Then); err != nil { + return err + } + indentLevel-- + } + } if len(attr.Else) > 0 { // } else { if _, err = g.w.WriteIndent(indentLevel, `} else {`+"\n"); err != nil { diff --git a/generator/generator_test.go b/generator/generator_test.go index 74754a19e..0225b380c 100644 --- a/generator/generator_test.go +++ b/generator/generator_test.go @@ -2,6 +2,7 @@ package generator import ( "bytes" + "strings" "testing" "github.com/a-h/templ/parser/v2" @@ -71,6 +72,58 @@ templ Hello(name string) { } } +func TestWriteConditionalAttributeElseIf(t *testing.T) { + w := new(bytes.Buffer) + g := generator{ + w: NewRangeWriter(w), + sourceMap: parser.NewSourceMap(), + } + attr := &parser.ConditionalAttribute{ + Expression: parser.Expression{Value: "a"}, + Then: []parser.Attribute{ + &parser.ConstantAttribute{ + Key: parser.ConstantAttributeKey{Name: "class"}, + Value: "then", + }, + }, + ElseIfs: []parser.ConditionalElseIfAttribute{ + { + Expression: parser.Expression{Value: "b"}, + Then: []parser.Attribute{ + &parser.ConstantAttribute{ + Key: parser.ConstantAttributeKey{Name: "class"}, + Value: "else-if", + }, + }, + }, + }, + Else: []parser.Attribute{ + &parser.ConstantAttribute{ + Key: parser.ConstantAttributeKey{Name: "class"}, + Value: "else", + }, + }, + } + + if err := g.writeConditionalAttribute(0, "div", attr); err != nil { + t.Fatalf("failed to write conditional attribute: %v", err) + } + + output := w.String() + for _, expected := range []string{ + "if a {\n", + "} else if b {\n", + "} else {\n", + `" class=\"then\""`, + `" class=\"else-if\""`, + `" class=\"else\""`, + } { + if !strings.Contains(output, expected) { + t.Errorf("expected generated output to contain %q, got:\n%s", expected, output) + } + } +} + func TestIsTrailingSpaceNeeded(t *testing.T) { inlineText := &parser.Text{Value: "hello", TrailingSpace: parser.SpaceHorizontal} newlineText := &parser.Text{Value: "hello", TrailingSpace: parser.SpaceVertical} diff --git a/internal/format/attributes_test.go b/internal/format/attributes_test.go index fcc9376e0..e1e904e67 100644 --- a/internal/format/attributes_test.go +++ b/internal/format/attributes_test.go @@ -202,6 +202,16 @@ func TestAttributes(t *testing.T) { Value: "then-class", }, }, + ElseIfs: []parser.ConditionalElseIfAttribute{ + { + Then: []parser.Attribute{ + &parser.ConstantAttribute{ + Key: parser.ConstantAttributeKey{Name: "class"}, + Value: "else-if-class", + }, + }, + }, + }, Else: []parser.Attribute{ &parser.ConstantAttribute{ Key: parser.ConstantAttributeKey{Name: "class"}, @@ -217,10 +227,14 @@ func TestAttributes(t *testing.T) { t.Helper() cond := children[0].(*parser.Element).Attributes[0].(*parser.ConditionalAttribute) thenAttr := cond.Then[0].(*parser.ConstantAttribute) + elseIfAttr := cond.ElseIfs[0].Then[0].(*parser.ConstantAttribute) elseAttr := cond.Else[0].(*parser.ConstantAttribute) if thenAttr.Value != "then-class" { t.Errorf("then branch: got %q, expected %q", thenAttr.Value, "then-class") } + if elseIfAttr.Value != "else-if-class" { + t.Errorf("else if branch: got %q, expected %q", elseIfAttr.Value, "else-if-class") + } if elseAttr.Value != "else-class" { t.Errorf("else branch: got %q, expected %q", elseAttr.Value, "else-class") } diff --git a/parser/v2/conditionalattributeparser.go b/parser/v2/conditionalattributeparser.go index 3ce6b396f..2c11f81f2 100644 --- a/parser/v2/conditionalattributeparser.go +++ b/parser/v2/conditionalattributeparser.go @@ -49,7 +49,12 @@ func (conditionalAttributeParser) Parse(pi *parse.Input) (r *ConditionalAttribut return } - // Read the optional 'Else' Nodes. + // Read the optional 'ElseIf' attributes. + if r.ElseIfs, _, err = parse.ZeroOrMore(attributeElseIfExpression).Parse(pi); err != nil { + return + } + + // Read the optional 'Else' attributes. if r.Else, ok, err = attributeElseExpression.Parse(pi); err != nil { return } @@ -71,6 +76,50 @@ func (conditionalAttributeParser) Parse(pi *parse.Input) (r *ConditionalAttribut return r, true, nil } +var attributeElseIfExpression parse.Parser[ConditionalElseIfAttribute] = attributeElseIfExpressionParser{} + +type attributeElseIfExpressionParser struct{} + +func (attributeElseIfExpressionParser) Parse(pi *parse.Input) (r ConditionalElseIfAttribute, matched bool, err error) { + start := pi.Index() + + // Check the prefix first. + if _, matched, err = parse.All(parse.OptionalWhitespace, closeBrace, parse.OptionalWhitespace, parse.String("else if")).Parse(pi); err != nil || !matched { + pi.Seek(start) + return + } + + // Rewind to the start of the `if` statement. + pi.Seek(pi.Index() - 2) + // Parse the Go if expression. + if r.Expression, err = parseGo("attribute else if", pi, goexpression.If); err != nil { + return + } + + // Eat " {". + if _, matched, err = openBraceWithOptionalPadding.Parse(pi); err != nil || !matched { + err = parse.Error("attribute else if: unterminated (missing closing '{\n')", pi.PositionAt(start)) + return + } + if _, _, err = parse.OptionalWhitespace.Parse(pi); err != nil { + return + } + + // Read the 'Then' attributes. + if r.Then, matched, err = (attributesParser{}).Parse(pi); err != nil || !matched { + err = parse.Error("attribute if: expected attributes in else if block, but none were found", pi.Position()) + return + } + + if len(r.Then) == 0 { + err = parse.Error("attribute if: invalid content or no attributes were found in the else if block", pi.Position()) + return + } + + r.Range = NewRange(pi.PositionAt(start), pi.Position()) + return r, true, nil +} + var attributeElseExpression parse.Parser[[]Attribute] = attributeElseExpressionParser{} type attributeElseExpressionParser struct{} diff --git a/parser/v2/elementparser_test.go b/parser/v2/elementparser_test.go index 72f455986..7bccaabd2 100644 --- a/parser/v2/elementparser_test.go +++ b/parser/v2/elementparser_test.go @@ -2845,6 +2845,52 @@ func TestElementFormatting(t *testing.T) { input: `
`, expected: `
`, }, + { + name: "conditional attribute else if is formatted on one line", + input: `
`, + expected: `
`, + }, + { + name: "conditional attribute multiple else if branches are formatted on one line", + input: `
`, + expected: `
`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2866,3 +2912,51 @@ func TestElementFormatting(t *testing.T) { }) } } + +func TestConditionalAttributeElseIfsParseAsBranches(t *testing.T) { + input := `
` + + result, matched, err := element.Parse(parse.NewInput(input)) + if err != nil { + t.Fatalf("parser error: %v", err) + } + if !matched { + t.Fatal("failed to parse element") + } + el, ok := result.(*Element) + if !ok { + t.Fatalf("expected Element, got %T", result) + } + if len(el.Attributes) != 1 { + t.Fatalf("expected 1 attribute, got %d", len(el.Attributes)) + } + attr, ok := el.Attributes[0].(*ConditionalAttribute) + if !ok { + t.Fatalf("expected ConditionalAttribute, got %T", el.Attributes[0]) + } + if got, want := attr.Expression.Value, "lint.Severity == 0"; got != want { + t.Fatalf("initial expression: got %q, want %q", got, want) + } + if got, want := len(attr.ElseIfs), 2; got != want { + t.Fatalf("else if count: got %d, want %d", got, want) + } + if got, want := attr.ElseIfs[0].Expression.Value, "lint.Severity == 1"; got != want { + t.Errorf("first else if expression: got %q, want %q", got, want) + } + if got, want := attr.ElseIfs[1].Expression.Value, "lint.Severity == 2"; got != want { + t.Errorf("second else if expression: got %q, want %q", got, want) + } + if got, want := len(attr.Else), 1; got != want { + t.Fatalf("else attribute count: got %d, want %d", got, want) + } +} diff --git a/parser/v2/types.go b/parser/v2/types.go index 4c268930c..949ea8ff1 100644 --- a/parser/v2/types.go +++ b/parser/v2/types.go @@ -1054,10 +1054,17 @@ func (sa *SpreadAttributes) Copy() Attribute { type ConditionalAttribute struct { Expression Expression Then []Attribute + ElseIfs []ConditionalElseIfAttribute Else []Attribute Range Range } +type ConditionalElseIfAttribute struct { + Expression Expression + Then []Attribute + Range Range +} + func (ca *ConditionalAttribute) String() string { sb := new(strings.Builder) _ = ca.Write(sb, 0) @@ -1065,24 +1072,23 @@ func (ca *ConditionalAttribute) String() string { } func (ca *ConditionalAttribute) Write(w io.Writer, indent int) error { - if err := writeIndent(w, indent, "if "); err != nil { - return err - } - if _, err := w.Write([]byte(ca.Expression.Value)); err != nil { - return err - } - if _, err := w.Write([]byte(" {\n")); err != nil { + if err := writeIndent(w, indent, "if ", ca.Expression.Value, " {\n"); err != nil { return err } { indent++ - for _, attr := range ca.Then { - if err := attr.Write(w, indent); err != nil { - return err - } - if _, err := w.Write([]byte("\n")); err != nil { - return err - } + if err := writeAttributesIndented(w, indent, ca.Then); err != nil { + return err + } + indent-- + } + for _, elseIf := range ca.ElseIfs { + if err := writeIndent(w, indent, "} else if ", elseIf.Expression.Value, " {\n"); err != nil { + return err + } + indent++ + if err := writeAttributesIndented(w, indent, elseIf.Then); err != nil { + return err } indent-- } @@ -1098,13 +1104,8 @@ func (ca *ConditionalAttribute) Write(w io.Writer, indent int) error { } { indent++ - for _, attr := range ca.Else { - if err := attr.Write(w, indent); err != nil { - return err - } - if _, err := w.Write([]byte("\n")); err != nil { - return err - } + if err := writeAttributesIndented(w, indent, ca.Else); err != nil { + return err } indent-- } @@ -1114,14 +1115,35 @@ func (ca *ConditionalAttribute) Write(w io.Writer, indent int) error { return nil } +func writeAttributesIndented(w io.Writer, indent int, attrs []Attribute) error { + for _, attr := range attrs { + if err := attr.Write(w, indent); err != nil { + return err + } + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + } + return nil +} + func (ca *ConditionalAttribute) Visit(v Visitor) error { return v.VisitConditionalAttribute(ca) } func (ca *ConditionalAttribute) Copy() Attribute { + elseIfs := make([]ConditionalElseIfAttribute, len(ca.ElseIfs)) + for i, elseIf := range ca.ElseIfs { + elseIfs[i] = ConditionalElseIfAttribute{ + Expression: elseIf.Expression, + Then: CopyAttributes(elseIf.Then), + Range: elseIf.Range, + } + } return &ConditionalAttribute{ Expression: ca.Expression, Then: CopyAttributes(ca.Then), + ElseIfs: elseIfs, Else: CopyAttributes(ca.Else), Range: ca.Range, } diff --git a/parser/v2/visitor/visitor.go b/parser/v2/visitor/visitor.go index 51a209a9f..1b0682c44 100644 --- a/parser/v2/visitor/visitor.go +++ b/parser/v2/visitor/visitor.go @@ -114,6 +114,13 @@ func New() *Visitor { return err } } + for _, elseIf := range n.ElseIfs { + for _, child := range elseIf.Then { + if err := child.Visit(v); err != nil { + return err + } + } + } for _, child := range n.Else { if err := child.Visit(v); err != nil { return err