diff --git a/cmd/templ/main.go b/cmd/templ/main.go index 0a9cff5d5..070baf410 100644 --- a/cmd/templ/main.go +++ b/cmd/templ/main.go @@ -13,7 +13,6 @@ import ( "github.com/a-h/templ/cmd/templ/fmtcmd" "github.com/a-h/templ/cmd/templ/generatecmd" "github.com/a-h/templ/cmd/templ/lspcmd" - "github.com/a-h/templ/cmd/templ/migratecmd" "github.com/fatih/color" ) @@ -34,7 +33,6 @@ commands: generate Generates Go code from templ files fmt Formats templ files lsp Starts a language server for templ files - migrate Migrates v1 templ files to v2 format version Prints the version ` @@ -46,8 +44,6 @@ func run(w io.Writer, args []string) (code int) { switch args[1] { case "generate": return generateCmd(w, args[2:]) - case "migrate": - return migrateCmd(w, args[2:]) case "fmt": return fmtCmd(w, args[2:]) case "lsp": @@ -189,44 +185,6 @@ func generateCmd(w io.Writer, args []string) (code int) { return 0 } -const migrateUsageText = `usage: templ migrate [ ...] - -Migrates v1 templ files to v2 format. - -Args: - -f string - Optionally migrate a single file, e.g. -f header.templ - -help - Print help and exit. - -path string - Migrates code for all files in path. -` - -func migrateCmd(w io.Writer, args []string) (code int) { - cmd := flag.NewFlagSet("migrate", flag.ExitOnError) - cmd.SetOutput(w) - fileName := cmd.String("f", "", "") - path := cmd.String("path", "", "") - helpFlag := cmd.Bool("help", false, "") - cmd.Usage = func() { - fmt.Fprint(w, migrateUsageText) - } - err := cmd.Parse(args) - if err != nil || *helpFlag || (*path == "" && *fileName == "") { - cmd.Usage() - return - } - err = migratecmd.Run(w, migratecmd.Arguments{ - FileName: *fileName, - Path: *path, - }) - if err != nil { - fmt.Fprintln(w, err.Error()) - return 1 - } - return 0 -} - const fmtUsageText = `usage: templ fmt [ ...] Format all files in directory: diff --git a/cmd/templ/main_test.go b/cmd/templ/main_test.go index 7f7a9f178..c0914ab0e 100644 --- a/cmd/templ/main_test.go +++ b/cmd/templ/main_test.go @@ -45,18 +45,6 @@ func TestMain(t *testing.T) { expected: templ.Version() + "\n", expectedCode: 0, }, - { - name: `"templ migrate" prints usage`, - args: []string{"templ", "migrate"}, - expected: migrateUsageText, - expectedCode: 0, - }, - { - name: `"templ migrate --help" prints usage`, - args: []string{"templ", "migrate", "--help"}, - expected: migrateUsageText, - expectedCode: 0, - }, { name: `"templ fmt --help" prints usage`, args: []string{"templ", "fmt", "--help"}, diff --git a/cmd/templ/migratecmd/main.go b/cmd/templ/migratecmd/main.go deleted file mode 100644 index bf6e9fbb3..000000000 --- a/cmd/templ/migratecmd/main.go +++ /dev/null @@ -1,314 +0,0 @@ -package migratecmd - -import ( - "bytes" - "errors" - "fmt" - "io" - "reflect" - "strings" - "time" - - "github.com/a-h/templ/cmd/templ/processor" - v1 "github.com/a-h/templ/parser/v1" - v2 "github.com/a-h/templ/parser/v2" - "github.com/natefinch/atomic" -) - -const workerCount = 4 - -type Arguments struct { - FileName string - Path string -} - -func Run(w io.Writer, args Arguments) (err error) { - if args.FileName != "" { - return processSingleFile(w, args.FileName) - } - return processPath(w, args.Path) -} - -func processSingleFile(w io.Writer, fileName string) error { - start := time.Now() - err := migrate(fileName) - fmt.Fprintf(w, "Migrated code for %q in %s\n", fileName, time.Since(start)) - return err -} - -func processPath(w io.Writer, path string) (err error) { - start := time.Now() - results := make(chan processor.Result) - go processor.Process(path, migrate, workerCount, results) - var successCount, errorCount int - for r := range results { - if r.Error != nil { - err = errors.Join(err, fmt.Errorf("%s: %w", r.FileName, r.Error)) - errorCount++ - continue - } - successCount++ - fmt.Fprintf(w, "%s complete in %v\n", r.FileName, r.Duration) - } - fmt.Fprintf(w, "Migrated code for %d templates with %d errors in %s\n", successCount+errorCount, errorCount, time.Since(start)) - return err -} - -func migrate(fileName string) (err error) { - // Check that it's actually a V1 file. - _, err = v2.Parse(fileName) - if err == nil { - return fmt.Errorf("migrate: %s able to parse file as V2, are you sure this needs to be migrated?", fileName) - } - if err != v2.ErrLegacyFileFormat { - return fmt.Errorf("migrate: %s unexpected error: %v", fileName, err) - } - // Parse. - v1Template, err := v1.Parse(fileName) - if err != nil { - return fmt.Errorf("migrate: %s v1 parsing error: %w", fileName, err) - } - // Convert. - var v2Template v2.TemplateFile - - // Copy the package and any imports. - sb := new(strings.Builder) - sb.WriteString("package " + v1Template.Package.Expression.Value) - sb.WriteString("\n") - if len(v1Template.Imports) > 0 { - sb.WriteString("\n") - for _, imp := range v1Template.Imports { - sb.WriteString("import ") - sb.WriteString(imp.Expression.Value) - sb.WriteString("\n") - } - } - sb.WriteString("\n") - v2Template.Package.Expression.Value = sb.String() - - // Work through the nodes. - v2Template.Nodes, err = migrateV1TemplateFileNodesToV2TemplateFileNodes(v1Template.Nodes) - if err != nil { - return fmt.Errorf("%s error migrating elements: %w", fileName, err) - } - - // Write the updated file. - w := new(bytes.Buffer) - err = v2Template.Write(w) - if err != nil { - return fmt.Errorf("%s formatting error: %w", fileName, err) - } - err = atomic.WriteFile(fileName, w) - if err != nil { - return fmt.Errorf("%s file write error: %w", fileName, err) - } - return -} - -func migrateV1TemplateFileNodesToV2TemplateFileNodes(in []v1.TemplateFileNode) (out []v2.TemplateFileNode, err error) { - if in == nil { - return - } - out = make([]v2.TemplateFileNode, len(in)) - for i, tfn := range in { - tfn := tfn - out[i], err = migrateV1TemplateFileNodeToV2TemplateFileNode(tfn) - if err != nil { - return - } - } - return -} - -func migrateV1TemplateFileNodeToV2TemplateFileNode(in v1.TemplateFileNode) (out v2.TemplateFileNode, err error) { - switch n := in.(type) { - case v1.ScriptTemplate: - return v2.ScriptTemplate{ - Name: v2.Expression{ - Value: n.Name.Value, - }, - Parameters: v2.Expression{ - Value: n.Parameters.Value, - }, - Value: n.Value, - }, nil - case v1.CSSTemplate: - var t v2.CSSTemplate - t.Expression.Value = n.Name.Value - t.Properties = make([]v2.CSSProperty, len(n.Properties)) - for i, p := range n.Properties { - t.Properties[i], err = migrateV1CSSPropertyToV2CSSProperty(p) - if err != nil { - return - } - } - return t, nil - case v1.HTMLTemplate: - var t v2.HTMLTemplate - t.Expression.Value = fmt.Sprintf("%s(%s)", n.Name.Value, n.Parameters.Value) - t.Children, err = migrateV1NodesToV2Nodes(n.Children) - if err != nil { - return - } - return t, nil - } - return nil, fmt.Errorf("migrate: unknown template file node type: %s.%s", reflect.TypeOf(in).PkgPath(), reflect.TypeOf(in).Name()) -} - -func migrateV1CSSPropertyToV2CSSProperty(in v1.CSSProperty) (out v2.CSSProperty, err error) { - switch p := in.(type) { - case v1.ConstantCSSProperty: - return v2.ConstantCSSProperty{Name: p.Name, Value: p.Value}, nil - case v1.ExpressionCSSProperty: - var ep v2.ExpressionCSSProperty - ep.Name = p.Name - ep.Value.Expression.Value = p.Value.Expression.Value - return ep, nil - } - return nil, fmt.Errorf("migrate: unknown CSS property type: %s", reflect.TypeOf(in).Name()) -} - -func migrateV1NodesToV2Nodes(in []v1.Node) (out []v2.Node, err error) { - if in == nil { - return - } - out = make([]v2.Node, len(in)) - for i, n := range in { - out[i], err = migrateV1NodeToV2Node(n) - if err != nil { - return - } - } - return -} - -func migrateV1NodeToV2Node(in v1.Node) (out v2.Node, err error) { - switch n := in.(type) { - case v1.Whitespace: - return v2.Whitespace{Value: n.Value}, nil - case v1.DocType: - return v2.DocType{Value: n.Value}, nil - case v1.Text: - return v2.Text{Value: n.Value}, nil - case v1.Element: - return migrateV1ElementToV2Element(n) - case v1.CallTemplateExpression: - cte := v2.CallTemplateExpression{ - Expression: v2.Expression{ - Value: n.Expression.Value, - }, - } - return cte, nil - case v1.IfExpression: - return migrateV1IfExpressionToV2IfExpression(n) - case v1.SwitchExpression: - return migrateV1SwitchExpressionToV2SwitchExpression(n) - case v1.ForExpression: - return migrateV1ForExpressionToV2ForExpression(n) - case v1.StringExpression: - se := v2.StringExpression{ - Expression: v2.Expression{ - Value: n.Expression.Value, - }, - } - return se, nil - } - return nil, fmt.Errorf("migrate: unknown node type: %s", reflect.TypeOf(in).Name()) -} - -func migrateV1ForExpressionToV2ForExpression(in v1.ForExpression) (out v2.ForExpression, err error) { - out.Expression.Value = in.Expression.Value - out.Children, err = migrateV1NodesToV2Nodes(in.Children) - if err != nil { - return - } - return -} - -func migrateV1SwitchExpressionToV2SwitchExpression(in v1.SwitchExpression) (out v2.SwitchExpression, err error) { - out.Expression.Value = in.Expression.Value - out.Cases = make([]v2.CaseExpression, len(in.Cases)) - for i, c := range in.Cases { - ce := v2.CaseExpression{ - Expression: v2.Expression{ - Value: "case " + c.Expression.Value + ":", - }, - } - ce.Children, err = migrateV1NodesToV2Nodes(c.Children) - if err != nil { - return - } - out.Cases[i] = ce - } - if in.Default != nil { - d := v2.CaseExpression{ - Expression: v2.Expression{ - Value: "default:", - }, - } - d.Children, err = migrateV1NodesToV2Nodes(in.Default) - if err != nil { - return - } - out.Cases = append(out.Cases, d) - } - return -} - -func migrateV1IfExpressionToV2IfExpression(in v1.IfExpression) (out v2.IfExpression, err error) { - out.Expression.Value = in.Expression.Value - out.Then, err = migrateV1NodesToV2Nodes(in.Then) - if err != nil { - return - } - out.Else, err = migrateV1NodesToV2Nodes(in.Else) - if err != nil { - return - } - return -} - -func migrateV1ElementToV2Element(in v1.Element) (out v2.Element, err error) { - out.Attributes = make([]v2.Attribute, len(in.Attributes)) - for i, attr := range in.Attributes { - out.Attributes[i], err = migrateV1AttributeToV2Attribute(attr) - if err != nil { - return - } - } - out.Children = make([]v2.Node, len(in.Children)) - for i, child := range in.Children { - out.Children[i], err = migrateV1NodeToV2Node(child) - if err != nil { - return - } - } - out.Name = in.Name - return out, nil -} - -func migrateV1AttributeToV2Attribute(in v1.Attribute) (out v2.Attribute, err error) { - switch attr := in.(type) { - case v1.BoolConstantAttribute: - return v2.BoolConstantAttribute{Name: attr.Name}, nil - case v1.ConstantAttribute: - return v2.ConstantAttribute{Name: attr.Name, Value: attr.Value}, nil - case v1.BoolExpressionAttribute: - bea := v2.BoolExpressionAttribute{ - Name: attr.Name, - Expression: v2.Expression{ - Value: attr.Expression.Value, - }, - } - return bea, nil - case v1.ExpressionAttribute: - ea := v2.ExpressionAttribute{ - Name: attr.Name, - Expression: v2.Expression{ - Value: attr.Expression.Value, - }, - } - return ea, nil - } - return nil, fmt.Errorf("migrate: unknown attribute type: %s", reflect.TypeOf(in).Name()) -} diff --git a/docs/docs/09-commands-and-tools/01-cli.md b/docs/docs/09-commands-and-tools/01-cli.md index 67cdaae0f..a24a3f62f 100644 --- a/docs/docs/09-commands-and-tools/01-cli.md +++ b/docs/docs/09-commands-and-tools/01-cli.md @@ -8,7 +8,6 @@ To see help text, you can run: templ generate --help templ fmt --help templ lsp --help - templ migrate --help templ version examples: templ generate diff --git a/docs/docs/14-faq/_category_.json b/docs/docs/14-faq/_category_.json new file mode 100644 index 000000000..15701ab78 --- /dev/null +++ b/docs/docs/14-faq/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 14, + "label": "FAQ" +} diff --git a/docs/docs/14-faq/index.md b/docs/docs/14-faq/index.md new file mode 100644 index 000000000..5322ad046 --- /dev/null +++ b/docs/docs/14-faq/index.md @@ -0,0 +1,7 @@ +# FAQ + +## How can I migrate from templ version 0.1.x to templ 0.2.x syntax? + +Versions of templ <= v0.2.663 include a `templ migrate` command that can migrate v1 syntax to v2. + +The v1 syntax used some extra characters for variable injection, e.g. `{%= name %}` whereas the latest (v2) syntax uses a single pair of braces within HTML, e.g. `{ name }`. diff --git a/go.mod b/go.mod index 3253ce13a..62f5961d5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.21 require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/a-h/htmlformat v0.0.0-20231108124658-5bd994fe268e - github.com/a-h/lexical v0.0.53 github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a github.com/a-h/pathvars v0.0.12 github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22 diff --git a/go.sum b/go.sum index 1684b1171..b7aab22ee 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAc github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/a-h/htmlformat v0.0.0-20231108124658-5bd994fe268e h1:Eog54DQpku7NpPNff9wzQYT61TGu9jjq5N8UhAkqIgw= github.com/a-h/htmlformat v0.0.0-20231108124658-5bd994fe268e/go.mod h1:FMIm5afKmEfarNbIXOaPHFY8X7fo+fRQB6I9MPG2nB0= -github.com/a-h/lexical v0.0.53 h1:uXaV05/iWmVe8A/TxUXxPrpe7z3/8AVbWmOUEbYPe+Q= -github.com/a-h/lexical v0.0.53/go.mod h1:d73jw5cgKXuYypRozNBuxRNFrTWQ3y5hVMG7rUjh1Qw= github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a h1:vlmAfVwFK9sRpDlJyuHY8htP+KfGHB2VH02u0SoIufk= github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/pathvars v0.0.12 h1:B4JaZGvHKNgNNlw8LMayPM/Hc0f3xZ2PXivu8YIl/X0= diff --git a/gomod2nix.toml b/gomod2nix.toml index 1c37d6bc2..0729bf89d 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -7,9 +7,6 @@ schema = 3 [mod."github.com/a-h/htmlformat"] version = "v0.0.0-20231108124658-5bd994fe268e" hash = "sha256-YSl9GsXhc0L2oKGZLwwjUtpe5W6ra6kk74zvQdsDCMU=" - [mod."github.com/a-h/lexical"] - version = "v0.0.53" - hash = "sha256-2nBWXgbkvl2lzrQmfsuxbOFm6BKdC3BcsFaGgaskhDM=" [mod."github.com/a-h/parse"] version = "v0.0.0-20240121214402-3caf7543159a" hash = "sha256-ee/g6xwwhtF7vVt3griUSh96Kz4z0hM5/tpXxHW6PZk=" diff --git a/parser/v1/calltemplateparser.go b/parser/v1/calltemplateparser.go deleted file mode 100644 index 536f3ad52..000000000 --- a/parser/v1/calltemplateparser.go +++ /dev/null @@ -1,46 +0,0 @@ -package parser - -import ( - "io" - - "github.com/a-h/lexical/parse" -) - -// newCallTemplateExpressionParser creates a new callTemplateExpressionParser. -func newCallTemplateExpressionParser() callTemplateExpressionParser { - return callTemplateExpressionParser{} -} - -var callTemplateExpressionStartParser = parse.Or(parse.String("{%! "), parse.String("{%!")) - -type callTemplateExpressionParser struct{} - -func (p callTemplateExpressionParser) Parse(pi parse.Input) parse.Result { - var r CallTemplateExpression - - // Check the prefix first. - prefixResult := callTemplateExpressionStartParser(pi) - if !prefixResult.Success { - return prefixResult - } - - // Once we have a prefix, we must have an expression that returns a template, followed by a tagEnd. - from := NewPositionFromInput(pi) - pr := parse.StringUntil(parse.Or(expressionEnd, newLine))(pi) - if pr.Error != nil && pr.Error != io.EOF { - return pr - } - // If there's no match, there's no tagEnd or newLine, which is an error. - if !pr.Success { - return parse.Failure("callTemplateExpressionParser", newParseError("call: unterminated (missing closing ' %}')", from, NewPositionFromInput(pi))) - } - r.Expression = NewExpression(pr.Item.(string), from, NewPositionFromInput(pi)) - - // Eat " %}". - from = NewPositionFromInput(pi) - if te := expressionEnd(pi); !te.Success { - return parse.Failure("callTemplateExpressionParser", newParseError("call: unterminated (missing closing ' %}')", from, NewPositionFromInput(pi))) - } - - return parse.Success("callTemplate", r, nil) -} diff --git a/parser/v1/calltemplateparser_test.go b/parser/v1/calltemplateparser_test.go deleted file mode 100644 index 292c9e79d..000000000 --- a/parser/v1/calltemplateparser_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/a-h/lexical/input" - "github.com/google/go-cmp/cmp" -) - -func TestCallTemplateExpressionParser(t *testing.T) { - var tests = []struct { - name string - input string - expected CallTemplateExpression - }{ - { - name: "call: simple", - input: `{%! Other(p.Test) %}`, - expected: CallTemplateExpression{ - Expression: Expression{ - Value: "Other(p.Test)", - Range: Range{ - From: Position{ - Index: 4, - Line: 1, - Col: 4, - }, - To: Position{ - Index: 17, - Line: 1, - Col: 17, - }, - }, - }, - }, - }, - { - name: "call: simple, missing start space", - input: `{%!Other(p.Test) %}`, - expected: CallTemplateExpression{ - Expression: Expression{ - Value: "Other(p.Test)", - Range: Range{ - From: Position{ - Index: 3, - Line: 1, - Col: 3, - }, - To: Position{ - Index: 16, - Line: 1, - Col: 16, - }, - }, - }, - }, - }, - { - name: "call: simple, missing start and end space", - input: `{%!Other(p.Test)%}`, - expected: CallTemplateExpression{ - Expression: Expression{ - Value: "Other(p.Test)", - Range: Range{ - From: Position{ - Index: 3, - Line: 1, - Col: 3, - }, - To: Position{ - Index: 16, - Line: 1, - Col: 16, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input) - result := newCallTemplateExpressionParser().Parse(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Errorf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} diff --git a/parser/v1/cssparser.go b/parser/v1/cssparser.go deleted file mode 100644 index f9ebb83ef..000000000 --- a/parser/v1/cssparser.go +++ /dev/null @@ -1,280 +0,0 @@ -package parser - -import ( - "io" - - "github.com/a-h/lexical/parse" -) - -// CSS. - -// CSS Parser. - -func newCSSParser() cssParser { - return cssParser{} -} - -type cssParser struct { -} - -var endCssParser = createEndParser("endcss") // {% endcss %} - -func (p cssParser) Parse(pi parse.Input) parse.Result { - r := CSSTemplate{ - Properties: []CSSProperty{}, - } - - // Parse the name. - pr := newCSSExpressionParser().Parse(pi) - if !pr.Success { - return pr - } - r.Name = pr.Item.(cssExpression).Name - - var from Position - for { - var pr parse.Result - - // Try for an expression CSS declaration. - // background-color: {%= constants.BackgroundColor %}; - pr = newExpressionCSSPropertyParser().Parse(pi) - if pr.Error != nil { - return pr - } - if pr.Success { - r.Properties = append(r.Properties, pr.Item.(CSSProperty)) - continue - } - - // Try for a constant CSS declaration. - // color: #ffffff; - pr = newConstantCSSPropertyParser().Parse(pi) - if pr.Error != nil { - return pr - } - if pr.Success { - r.Properties = append(r.Properties, pr.Item.(CSSProperty)) - continue - } - - // Eat any whitespace. - pr = optionalWhitespaceParser(pi) - if pr.Error != nil { - return pr - } - // {% endcss %} - from = NewPositionFromInput(pi) - if endCssParser(pi).Success { - return parse.Success("css", r, nil) - } - return parse.Failure("css", newParseError("expected {% endcss %} not found", from, NewPositionFromInput(pi))) - } -} - -// {% css Func() %} -type cssExpression struct { - Name Expression -} - -func newCSSExpressionParser() cssExpressionParser { - return cssExpressionParser{} -} - -type cssExpressionParser struct { -} - -var cssExpressionStartParser = createStartParser("css") - -var cssExpressionNameParser = parse.All(parse.WithStringConcatCombiner, - parse.Letter, - parse.Many(parse.WithStringConcatCombiner, 0, 1000, parse.Any(parse.Letter, parse.ZeroToNine)), -) - -func (p cssExpressionParser) Parse(pi parse.Input) parse.Result { - var r cssExpression - - // Check the prefix first. - prefixResult := cssExpressionStartParser(pi) - if !prefixResult.Success { - return prefixResult - } - - // Once we have the prefix, we must have a name and parameters. - // Read the name of the function. - from := NewPositionFromInput(pi) - pr := cssExpressionNameParser(pi) - if pr.Error != nil && pr.Error != io.EOF { - return pr - } - // If there's no match, the name wasn't correctly terminated. - if !pr.Success { - return parse.Failure("cssExpressionParser", newParseError("css expression: invalid name", from, NewPositionFromInput(pi))) - } - to := NewPositionFromInput(pi) - r.Name = NewExpression(pr.Item.(string), from, to) - from = to - - // Eat the open bracket. - if lb := parse.Rune('(')(pi); !lb.Success { - return parse.Failure("cssExpressionParser", newParseError("css expression: parameters missing open bracket", from, NewPositionFromInput(pi))) - } - - // Check there's no parameters. - from = NewPositionFromInput(pi) - pr = parse.StringUntil(parse.Rune(')'))(pi) - if pr.Error != nil && pr.Error != io.EOF { - return pr - } - // If there's no match, the name wasn't correctly terminated. - if !pr.Success { - return parse.Failure("cssExpressionParser", newParseError("css expression: parameters missing close bracket", from, NewPositionFromInput(pi))) - } - if len(pr.Item.(string)) > 1 { - return parse.Failure("cssExpressionParser", newParseError("css expression: found unexpected parameters", from, NewPositionFromInput(pi))) - } - - // Eat ") %}". - from = NewPositionFromInput(pi) - if lb := expressionFuncEnd(pi); !lb.Success { - return parse.Failure("cssExpressionParser", newParseError("css expression: unterminated (missing ') %}')", from, NewPositionFromInput(pi))) - } - - // Expect a newline. - from = NewPositionFromInput(pi) - if lb := newLine(pi); !lb.Success { - return parse.Failure("cssExpressionParser", newParseError("css expression: missing terminating newline", from, NewPositionFromInput(pi))) - } - - return parse.Success("cssExpressionParser", r, nil) -} - -// CSS property name parser. -var cssPropertyNameFirst = "abcdefghijklmnopqrstuvwxyz" -var cssPropertyNameSubsequent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" -var cssPropertyNameParser = parse.Then(parse.WithStringConcatCombiner, - parse.RuneIn(cssPropertyNameFirst), - parse.Many(parse.WithStringConcatCombiner, 0, 128, parse.RuneIn(cssPropertyNameSubsequent)), -) - -// background-color: {%= constants.BackgroundColor %}; -func newExpressionCSSPropertyParser() expressionCSSPropertyParser { - return expressionCSSPropertyParser{} -} - -type expressionCSSPropertyParser struct { -} - -func (p expressionCSSPropertyParser) Parse(pi parse.Input) parse.Result { - var r ExpressionCSSProperty - var pr parse.Result - start := pi.Index() - - // Optional whitespace. - if pr = optionalWhitespaceParser(pi); pr.Error != nil { - return pr - } - // Property name. - if pr = cssPropertyNameParser(pi); !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - r.Name = pr.Item.(string) - // : - pr = parse.All(parse.WithStringConcatCombiner, - optionalWhitespaceParser, - parse.Rune(':'), - optionalWhitespaceParser)(pi) - if !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - - // {%= string %} - pr = newStringExpressionParser().Parse(pi) - if !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - r.Value = pr.Item.(StringExpression) - - // ; - from := NewPositionFromInput(pi) - if pr = parse.String(";")(pi); !pr.Success { - return parse.Failure("expression css declaration", newParseError("missing expected semicolon (;)", from, NewPositionFromInput(pi))) - } - // \n - from = NewPositionFromInput(pi) - if pr = parse.String("\n")(pi); !pr.Success { - return parse.Failure("expression css declaration", newParseError("missing expected linebreak", from, NewPositionFromInput(pi))) - } - - return parse.Success("expression css declaration", r, nil) -} - -// background-color: #ffffff; -func newConstantCSSPropertyParser() constantCSSPropertyParser { - return constantCSSPropertyParser{} -} - -type constantCSSPropertyParser struct { -} - -func (p constantCSSPropertyParser) Parse(pi parse.Input) parse.Result { - var r ConstantCSSProperty - var pr parse.Result - start := pi.Index() - - // Optional whitespace. - if pr = optionalWhitespaceParser(pi); pr.Error != nil { - return pr - } - // Property name. - if pr = cssPropertyNameParser(pi); !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - r.Name = pr.Item.(string) - // : - pr = parse.All(parse.WithStringConcatCombiner, - optionalWhitespaceParser, - parse.Rune(':'), - optionalWhitespaceParser)(pi) - if !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - - // Everything until ';\n' - from := NewPositionFromInput(pi) - untilEnd := parse.All(parse.WithStringConcatCombiner, - optionalWhitespaceParser, - parse.String(";\n"), - ) - pr = parse.StringUntil(untilEnd)(pi) - if !pr.Success { - return parse.Failure("constant css declaration", newParseError("missing expected semicolon and linebreak (;\\n)", from, NewPositionFromInput(pi))) - } - r.Value = pr.Item.(string) - // Chomp the ;\n - pr = untilEnd(pi) - if !pr.Success { - return parse.Failure("constant css declaration", newParseError("failed to chomp semicolon and linebreak (;\\n)", from, NewPositionFromInput(pi))) - } - - return parse.Success("constant css declaration", r, nil) -} diff --git a/parser/v1/cssparser_test.go b/parser/v1/cssparser_test.go deleted file mode 100644 index 5b6e88a8c..000000000 --- a/parser/v1/cssparser_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/a-h/lexical/input" - "github.com/google/go-cmp/cmp" -) - -func TestExpressionCSSPropertyParser(t *testing.T) { - var tests = []struct { - name string - input string - expected ExpressionCSSProperty - }{ - { - name: "css: single constant property", - input: `background-color: {%= constants.BackgroundColor %};`, - expected: ExpressionCSSProperty{ - Name: "background-color", - Value: StringExpression{ - Expression: Expression{ - Value: "constants.BackgroundColor", - Range: Range{ - From: Position{ - Index: 22, - Line: 1, - Col: 22, - }, - To: Position{ - Index: 47, - Line: 1, - Col: 47, - }, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input + "\n") - result := newExpressionCSSPropertyParser().Parse(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Fatalf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} - -func TestConstantCSSPropertyParser(t *testing.T) { - var tests = []struct { - name string - input string - expected ConstantCSSProperty - }{ - { - name: "css: single constant property", - input: `background-color: #ffffff;`, - expected: ConstantCSSProperty{ - Name: "background-color", - Value: "#ffffff", - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input + "\n") - result := newConstantCSSPropertyParser().Parse(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Fatalf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} - -func TestCSSParser(t *testing.T) { - var tests = []struct { - name string - input string - expected CSSTemplate - }{ - { - name: "css: no parameters, no content", - input: `{% css Name() %} -{% endcss %}`, - expected: CSSTemplate{ - Name: Expression{ - Value: "Name", - Range: Range{ - From: Position{ - Index: 7, - Line: 1, - Col: 7, - }, - To: Position{ - Index: 11, - Line: 1, - Col: 11, - }, - }, - }, - Properties: []CSSProperty{}, - }, - }, - { - name: "css: without spaces", - input: `{%css Name()%} -{% endcss %}`, - expected: CSSTemplate{ - Name: Expression{ - Value: "Name", - Range: Range{ - From: Position{ - Index: 6, - Line: 1, - Col: 6, - }, - To: Position{ - Index: 10, - Line: 1, - Col: 10, - }, - }, - }, - Properties: []CSSProperty{}, - }, - }, - { - name: "css: single constant property", - input: `{% css Name() %} -background-color: #ffffff; -{% endcss %}`, - expected: CSSTemplate{ - Name: Expression{ - Value: "Name", - Range: Range{ - From: Position{ - Index: 7, - Line: 1, - Col: 7, - }, - To: Position{ - Index: 11, - Line: 1, - Col: 11, - }, - }, - }, - Properties: []CSSProperty{ - ConstantCSSProperty{ - Name: "background-color", - Value: "#ffffff", - }, - }, - }, - }, - { - name: "css: single expression property", - input: `{% css Name() %} -background-color: {%= constants.BackgroundColor %}; -{% endcss %}`, - expected: CSSTemplate{ - Name: Expression{ - Value: "Name", - Range: Range{ - From: Position{ - Index: 7, - Line: 1, - Col: 7, - }, - To: Position{ - Index: 11, - Line: 1, - Col: 11, - }, - }, - }, - Properties: []CSSProperty{ - ExpressionCSSProperty{ - Name: "background-color", - Value: StringExpression{ - Expression: Expression{ - Value: "constants.BackgroundColor", - Range: Range{ - From: Position{ - Index: 39, - Line: 2, - Col: 22, - }, - To: Position{ - Index: 64, - Line: 2, - Col: 47, - }, - }, - }, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input) - result := newCSSParser().Parse(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Fatalf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} diff --git a/parser/v1/doctypeparser.go b/parser/v1/doctypeparser.go deleted file mode 100644 index bee3fdae6..000000000 --- a/parser/v1/doctypeparser.go +++ /dev/null @@ -1,53 +0,0 @@ -package parser - -import ( - "io" - - "github.com/a-h/lexical/parse" -) - -func newDocTypeParser() docTypeParser { - return docTypeParser{} -} - -type docTypeParser struct { -} - -var doctypeStartParser = parse.StringInsensitive("') - dtr = parse.StringUntil(parse.Or(tagClose, tagOpen))(pi) - if dtr.Error != nil && dtr.Error != io.EOF { - return dtr - } - if !dtr.Success { - return parse.Failure("docTypeParser", newParseError("unclosed DOCTYPE", from, NewPositionFromInput(pi))) - } - r.Value = dtr.Item.(string) - - // Clear the final '>'. - from = NewPositionFromInput(pi) - dtr = tagClose(pi) - if dtr.Error != nil && dtr.Error != io.EOF { - return dtr - } - if !dtr.Success { - return parse.Failure("docTypeParser", newParseError("unclosed DOCTYPE", from, NewPositionFromInput(pi))) - } - - return parse.Success("docTypeParser", r, nil) -} diff --git a/parser/v1/doctypeparser_test.go b/parser/v1/doctypeparser_test.go deleted file mode 100644 index bb12e6e41..000000000 --- a/parser/v1/doctypeparser_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/a-h/lexical/input" - "github.com/google/go-cmp/cmp" -) - -func TestDocTypeParser(t *testing.T) { - var tests = []struct { - name string - input string - expected DocType - }{ - { - name: "HTML 5 doctype - uppercase", - input: ``, - expected: DocType{ - Value: "html", - }, - }, - { - name: "HTML 5 doctype - lowercase", - input: ``, - expected: DocType{ - Value: "html", - }, - }, - { - name: "HTML 4.01 doctype", - input: ``, - expected: DocType{ - Value: `HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"`, - }, - }, - { - name: "XHTML 1.1", - input: ``, - expected: DocType{ - Value: `html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input) - result := newDocTypeParser().Parse(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Fatalf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} - -func TestDocTypeParserErrors(t *testing.T) { - var tests = []struct { - name string - input string - expected error - }{ - { - name: "doctype unclosed", - input: ``, - expected: newParseError("unclosed DOCTYPE", - Position{ - Index: 17, - Line: 2, - Col: 2, - }, - Position{ - Index: 17, - Line: 2, - Col: 2, - }), - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input) - result := newDocTypeParser().Parse(input) - if diff := cmp.Diff(tt.expected, result.Error); diff != "" { - t.Errorf(diff) - } - }) - } -} diff --git a/parser/v1/elementparser.go b/parser/v1/elementparser.go deleted file mode 100644 index cc4d4c8b4..000000000 --- a/parser/v1/elementparser.go +++ /dev/null @@ -1,428 +0,0 @@ -package parser - -import ( - "fmt" - "html" - "io" - "strings" - - "github.com/a-h/lexical/input" - "github.com/a-h/lexical/parse" -) - -// Element. - -// Element open tag. -type elementOpenTag struct { - Name string - Attributes []Attribute -} - -func newElementOpenTagParser() elementOpenTagParser { - return elementOpenTagParser{} -} - -type elementOpenTagParser struct { -} - -func (p elementOpenTagParser) asElementOpenTag(parts []interface{}) (result interface{}, ok bool) { - return elementOpenTag{ - Name: parts[1].(string), - Attributes: parts[2].([]Attribute), - }, true -} - -func (p elementOpenTagParser) Parse(pi parse.Input) parse.Result { - return parse.All(p.asElementOpenTag, - parse.Rune('<'), - elementNameParser, - newAttributesParser().Parse, - parse.Optional(parse.WithStringConcatCombiner, whitespaceParser), - parse.Rune('>'), - )(pi) -} - -// Element close tag. -type elementCloseTag struct { - Name string -} - -func asElementCloseTag(parts []interface{}) (result interface{}, ok bool) { - return elementCloseTag{ - Name: parts[1].(string), - }, true -} - -var elementCloseTagParser = parse.All(asElementCloseTag, - parse.String("'), -) - -// Attribute name. -var attributeNameFirst = "abcdefghijklmnopqrstuvwxyz" -var attributeNameSubsequent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" -var attributeNameParser = parse.Then(parse.WithStringConcatCombiner, - parse.RuneIn(attributeNameFirst), - parse.Many(parse.WithStringConcatCombiner, 0, 128, parse.RuneIn(attributeNameSubsequent)), -) - -// Constant attribute. -var attributeConstantValueParser = parse.StringUntil(parse.Rune('"')) - -func newConstantAttributeParser() constantAttributeParser { - return constantAttributeParser{} -} - -type constantAttributeParser struct { -} - -func (p constantAttributeParser) asConstantAttribute(parts []interface{}) (result interface{}, ok bool) { - return ConstantAttribute{ - Name: parts[1].(string), - Value: html.UnescapeString(parts[4].(string)), - }, true -} - -func (p constantAttributeParser) Parse(pi parse.Input) parse.Result { - return parse.All(p.asConstantAttribute, - whitespaceParser, - attributeNameParser, - parse.Rune('='), - parse.Rune('"'), - attributeConstantValueParser, - parse.Rune('"'), - )(pi) -} - -// BoolConstantAttribute. -func newBoolConstantAttributeParser() boolConstantAttributeParser { - return boolConstantAttributeParser{} -} - -type boolConstantAttributeParser struct { -} - -func (p boolConstantAttributeParser) Parse(pi parse.Input) parse.Result { - var r BoolConstantAttribute - - start := pi.Index() - pr := whitespaceParser(pi) - if !pr.Success { - return pr - } - - pr = attributeNameParser(pi) - if !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - r.Name = pr.Item.(string) - - // We have a name, but if we have an equals sign, it's not a constant boolean attribute. - next, err := pi.Peek() - if err != nil { - return parse.Failure("boolConstantAttributeParser", fmt.Errorf("boolConstantAttributeParser: unexpected error reading after attribute name: %w", pr.Error)) - } - if next == '=' || next == '?' { - // It's one of the other attribute types. - err := rewind(pi, start) - if err != nil && err != input.ErrStartOfFile { - return parse.Failure("failed to rewind reader", err) - } - return parse.Failure("boolConstantAttributeParser", nil) - } - if !(next == ' ' || next == '\n' || next == '/') { - return parse.Failure("boolConstantAttributeParser", fmt.Errorf("boolConstantAttributeParser: expected attribute name to end with space, newline or '/>', but got %q", string(next))) - } - - return parse.Success("boolConstantAttributeParser", r, nil) -} - -// BoolExpressionAttribute. -func newBoolExpressionAttributeParser() boolExpressionAttributeParser { - return boolExpressionAttributeParser{} -} - -var boolExpressionStart = parse.Any(parse.String("?={%= "), parse.String("?={%=")) - -type boolExpressionAttributeParser struct { -} - -func (p boolExpressionAttributeParser) Parse(pi parse.Input) parse.Result { - var r BoolExpressionAttribute - - start := pi.Index() - pr := whitespaceParser(pi) - if !pr.Success { - return pr - } - - pr = attributeNameParser(pi) - if !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - r.Name = pr.Item.(string) - - // Check whether this is a boolean expression attribute. - if pr = boolExpressionStart(pi); !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - - // Once we've seen a expression prefix, read until the tag end. - from := NewPositionFromInput(pi) - pr = parse.StringUntil(expressionEnd)(pi) - if pr.Error != nil && pr.Error != io.EOF { - return parse.Failure("boolExpressionAttributeParser", fmt.Errorf("boolExpressionAttributeParser: failed to read until tag end: %w", pr.Error)) - } - // If there's no tag end, the string expression parser wasn't terminated. - if !pr.Success { - return parse.Failure("boolExpressionAttributeParser", newParseError("bool expression attribute not terminated", from, NewPositionFromInput(pi))) - } - - // Success! Create the expression. - to := NewPositionFromInput(pi) - r.Expression = NewExpression(pr.Item.(string), from, to) - - // Eat the tag end. - if te := expressionEnd(pi); !te.Success { - return parse.Failure("boolExpressionAttributeParser", newParseError("could not terminate boolean expression", from, NewPositionFromInput(pi))) - } - - return parse.Success("boolExpressionAttributeParser", r, nil) -} - -// ExpressionAttribute. -func newExpressionAttributeParser() expressionAttributeParser { - return expressionAttributeParser{} -} - -type expressionAttributeParser struct { -} - -func (p expressionAttributeParser) Parse(pi parse.Input) parse.Result { - var r ExpressionAttribute - - start := pi.Index() - pr := whitespaceParser(pi) - if !pr.Success { - return pr - } - - pr = attributeNameParser(pi) - if !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - r.Name = pr.Item.(string) - - if pr = parse.String("={%= ")(pi); !pr.Success { - err := rewind(pi, start) - if err != nil { - return parse.Failure("failed to rewind reader", err) - } - return pr - } - - // Once we've seen a expression prefix, read until the tag end. - from := NewPositionFromInput(pi) - pr = parse.StringUntil(expressionEnd)(pi) - if pr.Error != nil && pr.Error != io.EOF { - return parse.Failure("expressionAttributeParser", fmt.Errorf("expressionAttributeParser: failed to read until tag end: %w", pr.Error)) - } - // If there's no tag end, the string expression parser wasn't terminated. - if !pr.Success { - return parse.Failure("expressionAttributeParser", newParseError("expression attribute not terminated", from, NewPositionFromInput(pi))) - } - - // Success! Create the expression. - to := NewPositionFromInput(pi) - r.Expression = NewExpression(pr.Item.(string), from, to) - - // Eat the tag end. - if te := expressionEnd(pi); !te.Success { - return parse.Failure("expressionAttributeParser", newParseError("could not terminate string expression", from, NewPositionFromInput(pi))) - } - - return parse.Success("expressionAttributeParser", r, nil) -} - -func rewind(pi parse.Input, to int64) error { - for i := pi.Index(); i > to; i-- { - if _, err := pi.Retreat(); err != nil { - return err - } - } - return nil -} - -// Attributes. -func newAttributesParser() attributesParser { - return attributesParser{} -} - -type attributesParser struct { -} - -func (p attributesParser) asAttributeArray(parts []interface{}) (result interface{}, ok bool) { - op := make([]Attribute, len(parts)) - for i := 0; i < len(parts); i++ { - switch v := parts[i].(type) { - case BoolConstantAttribute: - op[i] = v - case ConstantAttribute: - op[i] = v - case BoolExpressionAttribute: - op[i] = v - case ExpressionAttribute: - op[i] = v - } - } - return op, true -} - -var attributeParser = parse.Any( - newBoolConstantAttributeParser().Parse, - newConstantAttributeParser().Parse, - newBoolExpressionAttributeParser().Parse, - newExpressionAttributeParser().Parse, -) - -func (p attributesParser) Parse(pi parse.Input) parse.Result { - return parse.Many(p.asAttributeArray, 0, 255, attributeParser)(pi) -} - -// Element name. -var elementNameFirst = "abcdefghijklmnopqrstuvwxyz" -var elementNameSubsequent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" -var elementNameParser = parse.Then(parse.WithStringConcatCombiner, - parse.RuneIn(elementNameFirst), - parse.Many(parse.WithStringConcatCombiner, 0, 15, parse.RuneIn(elementNameSubsequent)), -) - -// Element. -func newElementOpenCloseParser() elementOpenCloseParser { - return elementOpenCloseParser{} -} - -type elementOpenCloseParser struct { - SourceRangeToItemLookup SourceMap -} - -func (p elementOpenCloseParser) Parse(pi parse.Input) parse.Result { - var r Element - - // Check the open tag. - otr := newElementOpenTagParser().Parse(pi) - if otr.Error != nil || !otr.Success { - return otr - } - ot := otr.Item.(elementOpenTag) - r.Name = ot.Name - r.Attributes = ot.Attributes - - // Once we've got an open tag, the rest must be present. - from := NewPositionFromInput(pi) - tnpr := newTemplateNodeParser(nil).Parse(pi) - if !tnpr.Success { - if _, isParseError := tnpr.Error.(ParseError); isParseError { - return tnpr - } - return parse.Failure("elementOpenCloseParser", newParseError(fmt.Sprintf("<%s>: %v", r.Name, tnpr.Error), from, NewPositionFromInput(pi))) - } - if arr, isArray := tnpr.Item.([]Node); isArray { - r.Children = append(r.Children, arr...) - } - - // Close tag. - ectpr := elementCloseTagParser(pi) - if !ectpr.Success { - return parse.Failure("elementOpenCloseParser", newParseError(fmt.Sprintf("<%s>: expected end tag not present or invalid tag contents", r.Name), from, NewPositionFromInput(pi))) - } - if ct := ectpr.Item.(elementCloseTag); ct.Name != r.Name { - return parse.Failure("elementOpenCloseParser", newParseError(fmt.Sprintf("<%s>: mismatched end tag, expected '', got ''", r.Name, r.Name, ct.Name), from, NewPositionFromInput(pi))) - } - - return parse.Success("elementOpenCloseParser", r, nil) -} - -// Element self-closing tag. -func newElementSelfClosingParser() elementSelfClosingParser { - return elementSelfClosingParser{} -} - -type elementSelfClosingParser struct { - SourceRangeToItemLookup SourceMap -} - -func (p elementSelfClosingParser) asElement(parts []interface{}) (result interface{}, ok bool) { - return Element{ - Name: parts[1].(string), - Attributes: parts[2].([]Attribute), - }, true -} - -func (p elementSelfClosingParser) Parse(pi parse.Input) parse.Result { - return parse.All(p.asElement, - parse.Rune('<'), - elementNameParser, - newAttributesParser().Parse, - optionalWhitespaceParser, - parse.String("/>"), - )(pi) -} - -// Element -func newElementParser() elementParser { - return elementParser{} -} - -type elementParser struct { -} - -func (p elementParser) Parse(pi parse.Input) parse.Result { - var r Element - - // Self closing. - from := NewPositionFromInput(pi) - scr := newElementSelfClosingParser().Parse(pi) - if scr.Error != nil && scr.Error != io.EOF { - return scr - } - if scr.Success { - r = scr.Item.(Element) - if msgs, ok := r.Validate(); !ok { - return parse.Failure("elementParser", newParseError(fmt.Sprintf("<%s>: %s", r.Name, strings.Join(msgs, ", ")), from, NewPositionFromInput(pi))) - } - return parse.Success("elementParser", r, nil) - } - - // Open/close pair. - ocr := newElementOpenCloseParser().Parse(pi) - if ocr.Error != nil && ocr.Error != io.EOF { - return ocr - } - if ocr.Success { - r = ocr.Item.(Element) - if msgs, ok := r.Validate(); !ok { - return parse.Failure("elementParser", newParseError(fmt.Sprintf("<%s>: %s", r.Name, strings.Join(msgs, ", ")), from, NewPositionFromInput(pi))) - } - return parse.Success("elementParser", r, nil) - } - - return parse.Failure("elementParser", nil) -} diff --git a/parser/v1/elementparser_test.go b/parser/v1/elementparser_test.go deleted file mode 100644 index 22c416002..000000000 --- a/parser/v1/elementparser_test.go +++ /dev/null @@ -1,617 +0,0 @@ -package parser - -import ( - "strings" - "testing" - - "github.com/a-h/lexical/input" - "github.com/a-h/lexical/parse" - "github.com/google/go-cmp/cmp" -) - -func TestAttributeParser(t *testing.T) { - var tests = []struct { - name string - input string - parser parse.Function - expected interface{} - }{ - { - name: "element: open", - input: ``, - parser: newElementOpenTagParser().Parse, - expected: elementOpenTag{ - Name: "a", - Attributes: []Attribute{}, - }, - }, - { - name: "element: hyphen in name", - input: ``, - parser: newElementOpenTagParser().Parse, - expected: elementOpenTag{ - Name: "turbo-frame", - Attributes: []Attribute{}, - }, - }, - { - name: "element: open with attributes", - input: `
`, - parser: newElementOpenTagParser().Parse, - expected: elementOpenTag{ - Name: "div", - Attributes: []Attribute{ - ConstantAttribute{ - Name: "id", - Value: "123", - }, - ConstantAttribute{ - Name: "style", - Value: "padding: 10px", - }, - }, - }, - }, - { - name: "boolean expression attribute", - input: ` noshade?={%= true %}"`, - parser: newBoolExpressionAttributeParser().Parse, - expected: BoolExpressionAttribute{ - Name: "noshade", - Expression: Expression{ - Value: "true", - Range: Range{ - From: Position{ - Index: 14, - Line: 1, - Col: 14, - }, - To: Position{ - Index: 18, - Line: 1, - Col: 18, - }, - }, - }, - }, - }, - { - name: "boolean expression attribute without spaces", - input: ` noshade?={%=true%}"`, - parser: newBoolExpressionAttributeParser().Parse, - expected: BoolExpressionAttribute{ - Name: "noshade", - Expression: Expression{ - Value: "true", - Range: Range{ - From: Position{ - Index: 13, - Line: 1, - Col: 13, - }, - To: Position{ - Index: 17, - Line: 1, - Col: 17, - }, - }, - }, - }, - }, - { - name: "attribute parsing handles boolean expression attributes", - input: ` noshade?={%= true %}`, - parser: attributeParser, - expected: BoolExpressionAttribute{ - Name: "noshade", - Expression: Expression{ - Value: "true", - Range: Range{ - From: Position{ - Index: 14, - Line: 1, - Col: 14, - }, - To: Position{ - Index: 18, - Line: 1, - Col: 18, - }, - }, - }, - }, - }, - { - name: "constant attribute", - input: ` href="test"`, - parser: newConstantAttributeParser().Parse, - expected: ConstantAttribute{ - Name: "href", - Value: "test", - }, - }, - { - name: "attribute name with hyphens", - input: ` data-turbo-permanent="value"`, - parser: newConstantAttributeParser().Parse, - expected: ConstantAttribute{ - Name: "data-turbo-permanent", - Value: "value", - }, - }, - { - name: "empty attribute", - input: ` data=""`, - parser: newConstantAttributeParser().Parse, - expected: ConstantAttribute{ - Name: "data", - Value: "", - }, - }, - { - name: "attribute containing escaped text", - input: ` href="<">"`, - parser: newConstantAttributeParser().Parse, - expected: ConstantAttribute{ - Name: "href", - Value: `<">`, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input) - result := tt.parser(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Fatalf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} - -func TestElementParser(t *testing.T) { - var tests = []struct { - name string - input string - expected Element - }{ - { - name: "element: self-closing with single constant attribute", - input: ``, - expected: Element{ - Name: "a", - Attributes: []Attribute{ - ConstantAttribute{ - Name: "href", - Value: "test", - }, - }, - }, - }, - { - name: "element: self-closing with single bool expression attribute", - input: `
`, - expected: Element{ - Name: "hr", - Attributes: []Attribute{ - BoolExpressionAttribute{ - Name: "noshade", - Expression: Expression{ - Value: `true`, - Range: Range{ - From: Position{ - Index: 17, - Line: 1, - Col: 17, - }, - To: Position{ - - Index: 21, - Line: 1, - Col: 21, - }, - }, - }, - }, - }, - }, - }, - { - name: "element: self-closing with single expression attribute", - input: `
`, - expected: Element{ - Name: "a", - Attributes: []Attribute{ - ExpressionAttribute{ - Name: "href", - Expression: Expression{ - Value: `"test"`, - Range: Range{ - From: Position{ - Index: 12, - Line: 1, - Col: 12, - }, - To: Position{ - - Index: 18, - Line: 1, - Col: 18, - }, - }, - }, - }, - }, - }, - }, - { - name: "element: self-closing with multiple constant attributes", - input: ``, - expected: Element{ - Name: "a", - Attributes: []Attribute{ - ConstantAttribute{ - Name: "href", - Value: "test", - }, - ConstantAttribute{ - Name: "style", - Value: "text-underline: auto", - }, - }, - }, - }, - { - name: "element: self-closing with multiple boolean attributes", - input: `
`, - expected: Element{ - Name: "hr", - Attributes: []Attribute{ - BoolConstantAttribute{ - Name: "optionA", - }, - BoolExpressionAttribute{ - Name: "optionB", - Expression: Expression{ - Value: `true`, - Range: Range{ - From: Position{ - Index: 25, - Line: 1, - Col: 25, - }, - To: Position{ - - Index: 29, - Line: 1, - Col: 29, - }, - }, - }, - }, - ConstantAttribute{ - Name: "optionC", - Value: "other", - }, - }, - }, - }, - { - name: "element: self-closing with multiple constant and expr attributes", - input: `
`, - expected: Element{ - Name: "a", - Attributes: []Attribute{ - ConstantAttribute{ - Name: "href", - Value: "test", - }, - ExpressionAttribute{ - Name: "title", - Expression: Expression{ - Value: `localisation.Get("a_title")`, - Range: Range{ - From: Position{ - Index: 25, - Line: 1, - Col: 25, - }, - To: Position{ - - Index: 52, - Line: 1, - Col: 52, - }, - }, - }, - }, - ConstantAttribute{ - Name: "style", - Value: "text-underline: auto", - }, - }, - }, - }, - { - name: "element: self closing with no attributes", - input: `
`, - expected: Element{ - Name: "hr", - Attributes: []Attribute{}, - }, - }, - { - name: "element: self closing with attribute", - input: `
`, - expected: Element{ - Name: "hr", - Attributes: []Attribute{ - ConstantAttribute{ - Name: "style", - Value: "padding: 10px", - }, - }, - }, - }, - { - name: "element: open and close", - input: `
`, - expected: Element{ - Name: "a", - Attributes: []Attribute{}, - }, - }, - { - name: "element: open and close with text", - input: `The text`, - expected: Element{ - Name: "a", - Attributes: []Attribute{}, - Children: []Node{ - Text{ - Value: "The text", - }, - }, - }, - }, - { - name: "element: with self-closing child element", - input: ``, - expected: Element{ - Name: "a", - Attributes: []Attribute{}, - Children: []Node{ - Element{ - Name: "b", - Attributes: []Attribute{}, - }, - }, - }, - }, - { - name: "element: with non-self-closing child element", - input: ``, - expected: Element{ - Name: "a", - Attributes: []Attribute{}, - Children: []Node{ - Element{ - Name: "b", - Attributes: []Attribute{}, - }, - }, - }, - }, - { - name: "element: containing space", - input: ` `, - expected: Element{ - Name: "a", - Attributes: []Attribute{}, - Children: []Node{ - Whitespace{Value: " "}, - Element{ - Name: "b", - Attributes: []Attribute{}, - Children: []Node{ - Whitespace{Value: " "}, - }, - }, - Whitespace{Value: " "}, - }, - }, - }, - { - name: "element: with multiple child elements", - input: ``, - expected: Element{ - Name: "a", - Attributes: []Attribute{}, - Children: []Node{ - Element{ - Name: "b", - Attributes: []Attribute{}, - }, - Element{ - Name: "c", - Attributes: []Attribute{}, - Children: []Node{ - Element{ - Name: "d", - Attributes: []Attribute{}, - }, - }, - }, - }, - }, - }, - { - name: "element: empty", - input: `
`, - expected: Element{ - Name: "div", - Attributes: []Attribute{}, - }, - }, - { - name: "element: containing string expression", - input: `
{%= "test" %}
`, - expected: Element{ - Name: "div", - Attributes: []Attribute{}, - Children: []Node{ - StringExpression{ - Expression: Expression{ - Value: `"test"`, - Range: Range{ - From: Position{ - Index: 9, - Line: 1, - Col: 9, - }, - To: Position{ - Index: 15, - Line: 1, - Col: 15, - }, - }, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - input := input.NewFromString(tt.input) - result := newElementParser().Parse(input) - if result.Error != nil { - t.Fatalf("parser error: %v", result.Error) - } - if !result.Success { - t.Fatalf("failed to parse at %d", input.Index()) - } - if diff := cmp.Diff(tt.expected, result.Item); diff != "" { - t.Errorf(diff) - } - }) - } -} - -func TestElementParserErrors(t *testing.T) { - var tests = []struct { - name string - input string - expected error - }{ - { - name: "element: mismatched end tag", - input: `
`, - expected: newParseError(": mismatched end tag, expected '', got ''", - Position{ - Index: 3, - Line: 1, - Col: 3, - }, - Position{ - Index: 7, - Line: 1, - Col: 7, - }), - }, - { - name: "element: attempted use of expression for style attribute (open/close)", - input: ``, - expected: newParseError(`: invalid style attribute: style attributes cannot be a templ expression`, - Position{ - Index: 0, - Line: 1, - Col: 0, - }, - Position{ - Index: 26, - Line: 1, - Col: 26, - }), - }, - { - name: "element: attempted use of expression for style attribute (self-closing)", - input: ``, - expected: newParseError(`: invalid style attribute: style attributes cannot be a templ expression`, - Position{ - Index: 0, - Line: 1, - Col: 0, - }, - Position{ - Index: 23, - Line: 1, - Col: 23, - }), - }, - { - name: "element: script tags cannot contain non-text nodes", - input: ``, - expected: newParseError("