Fix for loop parsing and add @if/@for in attribute positions

This commit is contained in:
Michael Netshipise 2026-01-19 08:13:00 +02:00
parent 6d858472b2
commit b6a168a2b6
4 changed files with 17798 additions and 16599 deletions

View File

@ -9,7 +9,6 @@ module.exports = grammar({
conflicts: ($) => [ conflicts: ($) => [
[$.expression, $.pattern], [$.expression, $.pattern],
[$.rust_path, $.expression], [$.rust_path, $.expression],
[$.struct_pattern, $.expression],
[$.html_element], [$.html_element],
[$.self_closing_function_tag, $.container_function_tag], [$.self_closing_function_tag, $.container_function_tag],
], ],
@ -120,12 +119,14 @@ module.exports = grammar({
content_block: ($) => seq("{", repeat($.template_node), "}"), content_block: ($) => seq("{", repeat($.template_node), "}"),
// Template nodes // Template nodes
// Note: template_control_flow must come before template_expression
// to avoid @for/@if/@let being parsed as variable expressions
template_node: ($) => template_node: ($) =>
choice( choice(
$.html_element, $.html_element,
$.function_tag, $.function_tag,
$.template_expression,
$.template_control_flow, $.template_control_flow,
$.template_expression,
$.comment, $.comment,
$.embedded_language, $.embedded_language,
$.escape_at, $.escape_at,
@ -133,17 +134,18 @@ module.exports = grammar({
), ),
// HTML elements // HTML elements
// Note: attribute_or_control allows @if/@for in attribute position
html_element: ($) => html_element: ($) =>
choice( choice(
// Self-closing tag // Self-closing tag
seq("<", $.tag_name, repeat($.html_attribute), "/", ">"), seq("<", $.tag_name, repeat($.attribute_or_control), "/", ">"),
// Void elements (no closing tag needed) // Void elements (no closing tag needed)
seq("<", $.tag_name, repeat($.html_attribute), ">"), seq("<", $.tag_name, repeat($.attribute_or_control), ">"),
// Full element with content and closing tag // Full element with content and closing tag
seq( seq(
"<", "<",
$.tag_name, $.tag_name,
repeat($.html_attribute), repeat($.attribute_or_control),
">", ">",
repeat($.template_node), repeat($.template_node),
"</", "</",
@ -152,11 +154,47 @@ module.exports = grammar({
), ),
), ),
// Allow either HTML attributes or control flow (@if/@for) in attribute position
attribute_or_control: ($) =>
choice(
$.html_attribute,
$.attribute_control_flow,
),
// Control flow in attribute context - produces attributes conditionally
attribute_control_flow: ($) =>
choice(
$.attribute_if_statement,
$.attribute_for_loop,
),
attribute_if_statement: ($) =>
seq(
"@if",
$.expression,
"{",
repeat($.attribute_or_control),
"}",
optional(seq("else", "{", repeat($.attribute_or_control), "}")),
),
attribute_for_loop: ($) =>
seq(
"@for",
$.simple_pattern,
"in",
$.expression,
"{",
repeat($.attribute_or_control),
"}",
),
tag_name: ($) => /[a-zA-Z][a-zA-Z0-9-]*/, tag_name: ($) => /[a-zA-Z][a-zA-Z0-9-]*/,
html_attribute: ($) => html_attribute: ($) =>
seq($.attribute_name, optional(seq("=", $.attribute_value))), seq($.attribute_name, optional(seq("=", $.attribute_value))),
// Attribute names: allow @ for directives like @click, but not @if/@for/@let
attribute_name: ($) => /[a-zA-Z_:@][a-zA-Z0-9_:.-]*/, attribute_name: ($) => /[a-zA-Z_:@][a-zA-Z0-9_:.-]*/,
attribute_value: ($) => attribute_value: ($) =>
@ -236,7 +274,7 @@ module.exports = grammar({
seq( seq(
"@for", "@for",
optional(seq($.identifier, ":")), optional(seq($.identifier, ":")),
$.pattern, $.simple_pattern,
"in", "in",
$.expression, $.expression,
$.content_block, $.content_block,
@ -312,14 +350,23 @@ module.exports = grammar({
unquoted_value: ($) => /[^\s>=\/]+/, unquoted_value: ($) => /[^\s>=\/]+/,
// Patterns // Patterns - full patterns used in match arms
pattern: ($) => pattern: ($) =>
choice( choice(
$.wildcard_pattern, $.wildcard_pattern,
$.tuple_pattern, $.tuple_pattern,
$.struct_pattern, $.struct_pattern,
$.identifier_pattern,
$.literal, $.literal,
$.identifier_pattern,
),
// Simple pattern for for loops - no struct patterns to avoid ambiguity with content_block
simple_pattern: ($) =>
choice(
$.wildcard_pattern,
$.tuple_pattern,
$.literal,
$.identifier_pattern,
), ),
wildcard_pattern: ($) => "_", wildcard_pattern: ($) => "_",

View File

@ -607,11 +607,11 @@
}, },
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "template_expression" "name": "template_control_flow"
}, },
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "template_control_flow" "name": "template_expression"
}, },
{ {
"type": "SYMBOL", "type": "SYMBOL",
@ -649,7 +649,7 @@
"type": "REPEAT", "type": "REPEAT",
"content": { "content": {
"type": "SYMBOL", "type": "SYMBOL",
"name": "html_attribute" "name": "attribute_or_control"
} }
}, },
{ {
@ -677,7 +677,7 @@
"type": "REPEAT", "type": "REPEAT",
"content": { "content": {
"type": "SYMBOL", "type": "SYMBOL",
"name": "html_attribute" "name": "attribute_or_control"
} }
}, },
{ {
@ -701,7 +701,7 @@
"type": "REPEAT", "type": "REPEAT",
"content": { "content": {
"type": "SYMBOL", "type": "SYMBOL",
"name": "html_attribute" "name": "attribute_or_control"
} }
}, },
{ {
@ -731,6 +731,128 @@
} }
] ]
}, },
"attribute_or_control": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "html_attribute"
},
{
"type": "SYMBOL",
"name": "attribute_control_flow"
}
]
},
"attribute_control_flow": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "attribute_if_statement"
},
{
"type": "SYMBOL",
"name": "attribute_for_loop"
}
]
},
"attribute_if_statement": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "@if"
},
{
"type": "SYMBOL",
"name": "expression"
},
{
"type": "STRING",
"value": "{"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "attribute_or_control"
}
},
{
"type": "STRING",
"value": "}"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "else"
},
{
"type": "STRING",
"value": "{"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "attribute_or_control"
}
},
{
"type": "STRING",
"value": "}"
}
]
},
{
"type": "BLANK"
}
]
}
]
},
"attribute_for_loop": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "@for"
},
{
"type": "SYMBOL",
"name": "simple_pattern"
},
{
"type": "STRING",
"value": "in"
},
{
"type": "SYMBOL",
"name": "expression"
},
{
"type": "STRING",
"value": "{"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "attribute_or_control"
}
},
{
"type": "STRING",
"value": "}"
}
]
},
"tag_name": { "tag_name": {
"type": "PATTERN", "type": "PATTERN",
"value": "[a-zA-Z][a-zA-Z0-9-]*" "value": "[a-zA-Z][a-zA-Z0-9-]*"
@ -1172,7 +1294,7 @@
}, },
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "pattern" "name": "simple_pattern"
}, },
{ {
"type": "STRING", "type": "STRING",
@ -1578,13 +1700,34 @@
"type": "SYMBOL", "type": "SYMBOL",
"name": "struct_pattern" "name": "struct_pattern"
}, },
{
"type": "SYMBOL",
"name": "literal"
},
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "identifier_pattern" "name": "identifier_pattern"
}
]
},
"simple_pattern": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "wildcard_pattern"
},
{
"type": "SYMBOL",
"name": "tuple_pattern"
}, },
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "literal" "name": "literal"
},
{
"type": "SYMBOL",
"name": "identifier_pattern"
} }
] ]
}, },
@ -2830,10 +2973,6 @@
"rust_path", "rust_path",
"expression" "expression"
], ],
[
"struct_pattern",
"expression"
],
[ [
"html_element" "html_element"
], ],

View File

@ -67,6 +67,67 @@
] ]
} }
}, },
{
"type": "attribute_control_flow",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "attribute_for_loop",
"named": true
},
{
"type": "attribute_if_statement",
"named": true
}
]
}
},
{
"type": "attribute_for_loop",
"named": true,
"fields": {},
"children": {
"multiple": true,
"required": true,
"types": [
{
"type": "attribute_or_control",
"named": true
},
{
"type": "expression",
"named": true
},
{
"type": "simple_pattern",
"named": true
}
]
}
},
{
"type": "attribute_if_statement",
"named": true,
"fields": {},
"children": {
"multiple": true,
"required": true,
"types": [
{
"type": "attribute_or_control",
"named": true
},
{
"type": "expression",
"named": true
}
]
}
},
{ {
"type": "attribute_list", "type": "attribute_list",
"named": true, "named": true,
@ -82,6 +143,25 @@
] ]
} }
}, },
{
"type": "attribute_or_control",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "attribute_control_flow",
"named": true
},
{
"type": "html_attribute",
"named": true
}
]
}
},
{ {
"type": "attribute_reference", "type": "attribute_reference",
"named": true, "named": true,
@ -542,7 +622,7 @@
"named": true "named": true
}, },
{ {
"type": "pattern", "type": "simple_pattern",
"named": true "named": true
} }
] ]
@ -718,7 +798,7 @@
"required": true, "required": true,
"types": [ "types": [
{ {
"type": "html_attribute", "type": "attribute_or_control",
"named": true "named": true
}, },
{ {
@ -1230,6 +1310,33 @@
] ]
} }
}, },
{
"type": "simple_pattern",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier_pattern",
"named": true
},
{
"type": "literal",
"named": true
},
{
"type": "tuple_pattern",
"named": true
},
{
"type": "wildcard_pattern",
"named": true
}
]
}
},
{ {
"type": "slice_type", "type": "slice_type",
"named": true, "named": true,

34064
src/parser.c

File diff suppressed because it is too large Load Diff