Skip to main content

svelte_syntax/parse/component/
modern.rs

1use std::ops::Range;
2use std::sync::Arc;
3
4use html_escape::decode_html_entities as decode_html_entities_cow;
5use oxc_span::GetSpan;
6use tree_sitter::Node as TsNode;
7
8use super::{
9    AttributeKind, ElementKind, SvelteElementKind, classify_attribute_name, classify_element_name,
10    elements::is_void_element_name, find_first_named_child, is_component_name,
11    parse_identifier_name, parse_modern_attributes, source_location_from_point, text_for_node,
12};
13use crate::SourceLocation;
14use crate::ast::common::NameLocation as LegacyNameLocation;
15use crate::ast::common::{AttributeValueSyntax, ParseError, ParseErrorKind, Span};
16use crate::ast::legacy::Expression as LegacyExpression;
17use crate::ast::modern::*;
18use crate::{SourceId, SourceText};
19
20// ---------------------------------------------------------------------------
21// Incremental parsing support
22// ---------------------------------------------------------------------------
23
24/// Hint passed through recursive parse functions during incremental parsing.
25/// Contains changed ranges from tree-sitter and old AST nodes for reuse.
26pub(crate) struct IncrementalHint<'a> {
27    pub changed_ranges: &'a [std::ops::Range<usize>],
28    /// Old source text for content comparison during node matching.
29    pub old_source: &'a str,
30    /// Old fragment nodes available for reuse at this level.
31    pub old_nodes: &'a [Node],
32    /// Old root, available only at the top level for script/style matching.
33    pub old_root: Option<&'a Root>,
34}
35
36/// Returns `true` if any changed range overlaps the half-open byte interval `[start, end)`.
37fn any_range_overlaps(changed: &[std::ops::Range<usize>], start: usize, end: usize) -> bool {
38    changed.iter().any(|r| r.start < end && r.end > start)
39}
40
41/// Try to find a reusable old `Node` for a CST child that is outside all changed ranges.
42///
43/// Uses ordered matching: advances `*cursor` through `old_nodes` looking for a node
44/// whose byte length matches `new_len` AND whose source content is identical.
45/// Returns `Some(cloned_node)` on match.
46fn try_reuse_node(
47    old_source: &str,
48    new_source: &str,
49    old_nodes: &[Node],
50    cursor: &mut usize,
51    new_start: usize,
52    new_end: usize,
53) -> Option<Node> {
54    let new_len = new_end - new_start;
55    let new_text = new_source.get(new_start..new_end)?;
56    // Scan forward (skip at most a few old nodes that were removed or shifted).
57    let scan_limit = (*cursor + 4).min(old_nodes.len());
58    for i in *cursor..scan_limit {
59        let old = &old_nodes[i];
60        let old_start = old.start();
61        let old_end = old.end();
62        let old_len = old_end - old_start;
63        if old_len == new_len {
64            if let Some(old_text) = old_source.get(old_start..old_end) {
65                if old_text == new_text {
66                    *cursor = i + 1;
67                    return Some(old.clone());
68                }
69            }
70        }
71    }
72    None
73}
74
75/// Extract the child fragment nodes from a `Node`, if it has a fragment.
76fn node_child_nodes(node: &Node) -> &[Node] {
77    match node {
78        Node::RegularElement(el) => &el.fragment.nodes,
79        Node::Component(el) => &el.fragment.nodes,
80        Node::SlotElement(el) => &el.fragment.nodes,
81        Node::SvelteHead(el) => &el.fragment.nodes,
82        Node::SvelteBody(el) => &el.fragment.nodes,
83        Node::SvelteWindow(el) => &el.fragment.nodes,
84        Node::SvelteDocument(el) => &el.fragment.nodes,
85        Node::SvelteComponent(el) => &el.fragment.nodes,
86        Node::SvelteElement(el) => &el.fragment.nodes,
87        Node::SvelteSelf(el) => &el.fragment.nodes,
88        Node::SvelteFragment(el) => &el.fragment.nodes,
89        Node::SvelteBoundary(el) => &el.fragment.nodes,
90        Node::TitleElement(el) => &el.fragment.nodes,
91        Node::IfBlock(b) => &b.consequent.nodes,
92        Node::EachBlock(b) => &b.body.nodes,
93        Node::KeyBlock(b) => &b.fragment.nodes,
94        Node::AwaitBlock(_) | Node::SnippetBlock(_) => &[],
95        Node::Text(_) | Node::Comment(_) | Node::ExpressionTag(_) | Node::RenderTag(_)
96        | Node::HtmlTag(_) | Node::ConstTag(_) | Node::DebugTag(_) => &[],
97    }
98}
99
100/// Try to reuse a `Script` from the old root by matching script context.
101/// Determines context from the CST element's attributes and finds the old
102/// script with the same context.
103fn try_reuse_script(source: &str, element: TsNode<'_>, old_root: &Root) -> Option<Script> {
104    // Determine the context of the new CST script element.
105    let mut tag_cursor = element.walk();
106    let start_tag = element
107        .named_children(&mut tag_cursor)
108        .find(|c| c.kind() == "start_tag")?;
109    let attrs_text = text_for_node(source, start_tag);
110    let new_context = if attrs_text.contains("module")
111        || attrs_text.contains("context=\"module\"")
112        || attrs_text.contains("context='module'")
113    {
114        ScriptContext::Module
115    } else {
116        ScriptContext::Default
117    };
118
119    old_root
120        .scripts
121        .iter()
122        .find(|s| s.context == new_context)
123        .cloned()
124}
125
126/// Try to reuse the CSS from the old root.
127fn try_reuse_style(old_root: &Root) -> Option<Css> {
128    old_root.css.clone()
129}
130
131/// Find an old `Node` by byte length (non-consuming lookahead for building child hints).
132/// Returns a reference without advancing the cursor.
133fn find_old_node_by_kind<'a>(
134    old_nodes: &'a [Node],
135    cursor: &mut usize,
136    new_len: usize,
137    _kind: &str,
138) -> Option<&'a Node> {
139    let scan_limit = (*cursor + 4).min(old_nodes.len());
140    for i in *cursor..scan_limit {
141        let old = &old_nodes[i];
142        let old_len = old.end() - old.start();
143        if old_len == new_len {
144            *cursor = i + 1;
145            return Some(old);
146        }
147    }
148    None
149}
150
151/// Build a child `IncrementalHint` for a CST child that overlaps a changed range
152/// but has a corresponding old AST node whose children can still be partially reused.
153fn make_child_hint<'a>(
154    parent_hint: &'a IncrementalHint<'a>,
155    old_node_cursor: &mut usize,
156    child_start: usize,
157    child_end: usize,
158    kind: &str,
159) -> Option<IncrementalHint<'a>> {
160    let old = find_old_node_by_kind(
161        parent_hint.old_nodes,
162        old_node_cursor,
163        child_end - child_start,
164        kind,
165    )?;
166    let children = node_child_nodes(old);
167    if children.is_empty() {
168        return None;
169    }
170    Some(IncrementalHint {
171        changed_ranges: parent_hint.changed_ranges,
172        old_source: parent_hint.old_source,
173        old_nodes: children,
174        old_root: None,
175    })
176}
177
178// ---------------------------------------------------------------------------
179
180pub(crate) fn parse_root(source: &str, root: TsNode<'_>, loose: bool) -> Root {
181    parse_root_inner(source, root, loose, None)
182}
183
184pub(crate) fn parse_root_incremental(
185    source: &str,
186    root: TsNode<'_>,
187    loose: bool,
188    old_root: &Root,
189    old_source: &str,
190    changed_ranges: &[Range<usize>],
191) -> Root {
192    let hint = IncrementalHint {
193        changed_ranges,
194        old_source,
195        old_nodes: &old_root.fragment.nodes,
196        old_root: Some(old_root),
197    };
198    parse_root_inner(source, root, loose, Some(hint))
199}
200
201fn parse_root_inner(
202    source: &str,
203    root: TsNode<'_>,
204    loose: bool,
205    hint: Option<IncrementalHint<'_>>,
206) -> Root {
207    let errors = collect_parse_errors(source, root);
208
209    if root.kind() == "ERROR" {
210        let fragment_nodes = recover_modern_error_nodes(source, root, false);
211        return Root {
212            css: None,
213            styles: Box::new([]),
214            js: Box::new([]),
215            scripts: Box::new([]),
216            start: root.start_byte(),
217            end: root.end_byte(),
218            r#type: RootType::Root,
219            fragment: crate::ast::modern::Fragment {
220                r#type: crate::ast::modern::FragmentType::Fragment,
221                nodes: fragment_nodes.into_boxed_slice(),
222            },
223            options: None,
224            module: None,
225            instance: None,
226            comments: None,
227            errors: errors.into_boxed_slice(),
228        };
229    }
230
231    let mut css = None;
232    let mut styles = Vec::new();
233    let mut options = None;
234    let mut module = None;
235    let mut instance = None;
236    let mut js = Vec::new();
237    let mut fragment_nodes = Vec::new();
238    let mut root_comments = Vec::new();
239    let mut pending_script_comment: Option<Arc<str>> = None;
240    let mut previous_child_end = None;
241    let mut old_node_cursor = 0usize;
242
243    let mut cursor = root.walk();
244    for child in root.named_children(&mut cursor) {
245        if let Some(gap_start) = previous_child_end {
246            push_modern_gap_text(source, &mut fragment_nodes, gap_start, child.start_byte());
247        }
248
249        let child_start = child.start_byte();
250        let child_end = child.end_byte();
251
252        // Incremental reuse: if this child is outside all changed ranges,
253        // try to clone the corresponding old AST node instead of parsing.
254        if let Some(ref hint) = hint {
255            if !any_range_overlaps(hint.changed_ranges, child_start, child_end) {
256                // Scripts: reuse by matching context on old root.
257                if child.kind() == "element" {
258                    if let Some(name) = modern_element_name(source, child) {
259                        match classify_element_name(name.as_ref()) {
260                            ElementKind::Script => {
261                                if let Some(old_root) = hint.old_root {
262                                    if let Some(old_script) = try_reuse_script(source, child, old_root) {
263                                        js.push(old_script.clone());
264                                        match old_script.context {
265                                            ScriptContext::Module => {
266                                                if module.is_none() {
267                                                    module = Some(old_script);
268                                                }
269                                            }
270                                            ScriptContext::Default => {
271                                                if instance.is_none() {
272                                                    instance = Some(old_script);
273                                                }
274                                            }
275                                        }
276                                        pending_script_comment = None;
277                                        previous_child_end = Some(child_end);
278                                        continue;
279                                    }
280                                }
281                            }
282                            ElementKind::Style => {
283                                if let Some(old_root) = hint.old_root {
284                                    if let Some(old_style) = try_reuse_style(old_root) {
285                                        if css.is_none() {
286                                            css = Some(old_style.clone());
287                                        }
288                                        styles.push(old_style);
289                                        pending_script_comment = None;
290                                        previous_child_end = Some(child_end);
291                                        continue;
292                                    }
293                                }
294                            }
295                            _ => {}
296                        }
297                    }
298                }
299
300                // Fragment nodes: try ordered reuse by byte length.
301                if let Some(reused) = try_reuse_node(
302                    hint.old_source,
303                    source,
304                    hint.old_nodes,
305                    &mut old_node_cursor,
306                    child_start,
307                    child_end,
308                ) {
309                    fragment_nodes.push(reused);
310                    previous_child_end = Some(child_end);
311                    continue;
312                }
313            }
314        }
315
316        match child.kind() {
317            "text" | "entity" => {
318                let text_node = parse_modern_text(source, child);
319                if text_node.data.chars().all(char::is_whitespace) {
320                    push_modern_text_node(&mut fragment_nodes, text_node);
321                } else {
322                    pending_script_comment = None;
323                    push_modern_text_node(&mut fragment_nodes, text_node);
324                }
325            }
326            "comment" => {
327                let comment = parse_modern_comment(source, child);
328                pending_script_comment = Some(comment.data.clone());
329                fragment_nodes.push(crate::ast::modern::Node::Comment(comment));
330            }
331            "expression" => {
332                let tag = if loose {
333                    Some(parse_modern_expression_tag_loose(source, child))
334                } else {
335                    parse_modern_expression_tag(source, child)
336                };
337                if let Some(tag) = tag {
338                    fragment_nodes.push(crate::ast::modern::Node::ExpressionTag(tag));
339                }
340            }
341            kind if is_typed_block_kind(kind) => {
342                pending_script_comment = None;
343                let child_hint = hint.as_ref().and_then(|h| {
344                    make_child_hint(h, &mut old_node_cursor, child_start, child_end, kind)
345                });
346                if let Some(block_node) = parse_modern_block(source, child, child_hint.as_ref()) {
347                    fragment_nodes.push(block_node);
348                }
349            }
350            kind if is_typed_tag_kind(kind) => {
351                pending_script_comment = None;
352                if let Some(tag_node) = parse_modern_tag(source, child) {
353                    fragment_nodes.push(tag_node);
354                }
355            }
356            "element" => {
357                if let Some((recovered_nodes, recovered_comments)) =
358                    parse_modern_collapsed_comment_tag_sequence(source, child)
359                {
360                    pending_script_comment = None;
361                    fragment_nodes.extend(recovered_nodes);
362                    root_comments.extend(recovered_comments);
363                    previous_child_end = Some(child_end);
364                    continue;
365                }
366
367                if let Some(name) = modern_element_name(source, child) {
368                    match classify_element_name(name.as_ref()) {
369                        ElementKind::Script => {
370                            if let Some(script) = parse_modern_script(
371                                source,
372                                child,
373                                pending_script_comment.as_deref(),
374                            ) {
375                                js.push(script.clone());
376                                match script.context {
377                                    crate::ast::modern::ScriptContext::Module => {
378                                        if module.is_none() {
379                                            module = Some(script);
380                                        }
381                                    }
382                                    crate::ast::modern::ScriptContext::Default => {
383                                        if instance.is_none() {
384                                            instance = Some(script);
385                                        }
386                                    }
387                                }
388                                pending_script_comment = None;
389                                previous_child_end = Some(child_end);
390                                continue;
391                            }
392                        }
393                        ElementKind::Svelte(SvelteElementKind::Options) => {
394                            options = parse_modern_options(source, child);
395                            pending_script_comment = None;
396                            previous_child_end = Some(child_end);
397                            continue;
398                        }
399                        ElementKind::Style => {
400                            if let Some(style) = parse_modern_style(source, child) {
401                                if css.is_none() {
402                                    css = Some(style.clone());
403                                }
404                                styles.push(style);
405                                pending_script_comment = None;
406                                previous_child_end = Some(child_end);
407                                continue;
408                            }
409                        }
410                        _ => {}
411                    }
412                }
413
414                pending_script_comment = None;
415                let child_hint = hint.as_ref().and_then(|h| {
416                    make_child_hint(h, &mut old_node_cursor, child_start, child_end, "element")
417                });
418                fragment_nodes.push(parse_modern_element_node(
419                    source, child, false, false, loose, child_hint.as_ref(),
420                ));
421            }
422            "ERROR" => {
423                pending_script_comment = None;
424                let mut recovered = recover_modern_error_nodes(source, child, false);
425                fragment_nodes.append(&mut recovered);
426            }
427            _ => {}
428        }
429
430        previous_child_end = Some(child_end);
431    }
432
433    root_comments.extend(collect_modern_tag_comments(source, root));
434    root_comments.sort_by_key(|comment| {
435        (
436            comment.start,
437            comment.end,
438            match comment.r#type {
439                RootCommentType::Line => 0u8,
440                RootCommentType::Block => 1u8,
441            },
442        )
443    });
444    root_comments.dedup_by(|left, right| {
445        left.start == right.start
446            && left.end == right.end
447            && left.r#type == right.r#type
448            && left.value == right.value
449    });
450
451    Root {
452        css,
453        styles: styles.into_boxed_slice(),
454        js: Box::new([]),
455        scripts: js.into_boxed_slice(),
456        start: root.start_byte(),
457        end: root.end_byte(),
458        r#type: RootType::Root,
459        fragment: crate::ast::modern::Fragment {
460            r#type: crate::ast::modern::FragmentType::Fragment,
461            nodes: fragment_nodes.into_boxed_slice(),
462        },
463        options,
464        module,
465        instance,
466        comments: (!root_comments.is_empty()).then(|| root_comments.into_boxed_slice()),
467        errors: errors.into_boxed_slice(),
468    }
469}
470
471fn collect_parse_errors(source: &str, root: TsNode<'_>) -> Vec<ParseError> {
472    fn walk(
473        source: &str,
474        node: TsNode<'_>,
475        errors: &mut Vec<ParseError>,
476        parent_kind: Option<&str>,
477    ) {
478        if is_typed_block_kind(node.kind()) {
479            collect_block_parse_errors(source, node, errors);
480        } else if node.kind() == "ERROR" && !parent_kind.is_some_and(is_typed_block_kind) {
481            let error = parse_error_from_error_node(source, node);
482            let checkpoint = errors.len();
483
484            let mut cursor = node.walk();
485            for child in node.named_children(&mut cursor) {
486                walk(source, child, errors, Some(node.kind()));
487            }
488
489            if let Some(error) = error
490                && keep_error(source, node, &error, errors.len() > checkpoint)
491            {
492                errors.push(error);
493            }
494            return;
495        } else if (node.kind() != "orphan_branch" || parent_kind != Some("ERROR"))
496            && let Some(error) = parse_error_from_non_error_node(source, node)
497        {
498            errors.push(error);
499        }
500
501        let mut cursor = node.walk();
502        for child in node.named_children(&mut cursor) {
503            walk(source, child, errors, Some(node.kind()));
504        }
505    }
506
507    fn keep_error(
508        _source: &str,
509        node: TsNode<'_>,
510        error: &ParseError,
511        has_descendant_error: bool,
512    ) -> bool {
513        match error.kind {
514            ParseErrorKind::BlockUnclosed => {
515                if has_descendant_error {
516                    return false;
517                }
518
519                // Don't emit unclosed block error for snippet blocks
520                let is_snippet = find_direct_named_child(node, "snippet_block").is_some()
521                    || find_direct_named_child(node, "snippet_name").is_some();
522                !is_snippet
523            }
524            _ => true,
525        }
526    }
527
528    let mut errors = Vec::new();
529    walk(source, root, &mut errors, None);
530    errors.sort_by_key(|error| (error.start, error.end));
531    errors.dedup_by(|left, right| left.start == right.start && left.end == right.end);
532    errors
533}
534
535fn collect_block_parse_errors(source: &str, block: TsNode<'_>, errors: &mut Vec<ParseError>) {
536    let children = named_children_vec(block);
537    let Some(block_kind) = BlockKind::from_node_kind(block.kind()) else {
538        return;
539    };
540
541    let body_start = match block_kind {
542        BlockKind::If => body_start_index(block, &children, &["expression"]),
543        BlockKind::Each => {
544            body_start_index(block, &children, &["expression", "binding", "index", "key"])
545        }
546        BlockKind::Key => body_start_index(block, &children, &["expression"]),
547        BlockKind::Await => {
548            body_start_index(block, &children, &["expression", "binding", "pending"])
549        }
550        BlockKind::Snippet => {
551            body_start_index(block, &children, &["name", "type_parameters", "parameters"])
552        }
553    };
554
555    // For await blocks, initialize previous from the pending section so that
556    // branches can detect unclosed elements in the preceding section content.
557    let mut previous: Option<TsNode<'_>> = None;
558    if matches!(block_kind, BlockKind::Await)
559        && let Some(pending) = block.child_by_field_name("pending")
560    {
561        if let Some((branch_kind, start)) = branch_in_section_container(source, pending) {
562            let kind = if block_kind.accepts(branch_kind) {
563                ParseErrorKind::BlockInvalidContinuationPlacement
564            } else {
565                block_kind.expected_branch_error()
566            };
567            errors.push(ParseError {
568                kind,
569                start,
570                end: start,
571            });
572            return;
573        }
574        previous = Some(pending);
575    }
576
577    let mut has_end = false;
578    let mut has_specific_error = false;
579
580    // Pre-check: any body child with parse errors (suppresses generic block_unclosed later)
581    let body_has_error = children[body_start..].iter().any(|c| c.has_error());
582
583    // Check ERROR children in the header range for misplaced branches
584    for child in children.iter().take(body_start) {
585        if child.kind() == "ERROR"
586            && let Some(branch_kind) = error_branch_kind(source, *child)
587        {
588            has_specific_error = true;
589            let start = branch_start_in_error(source, *child);
590            let kind = if block_kind.accepts(branch_kind) {
591                innermost_unclosed_block_kind(source, *child)
592                    .map(BlockKind::expected_branch_error)
593                    .or_else(|| scope_aware_branch_error(source, block_kind, branch_kind, None))
594                    .unwrap_or(ParseErrorKind::BlockInvalidContinuationPlacement)
595            } else {
596                block_kind.expected_branch_error()
597            };
598            errors.push(ParseError {
599                kind,
600                start,
601                end: start,
602            });
603        }
604    }
605
606    for child in children.into_iter().skip(body_start) {
607        match child.kind() {
608            "text" | "entity"
609                if text_for_node(source, child)
610                    .chars()
611                    .all(char::is_whitespace) => {}
612            "comment" => {}
613            "block_end" => {
614                has_end = true;
615                break;
616            }
617            "else_if_clause" | "else_clause" => {
618                let branch_kind = match child.kind() {
619                    "else_if_clause" => BlockBranchKind::ElseIf,
620                    _ => BlockBranchKind::Else,
621                };
622                let kind = scope_aware_branch_error(source, block_kind, branch_kind, previous);
623
624                if let Some(kind) = kind {
625                    has_specific_error = true;
626                    errors.push(ParseError {
627                        kind,
628                        start: child.start_byte().saturating_add(1),
629                        end: child.start_byte().saturating_add(1),
630                    });
631                }
632            }
633            "await_branch" => {
634                let branch_kind = find_first_named_child(child, "branch_kind")
635                    .and_then(|n| n.utf8_text(source.as_bytes()).ok())
636                    .and_then(|s| s.trim().parse::<BlockBranchKind>().ok());
637                if let Some(branch_kind) = branch_kind {
638                    let kind = scope_aware_branch_error(source, block_kind, branch_kind, previous);
639                    if let Some(kind) = kind {
640                        has_specific_error = true;
641                        errors.push(ParseError {
642                            kind,
643                            start: child.start_byte().saturating_add(1),
644                            end: child.start_byte().saturating_add(1),
645                        });
646                    }
647                }
648            }
649            "orphan_branch" => {
650                if let Some(branch_kind) = branch_kind_from_node(source, child) {
651                    let kind = scope_aware_branch_error(source, block_kind, branch_kind, previous)
652                        .unwrap_or_else(|| {
653                            if block_kind.accepts(branch_kind) {
654                                ParseErrorKind::BlockInvalidContinuationPlacement
655                            } else {
656                                block_kind.expected_branch_error()
657                            }
658                        });
659                    has_specific_error = true;
660                    let start = branch_start(child);
661                    errors.push(ParseError {
662                        kind,
663                        start,
664                        end: start,
665                    });
666                }
667            }
668            "ERROR" => {
669                if let Some(branch_kind) = error_branch_kind(source, child) {
670                    has_specific_error = true;
671                    let start = branch_start_in_error(source, child);
672                    let kind = if block_kind.accepts(branch_kind) {
673                        innermost_unclosed_block_kind(source, child)
674                            .map(BlockKind::expected_branch_error)
675                            .or_else(|| {
676                                scope_aware_branch_error(source, block_kind, branch_kind, previous)
677                            })
678                            .unwrap_or(ParseErrorKind::BlockInvalidContinuationPlacement)
679                    } else {
680                        block_kind.expected_branch_error()
681                    };
682                    errors.push(ParseError {
683                        kind,
684                        start,
685                        end: start,
686                    });
687                }
688                previous = Some(child);
689            }
690            _ => previous = Some(child),
691        }
692    }
693
694    if !has_end && !has_specific_error {
695        // Check for missing } in block start (ERROR between header and closing brace)
696        if let Some(pos) = missing_brace_in_block_start(block) {
697            errors.push(ParseError {
698                kind: ParseErrorKind::ExpectedTokenRightBrace,
699                start: pos,
700                end: pos,
701            });
702            return;
703        }
704
705        // If a body child has parse errors, the real error is in the child — suppress
706        // generic block_unclosed so the child's specific error takes priority.
707        if body_has_error {
708            return;
709        }
710
711        if let Some((branch, branch_pos)) = next_branch_after_node(source, block) {
712            if !block_kind.accepts(branch) {
713                errors.push(ParseError {
714                    kind: block_kind.expected_branch_error(),
715                    start: branch_pos,
716                    end: branch_pos,
717                });
718            }
719            // Valid or not, don't report generic block_unclosed — the block's
720            // continuation exists but couldn't be included due to a deeper error.
721            return;
722        }
723
724        errors.push(ParseError {
725            kind: ParseErrorKind::BlockUnclosed,
726            start: block.start_byte(),
727            end: block.start_byte().saturating_add(1),
728        });
729    }
730}
731
732/// Determine the error kind when a branch appears after potentially unclosed content.
733/// If the previous node contains an unclosed inner block, report that block's expected
734/// branch error instead of a generic continuation placement error.
735fn scope_aware_branch_error(
736    source: &str,
737    block_kind: BlockKind,
738    branch_kind: BlockBranchKind,
739    previous: Option<TsNode<'_>>,
740) -> Option<ParseErrorKind> {
741    if block_kind.accepts(branch_kind) {
742        if let Some(prev) = previous
743            && node_leaves_scope_open(source, prev)
744        {
745            innermost_unclosed_block_kind(source, prev)
746                .map(|ik| ik.expected_branch_error())
747                .or(Some(ParseErrorKind::BlockInvalidContinuationPlacement))
748        } else {
749            None
750        }
751    } else {
752        Some(block_kind.expected_branch_error())
753    }
754}
755
756/// Find the innermost unclosed typed block within a node, recursing through
757/// section containers (await_pending, await_branch_children).
758fn innermost_unclosed_block_kind(source: &str, node: TsNode<'_>) -> Option<BlockKind> {
759    if is_typed_block_kind(node.kind()) && !has_named_descendant(node, "block_end") {
760        return BlockKind::from_node_kind(node.kind());
761    }
762    match node.kind() {
763        "await_pending" | "await_branch_children" => last_significant_child(source, node)
764            .and_then(|child| innermost_unclosed_block_kind(source, child)),
765        _ => None,
766    }
767}
768
769fn branch_kind_from_node(source: &str, node: TsNode<'_>) -> Option<BlockBranchKind> {
770    match node.kind() {
771        "else_if_clause" => Some(BlockBranchKind::ElseIf),
772        "else_clause" => Some(BlockBranchKind::Else),
773        "orphan_branch" => node
774            .child_by_field_name("kind")
775            .and_then(|kind| kind.utf8_text(source.as_bytes()).ok())
776            .and_then(|text| text.trim().parse().ok()),
777        "await_branch" => find_first_named_child(node, "branch_kind")
778            .and_then(|kind| kind.utf8_text(source.as_bytes()).ok())
779            .and_then(|text| text.trim().parse().ok()),
780        "branch_kind" | "shorthand_kind" => node
781            .utf8_text(source.as_bytes())
782            .ok()
783            .and_then(|text| text.trim().parse().ok()),
784        _ => None,
785    }
786}
787
788fn branch_start(node: TsNode<'_>) -> usize {
789    node.start_byte().saturating_add(1)
790}
791
792/// Check if a typed block's start has an ERROR between header fields and the
793/// closing `}`, indicating a missing `}` in the block start syntax.
794fn missing_brace_in_block_start(block: TsNode<'_>) -> Option<usize> {
795    let mut cursor = block.walk();
796    let all: Vec<_> = block.children(&mut cursor).collect();
797    for (i, child) in all.iter().enumerate() {
798        if child.kind() == "ERROR"
799            && child.is_named()
800            && all[i + 1..]
801                .iter()
802                .any(|next| !next.is_named() && next.kind() == "}")
803        {
804            return Some(child.start_byte());
805        }
806    }
807    None
808}
809
810fn parse_error_from_error_node(source: &str, error: TsNode<'_>) -> Option<ParseError> {
811    // Check for ERROR containing a valid branch structure with an invalid inner branch.
812    // E.g., {:then bar}\n{:else if} inside an await context — the {:else if} is invalid.
813    {
814        let mut cursor = error.walk();
815        let named_children = error.named_children(&mut cursor).collect::<Vec<_>>();
816        for (index, child) in named_children.iter().copied().enumerate() {
817            let context_kind = match child.kind() {
818                "await_branch" => Some(BlockKind::Await),
819                "else_clause" | "else_if_clause" => Some(BlockKind::If),
820                _ => None,
821            };
822            if let Some(context_kind) = context_kind {
823                for next in named_children.iter().copied().skip(index + 1) {
824                    if let Some(inner_branch) = branch_kind_from_node(source, next) {
825                        if !context_kind.accepts(inner_branch) {
826                            let pos = branch_start(next);
827                            return Some(ParseError {
828                                kind: context_kind.expected_branch_error(),
829                                start: pos,
830                                end: pos,
831                            });
832                        }
833                        break;
834                    }
835                }
836            }
837        }
838    }
839
840    // Check for typed blocks inside ERROR (unclosed block recovery)
841    {
842        let mut cursor = error.walk();
843        if let Some(typed_block) = error
844            .named_children(&mut cursor)
845            .find(|c| is_typed_block_kind(c.kind()))
846            && !has_named_descendant(typed_block, "block_end")
847        {
848            if let Some((branch_kind, start)) = next_branch_after_node(source, typed_block) {
849                let block_kind = BlockKind::from_node_kind(typed_block.kind())
850                    .expect("typed block should map to BlockKind");
851                if !block_kind.accepts(branch_kind) {
852                    return Some(ParseError {
853                        kind: block_kind.expected_branch_error(),
854                        start,
855                        end: start,
856                    });
857                }
858            }
859            return Some(ParseError {
860                kind: ParseErrorKind::BlockUnclosed,
861                start: typed_block.start_byte(),
862                end: typed_block.start_byte().saturating_add(1),
863            });
864        }
865    }
866
867    if let Some(name) = raw_text_error_name(source, error) {
868        let raw_text = find_direct_named_child(error, "raw_text");
869        let kind = match name.as_ref() {
870            "script" if raw_text.is_some_and(|node| node.start_byte() == node.end_byte()) => {
871                ParseErrorKind::UnexpectedEof
872            }
873            "script" => ParseErrorKind::ElementUnclosed { name },
874            "style" if raw_text.is_some_and(|node| node.start_byte() == node.end_byte()) => {
875                ParseErrorKind::ExpectedTokenStyleClose
876            }
877            "style" => ParseErrorKind::CssExpectedIdentifier,
878            _ => {
879                let start_tag = find_direct_named_child(error, "start_tag")?;
880                return Some(ParseError {
881                    kind: ParseErrorKind::ElementUnclosed { name },
882                    start: start_tag.start_byte(),
883                    end: start_tag.start_byte().saturating_add(1),
884                });
885            }
886        };
887
888        let (start, end) = match kind {
889            ParseErrorKind::CssExpectedIdentifier => raw_text
890                .map(|node| (node.start_byte(), node.start_byte()))
891                .unwrap_or((error.end_byte(), error.end_byte())),
892            _ => (error.end_byte(), error.end_byte()),
893        };
894
895        return Some(ParseError { kind, start, end });
896    }
897
898    if let Some(start_tag) = find_direct_named_child(error, "start_tag")
899        && find_direct_named_child(error, "end_tag").is_none()
900        && find_direct_named_child(error, "self_closing_tag").is_none()
901    {
902        let tag_name = find_direct_named_child(start_tag, "tag_name")?;
903        return Some(ParseError {
904            kind: ParseErrorKind::ElementUnclosed {
905                name: text_for_node(source, tag_name),
906            },
907            start: start_tag.start_byte(),
908            end: start_tag.start_byte().saturating_add(1),
909        });
910    }
911
912    if let Some(tag_name) = invalid_closing_tag_name(source, error) {
913        return Some(ParseError {
914            kind: ParseErrorKind::ElementInvalidClosingTag { name: tag_name },
915            start: error.start_byte(),
916            end: error.start_byte(),
917        });
918    }
919
920    let raw = source
921        .get(error.start_byte()..error.end_byte())
922        .unwrap_or_default();
923    if raw.starts_with("<!--") {
924        return Some(ParseError {
925            kind: ParseErrorKind::ExpectedTokenCommentClose,
926            start: error.end_byte(),
927            end: error.end_byte(),
928        });
929    }
930
931    if error_branch_kind(source, error).is_some() {
932        return Some(ParseError {
933            kind: ParseErrorKind::BlockInvalidContinuationPlacement,
934            start: error.start_byte().saturating_add(1),
935            end: error.start_byte().saturating_add(1),
936        });
937    }
938
939    if raw == "<" {
940        return Some(ParseError {
941            kind: ParseErrorKind::UnexpectedEof,
942            start: error.end_byte(),
943            end: error.end_byte(),
944        });
945    }
946
947    None
948}
949
950fn parse_error_from_non_error_node(source: &str, node: TsNode<'_>) -> Option<ParseError> {
951    if let Some(error) = malformed_special_tag_whitespace_error(source, node) {
952        return Some(error);
953    }
954
955    if let Some(error) = malformed_block_whitespace_error(node) {
956        return Some(error);
957    }
958
959    if let Some(error) = special_tag_expression_parse_error(source, node) {
960        return Some(error);
961    }
962
963    if node.kind() == "erroneous_end_tag"
964        && let Some(name) = erroneous_end_tag_name(source, node)
965    {
966        if let Some(reason) = autoclosed_by(source, node, name.as_ref()) {
967            return Some(ParseError {
968                kind: ParseErrorKind::ElementInvalidClosingTagAutoclosed { name, reason },
969                start: node.start_byte(),
970                end: node.start_byte(),
971            });
972        }
973        return Some(ParseError {
974            kind: ParseErrorKind::ElementInvalidClosingTag { name },
975            start: node.start_byte(),
976            end: node.start_byte(),
977        });
978    }
979
980    if node.kind() == "expression"
981        && let Some(missing) = find_missing(node, "}")
982    {
983        let start = missing_right_brace_pos(node).unwrap_or_else(|| missing.start_byte());
984        return Some(ParseError {
985            kind: ParseErrorKind::ExpectedTokenRightBrace,
986            start,
987            end: start,
988        });
989    }
990
991    if node.kind() == "expression" && is_attribute_placement_expression(source, node) {
992        return None;
993    }
994
995    if node.kind() == "orphan_branch" {
996        let start = branch_start(node);
997        return Some(ParseError {
998            kind: ParseErrorKind::BlockInvalidContinuationPlacement,
999            start,
1000            end: start,
1001        });
1002    }
1003
1004    if node.kind() == "expression"
1005        && let Some((start, message)) = parse_modern_expression_error(source, node)
1006    {
1007        // Don't report JS parse errors for expressions that look like block ends
1008        // (e.g., {/if} misidentified as expression when block structure is broken).
1009        let raw = source
1010            .get(node.start_byte()..node.end_byte())
1011            .unwrap_or_default();
1012        if raw.starts_with("{/") && raw.ends_with('}') {
1013            let inner = &raw[2..raw.len() - 1];
1014            if matches!(inner, "if" | "each" | "await" | "key" | "snippet") {
1015                return None;
1016            }
1017        }
1018        return Some(ParseError {
1019            kind: ParseErrorKind::JsParseError { message },
1020            start,
1021            end: start,
1022        });
1023    }
1024
1025    if node.kind() == "self_closing_tag"
1026        && node.end_byte() == source.len()
1027        && find_missing(node, "/>").is_some()
1028    {
1029        return Some(ParseError {
1030            kind: ParseErrorKind::UnexpectedEof,
1031            start: source.len(),
1032            end: source.len(),
1033        });
1034    }
1035
1036    if node.kind() == "element"
1037        && let Some(start_tag) = find_direct_named_child(node, "start_tag")
1038    {
1039        let raw = source
1040            .get(start_tag.start_byte()..start_tag.end_byte())
1041            .unwrap_or_default();
1042        if !raw.contains('>') && start_tag.end_byte() == source.len() {
1043            return Some(ParseError {
1044                kind: ParseErrorKind::UnexpectedEof,
1045                start: source.len(),
1046                end: source.len(),
1047            });
1048        }
1049
1050        // Element with start_tag but no end_tag at document EOF → unclosed element
1051        if find_direct_named_child(node, "end_tag").is_none()
1052            && find_direct_named_child(node, "self_closing_tag").is_none()
1053            && node.end_byte() == source.len()
1054            && let Some(tag_name) = find_direct_named_child(start_tag, "tag_name")
1055        {
1056            let name = text_for_node(source, tag_name);
1057            if !is_void_element_name(name.as_ref()) {
1058                return Some(ParseError {
1059                    kind: ParseErrorKind::ElementUnclosed { name },
1060                    start: start_tag.start_byte(),
1061                    end: start_tag.start_byte().saturating_add(1),
1062                });
1063            }
1064        }
1065    }
1066
1067    None
1068}
1069
1070fn malformed_special_tag_whitespace_error(_source: &str, node: TsNode<'_>) -> Option<ParseError> {
1071    if !is_typed_tag_kind(node.kind()) {
1072        return None;
1073    }
1074
1075    // Typed tags with missing whitespace between keyword and expression parse as
1076    // e.g. html_tag(ERROR(...)) with no expression field. Detect this pattern.
1077    let has_expression = node.child_by_field_name("expression").is_some();
1078    if has_expression {
1079        return None;
1080    }
1081
1082    let has_error = find_direct_named_child(node, "ERROR").is_some();
1083    if !has_error {
1084        return None;
1085    }
1086
1087    // The keyword length: html=4, debug=5, const=5, render=6, attach=6
1088    let keyword_len = match node.kind() {
1089        "html_tag" => 4,
1090        "debug_tag" => 5,
1091        "const_tag" => 5,
1092        "render_tag" => 6,
1093        "attach_tag" => 6,
1094        _ => return None,
1095    };
1096
1097    // Position right after {@ + keyword
1098    let start = node.start_byte() + 2 + keyword_len;
1099    Some(ParseError {
1100        kind: ParseErrorKind::ExpectedWhitespace,
1101        start,
1102        end: start,
1103    })
1104}
1105
1106fn malformed_block_whitespace_error(node: TsNode<'_>) -> Option<ParseError> {
1107    if node.kind() != "malformed_block" {
1108        return None;
1109    }
1110
1111    let sigil = node.child_by_field_name("kind")?;
1112    Some(ParseError {
1113        kind: ParseErrorKind::BlockUnexpectedCharacter,
1114        start: node.start_byte(),
1115        end: sigil.end_byte(),
1116    })
1117}
1118
1119fn special_tag_expression_parse_error(source: &str, node: TsNode<'_>) -> Option<ParseError> {
1120    if !is_typed_tag_kind(node.kind()) || is_attribute_value_tag(node) {
1121        return None;
1122    }
1123
1124    let expr = special_tag_expression_node(node)?;
1125    let (start, message) = parse_modern_expression_error(source, expr)?;
1126    Some(ParseError {
1127        kind: ParseErrorKind::JsParseError { message },
1128        start,
1129        end: start,
1130    })
1131}
1132
1133fn is_attribute_value_tag(mut node: TsNode<'_>) -> bool {
1134    while let Some(parent) = node.parent() {
1135        match parent.kind() {
1136            "quoted_attribute_value" | "unquoted_attribute_value" => return true,
1137            "attribute" | "start_tag" | "self_closing_tag" | "element" | "document" => {
1138                return false;
1139            }
1140            _ => node = parent,
1141        }
1142    }
1143    false
1144}
1145
1146fn is_attribute_placement_expression(_source: &str, node: TsNode<'_>) -> bool {
1147    if !is_attribute_value_expression(node) {
1148        return false;
1149    }
1150
1151    if !node.has_error() {
1152        return false;
1153    }
1154
1155    let mut cursor = node.walk();
1156    for child in node.named_children(&mut cursor) {
1157        if child.kind() != "ERROR" {
1158            continue;
1159        }
1160
1161        let mut error_cursor = child.walk();
1162        if child.named_children(&mut error_cursor).any(|error_child| {
1163            is_typed_block_kind(error_child.kind()) || is_typed_tag_kind(error_child.kind())
1164        }) {
1165            return true;
1166        }
1167    }
1168
1169    false
1170}
1171
1172fn is_attribute_value_expression(mut node: TsNode<'_>) -> bool {
1173    while let Some(parent) = node.parent() {
1174        match parent.kind() {
1175            "quoted_attribute_value" | "unquoted_attribute_value" => return true,
1176            "attribute" | "start_tag" | "self_closing_tag" | "element" | "document" => {
1177                return false;
1178            }
1179            _ => node = parent,
1180        }
1181    }
1182    false
1183}
1184
1185fn missing_right_brace_pos(node: TsNode<'_>) -> Option<usize> {
1186    let mut current = node;
1187    while let Some(parent) = current.parent() {
1188        if parent.kind() == "self_closing_tag" {
1189            return Some(parent.end_byte().saturating_sub(1));
1190        }
1191        if matches!(parent.kind(), "element" | "document") {
1192            break;
1193        }
1194        current = parent;
1195    }
1196    None
1197}
1198
1199fn autoclosed_by(source: &str, node: TsNode<'_>, name: &str) -> Option<Arc<str>> {
1200    let reason = previous_significant_sibling(source, node)?;
1201    if reason.kind() != "element" {
1202        return None;
1203    }
1204    let reason_name = cst_element_name(source, reason)?;
1205    let current = previous_significant_sibling(source, reason)?;
1206    if current.kind() != "element" || has_cst_end_tag(current) {
1207        return None;
1208    }
1209    let current_name = cst_element_name(source, current)?;
1210    if current_name.as_ref() != name || !closing_tag_omitted(name, reason_name.as_ref()) {
1211        return None;
1212    }
1213    Some(reason_name)
1214}
1215
1216fn previous_significant_sibling<'tree>(source: &str, node: TsNode<'tree>) -> Option<TsNode<'tree>> {
1217    let mut current = node.prev_named_sibling();
1218    while let Some(sibling) = current {
1219        match sibling.kind() {
1220            "comment" => current = sibling.prev_named_sibling(),
1221            "text" | "entity" => {
1222                if text_for_node(source, sibling)
1223                    .chars()
1224                    .all(char::is_whitespace)
1225                {
1226                    current = sibling.prev_named_sibling();
1227                    continue;
1228                }
1229                return Some(sibling);
1230            }
1231            _ => return Some(sibling),
1232        }
1233    }
1234    None
1235}
1236
1237fn cst_element_name(source: &str, node: TsNode<'_>) -> Option<Arc<str>> {
1238    let start_tag = find_direct_named_child(node, "start_tag")
1239        .or_else(|| find_direct_named_child(node, "self_closing_tag"))?;
1240    let tag_name = find_direct_named_child(start_tag, "tag_name")?;
1241    Some(text_for_node(source, tag_name))
1242}
1243
1244fn has_cst_end_tag(node: TsNode<'_>) -> bool {
1245    find_direct_named_child(node, "end_tag").is_some()
1246}
1247
1248fn closing_tag_omitted(current: &str, next: &str) -> bool {
1249    matches!(
1250        (current, next),
1251        ("li", "li")
1252            | ("dt", "dt" | "dd")
1253            | ("dd", "dt" | "dd")
1254            | (
1255                "p",
1256                "address"
1257                    | "article"
1258                    | "aside"
1259                    | "blockquote"
1260                    | "div"
1261                    | "dl"
1262                    | "fieldset"
1263                    | "footer"
1264                    | "form"
1265                    | "h1"
1266                    | "h2"
1267                    | "h3"
1268                    | "h4"
1269                    | "h5"
1270                    | "h6"
1271                    | "header"
1272                    | "hgroup"
1273                    | "hr"
1274                    | "main"
1275                    | "menu"
1276                    | "nav"
1277                    | "ol"
1278                    | "p"
1279                    | "pre"
1280                    | "section"
1281                    | "table"
1282                    | "ul"
1283            )
1284            | ("rt", "rt" | "rp")
1285            | ("rp", "rt" | "rp")
1286            | ("optgroup", "optgroup")
1287            | ("option", "option" | "optgroup")
1288            | ("thead", "tbody" | "tfoot")
1289            | ("tbody", "tbody" | "tfoot")
1290            | ("tfoot", "tbody")
1291            | ("tr", "tr" | "tbody")
1292            | ("td", "td" | "th" | "tr")
1293            | ("th", "td" | "th" | "tr")
1294    )
1295}
1296
1297fn error_branch_kind(source: &str, node: TsNode<'_>) -> Option<BlockBranchKind> {
1298    let children = named_children_vec(node);
1299
1300    for child in &children {
1301        if let Some(kind) = branch_kind_from_node(source, *child) {
1302            return Some(kind);
1303        }
1304    }
1305
1306    if children.len() == 1 && children[0].kind() == "ERROR" {
1307        return error_branch_kind(source, children[0]);
1308    }
1309
1310    None
1311}
1312
1313fn branch_start_in_error(source: &str, node: TsNode<'_>) -> usize {
1314    let children = named_children_vec(node);
1315    for child in &children {
1316        if branch_kind_from_node(source, *child).is_some() {
1317            return branch_start(*child);
1318        }
1319    }
1320
1321    if children.len() == 1 && children[0].kind() == "ERROR" {
1322        return branch_start_in_error(source, children[0]);
1323    }
1324
1325    node.start_byte().saturating_add(1)
1326}
1327
1328fn next_branch_after_node(source: &str, node: TsNode<'_>) -> Option<(BlockBranchKind, usize)> {
1329    let mut current = node.next_named_sibling();
1330    while let Some(sibling) = current {
1331        match sibling.kind() {
1332            "comment" => current = sibling.next_named_sibling(),
1333            "text" | "entity"
1334                if text_for_node(source, sibling)
1335                    .chars()
1336                    .all(char::is_whitespace) =>
1337            {
1338                current = sibling.next_named_sibling();
1339            }
1340            "ERROR" => {
1341                let kind = error_branch_kind(source, sibling)?;
1342                return Some((kind, branch_start_in_error(source, sibling)));
1343            }
1344            _ => {
1345                let kind = branch_kind_from_node(source, sibling)?;
1346                return Some((kind, branch_start(sibling)));
1347            }
1348        }
1349    }
1350
1351    let parent = node.parent()?;
1352    match parent.kind() {
1353        "await_pending" | "await_branch_children" => next_branch_after_node(source, parent),
1354        _ => None,
1355    }
1356}
1357
1358fn branch_in_section_container(source: &str, node: TsNode<'_>) -> Option<(BlockBranchKind, usize)> {
1359    let child = last_significant_child(source, node)?;
1360    match child.kind() {
1361        "ERROR" => {
1362            let kind = error_branch_kind(source, child)?;
1363            Some((kind, branch_start_in_error(source, child)))
1364        }
1365        _ => {
1366            let kind = branch_kind_from_node(source, child)?;
1367            Some((kind, branch_start(child)))
1368        }
1369    }
1370}
1371
1372fn node_leaves_scope_open(source: &str, node: TsNode<'_>) -> bool {
1373    match node.kind() {
1374        "start_tag" => true,
1375        "element" => {
1376            !has_named_descendant(node, "end_tag")
1377                && !has_named_descendant(node, "self_closing_tag")
1378        }
1379        kind if is_typed_block_kind(kind) => !has_named_descendant(node, "block_end"),
1380        "await_pending" | "await_branch_children" => last_significant_child(source, node)
1381            .is_some_and(|child| node_leaves_scope_open(source, child)),
1382        "ERROR" => true,
1383        _ => false,
1384    }
1385}
1386
1387fn has_named_descendant(node: TsNode<'_>, kind: &str) -> bool {
1388    let mut cursor = node.walk();
1389    node.named_children(&mut cursor)
1390        .any(|child| child.kind() == kind || has_named_descendant(child, kind))
1391}
1392
1393fn has_typed_block_descendant(node: TsNode<'_>) -> bool {
1394    let mut cursor = node.walk();
1395    node.named_children(&mut cursor)
1396        .any(|child| is_typed_block_kind(child.kind()) || has_typed_block_descendant(child))
1397}
1398
1399fn find_missing<'a>(node: TsNode<'a>, kind: &str) -> Option<TsNode<'a>> {
1400    for index in 0..node.child_count() {
1401        let child = node.child(index as u32)?;
1402        if child.is_missing() && child.kind() == kind {
1403            return Some(child);
1404        }
1405        if let Some(found) = find_missing(child, kind) {
1406            return Some(found);
1407        }
1408    }
1409    None
1410}
1411
1412fn invalid_closing_tag_name(source: &str, node: TsNode<'_>) -> Option<Arc<str>> {
1413    if has_named_descendant(node, "start_tag")
1414        || has_named_descendant(node, "self_closing_tag")
1415        || has_typed_block_descendant(node)
1416    {
1417        return None;
1418    }
1419
1420    find_descendant(node, |child| child.kind() == "tag_name").map(|tag| text_for_node(source, tag))
1421}
1422
1423fn erroneous_end_tag_name(source: &str, node: TsNode<'_>) -> Option<Arc<str>> {
1424    find_descendant(node, |child| child.kind() == "erroneous_end_tag_name")
1425        .map(|name| text_for_node(source, name))
1426}
1427
1428fn find_descendant<'a, F>(node: TsNode<'a>, matches: F) -> Option<TsNode<'a>>
1429where
1430    F: Fn(TsNode<'a>) -> bool + Copy,
1431{
1432    if matches(node) {
1433        return Some(node);
1434    }
1435
1436    for index in 0..node.child_count() {
1437        let Some(child) = node.child(index as u32) else {
1438            continue;
1439        };
1440        if let Some(found) = find_descendant(child, matches) {
1441            return Some(found);
1442        }
1443    }
1444
1445    None
1446}
1447
1448fn last_significant_child<'a>(source: &str, node: TsNode<'a>) -> Option<TsNode<'a>> {
1449    named_children_vec(node).into_iter().rev().find(|child| {
1450        !matches!(child.kind(), "comment")
1451            && (!(matches!(child.kind(), "text" | "entity"))
1452                || !text_for_node(source, *child)
1453                    .chars()
1454                    .all(char::is_whitespace))
1455    })
1456}
1457
1458fn raw_text_error_name(source: &str, error: TsNode<'_>) -> Option<Arc<str>> {
1459    let start_tag = find_direct_named_child(error, "start_tag")?;
1460    let name = find_direct_named_child(start_tag, "tag_name")?;
1461    Some(text_for_node(source, name))
1462}
1463
1464fn find_direct_named_child<'a>(node: TsNode<'a>, kind: &str) -> Option<TsNode<'a>> {
1465    let mut cursor = node.walk();
1466    node.named_children(&mut cursor)
1467        .find(|child| child.kind() == kind)
1468}
1469
1470fn parse_modern_text(source: &str, node: TsNode<'_>) -> Text {
1471    let raw = text_for_node(source, node);
1472    let data = Arc::from(decode_html_entities_cow(raw.as_ref()).into_owned());
1473
1474    Text {
1475        start: node.start_byte(),
1476        end: node.end_byte(),
1477        raw,
1478        data,
1479    }
1480}
1481
1482pub(crate) fn parse_modern_comment(source: &str, node: TsNode<'_>) -> Comment {
1483    let raw = text_for_node(source, node);
1484    let data_raw: &str = raw.as_ref();
1485    let data_raw: &str = if let Some(tail) = data_raw.strip_prefix("<!--") {
1486        tail.strip_suffix("-->").unwrap_or(tail)
1487    } else {
1488        data_raw
1489    };
1490    let data: Arc<str> = Arc::from(data_raw);
1491
1492    Comment {
1493        start: node.start_byte(),
1494        end: node.end_byte(),
1495        data,
1496    }
1497}
1498
1499pub(crate) fn push_modern_text_node(nodes: &mut Vec<Node>, text: Text) {
1500    if let Some(Node::Text(last)) = nodes.last_mut()
1501        && last.end == text.start
1502    {
1503        let merged_raw = format!("{}{}", last.raw, text.raw);
1504        let merged_data = format!("{}{}", last.data, text.data);
1505        last.end = text.end;
1506        last.raw = Arc::from(merged_raw);
1507        last.data = Arc::from(merged_data);
1508        return;
1509    }
1510
1511    nodes.push(Node::Text(text));
1512}
1513
1514pub fn modern_node_start(node: &Node) -> usize {
1515    node.start()
1516}
1517
1518pub fn modern_node_end(node: &Node) -> usize {
1519    node.end()
1520}
1521
1522pub(super) fn parse_modern_script(
1523    source: &str,
1524    element: TsNode<'_>,
1525    _leading_comment: Option<&str>,
1526) -> Option<Script> {
1527    let start_tag = find_first_named_child(element, "start_tag")?;
1528    let end_tag = find_first_named_child(element, "end_tag")?;
1529    let attributes = parse_modern_attributes(source, start_tag, false);
1530
1531    let context = attributes
1532        .iter()
1533        .find_map(|attribute| match attribute {
1534            Attribute::Attribute(NamedAttribute { name, value, .. })
1535                if name.as_ref() == "module" =>
1536            {
1537                Some(ScriptContext::Module)
1538            }
1539            Attribute::Attribute(NamedAttribute { name, value, .. })
1540                if name.as_ref() == "context" && modern_attribute_value_is_module(value) =>
1541            {
1542                Some(ScriptContext::Module)
1543            }
1544            _ => None,
1545        })
1546        .unwrap_or(ScriptContext::Default);
1547
1548    let is_ts = attributes.iter().any(|attribute| {
1549        matches!(
1550            attribute,
1551            Attribute::Attribute(NamedAttribute { name, value, .. })
1552                if name.as_ref() == "lang"
1553                    && matches!(
1554                        value,
1555                        AttributeValueList::Values(values)
1556                            if matches!(
1557                                values.first(),
1558                                Some(AttributeValue::Text(Text { data, .. }))
1559                                    if data.as_ref() == "ts"
1560                            )
1561                    )
1562        )
1563    });
1564
1565    let content_start = start_tag.end_byte();
1566    let content_end = end_tag.start_byte();
1567    let content_source = source.get(content_start..content_end).unwrap_or_default();
1568    let content = crate::parse::parse_modern_program_content_with_offsets(
1569        content_source,
1570        content_start,
1571        start_tag.start_position().row + 1,
1572        0,
1573        end_tag.end_position().row + 1,
1574        end_tag.end_position().column,
1575        is_ts,
1576    )
1577    .unwrap_or_else(|| crate::parse::ParsedProgramContent {
1578        parsed: Arc::new(crate::js::ParsedJsProgram::parse(
1579            content_source,
1580            if is_ts {
1581                oxc_span::SourceType::ts().with_module(true)
1582            } else {
1583                oxc_span::SourceType::mjs()
1584            },
1585        )),
1586    });
1587
1588    Some(Script {
1589        r#type: ScriptType::Script,
1590        start: element.start_byte(),
1591        end: element.end_byte(),
1592        content_start,
1593        content_end,
1594        context,
1595        content: content.parsed,
1596        attributes: attributes.into_boxed_slice(),
1597    })
1598}
1599
1600pub(super) fn parse_modern_options(source: &str, element: TsNode<'_>) -> Option<Options> {
1601    let tag_node = find_first_named_child(element, "self_closing_tag")
1602        .or_else(|| find_first_named_child(element, "start_tag"))?;
1603    let attributes = parse_modern_attributes(source, tag_node, false);
1604    let fragment = parse_modern_options_fragment(source, element);
1605
1606    let mut custom_element = None;
1607    let mut runes = None;
1608
1609    for attribute in &attributes {
1610        if let Attribute::Attribute(NamedAttribute {
1611            name,
1612            value,
1613            value_syntax,
1614            ..
1615        }) = attribute
1616        {
1617            if name.as_ref() == "customElement"
1618                && let AttributeValueList::Values(values) = value
1619                && let Some(AttributeValue::Text(Text { data, .. })) = values.first()
1620            {
1621                custom_element = Some(CustomElement { tag: data.clone() });
1622            }
1623
1624            if name.as_ref() == "runes" {
1625                match value_syntax {
1626                    AttributeValueSyntax::Boolean => runes = Some(true),
1627                    _ if matches!(value, AttributeValueList::ExpressionTag(_)) => {
1628                        let AttributeValueList::ExpressionTag(tag) = value else {
1629                            unreachable!("checked expression tag");
1630                        };
1631                        if expression_literal_bool(&tag.expression).is_some() {
1632                            runes = expression_literal_bool(&tag.expression);
1633                        }
1634                    }
1635                    _ => {}
1636                }
1637            }
1638        }
1639    }
1640
1641    Some(Options {
1642        start: element.start_byte(),
1643        end: element.end_byte(),
1644        attributes: attributes.into_boxed_slice(),
1645        fragment,
1646        custom_element,
1647        runes,
1648    })
1649}
1650
1651fn parse_modern_options_fragment(source: &str, element: TsNode<'_>) -> Fragment {
1652    let mut nodes = Vec::new();
1653    let mut cursor = element.walk();
1654
1655    for child in element.named_children(&mut cursor) {
1656        match child.kind() {
1657            "start_tag" | "self_closing_tag" | "end_tag" => {}
1658            "text" | "entity" | "raw_text" => {
1659                push_modern_text_node(&mut nodes, parse_modern_text(source, child));
1660            }
1661            "comment" => nodes.push(Node::Comment(parse_modern_comment(source, child))),
1662            "expression" => {
1663                if let Some(tag) = parse_modern_expression_tag(source, child) {
1664                    nodes.push(Node::ExpressionTag(tag));
1665                }
1666            }
1667            kind if is_typed_tag_kind(kind) => {
1668                if let Some(tag) = parse_modern_tag(source, child) {
1669                    nodes.push(tag);
1670                }
1671            }
1672            kind if is_typed_block_kind(kind) => {
1673                if let Some(block) = parse_modern_block(source, child, None) {
1674                    nodes.push(block);
1675                }
1676            }
1677            "element" => nodes.push(parse_modern_element_node(
1678                source, child, false, false, false, None,
1679            )),
1680            "ERROR" => {
1681                let mut recovered = recover_modern_error_nodes(source, child, false);
1682                nodes.append(&mut recovered);
1683            }
1684            _ => {}
1685        }
1686    }
1687
1688    Fragment {
1689        r#type: FragmentType::Fragment,
1690        nodes: nodes.into_boxed_slice(),
1691    }
1692}
1693
1694pub(super) fn parse_modern_style(source: &str, element: TsNode<'_>) -> Option<Css> {
1695    let start_tag = find_first_named_child(element, "start_tag")?;
1696    let end_tag = find_first_named_child(element, "end_tag");
1697    let content_start = start_tag.end_byte();
1698    let content_end = end_tag
1699        .map(|node: TsNode<'_>| node.start_byte())
1700        .unwrap_or(element.end_byte());
1701    let attributes = parse_modern_attributes(source, start_tag, false).into_boxed_slice();
1702
1703    let children = crate::parse::parse_modern_css_nodes(source, content_start, content_end);
1704
1705    Some(Css {
1706        r#type: CssType::StyleSheet,
1707        start: element.start_byte(),
1708        end: element.end_byte(),
1709        attributes,
1710        children: children.into_boxed_slice(),
1711        content: CssContent {
1712            start: content_start,
1713            end: content_end,
1714            styles: Arc::from(source.get(content_start..content_end).unwrap_or_default()),
1715            comment: None,
1716        },
1717    })
1718}
1719
1720fn modern_attribute_value_is_module(value: &AttributeValueList) -> bool {
1721    match value {
1722        AttributeValueList::Boolean(_) => false,
1723        AttributeValueList::Values(values) => values.iter().any(|value| {
1724            matches!(
1725                value,
1726                AttributeValue::Text(Text { data, .. }) if data.as_ref() == "module"
1727            )
1728        }),
1729        AttributeValueList::ExpressionTag(tag) => {
1730            expression_identifier_name(&tag.expression)
1731                .is_some_and(|name| name.as_ref() == "module")
1732                || expression_literal_string(&tag.expression)
1733                    .is_some_and(|value| value.as_ref() == "module")
1734        }
1735    }
1736}
1737
1738pub(super) fn modern_element_name(source: &str, element: TsNode<'_>) -> Option<Arc<str>> {
1739    let mut cursor = element.walk();
1740    for child in element.named_children(&mut cursor) {
1741        match child.kind() {
1742            "start_tag" | "self_closing_tag" => {
1743                if let Some(tag_name) = find_first_named_child(child, "tag_name") {
1744                    return Some(text_for_node(source, tag_name));
1745                }
1746            }
1747            _ => {}
1748        }
1749    }
1750    None
1751}
1752
1753pub(super) fn recover_modern_error_nodes(
1754    source: &str,
1755    error_node: TsNode<'_>,
1756    in_shadowroot_template: bool,
1757) -> Vec<crate::ast::modern::Node> {
1758    if let Some(block) = recover_malformed_snippet_block(source, error_node) {
1759        return vec![crate::ast::modern::Node::SnippetBlock(block)];
1760    }
1761    let error_children = named_children_vec(error_node);
1762    parse_modern_nodes_slice(source, &error_children, in_shadowroot_template)
1763}
1764
1765fn parse_modern_collapsed_comment_tag_sequence(
1766    source: &str,
1767    node: TsNode<'_>,
1768) -> Option<(Vec<crate::ast::modern::Node>, Vec<RootComment>)> {
1769    if node.kind() != "element" {
1770        return None;
1771    }
1772
1773    let start_tag = find_first_named_child(node, "start_tag")?;
1774    if start_tag.start_byte() != node.start_byte() || start_tag.end_byte() != node.end_byte() {
1775        return None;
1776    }
1777
1778    let raw = text_for_node(source, start_tag);
1779    let raw_ref = raw.as_ref();
1780    if !(raw_ref.contains("//") || raw_ref.contains("/*")) || !raw_ref.contains("</") {
1781        return None;
1782    }
1783
1784    parse_collapsed_tag_sequence_from_text(source, node.start_byte(), raw_ref)
1785}
1786
1787fn parse_collapsed_tag_sequence_from_text(
1788    source: &str,
1789    base: usize,
1790    raw: &str,
1791) -> Option<(Vec<crate::ast::modern::Node>, Vec<RootComment>)> {
1792    let bytes = raw.as_bytes();
1793    let mut index = 0usize;
1794    let mut nodes = Vec::new();
1795    let mut comments = Vec::new();
1796
1797    while index < bytes.len() {
1798        if bytes[index].is_ascii_whitespace() {
1799            let ws_start = index;
1800            while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1801                index += 1;
1802            }
1803            if index > ws_start {
1804                push_modern_text_node(
1805                    &mut nodes,
1806                    Text {
1807                        start: base + ws_start,
1808                        end: base + index,
1809                        raw: Arc::from(&raw[ws_start..index]),
1810                        data: Arc::from(&raw[ws_start..index]),
1811                    },
1812                );
1813            }
1814            continue;
1815        }
1816
1817        if bytes.get(index) != Some(&b'<') {
1818            let text_start = index;
1819            while index < bytes.len() && bytes[index] != b'<' {
1820                index += 1;
1821            }
1822            push_modern_text_node(
1823                &mut nodes,
1824                Text {
1825                    start: base + text_start,
1826                    end: base + index,
1827                    raw: Arc::from(&raw[text_start..index]),
1828                    data: Arc::from(&raw[text_start..index]),
1829                },
1830            );
1831            continue;
1832        }
1833
1834        let tag_start = index;
1835        index += 1;
1836        if bytes.get(index) == Some(&b'/') {
1837            break;
1838        }
1839
1840        let name_start = index;
1841        while index < bytes.len()
1842            && (bytes[index].is_ascii_alphanumeric()
1843                || bytes[index] == b'-'
1844                || bytes[index] == b':')
1845        {
1846            index += 1;
1847        }
1848        if index == name_start {
1849            return None;
1850        }
1851        let name = &raw[name_start..index];
1852
1853        let mut attributes = Vec::new();
1854        loop {
1855            while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1856                index += 1;
1857            }
1858            if index >= bytes.len() {
1859                return None;
1860            }
1861            if bytes[index] == b'>' {
1862                index += 1;
1863                break;
1864            }
1865
1866            if bytes[index] == b'/' && bytes.get(index + 1) == Some(&b'/') {
1867                let comment_start = index;
1868                index += 2;
1869                let value_start = index;
1870                while index < bytes.len() && bytes[index] != b'\n' {
1871                    index += 1;
1872                }
1873                let comment_end = index;
1874                comments.push(modern_root_comment(
1875                    source,
1876                    RootCommentType::Line,
1877                    base + comment_start,
1878                    base + comment_end,
1879                    Arc::from(&raw[value_start..comment_end]),
1880                ));
1881                continue;
1882            }
1883
1884            if bytes[index] == b'/' && bytes.get(index + 1) == Some(&b'*') {
1885                let comment_start = index;
1886                index += 2;
1887                let value_start = index;
1888                let tail = &raw[index..];
1889                let rel_end = tail.find("*/")?;
1890                let value_end = index + rel_end;
1891                index = value_end + 2;
1892                comments.push(modern_root_comment(
1893                    source,
1894                    RootCommentType::Block,
1895                    base + comment_start,
1896                    base + index,
1897                    Arc::from(&raw[value_start..value_end]),
1898                ));
1899                continue;
1900            }
1901
1902            let attr_start = index;
1903            while index < bytes.len()
1904                && !bytes[index].is_ascii_whitespace()
1905                && bytes[index] != b'='
1906                && bytes[index] != b'>'
1907            {
1908                index += 1;
1909            }
1910            if index == attr_start {
1911                return None;
1912            }
1913            let attr_name = &raw[attr_start..index];
1914            let name_loc = modern_name_location(source, base + attr_start, base + index);
1915
1916            while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1917                index += 1;
1918            }
1919
1920            let value = if bytes.get(index) == Some(&b'=') {
1921                index += 1;
1922                while index < bytes.len() && bytes[index].is_ascii_whitespace() {
1923                    index += 1;
1924                }
1925
1926                if let Some(quote) = bytes
1927                    .get(index)
1928                    .copied()
1929                    .filter(|q| *q == b'"' || *q == b'\'')
1930                {
1931                    index += 1;
1932                    let value_start = index;
1933                    while index < bytes.len() && bytes[index] != quote {
1934                        index += 1;
1935                    }
1936                    let value_end = index;
1937                    if bytes.get(index) == Some(&quote) {
1938                        index += 1;
1939                    }
1940
1941                    AttributeValueList::Values(
1942                        vec![AttributeValue::Text(Text {
1943                            start: base + value_start,
1944                            end: base + value_end,
1945                            raw: Arc::from(&raw[value_start..value_end]),
1946                            data: Arc::from(&raw[value_start..value_end]),
1947                        })]
1948                        .into_boxed_slice(),
1949                    )
1950                } else {
1951                    AttributeValueList::Boolean(true)
1952                }
1953            } else {
1954                AttributeValueList::Boolean(true)
1955            };
1956            let value_syntax = match &value {
1957                AttributeValueList::Boolean(_) => AttributeValueSyntax::Boolean,
1958                AttributeValueList::Values(_) | AttributeValueList::ExpressionTag(_) => {
1959                    AttributeValueSyntax::Quoted
1960                }
1961            };
1962
1963            attributes.push(Attribute::Attribute(NamedAttribute {
1964                start: base + attr_start,
1965                end: base + index,
1966                name: Arc::from(attr_name),
1967                name_loc,
1968                value,
1969                value_syntax,
1970                error: None,
1971            }));
1972        }
1973
1974        let close_tag = format!("</{name}>");
1975        let close_rel = raw[index..].find(&close_tag)?;
1976        let close_start = index + close_rel;
1977        let close_end = close_start + close_tag.len();
1978
1979        let name_loc =
1980            modern_name_location(source, base + name_start, base + name_start + name.len());
1981        nodes.push(crate::ast::modern::Node::RegularElement(RegularElement {
1982            start: base + tag_start,
1983            end: base + close_end,
1984            name: Arc::from(name),
1985            name_loc,
1986            self_closing: false,
1987            has_end_tag: true,
1988            attributes: attributes.into_boxed_slice(),
1989            fragment: crate::ast::modern::Fragment {
1990                r#type: crate::ast::modern::FragmentType::Fragment,
1991                nodes: Box::new([]),
1992            },
1993        }));
1994
1995        index = close_end;
1996    }
1997
1998    Some((nodes, comments))
1999}
2000
2001pub(super) fn modern_name_location(source: &str, start: usize, end: usize) -> LegacyNameLocation {
2002    LegacyNameLocation {
2003        start: source_location_at_offset(source, start),
2004        end: source_location_at_offset(source, end),
2005    }
2006}
2007
2008pub(super) fn modern_root_comment(
2009    source: &str,
2010    kind: RootCommentType,
2011    start: usize,
2012    end: usize,
2013    value: Arc<str>,
2014) -> RootComment {
2015    RootComment {
2016        r#type: kind,
2017        start,
2018        end,
2019        value,
2020        loc: LegacyNameLocation {
2021            start: source_location_at_offset(source, start),
2022            end: source_location_at_offset(source, end),
2023        },
2024    }
2025}
2026
2027fn collect_modern_tag_comments(source: &str, root: TsNode<'_>) -> Vec<RootComment> {
2028    let mut out = Vec::new();
2029    let mut stack = vec![root];
2030
2031    while let Some(node) = stack.pop() {
2032        let mut cursor = node.walk();
2033        for child in node.named_children(&mut cursor) {
2034            if child.kind() == "tag_comment"
2035                && let Some(comment) = parse_modern_tag_comment(source, child)
2036            {
2037                out.push(comment);
2038            }
2039            stack.push(child);
2040        }
2041    }
2042
2043    out
2044}
2045
2046fn parse_modern_tag_comment(source: &str, node: TsNode<'_>) -> Option<RootComment> {
2047    let raw = text_for_node(source, node);
2048    let raw_ref = raw.as_ref();
2049
2050    if let Some(value) = raw_ref.strip_prefix("//") {
2051        return Some(modern_root_comment(
2052            source,
2053            RootCommentType::Line,
2054            node.start_byte(),
2055            node.end_byte(),
2056            Arc::from(value),
2057        ));
2058    }
2059
2060    if let Some(tail) = raw_ref.strip_prefix("/*")
2061        && let Some(inner) = tail.strip_suffix("*/")
2062    {
2063        return Some(modern_root_comment(
2064            source,
2065            RootCommentType::Block,
2066            node.start_byte(),
2067            node.end_byte(),
2068            Arc::from(inner),
2069        ));
2070    }
2071
2072    None
2073}
2074
2075fn parse_modern_block(
2076    source: &str,
2077    block: TsNode<'_>,
2078    _hint: Option<&IncrementalHint<'_>>,
2079) -> Option<Node> {
2080    // TODO: Thread hint into individual block parsers for fragment reuse.
2081    match block.kind() {
2082        "if_block" => parse_modern_if_block(source, block).map(Node::IfBlock),
2083        "each_block" => parse_modern_each_block(source, block).map(Node::EachBlock),
2084        "key_block" => parse_modern_key_block(source, block).map(Node::KeyBlock),
2085        "await_block" => parse_modern_await_block(source, block).map(Node::AwaitBlock),
2086        "snippet_block" => parse_modern_snippet_block(source, block).map(Node::SnippetBlock),
2087        _ => None,
2088    }
2089}
2090
2091fn parse_modern_tag(source: &str, tag: TsNode<'_>) -> Option<Node> {
2092    match tag.kind() {
2093        "render_tag" => Some(Node::RenderTag(RenderTag {
2094            start: tag.start_byte(),
2095            end: tag.end_byte(),
2096            expression: parse_special_tag_expression(source, tag)?,
2097        })),
2098        "html_tag" => Some(Node::HtmlTag(HtmlTag {
2099            start: tag.start_byte(),
2100            end: tag.end_byte(),
2101            expression: parse_special_tag_expression(source, tag)?,
2102        })),
2103        "const_tag" => Some(Node::ConstTag(ConstTag {
2104            start: tag.start_byte(),
2105            end: tag.end_byte(),
2106            declaration: parse_const_tag_declaration(source, tag)
2107                .or_else(|| parse_special_tag_expression(source, tag))?,
2108        })),
2109        "debug_tag" => {
2110            let arguments = parse_modern_debug_tag_arguments(source, tag);
2111            let identifiers = debug_tag_identifiers(&arguments);
2112            Some(Node::DebugTag(DebugTag {
2113                start: tag.start_byte(),
2114                end: tag.end_byte(),
2115                arguments,
2116                identifiers,
2117            }))
2118        }
2119        _ => None,
2120    }
2121}
2122
2123fn special_tag_expression_node(tag: TsNode<'_>) -> Option<TsNode<'_>> {
2124    find_first_named_child(tag, "expression_value")
2125        .or_else(|| find_first_named_child(tag, "expression"))
2126}
2127
2128fn parse_special_tag_expression(source: &str, tag: TsNode<'_>) -> Option<Expression> {
2129    special_tag_expression_node(tag).and_then(|node| parse_modern_expression_field(source, node))
2130}
2131
2132fn parse_const_tag_declaration(source: &str, tag: TsNode<'_>) -> Option<Expression> {
2133    if tag.kind() != "const_tag" || tag.end_byte() <= tag.start_byte() + 3 {
2134        return None;
2135    }
2136
2137    let declaration_source = source.get(tag.start_byte() + 2..tag.end_byte().saturating_sub(1))?;
2138    let program = crate::parse::parse_modern_program_content_with_offsets(
2139        declaration_source,
2140        tag.start_byte() + 2,
2141        tag.start_position().row + 1,
2142        tag.start_position().column + 2,
2143        tag.end_position().row + 1,
2144        tag.end_position().column.saturating_sub(1),
2145        true,
2146    )?;
2147
2148    let [declaration] = program.parsed.program().body.as_slice() else {
2149        return None;
2150    };
2151
2152    let span = declaration.span();
2153    Some(Expression::from_statement(
2154        program.parsed,
2155        0,
2156        tag.start_byte() + 2 + span.start as usize,
2157        tag.start_byte() + 2 + span.end as usize,
2158    ))
2159}
2160
2161fn parse_modern_debug_tag_arguments(source: &str, tag: TsNode<'_>) -> Box<[Expression]> {
2162    let expr_node = special_tag_expression_node(tag);
2163    let Some(expr_node) = expr_node else {
2164        return Box::new([]);
2165    };
2166
2167    parse_modern_expression_field(source, expr_node)
2168        .map(split_debug_tag_arguments)
2169        .unwrap_or_default()
2170}
2171
2172pub(crate) fn split_debug_tag_arguments(expression: Expression) -> Box<[Expression]> {
2173    crate::parse::oxc_query::split_debug_tag_arguments(expression)
2174}
2175
2176fn debug_tag_identifiers(arguments: &[Expression]) -> Box<[Identifier]> {
2177    arguments
2178        .iter()
2179        .filter_map(|argument| modern_identifier_from_expression(argument.clone()))
2180        .collect::<Vec<_>>()
2181        .into_boxed_slice()
2182}
2183
2184fn modern_identifier_from_expression(expression: Expression) -> Option<Identifier> {
2185    let name = crate::parse::oxc_query::expression_identifier_name(&expression)?;
2186    Some(Identifier {
2187        start: expression.start,
2188        end: expression.end,
2189        loc: None,
2190        name,
2191    })
2192}
2193
2194fn parse_modern_if_block(source: &str, block: TsNode<'_>) -> Option<IfBlock> {
2195    let children = named_children_vec(block);
2196
2197    let test_expr = block
2198        .child_by_field_name("expression")
2199        .map(|node| parse_modern_expression_field_or_empty(source, node))
2200        .unwrap_or_else(|| modern_empty_identifier_at_block_tag_end(block));
2201
2202    let end_idx = children
2203        .iter()
2204        .rposition(|c| c.kind() == "block_end")
2205        .unwrap_or(children.len());
2206    let body_start = body_start_index(block, &children, &["expression"]);
2207    let branch_indices: Vec<usize> = children
2208        .iter()
2209        .enumerate()
2210        .filter_map(|(idx, node)| {
2211            matches!(node.kind(), "else_if_clause" | "else_clause").then_some(idx)
2212        })
2213        .collect();
2214
2215    let consequent_end = branch_indices.first().copied().unwrap_or(end_idx);
2216    let consequent = Fragment {
2217        r#type: FragmentType::Fragment,
2218        nodes: parse_modern_nodes_slice(source, &children[body_start..consequent_end], false)
2219            .into_boxed_slice(),
2220    };
2221
2222    let alternate = if branch_indices.is_empty() {
2223        None
2224    } else {
2225        parse_modern_alternate(source, &children, &branch_indices, 0, end_idx).map(Box::new)
2226    };
2227
2228    Some(IfBlock {
2229        elseif: false,
2230        start: block.start_byte(),
2231        end: block.end_byte(),
2232        test: test_expr,
2233        consequent,
2234        alternate,
2235    })
2236}
2237
2238fn parse_modern_each_block(source: &str, block: TsNode<'_>) -> Option<EachBlock> {
2239    let children = named_children_vec(block);
2240    let end_idx = children
2241        .iter()
2242        .rposition(|c| c.kind() == "block_end")
2243        .unwrap_or(children.len());
2244    let has_as_clause = cst_node_has_direct_token(block, "as");
2245
2246    let mut expression = block
2247        .child_by_field_name("expression")
2248        .map(|node| parse_modern_expression_field_or_empty(source, node))
2249        .unwrap_or_else(|| modern_empty_identifier_at_block_tag_end(block));
2250
2251    let (context, context_error) = block
2252        .child_by_field_name("binding")
2253        .map(|node| parse_modern_binding_field_with_error(source, node, true))
2254        .unwrap_or((None, None));
2255
2256    let mut index = block
2257        .child_by_field_name("index")
2258        .map(|node| text_for_node(source, node).trim().to_string())
2259        .filter(|text| !text.is_empty())
2260        .map(Arc::<str>::from);
2261
2262    let mut key = block
2263        .child_by_field_name("key")
2264        .map(|node| parse_modern_expression_field_or_empty(source, node));
2265
2266    let mut invalid_key_without_as = false;
2267    if !has_as_clause
2268        && context.is_none()
2269        && key.is_none()
2270        && let Some(expression_field) = block.child_by_field_name("expression")
2271        && let Some(recovered) = recover_each_header_without_as_key(source, expression_field)
2272    {
2273        expression = recovered.expression;
2274        index = recovered.index;
2275        key = Some(recovered.key);
2276        invalid_key_without_as = true;
2277    }
2278
2279    let body_start = body_start_index(block, &children, &["expression", "binding", "index", "key"]);
2280    let branch_indices: Vec<usize> = children
2281        .iter()
2282        .enumerate()
2283        .filter_map(|(idx, node)| (node.kind() == "else_clause").then_some(idx))
2284        .collect();
2285
2286    let body_end = branch_indices.first().copied().unwrap_or(end_idx);
2287    let body_nodes = parse_modern_nodes_slice(source, &children[body_start..body_end], false);
2288    let fallback = branch_indices.iter().find_map(|branch_index| {
2289        let branch = *children.get(*branch_index)?;
2290        if branch.kind() != "else_clause" {
2291            return None;
2292        }
2293        // In the new grammar, else_clause body nodes are children of the clause itself
2294        let clause_children = named_children_vec(branch);
2295        Some(Fragment {
2296            r#type: FragmentType::Fragment,
2297            nodes: parse_modern_nodes_slice(source, &clause_children, false).into_boxed_slice(),
2298        })
2299    });
2300
2301    Some(EachBlock {
2302        start: block.start_byte(),
2303        end: block.end_byte(),
2304        expression,
2305        body: Fragment {
2306            r#type: FragmentType::Fragment,
2307            nodes: body_nodes.into_boxed_slice(),
2308        },
2309        has_as_clause,
2310        invalid_key_without_as,
2311        context,
2312        context_error,
2313        index,
2314        key,
2315        fallback,
2316    })
2317}
2318
2319struct EachHeaderMissingAsRecovery {
2320    expression: Expression,
2321    index: Option<Arc<str>>,
2322    key: Expression,
2323}
2324
2325fn recover_each_header_without_as_key(
2326    source: &str,
2327    expression_field: TsNode<'_>,
2328) -> Option<EachHeaderMissingAsRecovery> {
2329    let raw = expression_field.utf8_text(source.as_bytes()).ok()?;
2330    let trimmed = raw.trim();
2331    if trimmed.is_empty() {
2332        return None;
2333    }
2334    let field_abs = expression_field.start_byte() + raw.find(trimmed).unwrap_or(0);
2335    let segments = split_top_level_commas(trimmed);
2336    if segments.len() < 2 {
2337        return None;
2338    }
2339
2340    let expression_segment = segments.first()?.0.trim();
2341    if expression_segment.is_empty() {
2342        return None;
2343    }
2344    let expression_abs = field_abs + trimmed.find(expression_segment).unwrap_or(0);
2345    let (expression_line, expression_col) = line_column_at_offset(source, expression_abs);
2346    let expression = parse_modern_expression_from_text(
2347        expression_segment,
2348        expression_abs,
2349        expression_line,
2350        expression_col,
2351    )?;
2352
2353    let tail_offset = segments.get(1)?.1;
2354    let tail = trimmed.get(tail_offset..)?.trim();
2355    let tail_abs = field_abs + tail_offset + trimmed.get(tail_offset..)?.find(tail).unwrap_or(0);
2356    let (binding_raw, key_raw, key_inner_offset) = split_trailing_parenthesized_group(tail)?;
2357
2358    let binding = binding_raw.trim();
2359    if binding.is_empty() || parse_identifier_name(binding).is_none() {
2360        return None;
2361    }
2362    let index = Some(Arc::<str>::from(binding));
2363
2364    let key_expression = key_raw.trim();
2365    if key_expression.is_empty() {
2366        return None;
2367    }
2368    let key_abs = tail_abs + key_inner_offset + key_raw.find(key_expression).unwrap_or(0);
2369    let (key_line, key_col) = line_column_at_offset(source, key_abs);
2370    let key = parse_modern_expression_from_text(key_expression, key_abs, key_line, key_col)?;
2371
2372    Some(EachHeaderMissingAsRecovery {
2373        expression,
2374        index,
2375        key,
2376    })
2377}
2378
2379fn split_trailing_parenthesized_group(text: &str) -> Option<(&str, &str, usize)> {
2380    let trimmed = text.trim_end();
2381    if !trimmed.ends_with(')') {
2382        return None;
2383    }
2384
2385    let mut depth = 0usize;
2386    for (idx, ch) in trimmed.char_indices().rev() {
2387        match ch {
2388            ')' => depth += 1,
2389            '(' => {
2390                depth = depth.saturating_sub(1);
2391                if depth == 0 {
2392                    let before = &trimmed[..idx];
2393                    let inner_start = idx + ch.len_utf8();
2394                    let inner = &trimmed[inner_start..trimmed.len() - 1];
2395                    return Some((before, inner, inner_start));
2396                }
2397            }
2398            _ => {}
2399        }
2400    }
2401
2402    None
2403}
2404
2405fn parse_modern_key_block(source: &str, block: TsNode<'_>) -> Option<KeyBlock> {
2406    let children = named_children_vec(block);
2407    let end_idx = children
2408        .iter()
2409        .rposition(|c| c.kind() == "block_end")
2410        .unwrap_or(children.len());
2411
2412    let expression = block
2413        .child_by_field_name("expression")
2414        .and_then(|node| parse_modern_expression_field(source, node))?;
2415    let body_start = body_start_index(block, &children, &["expression"]);
2416    let fragment = Fragment {
2417        r#type: FragmentType::Fragment,
2418        nodes: parse_modern_nodes_slice(source, &children[body_start..end_idx], false)
2419            .into_boxed_slice(),
2420    };
2421
2422    Some(KeyBlock {
2423        start: block.start_byte(),
2424        end: block.end_byte(),
2425        expression,
2426        fragment,
2427    })
2428}
2429
2430fn parse_modern_await_block(source: &str, block: TsNode<'_>) -> Option<AwaitBlock> {
2431    let children = named_children_vec(block);
2432    let end_idx = children
2433        .iter()
2434        .rposition(|c| c.kind() == "block_end")
2435        .unwrap_or(children.len());
2436
2437    // Detect shorthand: {#await expr then v}...{/await}
2438    let inline_kind = find_first_named_child(block, "shorthand_kind")
2439        .and_then(|node| node.utf8_text(source.as_bytes()).ok())
2440        .map(str::trim)
2441        .and_then(BlockBranchKind::parse_await_shorthand)
2442        .or_else(|| {
2443            if cst_node_has_direct_token(block, "then") {
2444                Some(BlockBranchKind::Then)
2445            } else if cst_node_has_direct_token(block, "catch") {
2446                Some(BlockBranchKind::Catch)
2447            } else {
2448                None
2449            }
2450        });
2451
2452    let inline_binding_field = block
2453        .child_by_field_name("binding")
2454        .and_then(|node| parse_modern_binding_field(source, node, true));
2455    let expression = block
2456        .child_by_field_name("expression")
2457        .map(|node| parse_modern_expression_field_or_empty(source, node))
2458        .unwrap_or_else(|| modern_empty_identifier_at_block_tag_end(block));
2459
2460    let branch_indices: Vec<usize> = children
2461        .iter()
2462        .enumerate()
2463        .filter_map(|(idx, node)| (node.kind() == "await_branch").then_some(idx))
2464        .collect();
2465    let first_branch_idx = branch_indices.first().copied().unwrap_or(end_idx);
2466
2467    let parse_await_children_field = |node: TsNode<'_>| -> Vec<crate::ast::modern::Node> {
2468        let child_nodes = named_children_vec(node);
2469        parse_modern_nodes_slice(source, &child_nodes, false)
2470    };
2471
2472    let pending = if inline_kind.is_some() {
2473        None
2474    } else {
2475        let mut pending_nodes = Vec::new();
2476
2477        if let Some(pending_node) = block
2478            .child_by_field_name("pending")
2479            .filter(|node| node.kind() == "await_pending")
2480        {
2481            pending_nodes.extend(parse_await_children_field(pending_node));
2482        }
2483
2484        let body_start = body_start_index(block, &children, &["expression", "binding", "pending"]);
2485        for node in &children[body_start..first_branch_idx] {
2486            if node.kind() == "await_pending" {
2487                continue;
2488            }
2489            let mut recovered = parse_modern_nodes_slice(source, std::slice::from_ref(node), false);
2490            if recovered.is_empty()
2491                && node.kind() == "ERROR"
2492                && let Some(text) = recover_await_error_pending_text(source, *node)
2493            {
2494                push_modern_text_node(&mut recovered, text);
2495            }
2496            pending_nodes.extend(recovered);
2497        }
2498
2499        (branch_indices.is_empty() || !pending_nodes.is_empty()).then_some(Fragment {
2500            r#type: FragmentType::Fragment,
2501            nodes: pending_nodes.into_boxed_slice(),
2502        })
2503    };
2504
2505    let inline_binding = inline_binding_field;
2506    let mut value = None;
2507    let mut error = None;
2508    let mut then_fragment = None;
2509    let mut catch_fragment = None;
2510
2511    match inline_kind {
2512        Some(BlockBranchKind::Then) => value = inline_binding,
2513        Some(BlockBranchKind::Catch) => error = inline_binding,
2514        _ => {}
2515    }
2516
2517    if let Some(inline_branch_kind) = inline_kind {
2518        let inline_nodes = find_first_named_child(block, "await_branch_children")
2519            .map(parse_await_children_field)
2520            .unwrap_or_default();
2521
2522        let fragment = Fragment {
2523            r#type: FragmentType::Fragment,
2524            nodes: inline_nodes.into_boxed_slice(),
2525        };
2526
2527        match inline_branch_kind {
2528            BlockBranchKind::Then => then_fragment = Some(fragment),
2529            BlockBranchKind::Catch => catch_fragment = Some(fragment),
2530            _ => {}
2531        }
2532    }
2533
2534    for branch_child_idx in branch_indices.iter().copied() {
2535        let branch_node = *children.get(branch_child_idx)?;
2536
2537        let kind = find_first_named_child(branch_node, "branch_kind")
2538            .and_then(|n| n.utf8_text(source.as_bytes()).ok())
2539            .and_then(|s| BlockBranchKind::parse_await_shorthand(s.trim()));
2540        let Some(kind) = kind else {
2541            continue;
2542        };
2543
2544        let binding_expr = branch_node
2545            .child_by_field_name("binding")
2546            .and_then(|node| parse_modern_binding_field(source, node, true));
2547
2548        let fragment_nodes = find_first_named_child(branch_node, "await_branch_children")
2549            .map(parse_await_children_field)
2550            .unwrap_or_default();
2551        let fragment = Fragment {
2552            r#type: FragmentType::Fragment,
2553            nodes: fragment_nodes.into_boxed_slice(),
2554        };
2555
2556        match kind {
2557            BlockBranchKind::Then => {
2558                if value.is_none() {
2559                    value = binding_expr;
2560                }
2561                then_fragment = Some(fragment);
2562            }
2563            BlockBranchKind::Catch => {
2564                if error.is_none() {
2565                    error = binding_expr;
2566                }
2567                catch_fragment = Some(fragment);
2568            }
2569            _ => {}
2570        }
2571    }
2572
2573    Some(AwaitBlock {
2574        start: block.start_byte(),
2575        end: block.end_byte(),
2576        expression,
2577        value,
2578        error,
2579        pending,
2580        then: then_fragment,
2581        catch: catch_fragment,
2582    })
2583}
2584
2585fn recover_await_error_pending_text(source: &str, error_node: TsNode<'_>) -> Option<Text> {
2586    let start = error_node.start_byte();
2587    let end = error_node.end_byte();
2588    if start >= end || end > source.len() {
2589        return None;
2590    }
2591
2592    let raw = &source[start..end];
2593    let close = raw.find('}')?;
2594    let tail = raw
2595        .get((close + 1)..)?
2596        .trim_start_matches(char::is_whitespace);
2597    if tail.is_empty() {
2598        return None;
2599    }
2600
2601    let tail_start = start + close + 1 + (raw[(close + 1)..].len() - tail.len());
2602    Some(Text {
2603        start: tail_start,
2604        end,
2605        raw: Arc::from(tail),
2606        data: Arc::from(decode_html_entities_cow(tail).into_owned()),
2607    })
2608}
2609
2610pub fn find_matching_brace_close(source: &str, open_index: usize, limit: usize) -> Option<usize> {
2611    let bytes = source.as_bytes();
2612    let mut index = open_index;
2613    let mut depth = 0usize;
2614    let mut in_single = false;
2615    let mut in_double = false;
2616    let mut in_template = false;
2617    let mut in_line_comment = false;
2618    let mut in_block_comment = false;
2619    let mut escaped = false;
2620
2621    while index < limit {
2622        let byte = *bytes.get(index)?;
2623        let ch = byte as char;
2624        let next = bytes.get(index + 1).copied().unwrap_or_default() as char;
2625
2626        if in_line_comment {
2627            if ch == '\n' || ch == '\r' {
2628                in_line_comment = false;
2629            }
2630            index += 1;
2631            continue;
2632        }
2633
2634        if in_block_comment {
2635            if ch == '*' && next == '/' {
2636                in_block_comment = false;
2637                index += 2;
2638                continue;
2639            }
2640            index += 1;
2641            continue;
2642        }
2643
2644        if escaped {
2645            escaped = false;
2646            index += 1;
2647            continue;
2648        }
2649
2650        if in_single {
2651            if ch == '\\' {
2652                escaped = true;
2653            } else if ch == '\'' {
2654                in_single = false;
2655            }
2656            index += 1;
2657            continue;
2658        }
2659
2660        if in_double {
2661            if ch == '\\' {
2662                escaped = true;
2663            } else if ch == '"' {
2664                in_double = false;
2665            }
2666            index += 1;
2667            continue;
2668        }
2669
2670        if in_template {
2671            if ch == '\\' {
2672                escaped = true;
2673            } else if ch == '`' {
2674                in_template = false;
2675            }
2676            index += 1;
2677            continue;
2678        }
2679
2680        if ch == '/' && next == '/' {
2681            in_line_comment = true;
2682            index += 2;
2683            continue;
2684        }
2685
2686        if ch == '/' && next == '*' {
2687            in_block_comment = true;
2688            index += 2;
2689            continue;
2690        }
2691
2692        match ch {
2693            '\'' => in_single = true,
2694            '"' => in_double = true,
2695            '`' => in_template = true,
2696            '{' => depth += 1,
2697            '}' => {
2698                depth = depth.saturating_sub(1);
2699                if depth == 0 {
2700                    return Some(index);
2701                }
2702            }
2703            _ => {}
2704        }
2705
2706        index += 1;
2707    }
2708
2709    None
2710}
2711
2712pub(crate) fn parse_modern_expression_field(source: &str, node: TsNode<'_>) -> Option<Expression> {
2713    let raw = node.utf8_text(source.as_bytes()).ok()?;
2714    let trimmed = raw.trim();
2715    if trimmed.is_empty() {
2716        return None;
2717    }
2718
2719    let leading = raw.find(trimmed).unwrap_or(0);
2720    let abs = node.start_byte() + leading;
2721    let (line, column) = line_column_at_offset(source, abs);
2722    parse_modern_expression_from_text(trimmed, abs, line, column)
2723}
2724
2725fn parse_modern_expression_field_or_empty(source: &str, node: TsNode<'_>) -> Expression {
2726    parse_modern_expression_field(source, node)
2727        .unwrap_or_else(|| modern_empty_identifier_expression_for_field(source, node))
2728}
2729
2730fn modern_empty_identifier_expression_for_field(source: &str, node: TsNode<'_>) -> Expression {
2731    let raw = node.utf8_text(source.as_bytes()).ok().unwrap_or_default();
2732    let trimmed = raw.trim();
2733    if trimmed.is_empty() {
2734        // For zero-width nodes, use start_byte directly (equals end_byte).
2735        // For non-zero-width nodes with only whitespace, end_byte - 1 is the last char.
2736        let pos = if node.start_byte() == node.end_byte() {
2737            node.start_byte()
2738        } else {
2739            node.end_byte().saturating_sub(1)
2740        };
2741        return modern_empty_identifier_expression_span(pos, 0);
2742    }
2743
2744    let leading = raw.find(trimmed).unwrap_or(0);
2745    let start = node.start_byte() + leading;
2746    modern_empty_identifier_expression_span(start, trimmed.len())
2747}
2748
2749fn modern_empty_identifier_at_block_tag_end(node: TsNode<'_>) -> Expression {
2750    modern_empty_identifier_expression_span(node.end_byte().saturating_sub(1), 0)
2751}
2752
2753fn parse_modern_binding_field(
2754    source: &str,
2755    node: TsNode<'_>,
2756    with_character: bool,
2757) -> Option<Expression> {
2758    parse_modern_binding_field_with_error(source, node, with_character).0
2759}
2760
2761fn parse_modern_binding_field_with_error(
2762    source: &str,
2763    node: TsNode<'_>,
2764    with_character: bool,
2765) -> (Option<Expression>, Option<ParseError>) {
2766    let Ok(raw) = node.utf8_text(source.as_bytes()) else {
2767        return (None, None);
2768    };
2769    let trimmed = raw.trim();
2770    if trimmed.is_empty() {
2771        return (None, None);
2772    }
2773
2774    let leading = raw.find(trimmed).unwrap_or(0);
2775    let abs = node.start_byte() + leading;
2776    let (line, column) = line_column_at_offset(source, abs);
2777
2778    if let Some(word) = reserved_binding_word(trimmed) {
2779        return (
2780            None,
2781            Some(ParseError {
2782                kind: ParseErrorKind::UnexpectedReservedWord {
2783                    word: Arc::from(word),
2784                },
2785                start: abs,
2786                end: abs,
2787            }),
2788        );
2789    }
2790
2791    // Check for comma after rest element in the pattern text before parsing.
2792    if let Some(comma_pos) = find_rest_comma_in_text(trimmed) {
2793        return (
2794            None,
2795            Some(ParseError {
2796                kind: ParseErrorKind::JsParseError {
2797                    message: Arc::from("Comma is not permitted after the rest element"),
2798                },
2799                start: abs + comma_pos,
2800                end: abs + comma_pos,
2801            }),
2802        );
2803    }
2804
2805    if let Some(mut expression) = parse_pattern_with_oxc(trimmed, abs, line, column) {
2806        if with_character {
2807            set_expression_character(source, &mut expression);
2808        }
2809        return (Some(expression), None);
2810    }
2811
2812    if let Some((start, message)) = reserved_binding_pattern_error(trimmed, abs) {
2813        return (
2814            None,
2815            Some(ParseError {
2816                kind: ParseErrorKind::JsParseError { message },
2817                start,
2818                end: start,
2819            }),
2820        );
2821    }
2822
2823    if let Some(expression) = parse_modern_expression_from_text(trimmed, abs, line, column)
2824        && let Some((start, message)) = invalid_binding_expression_error(&expression)
2825    {
2826        return (
2827            None,
2828            Some(ParseError {
2829                kind: ParseErrorKind::JsParseError { message },
2830                start,
2831                end: start,
2832            }),
2833        );
2834    }
2835
2836    let error =
2837        parse_pattern_error_from_text(trimmed, abs, line, column).map(|(start, message)| {
2838            ParseError {
2839                kind: ParseErrorKind::JsParseError { message },
2840                start,
2841                end: start,
2842            }
2843        });
2844    (None, error)
2845}
2846
2847fn is_js_reserved_word(text: &str) -> bool {
2848    matches!(
2849        text,
2850        "await"
2851            | "break"
2852            | "case"
2853            | "catch"
2854            | "class"
2855            | "const"
2856            | "continue"
2857            | "debugger"
2858            | "default"
2859            | "delete"
2860            | "do"
2861            | "else"
2862            | "enum"
2863            | "export"
2864            | "extends"
2865            | "false"
2866            | "finally"
2867            | "for"
2868            | "function"
2869            | "if"
2870            | "import"
2871            | "in"
2872            | "instanceof"
2873            | "new"
2874            | "null"
2875            | "return"
2876            | "super"
2877            | "switch"
2878            | "this"
2879            | "throw"
2880            | "true"
2881            | "try"
2882            | "typeof"
2883            | "var"
2884            | "void"
2885            | "while"
2886            | "with"
2887            | "yield"
2888    )
2889}
2890
2891fn reserved_binding_word(text: &str) -> Option<&str> {
2892    let word = leading_identifier_word(text)?;
2893    let tail = &text[word.len()..];
2894    let tail = tail.trim_matches(|ch: char| ch.is_whitespace() || ch == '}');
2895    (is_js_reserved_word(word) && tail.is_empty()).then_some(word)
2896}
2897
2898fn reserved_binding_pattern_error(text: &str, start: usize) -> Option<(usize, Arc<str>)> {
2899    let trimmed = text.trim();
2900    if trimmed.starts_with('[') {
2901        return reserved_array_binding_error(trimmed, start);
2902    }
2903    if trimmed.starts_with('{') {
2904        return reserved_object_binding_error(trimmed, start);
2905    }
2906    None
2907}
2908
2909fn reserved_array_binding_error(text: &str, start: usize) -> Option<(usize, Arc<str>)> {
2910    let close = text.rfind(']')?;
2911    let inner = &text[1..close];
2912    let leading = inner.find(|ch: char| !ch.is_whitespace())?;
2913    let word = leading_identifier_word(&inner[leading..])?;
2914    is_js_reserved_word(word).then_some((start + 1 + leading, Arc::from("Unexpected token")))
2915}
2916
2917fn reserved_object_binding_error(text: &str, start: usize) -> Option<(usize, Arc<str>)> {
2918    let close = text.rfind('}')?;
2919    let inner = &text[1..close];
2920    let leading = inner.find(|ch: char| !ch.is_whitespace())?;
2921    let rest = &inner[leading..];
2922    let word = leading_identifier_word(rest)?;
2923    if !is_js_reserved_word(word) {
2924        return None;
2925    }
2926    let tail = rest[word.len()..].trim_start();
2927    (tail.is_empty() || matches!(tail.chars().next(), Some(','))).then_some((
2928        start + 1 + leading,
2929        Arc::from(format!("Unexpected keyword '{word}'")),
2930    ))
2931}
2932
2933fn leading_identifier_word(text: &str) -> Option<&str> {
2934    let mut end = 0usize;
2935    for (idx, ch) in text.char_indices() {
2936        let ok = if idx == 0 {
2937            ch == '_' || ch == '$' || ch.is_ascii_alphabetic()
2938        } else {
2939            ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()
2940        };
2941        if !ok {
2942            break;
2943        }
2944        end = idx + ch.len_utf8();
2945    }
2946    (end > 0).then_some(&text[..end])
2947}
2948
2949fn invalid_binding_expression_error(expression: &Expression) -> Option<(usize, Arc<str>)> {
2950    crate::parse::oxc_query::invalid_binding_expression_error(expression)
2951}
2952
2953fn parse_pattern_error_from_text(
2954    text: &str,
2955    start_byte: usize,
2956    line: usize,
2957    column: usize,
2958) -> Option<(usize, Arc<str>)> {
2959    let wrapped = format!("({text})=>{{}}");
2960    let base_column = column.saturating_sub(1);
2961    crate::parse::parse_modern_expression_error_detail_with_oxc(
2962        &wrapped,
2963        start_byte.saturating_sub(1),
2964        line,
2965        base_column,
2966    )
2967}
2968
2969fn parse_modern_snippet_block(source: &str, block: TsNode<'_>) -> Option<SnippetBlock> {
2970    let children = named_children_vec(block);
2971    let end_idx = children
2972        .iter()
2973        .rposition(|c| c.kind() == "block_end")
2974        .unwrap_or(children.len());
2975
2976    let name_node = block
2977        .child_by_field_name("name")
2978        .or_else(|| block.child_by_field_name("expression"));
2979    let type_params_node = block.child_by_field_name("type_parameters");
2980    let params_node = block.child_by_field_name("parameters");
2981    let expression = parse_snippet_name(source, block, name_node);
2982    let type_params = parse_snippet_type_params(source, type_params_node);
2983    let parameters = parse_snippet_params(source, params_node);
2984    let body_start = body_start_index(block, &children, &["name", "type_parameters", "parameters"]);
2985    let body_nodes = parse_modern_nodes_slice(source, &children[body_start..end_idx], false);
2986
2987    // Detect the missing-paren recovery rule from the CST: the grammar's prec(-1)
2988    // alternative matches `( params }` (no `)`) and tree-sitter inserts MISSING "}".
2989    // If block has a MISSING child and no `)` anonymous child, it's missing `)`.
2990    let has_lparen = (0..block.child_count())
2991        .filter_map(|i| block.child(i as u32))
2992        .any(|c| !c.is_named() && c.kind() == "(");
2993    let header_error = if block.has_error() && params_node.is_some() && has_lparen {
2994        let has_rparen = (0..block.child_count())
2995            .filter_map(|i| block.child(i as u32))
2996            .any(|c| !c.is_named() && c.kind() == ")");
2997        if has_rparen {
2998            // Has `)` but MISSING `}` — missing right brace
2999            let error_pos =
3000                snippet_header_error_pos(block, name_node, type_params_node, params_node);
3001            Some(SnippetHeaderError {
3002                kind: SnippetHeaderErrorKind::ExpectedRightBrace,
3003                start: error_pos,
3004                end: error_pos,
3005            })
3006        } else {
3007            // Has `(` and params but no `)` — missing right paren
3008            // Position at end of block_end node to match JS compiler behavior
3009            let error_pos = children
3010                .get(end_idx)
3011                .map(|n| n.end_byte())
3012                .unwrap_or(block.end_byte().saturating_sub(1));
3013            Some(SnippetHeaderError {
3014                kind: SnippetHeaderErrorKind::ExpectedRightParen,
3015                start: error_pos,
3016                end: error_pos,
3017            })
3018        }
3019    } else if block.has_error() && params_node.is_none() {
3020        let missing_right_brace = name_node
3021            .and_then(|name| source.get(name.end_byte()..block.end_byte()))
3022            .is_some_and(|tail| {
3023                tail.find('(')
3024                    .zip(tail.find(')'))
3025                    .is_some_and(|(left, right)| left < right)
3026            });
3027        missing_right_brace.then_some(SnippetHeaderError {
3028            kind: SnippetHeaderErrorKind::ExpectedRightBrace,
3029            start: block.start_byte(),
3030            end: block.start_byte(),
3031        })
3032    } else {
3033        None
3034    };
3035
3036    Some(SnippetBlock {
3037        start: block.start_byte(),
3038        end: block.end_byte(),
3039        expression,
3040        type_params,
3041        parameters: parameters.into_boxed_slice(),
3042        body: Fragment {
3043            r#type: FragmentType::Fragment,
3044            nodes: body_nodes.into_boxed_slice(),
3045        },
3046        header_error,
3047    })
3048}
3049
3050fn recover_malformed_snippet_block(source: &str, error_node: TsNode<'_>) -> Option<SnippetBlock> {
3051    // Don't recover ERROR nodes whose text starts with a branch opener ({:else, {:then, {:catch}).
3052    // Tree-sitter reuses snippet_name for identifiers inside those, so they'd be
3053    // misidentified as malformed snippets.
3054    let raw = source
3055        .get(error_node.start_byte()..error_node.end_byte())
3056        .unwrap_or_default();
3057    if raw.starts_with("{:") {
3058        return None;
3059    }
3060    recover_snippet_block_missing_right_brace(source, error_node)
3061        .or_else(|| recover_snippet_block_missing_right_paren(source, error_node))
3062}
3063
3064fn recover_snippet_block_missing_right_brace(
3065    source: &str,
3066    error_node: TsNode<'_>,
3067) -> Option<SnippetBlock> {
3068    let start_node = find_snippet_start(error_node, source)?;
3069    let name_node = start_node
3070        .child_by_field_name("name")
3071        .or_else(|| start_node.child_by_field_name("expression"))
3072        .or_else(|| find_named_descendant(start_node, "snippet_name"));
3073    let type_params_node = start_node
3074        .child_by_field_name("type_parameters")
3075        .or_else(|| find_named_descendant(start_node, "snippet_type_parameters"));
3076    let params_node = start_node
3077        .child_by_field_name("parameters")
3078        .or_else(|| find_named_descendant(start_node, "snippet_parameters"));
3079    let has_lparen = (0..start_node.child_count())
3080        .filter_map(|i| start_node.child(i as u32))
3081        .any(|c| !c.is_named() && c.kind() == "(");
3082
3083    let header_error = if params_node.is_some() && has_lparen {
3084        let error_pos =
3085            snippet_header_error_pos(start_node, name_node, type_params_node, params_node);
3086        Some(SnippetHeaderError {
3087            kind: SnippetHeaderErrorKind::ExpectedRightBrace,
3088            start: error_pos,
3089            end: error_pos,
3090        })
3091    } else {
3092        None
3093    };
3094
3095    Some(SnippetBlock {
3096        start: error_node.start_byte(),
3097        end: error_node.end_byte(),
3098        expression: parse_snippet_name(source, start_node, name_node),
3099        type_params: parse_snippet_type_params(source, type_params_node),
3100        parameters: parse_snippet_params(source, params_node).into_boxed_slice(),
3101        body: snippet_recovery_body_fragment(source, error_node),
3102        header_error,
3103    })
3104}
3105
3106fn recover_snippet_block_missing_right_paren(
3107    source: &str,
3108    error_node: TsNode<'_>,
3109) -> Option<SnippetBlock> {
3110    // In the new grammar, look for snippet_name instead of block_kind
3111    find_named_descendant(error_node, "snippet_name")?;
3112
3113    let name_node = find_named_descendant(error_node, "snippet_name");
3114    let type_params_node = find_named_descendant(error_node, "snippet_type_parameters");
3115    let params_node = find_named_descendant(error_node, "snippet_parameters");
3116    let error_pos = error_node.end_byte().saturating_sub(1);
3117
3118    Some(SnippetBlock {
3119        start: error_node.start_byte(),
3120        end: error_node.end_byte(),
3121        expression: parse_snippet_name(source, error_node, name_node),
3122        type_params: parse_snippet_type_params(source, type_params_node),
3123        parameters: parse_snippet_params(source, params_node).into_boxed_slice(),
3124        body: empty_fragment(),
3125        header_error: Some(SnippetHeaderError {
3126            kind: SnippetHeaderErrorKind::ExpectedRightParen,
3127            start: error_pos,
3128            end: error_pos,
3129        }),
3130    })
3131}
3132
3133pub(crate) fn parse_snippet_type_params(
3134    source: &str,
3135    node: Option<TsNode<'_>>,
3136) -> Option<Arc<str>> {
3137    let raw = node?.utf8_text(source.as_bytes()).ok()?.trim();
3138    let inner = raw
3139        .strip_prefix('<')
3140        .and_then(|tail| tail.strip_suffix('>'))
3141        .unwrap_or(raw)
3142        .trim();
3143    (!inner.is_empty()).then(|| Arc::from(inner))
3144}
3145
3146pub(crate) fn parse_snippet_params(
3147    source: &str,
3148    params_node: Option<TsNode<'_>>,
3149) -> Vec<Expression> {
3150    let Some(params_node) = params_node else {
3151        return Vec::new();
3152    };
3153    let raw = text_for_node(source, params_node);
3154    let Some(parsed) = crate::js::ParsedJsParameters::parse(raw.as_ref()).ok().map(Arc::new) else {
3155        return named_children_vec(params_node)
3156            .into_iter()
3157            .filter(|node| node.kind() == "pattern")
3158            .filter_map(|node| parse_modern_binding_field(source, node, false))
3159            .collect::<Vec<_>>();
3160    };
3161
3162    let mut parameters = parsed
3163        .parameters()
3164        .items
3165        .iter()
3166        .enumerate()
3167        .map(|(index, parameter)| {
3168            let span = parameter.span();
3169            Expression::from_parameter_item(
3170                parsed.clone(),
3171                index,
3172                params_node.start_byte() + span.start as usize - 1,
3173                params_node.start_byte() + span.end as usize - 1,
3174            )
3175        })
3176        .collect::<Vec<_>>();
3177
3178    if let Some(rest) = parsed.rest_parameter() {
3179        let span = rest.span();
3180        parameters.push(Expression::from_rest_parameter(
3181            parsed,
3182            params_node.start_byte() + span.start as usize - 1,
3183            params_node.start_byte() + span.end as usize - 1,
3184        ));
3185    }
3186
3187    parameters
3188}
3189
3190pub(crate) fn parse_snippet_name(
3191    source: &str,
3192    owner: TsNode<'_>,
3193    name_node: Option<TsNode<'_>>,
3194) -> Expression {
3195    let mut expression = if let Some(name_node) = name_node {
3196        let raw = name_node
3197            .utf8_text(source.as_bytes())
3198            .ok()
3199            .unwrap_or_default();
3200        let trimmed = raw.trim();
3201        if trimmed.is_empty() {
3202            // Zero-width or whitespace-only name — produce empty identifier with loc
3203            let start = if name_node.start_byte() == name_node.end_byte() {
3204                name_node.start_byte()
3205            } else {
3206                name_node.end_byte().saturating_sub(1)
3207            };
3208            let (line, column) = line_column_at_offset(source, start);
3209            modern_identifier_expression_with_loc(Arc::from(""), start, start, line, column)
3210        } else {
3211            parse_modern_expression_field_or_empty(source, name_node)
3212        }
3213    } else {
3214        // No name node at all — fallback using first body child position
3215        let start = named_children_vec(owner)
3216            .first()
3217            .map(|n| n.start_byte().saturating_sub(1))
3218            .unwrap_or_else(|| owner.end_byte().saturating_sub(1));
3219        let (line, column) = line_column_at_offset(source, start);
3220        modern_identifier_expression_with_loc(Arc::from(""), start, start, line, column)
3221    };
3222    set_expression_character(source, &mut expression);
3223    expression
3224}
3225
3226fn empty_fragment() -> Fragment {
3227    Fragment {
3228        r#type: FragmentType::Fragment,
3229        nodes: Box::new([]),
3230    }
3231}
3232
3233fn snippet_recovery_body_fragment(source: &str, error_node: TsNode<'_>) -> Fragment {
3234    let Some(raw) = source.get(error_node.start_byte()..error_node.end_byte()) else {
3235        return empty_fragment();
3236    };
3237    let Some(header_close) = raw.find('}') else {
3238        return empty_fragment();
3239    };
3240    let Some(block_end_start) = raw.rfind("{/snippet}") else {
3241        return empty_fragment();
3242    };
3243    if header_close + 1 >= block_end_start {
3244        return empty_fragment();
3245    }
3246
3247    let body_start = error_node.start_byte() + header_close + 1;
3248    let body_end = error_node.start_byte() + block_end_start;
3249    let Some(body_raw) = source.get(body_start..body_end) else {
3250        return empty_fragment();
3251    };
3252    if body_raw.is_empty() {
3253        return empty_fragment();
3254    }
3255
3256    Fragment {
3257        r#type: FragmentType::Fragment,
3258        nodes: Box::new([Node::Text(Text {
3259            start: body_start,
3260            end: body_end,
3261            raw: Arc::from(body_raw),
3262            data: Arc::from(body_raw),
3263        })]),
3264    }
3265}
3266
3267fn snippet_header_error_pos(
3268    start_node: TsNode<'_>,
3269    name_node: Option<TsNode<'_>>,
3270    type_params_node: Option<TsNode<'_>>,
3271    params_node: Option<TsNode<'_>>,
3272) -> usize {
3273    if let Some(error) = find_direct_named_child(start_node, "ERROR")
3274        .or_else(|| find_named_descendant(start_node, "ERROR"))
3275    {
3276        return error.start_byte();
3277    }
3278
3279    params_node
3280        .or(type_params_node)
3281        .or(name_node)
3282        .map(|node| node.end_byte())
3283        .unwrap_or_else(|| start_node.end_byte().saturating_sub(1))
3284}
3285
3286fn find_snippet_start<'tree>(node: TsNode<'tree>, _source: &str) -> Option<TsNode<'tree>> {
3287    if node.kind() == "snippet_block" {
3288        return Some(node);
3289    }
3290    // Also match nodes that contain snippet_name (partial snippet in ERROR)
3291    if find_direct_named_child(node, "snippet_name").is_some() {
3292        return Some(node);
3293    }
3294
3295    for index in 0..node.child_count() {
3296        let Some(child) = node.child(index as u32) else {
3297            continue;
3298        };
3299        if let Some(found) = find_snippet_start(child, _source) {
3300            return Some(found);
3301        }
3302    }
3303    None
3304}
3305
3306fn find_named_descendant<'tree>(node: TsNode<'tree>, kind: &str) -> Option<TsNode<'tree>> {
3307    if node.kind() == kind {
3308        return Some(node);
3309    }
3310
3311    for index in 0..node.child_count() {
3312        let Some(child) = node.child(index as u32) else {
3313            continue;
3314        };
3315        if let Some(found) = find_named_descendant(child, kind) {
3316            return Some(found);
3317        }
3318    }
3319    None
3320}
3321
3322pub(crate) fn split_top_level_commas(text: &str) -> Vec<(&str, usize)> {
3323    let mut segments = Vec::new();
3324    let mut start = 0usize;
3325    let mut depth_paren = 0usize;
3326    let mut depth_brace = 0usize;
3327    let mut depth_bracket = 0usize;
3328    let bytes = text.as_bytes();
3329
3330    for (idx, byte) in bytes.iter().enumerate() {
3331        match *byte {
3332            b'(' => depth_paren += 1,
3333            b')' => depth_paren = depth_paren.saturating_sub(1),
3334            b'{' => depth_brace += 1,
3335            b'}' => depth_brace = depth_brace.saturating_sub(1),
3336            b'[' => depth_bracket += 1,
3337            b']' => depth_bracket = depth_bracket.saturating_sub(1),
3338            b',' if depth_paren == 0 && depth_brace == 0 && depth_bracket == 0 => {
3339                segments.push((&text[start..idx], start));
3340                start = idx + 1;
3341            }
3342            _ => {}
3343        }
3344    }
3345
3346    if start <= text.len() {
3347        segments.push((&text[start..], start));
3348    }
3349
3350    segments
3351}
3352
3353pub(crate) fn parse_pattern_with_oxc(
3354    text: &str,
3355    abs_start: usize,
3356    line: usize,
3357    column: usize,
3358) -> Option<Expression> {
3359    let trimmed = text.trim();
3360    if trimmed.is_empty() {
3361        return None;
3362    }
3363
3364    let leading_ws = text.find(trimmed).unwrap_or(0);
3365    let start = abs_start + leading_ws;
3366    let parsed = Arc::new(crate::js::ParsedJsPattern::parse(trimmed).ok()?);
3367    let end = start + trimmed.len();
3368    let mut expression = Expression::from_pattern(parsed, start, end);
3369    expression.syntax.parens = leading_parens(trimmed, start, expression.start);
3370    let _ = (line, column);
3371    Some(expression)
3372}
3373
3374/// Scans pattern text for `...identifier,` (rest element followed by comma)
3375/// which is invalid in destructuring patterns. Returns the byte offset of
3376/// the comma within the text.
3377fn find_rest_comma_in_text(text: &str) -> Option<usize> {
3378    let bytes = text.as_bytes();
3379    let mut i = 0;
3380    let mut brace_depth: i32 = 0;
3381    let mut bracket_depth: i32 = 0;
3382
3383    while i < bytes.len() {
3384        match bytes[i] {
3385            b'{' => brace_depth += 1,
3386            b'}' => brace_depth -= 1,
3387            b'[' => bracket_depth += 1,
3388            b']' => bracket_depth -= 1,
3389            b'.' if i + 2 < bytes.len() && bytes[i + 1] == b'.' && bytes[i + 2] == b'.' => {
3390                // Found `...` — skip past the identifier to see if a comma follows
3391                let rest_start = i;
3392                i += 3;
3393                // Skip whitespace
3394                while i < bytes.len() && bytes[i].is_ascii_whitespace() {
3395                    i += 1;
3396                }
3397                // Skip identifier
3398                let id_start = i;
3399                while i < bytes.len()
3400                    && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' || bytes[i] == b'$')
3401                {
3402                    i += 1;
3403                }
3404                if i > id_start {
3405                    // Skip whitespace after identifier
3406                    while i < bytes.len() && bytes[i].is_ascii_whitespace() {
3407                        i += 1;
3408                    }
3409                    if i < bytes.len() && bytes[i] == b',' {
3410                        // Check context: only inside `{` or `[` destructuring
3411                        if brace_depth > 0 || bracket_depth > 0 {
3412                            return Some(i);
3413                        }
3414                    }
3415                }
3416                let _ = rest_start;
3417                continue;
3418            }
3419            _ => {}
3420        }
3421        i += 1;
3422    }
3423    None
3424}
3425
3426pub fn line_column_at_offset(source: &str, offset: usize) -> (usize, usize) {
3427    SourceText::new(SourceId::new(0), source, None).line_column_at_offset(offset)
3428}
3429
3430fn source_location_at_offset(source: &str, offset: usize) -> SourceLocation {
3431    SourceText::new(SourceId::new(0), source, None).location_at_offset(offset)
3432}
3433
3434fn set_expression_character(_source: &str, _expression: &mut Expression) {}
3435
3436fn parse_modern_element_node(
3437    source: &str,
3438    node: TsNode<'_>,
3439    in_shadowroot_template: bool,
3440    in_svelte_head: bool,
3441    loose: bool,
3442    hint: Option<&IncrementalHint<'_>>,
3443) -> Node {
3444    let mut tag_cursor = node.walk();
3445    let mut start_tag: Option<TsNode<'_>> = None;
3446    let mut end_tag: Option<TsNode<'_>> = None;
3447    let mut self_closing_tag: Option<TsNode<'_>> = None;
3448    let mut trailing_text: Option<TsNode<'_>> = None;
3449    for child in node.named_children(&mut tag_cursor) {
3450        match child.kind() {
3451            "start_tag" => start_tag = Some(child),
3452            "end_tag" => end_tag = Some(child),
3453            "self_closing_tag" => self_closing_tag = Some(child),
3454            "text" if trailing_text.is_none() => {
3455                trailing_text = Some(child);
3456            }
3457            _ => {}
3458        }
3459    }
3460
3461    if let (Some(start_tag_node), Some(text_node)) = (start_tag, trailing_text)
3462        && text_node.start_byte() != start_tag_node.end_byte()
3463    {
3464        trailing_text = None;
3465    }
3466
3467    if let Some(start_tag) = start_tag
3468        && end_tag.is_none()
3469        && self_closing_tag.is_none()
3470        && !text_for_node(source, start_tag).trim_end().ends_with('>')
3471    {
3472        return parse_modern_loose_start_tag_node(source, start_tag, trailing_text);
3473    }
3474
3475    let element =
3476        parse_modern_regular_element(source, node, in_shadowroot_template, in_svelte_head, loose, hint);
3477    classify_modern_element(element, in_shadowroot_template, in_svelte_head)
3478}
3479
3480/// Extract the `this={expr}` attribute's expression from an attribute list,
3481/// returning the expression and the remaining attributes with `this` removed.
3482fn extract_this_expression(attributes: Box<[Attribute]>) -> (Option<Expression>, Box<[Attribute]>) {
3483    let mut this_expr = None;
3484    let mut remaining = Vec::with_capacity(attributes.len());
3485
3486    for attr in Vec::from(attributes) {
3487        if this_expr.is_none()
3488            && let Attribute::Attribute(ref named) = attr
3489            && classify_attribute_name(named.name.as_ref()) == AttributeKind::This
3490            && let AttributeValueList::ExpressionTag(ref tag) = named.value
3491        {
3492            this_expr = Some(tag.expression.clone());
3493            continue;
3494        }
3495        if this_expr.is_none()
3496            && let Attribute::Attribute(ref named) = attr
3497            && classify_attribute_name(named.name.as_ref()) == AttributeKind::This
3498            && let AttributeValueList::Values(ref values) = named.value
3499            && values.len() == 1
3500            && let AttributeValue::Text(text) = &values[0]
3501        {
3502            this_expr = Some(modern_string_literal_expression(
3503                text.data.clone(),
3504                text.start,
3505                text.end,
3506            ));
3507            continue;
3508        }
3509        remaining.push(attr);
3510    }
3511
3512    (this_expr, remaining.into_boxed_slice())
3513}
3514
3515fn modern_string_literal_expression(value: Arc<str>, start: usize, end: usize) -> Expression {
3516    let raw = format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'"));
3517    match crate::js::ParsedJsExpression::parse(raw, oxc_span::SourceType::ts().with_module(true)) {
3518        Ok(parsed) => Expression::from_expression(Arc::new(parsed), start, end),
3519        Err(_) => Expression::empty(start, end),
3520    }
3521}
3522
3523/// Classify a parsed `RegularElement` into the correct Svelte* node type.
3524fn classify_modern_element(
3525    element: RegularElement,
3526    in_shadowroot_template: bool,
3527    in_svelte_head: bool,
3528) -> Node {
3529    match classify_element_name(element.name.as_ref()) {
3530        ElementKind::Slot if !in_shadowroot_template => Node::SlotElement(SlotElement {
3531            start: element.start,
3532            end: element.end,
3533            name: element.name,
3534            name_loc: element.name_loc,
3535            attributes: element.attributes,
3536            fragment: element.fragment,
3537        }),
3538        ElementKind::Svelte(kind) => classify_svelte_element(element, kind),
3539        _ if element.name.as_ref() == "title" && in_svelte_head => {
3540            Node::TitleElement(crate::ast::modern::TitleElement {
3541                start: element.start,
3542                end: element.end,
3543                name: element.name,
3544                name_loc: element.name_loc,
3545                attributes: element.attributes,
3546                fragment: element.fragment,
3547            })
3548        }
3549        _ if is_component_name(element.name.as_ref()) => Node::Component(Component {
3550            start: element.start,
3551            end: element.end,
3552            name: element.name,
3553            name_loc: element.name_loc,
3554            attributes: element.attributes,
3555            fragment: element.fragment,
3556        }),
3557        _ => Node::RegularElement(element),
3558    }
3559}
3560
3561fn classify_svelte_element(element: RegularElement, kind: SvelteElementKind) -> Node {
3562    match kind {
3563        SvelteElementKind::Head => Node::SvelteHead(crate::ast::modern::SvelteHead {
3564            start: element.start,
3565            end: element.end,
3566            name: element.name,
3567            name_loc: element.name_loc,
3568            attributes: element.attributes,
3569            fragment: element.fragment,
3570        }),
3571        SvelteElementKind::Body => Node::SvelteBody(crate::ast::modern::SvelteBody {
3572            start: element.start,
3573            end: element.end,
3574            name: element.name,
3575            name_loc: element.name_loc,
3576            attributes: element.attributes,
3577            fragment: element.fragment,
3578        }),
3579        SvelteElementKind::Window => Node::SvelteWindow(crate::ast::modern::SvelteWindow {
3580            start: element.start,
3581            end: element.end,
3582            name: element.name,
3583            name_loc: element.name_loc,
3584            attributes: element.attributes,
3585            fragment: element.fragment,
3586        }),
3587        SvelteElementKind::Document => Node::SvelteDocument(crate::ast::modern::SvelteDocument {
3588            start: element.start,
3589            end: element.end,
3590            name: element.name,
3591            name_loc: element.name_loc,
3592            attributes: element.attributes,
3593            fragment: element.fragment,
3594        }),
3595        SvelteElementKind::Component => {
3596            let (expression, attributes) = extract_this_expression(element.attributes);
3597            Node::SvelteComponent(crate::ast::modern::SvelteComponent {
3598                start: element.start,
3599                end: element.end,
3600                name: element.name,
3601                name_loc: element.name_loc,
3602                attributes,
3603                fragment: element.fragment,
3604                expression,
3605            })
3606        }
3607        SvelteElementKind::Element => {
3608            let (expression, attributes) = extract_this_expression(element.attributes);
3609            Node::SvelteElement(crate::ast::modern::SvelteElement {
3610                start: element.start,
3611                end: element.end,
3612                name: element.name,
3613                name_loc: element.name_loc,
3614                attributes,
3615                fragment: element.fragment,
3616                expression,
3617            })
3618        }
3619        SvelteElementKind::SelfTag => Node::SvelteSelf(crate::ast::modern::SvelteSelf {
3620            start: element.start,
3621            end: element.end,
3622            name: element.name,
3623            name_loc: element.name_loc,
3624            attributes: element.attributes,
3625            fragment: element.fragment,
3626        }),
3627        SvelteElementKind::Fragment => Node::SvelteFragment(crate::ast::modern::SvelteFragment {
3628            start: element.start,
3629            end: element.end,
3630            name: element.name,
3631            name_loc: element.name_loc,
3632            attributes: element.attributes,
3633            fragment: element.fragment,
3634        }),
3635        SvelteElementKind::Boundary => Node::SvelteBoundary(crate::ast::modern::SvelteBoundary {
3636            start: element.start,
3637            end: element.end,
3638            name: element.name,
3639            name_loc: element.name_loc,
3640            attributes: element.attributes,
3641            fragment: element.fragment,
3642        }),
3643        // Options is handled at the root level, Unknown falls through to RegularElement
3644        SvelteElementKind::Options | SvelteElementKind::Unknown => Node::RegularElement(element),
3645    }
3646}
3647
3648fn parse_modern_regular_element(
3649    source: &str,
3650    node: TsNode<'_>,
3651    in_shadowroot_template: bool,
3652    in_svelte_head: bool,
3653    loose: bool,
3654    hint: Option<&IncrementalHint<'_>>,
3655) -> RegularElement {
3656    let mut cursor = node.walk();
3657    let mut start_tag: Option<TsNode<'_>> = None;
3658    let mut end_tag: Option<TsNode<'_>> = None;
3659    let mut self_closing_tag: Option<TsNode<'_>> = None;
3660
3661    for child in node.named_children(&mut cursor) {
3662        match child.kind() {
3663            "start_tag" => start_tag = Some(child),
3664            "end_tag" => end_tag = Some(child),
3665            "self_closing_tag" => self_closing_tag = Some(child),
3666            _ => {}
3667        }
3668    }
3669
3670    let tag_node = start_tag.or(self_closing_tag);
3671    let tag_name = tag_node.and_then(|tag| find_first_named_child(tag, "tag_name"));
3672
3673    let name = tag_name
3674        .map(|tag_name| text_for_node(source, tag_name))
3675        .unwrap_or_else(|| Arc::from(""));
3676
3677    let name_loc = if let Some(tag_name) = tag_name {
3678        LegacyNameLocation {
3679            start: source_location_from_point(
3680                source,
3681                tag_name.start_position(),
3682                tag_name.start_byte(),
3683            ),
3684            end: source_location_from_point(source, tag_name.end_position(), tag_name.end_byte()),
3685        }
3686    } else {
3687        LegacyNameLocation {
3688            start: source_location_from_point(source, node.start_position(), node.start_byte()),
3689            end: source_location_from_point(source, node.start_position(), node.start_byte()),
3690        }
3691    };
3692
3693    let attributes = tag_node
3694        .map(|tag| parse_modern_attributes(source, tag, loose))
3695        .unwrap_or_default();
3696
3697    let is_shadowroot_template =
3698        matches!(classify_element_name(name.as_ref()), ElementKind::Template)
3699            && attributes.iter().any(|attr| {
3700                matches!(
3701                    attr,
3702                    Attribute::Attribute(NamedAttribute { name, .. })
3703                        if name.as_ref() == "shadowrootmode"
3704                )
3705            });
3706
3707    let children_in_svelte_head = in_svelte_head
3708        || matches!(
3709            classify_element_name(name.as_ref()),
3710            ElementKind::Svelte(SvelteElementKind::Head)
3711        );
3712
3713    let mut fragment_nodes = Vec::new();
3714    let malformed_unclosed_start_tag = start_tag
3715        .map(|tag| {
3716            end_tag.is_none()
3717                && self_closing_tag.is_none()
3718                && !text_for_node(source, tag).trim_end().ends_with('>')
3719        })
3720        .unwrap_or(false);
3721    let mut old_node_cursor = 0usize;
3722    let mut inner_cursor = node.walk();
3723    for child in node.named_children(&mut inner_cursor) {
3724        if malformed_unclosed_start_tag
3725            && let Some(tag) = start_tag
3726            && child.start_byte() >= tag.end_byte()
3727            && child.kind() != "start_tag"
3728        {
3729            continue;
3730        }
3731
3732        if start_tag.is_some_and(|tag| tag.has_error())
3733            && end_tag.is_none()
3734            && self_closing_tag.is_none()
3735            && child.kind() == "text"
3736            && source
3737                .get(child.start_byte()..child.end_byte())
3738                .is_some_and(|raw| raw.contains("/>"))
3739        {
3740            continue;
3741        }
3742
3743        let child_start = child.start_byte();
3744        let child_end = child.end_byte();
3745
3746        // Incremental reuse for element children.
3747        if let Some(ref hint) = hint {
3748            if !any_range_overlaps(hint.changed_ranges, child_start, child_end) {
3749                if let Some(reused) = try_reuse_node(
3750                    hint.old_source,
3751                    source,
3752                    hint.old_nodes,
3753                    &mut old_node_cursor,
3754                    child_start,
3755                    child_end,
3756                ) {
3757                    fragment_nodes.push(reused);
3758                    continue;
3759                }
3760            }
3761        }
3762
3763        match child.kind() {
3764            "start_tag" if Some(child) != start_tag && Some(child) != self_closing_tag => {
3765                fragment_nodes.push(parse_modern_loose_start_tag_node(source, child, None));
3766            }
3767            "end_tag" | "self_closing_tag" => {}
3768            "text" | "entity" | "raw_text" => {
3769                push_modern_text_node(&mut fragment_nodes, parse_modern_text(source, child));
3770            }
3771            "comment" => fragment_nodes.push(Node::Comment(parse_modern_comment(source, child))),
3772            "expression" => {
3773                if let Some(tag) = parse_modern_expression_tag(source, child) {
3774                    fragment_nodes.push(Node::ExpressionTag(tag));
3775                }
3776            }
3777            "element" => {
3778                let child_hint = hint.as_ref().and_then(|h| {
3779                    make_child_hint(h, &mut old_node_cursor, child_start, child_end, "element")
3780                });
3781                fragment_nodes.push(parse_modern_element_node(
3782                    source,
3783                    child,
3784                    in_shadowroot_template || is_shadowroot_template,
3785                    children_in_svelte_head,
3786                    loose,
3787                    child_hint.as_ref(),
3788                ));
3789            }
3790            kind if is_typed_block_kind(kind) => {
3791                let child_hint = hint.as_ref().and_then(|h| {
3792                    make_child_hint(h, &mut old_node_cursor, child_start, child_end, kind)
3793                });
3794                if let Some(block_node) = parse_modern_block(source, child, child_hint.as_ref()) {
3795                    fragment_nodes.push(block_node);
3796                }
3797            }
3798            kind if is_typed_tag_kind(kind) => {
3799                if let Some(tag_node) = parse_modern_tag(source, child) {
3800                    fragment_nodes.push(tag_node);
3801                }
3802            }
3803            "ERROR" => {
3804                fragment_nodes.extend(recover_modern_error_nodes(
3805                    source,
3806                    child,
3807                    in_shadowroot_template || is_shadowroot_template,
3808                ));
3809            }
3810            _ => {}
3811        }
3812    }
3813
3814    RegularElement {
3815        start: node.start_byte(),
3816        end: node.end_byte(),
3817        name,
3818        name_loc,
3819        self_closing: self_closing_tag.is_some(),
3820        has_end_tag: end_tag.is_some(),
3821        attributes: attributes.into_boxed_slice(),
3822        fragment: Fragment {
3823            r#type: FragmentType::Fragment,
3824            nodes: fragment_nodes.into_boxed_slice(),
3825        },
3826    }
3827}
3828
3829fn parse_modern_alternate(
3830    source: &str,
3831    children: &[TsNode<'_>],
3832    branch_indices: &[usize],
3833    branch_index: usize,
3834    block_end_idx: usize,
3835) -> Option<Alternate> {
3836    let branch_child_idx = *branch_indices.get(branch_index)?;
3837    let branch = *children.get(branch_child_idx)?;
3838
3839    match branch.kind() {
3840        "else_if_clause" => {
3841            let test = branch
3842                .child_by_field_name("expression")
3843                .map(|node| parse_modern_expression_field_or_empty(source, node))
3844                .unwrap_or_else(|| modern_empty_identifier_at_block_tag_end(branch));
3845            // In the new grammar, body nodes are children of else_if_clause itself
3846            let clause_children = named_children_vec(branch);
3847            let clause_body_start = body_start_index(branch, &clause_children, &["expression"]);
3848            let consequent = Fragment {
3849                r#type: FragmentType::Fragment,
3850                nodes: parse_modern_nodes_slice(
3851                    source,
3852                    &clause_children[clause_body_start..],
3853                    false,
3854                )
3855                .into_boxed_slice(),
3856            };
3857
3858            let nested_alternate = if branch_index + 1 < branch_indices.len() {
3859                parse_modern_alternate(
3860                    source,
3861                    children,
3862                    branch_indices,
3863                    branch_index + 1,
3864                    block_end_idx,
3865                )
3866                .map(Box::new)
3867            } else {
3868                None
3869            };
3870
3871            let nested_if = IfBlock {
3872                elseif: true,
3873                start: branch.start_byte(),
3874                end: children
3875                    .get(block_end_idx)
3876                    .map(|n| n.end_byte())
3877                    .unwrap_or(branch.end_byte()),
3878                test,
3879                consequent,
3880                alternate: nested_alternate,
3881            };
3882
3883            Some(Alternate::Fragment(Fragment {
3884                r#type: FragmentType::Fragment,
3885                nodes: vec![Node::IfBlock(nested_if)].into_boxed_slice(),
3886            }))
3887        }
3888        "else_clause" => {
3889            // In the new grammar, body nodes are children of else_clause itself
3890            let clause_children = named_children_vec(branch);
3891            Some(Alternate::Fragment(Fragment {
3892                r#type: FragmentType::Fragment,
3893                nodes: parse_modern_nodes_slice(source, &clause_children, false).into_boxed_slice(),
3894            }))
3895        }
3896        _ => {
3897            if branch_index + 1 < branch_indices.len() {
3898                parse_modern_alternate(
3899                    source,
3900                    children,
3901                    branch_indices,
3902                    branch_index + 1,
3903                    block_end_idx,
3904                )
3905            } else {
3906                None
3907            }
3908        }
3909    }
3910}
3911
3912fn parse_modern_nodes_slice(
3913    source: &str,
3914    nodes: &[TsNode<'_>],
3915    in_shadowroot_template: bool,
3916) -> Vec<Node> {
3917    let mut out = Vec::new();
3918    let mut previous_end = None;
3919
3920    let mut index = 0usize;
3921    while index < nodes.len() {
3922        let node = nodes[index];
3923        if let Some(gap_start) = previous_end {
3924            push_modern_gap_text(source, &mut out, gap_start, node.start_byte());
3925        }
3926
3927        match node.kind() {
3928            "text" | "entity" => push_modern_text_node(&mut out, parse_modern_text(source, node)),
3929            "comment" => out.push(Node::Comment(parse_modern_comment(source, node))),
3930            "expression" => {
3931                if let Some(tag) = parse_modern_expression_tag(source, node) {
3932                    out.push(Node::ExpressionTag(tag));
3933                }
3934            }
3935            "element" => out.push(parse_modern_element_node(
3936                source,
3937                node,
3938                in_shadowroot_template,
3939                false,
3940                false,
3941                None,
3942            )),
3943            "start_tag" => {
3944                if let Some(name) = start_end_tag_name(source, node)
3945                    && let Some(close_index) =
3946                        find_matching_loose_end_tag(source, nodes, index, name.as_ref())
3947                {
3948                    let child_nodes = parse_modern_nodes_slice(
3949                        source,
3950                        &nodes[(index + 1)..close_index],
3951                        in_shadowroot_template,
3952                    );
3953                    out.push(parse_modern_loose_start_tag_node_with_fragment(
3954                        source,
3955                        node,
3956                        child_nodes,
3957                        Some(nodes[close_index].end_byte()),
3958                    ));
3959                    index = close_index + 1;
3960                    continue;
3961                }
3962
3963                let mut stop = nodes.len();
3964                for (lookahead, candidate) in nodes.iter().enumerate().skip(index + 1) {
3965                    if is_loose_start_tag_boundary(*candidate) {
3966                        stop = lookahead;
3967                        break;
3968                    }
3969                }
3970
3971                let child_nodes = parse_modern_nodes_slice(
3972                    source,
3973                    &nodes[(index + 1)..stop],
3974                    in_shadowroot_template,
3975                );
3976                let end_override = (stop > index + 1).then(|| nodes[stop - 1].end_byte());
3977                out.push(parse_modern_loose_start_tag_node_with_fragment(
3978                    source,
3979                    node,
3980                    child_nodes,
3981                    end_override,
3982                ));
3983                index = stop;
3984                continue;
3985            }
3986            "self_closing_tag" => out.push(parse_modern_loose_start_tag_node(source, node, None)),
3987            kind if is_typed_block_kind(kind) => {
3988                if let Some(block_node) = parse_modern_block(source, node, None) {
3989                    out.push(block_node);
3990                }
3991            }
3992            kind if is_typed_tag_kind(kind) => {
3993                if let Some(tag_node) = parse_modern_tag(source, node) {
3994                    out.push(tag_node);
3995                }
3996            }
3997            "tag_name" => out.push(parse_modern_loose_tag_name_node(source, node)),
3998            "ERROR" => {
3999                out.extend(recover_modern_error_nodes(
4000                    source,
4001                    node,
4002                    in_shadowroot_template,
4003                ));
4004            }
4005            _ => {}
4006        }
4007
4008        previous_end = Some(node.end_byte());
4009        index += 1;
4010    }
4011
4012    out
4013}
4014
4015fn push_modern_gap_text(source: &str, nodes: &mut Vec<Node>, start: usize, end: usize) {
4016    if start >= end {
4017        return;
4018    }
4019    let Some(raw) = source.get(start..end) else {
4020        return;
4021    };
4022    if raw.is_empty() {
4023        return;
4024    }
4025    push_modern_text_node(
4026        nodes,
4027        Text {
4028            start,
4029            end,
4030            raw: Arc::from(raw),
4031            data: Arc::from(raw),
4032        },
4033    );
4034}
4035
4036#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4037enum BlockKind {
4038    If,
4039    Each,
4040    Await,
4041    Key,
4042    Snippet,
4043}
4044
4045impl BlockKind {
4046    fn from_node_kind(kind: &str) -> Option<Self> {
4047        match kind {
4048            "if_block" => Some(Self::If),
4049            "each_block" => Some(Self::Each),
4050            "await_block" => Some(Self::Await),
4051            "key_block" => Some(Self::Key),
4052            "snippet_block" => Some(Self::Snippet),
4053            _ => None,
4054        }
4055    }
4056}
4057
4058impl std::str::FromStr for BlockKind {
4059    type Err = ();
4060
4061    fn from_str(raw: &str) -> Result<Self, Self::Err> {
4062        match raw {
4063            "if" => Ok(Self::If),
4064            "each" => Ok(Self::Each),
4065            "await" => Ok(Self::Await),
4066            "key" => Ok(Self::Key),
4067            "snippet" => Ok(Self::Snippet),
4068            _ => Err(()),
4069        }
4070    }
4071}
4072
4073#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4074enum BlockBranchKind {
4075    Else,
4076    ElseIf,
4077    Then,
4078    Catch,
4079}
4080
4081impl std::str::FromStr for BlockBranchKind {
4082    type Err = ();
4083
4084    fn from_str(raw: &str) -> Result<Self, Self::Err> {
4085        match raw {
4086            "else" => Ok(Self::Else),
4087            "else if" => Ok(Self::ElseIf),
4088            "then" => Ok(Self::Then),
4089            "catch" => Ok(Self::Catch),
4090            _ => Err(()),
4091        }
4092    }
4093}
4094
4095impl BlockBranchKind {
4096    fn parse_await_shorthand(raw: &str) -> Option<Self> {
4097        match raw {
4098            "then" => Some(Self::Then),
4099            "catch" => Some(Self::Catch),
4100            _ => None,
4101        }
4102    }
4103}
4104
4105impl BlockKind {
4106    fn accepts(self, branch: BlockBranchKind) -> bool {
4107        match self {
4108            Self::If => matches!(branch, BlockBranchKind::Else | BlockBranchKind::ElseIf),
4109            Self::Each => branch == BlockBranchKind::Else,
4110            Self::Await => matches!(branch, BlockBranchKind::Then | BlockBranchKind::Catch),
4111            Self::Key | Self::Snippet => false,
4112        }
4113    }
4114
4115    fn expected_branch_error(self) -> ParseErrorKind {
4116        match self {
4117            Self::Await => ParseErrorKind::ExpectedTokenAwaitBranch,
4118            Self::If | Self::Each | Self::Key | Self::Snippet => ParseErrorKind::ExpectedTokenElse,
4119        }
4120    }
4121}
4122
4123pub(crate) fn is_typed_block_kind(kind: &str) -> bool {
4124    matches!(
4125        kind,
4126        "if_block" | "each_block" | "await_block" | "key_block" | "snippet_block"
4127    )
4128}
4129
4130pub(crate) fn is_typed_tag_kind(kind: &str) -> bool {
4131    matches!(
4132        kind,
4133        "html_tag" | "debug_tag" | "const_tag" | "render_tag" | "attach_tag"
4134    )
4135}
4136
4137/// Find the index in `children` where body nodes start, by skipping
4138/// field children from the hidden block start rule.
4139pub(crate) fn body_start_index(
4140    block: TsNode<'_>,
4141    children: &[TsNode<'_>],
4142    field_names: &[&str],
4143) -> usize {
4144    let mut max_idx = 0;
4145    for name in field_names {
4146        if let Some(field_node) = block.child_by_field_name(name)
4147            && let Some(idx) = children.iter().position(|c| c.id() == field_node.id())
4148        {
4149            max_idx = max_idx.max(idx + 1);
4150        }
4151    }
4152    max_idx
4153}
4154
4155fn cst_node_has_direct_token(node: TsNode<'_>, token: &str) -> bool {
4156    let mut cursor = node.walk();
4157    node.children(&mut cursor)
4158        .any(|child| !child.is_named() && child.kind() == token)
4159}
4160
4161pub fn parse_modern_expression_tag(source: &str, node: TsNode<'_>) -> Option<ExpressionTag> {
4162    let expression = parse_modern_expression(source, node)?;
4163
4164    Some(ExpressionTag {
4165        r#type: ExpressionTagType::ExpressionTag,
4166        start: node.start_byte(),
4167        end: node.end_byte(),
4168        expression,
4169    })
4170}
4171
4172/// Loose-mode expression tag: always produces a tag, falling back to an empty
4173/// Identifier when the expression cannot be parsed.
4174pub(crate) fn parse_modern_expression_tag_loose(source: &str, node: TsNode<'_>) -> ExpressionTag {
4175    let expression = parse_modern_expression(source, node)
4176        .unwrap_or_else(|| loose_empty_expression_for_braces(source, node));
4177
4178    ExpressionTag {
4179        r#type: ExpressionTagType::ExpressionTag,
4180        start: node.start_byte(),
4181        end: node.end_byte(),
4182        expression,
4183    }
4184}
4185
4186/// Produce an empty Identifier expression spanning the content between `{` and `}`.
4187fn loose_empty_expression_for_braces(source: &str, node: TsNode<'_>) -> Expression {
4188    let raw = node.utf8_text(source.as_bytes()).ok().unwrap_or_default();
4189    let inner_start = node.start_byte().saturating_add(1);
4190    let inner_end = if raw.ends_with('}') {
4191        node.end_byte().saturating_sub(1)
4192    } else {
4193        node.end_byte()
4194    };
4195    modern_empty_identifier_expression_span(inner_start, inner_end.saturating_sub(inner_start))
4196}
4197
4198fn loose_tag_name_range(
4199    source: &str,
4200    start: usize,
4201    fallback_end: usize,
4202) -> Option<(Arc<str>, usize)> {
4203    let raw = source.get(start..)?;
4204    let len = raw
4205        .chars()
4206        .take_while(|ch| !ch.is_whitespace() && *ch != '>' && *ch != '/')
4207        .map(char::len_utf8)
4208        .sum::<usize>();
4209
4210    if len == 0 {
4211        let fallback = source.get(start..fallback_end).unwrap_or_default();
4212        if fallback.is_empty() {
4213            return None;
4214        }
4215        return Some((Arc::from(fallback), fallback_end));
4216    }
4217
4218    let end = start + len;
4219    let text = source.get(start..end).unwrap_or_default();
4220    Some((Arc::from(text), end))
4221}
4222
4223fn loose_tag_name_and_loc(
4224    source: &str,
4225    container: TsNode<'_>,
4226    name_node: Option<TsNode<'_>>,
4227) -> (Arc<str>, LegacyNameLocation) {
4228    let name_start = name_node.map(|node| node.start_byte()).unwrap_or_else(|| {
4229        container
4230            .start_byte()
4231            .saturating_add(1)
4232            .min(container.end_byte())
4233    });
4234    let fallback_end = name_node.map(|node| node.end_byte()).unwrap_or(name_start);
4235
4236    if let Some((name, name_end)) = loose_tag_name_range(source, name_start, fallback_end) {
4237        return (
4238            name,
4239            LegacyNameLocation {
4240                start: source_location_from_point(
4241                    source,
4242                    name_node
4243                        .map(|node| node.start_position())
4244                        .unwrap_or_else(|| container.start_position()),
4245                    name_start,
4246                ),
4247                end: source_location_at_offset(source, name_end),
4248            },
4249        );
4250    }
4251
4252    (
4253        Arc::from(""),
4254        LegacyNameLocation {
4255            start: source_location_from_point(
4256                source,
4257                container.start_position(),
4258                container.start_byte(),
4259            ),
4260            end: source_location_from_point(
4261                source,
4262                container.start_position(),
4263                container.start_byte(),
4264            ),
4265        },
4266    )
4267}
4268
4269fn parse_modern_loose_tag_name_node(source: &str, node: TsNode<'_>) -> Node {
4270    let (name, name_loc) = loose_tag_name_and_loc(source, node, Some(node));
4271    let start = if node.start_byte() > 0
4272        && source.as_bytes().get(node.start_byte().saturating_sub(1)) == Some(&b'<')
4273    {
4274        node.start_byte() - 1
4275    } else {
4276        node.start_byte()
4277    };
4278    let end = name_loc.end.character;
4279
4280    let fragment = Fragment {
4281        r#type: FragmentType::Fragment,
4282        nodes: Box::new([]),
4283    };
4284
4285    let element = RegularElement {
4286        start,
4287        end,
4288        name,
4289        name_loc,
4290        self_closing: node.kind() == "self_closing_tag",
4291        has_end_tag: false,
4292        attributes: Box::new([]),
4293        fragment,
4294    };
4295    classify_modern_element(element, false, false)
4296}
4297
4298fn parse_modern_loose_start_tag_node(
4299    source: &str,
4300    node: TsNode<'_>,
4301    trailing_text: Option<TsNode<'_>>,
4302) -> Node {
4303    let end_override = trailing_text.map(|text| text.end_byte());
4304    let fragment_nodes = trailing_text
4305        .map(|text| vec![Node::Text(parse_modern_text(source, text))])
4306        .unwrap_or_default();
4307    parse_modern_loose_start_tag_node_with_fragment(source, node, fragment_nodes, end_override)
4308}
4309
4310fn parse_modern_loose_start_tag_node_with_fragment(
4311    source: &str,
4312    node: TsNode<'_>,
4313    fragment_nodes: Vec<Node>,
4314    end_override: Option<usize>,
4315) -> Node {
4316    let name_node = find_first_named_child(node, "tag_name");
4317    let (name, name_loc) = loose_tag_name_and_loc(source, node, name_node);
4318
4319    let end = end_override.unwrap_or_else(|| node.end_byte());
4320
4321    let attributes = parse_modern_attributes(source, node, false);
4322    let fragment = Fragment {
4323        r#type: FragmentType::Fragment,
4324        nodes: fragment_nodes.into_boxed_slice(),
4325    };
4326
4327    let element = RegularElement {
4328        start: node.start_byte(),
4329        end,
4330        name,
4331        name_loc,
4332        self_closing: node.kind() == "self_closing_tag",
4333        has_end_tag: false,
4334        attributes: attributes.into_boxed_slice(),
4335        fragment,
4336    };
4337    classify_modern_element(element, false, false)
4338}
4339
4340fn is_loose_start_tag_boundary(node: TsNode<'_>) -> bool {
4341    matches!(
4342        node.kind(),
4343        "start_tag"
4344            | "self_closing_tag"
4345            | "end_tag"
4346            | "block_end"
4347            | "else_if_clause"
4348            | "else_clause"
4349            | "await_branch"
4350    ) || is_typed_block_kind(node.kind())
4351}
4352
4353fn start_end_tag_name(source: &str, node: TsNode<'_>) -> Option<Arc<str>> {
4354    find_first_named_child(node, "tag_name").map(|name| text_for_node(source, name))
4355}
4356
4357fn find_matching_loose_end_tag(
4358    source: &str,
4359    nodes: &[TsNode<'_>],
4360    start_index: usize,
4361    target_name: &str,
4362) -> Option<usize> {
4363    let mut depth = 0usize;
4364
4365    for (index, node) in nodes.iter().enumerate().skip(start_index + 1) {
4366        match node.kind() {
4367            "start_tag" => {
4368                if let Some(name) = start_end_tag_name(source, *node)
4369                    && name.as_ref() == target_name
4370                {
4371                    depth += 1;
4372                }
4373            }
4374            "end_tag" => {
4375                if let Some(name) = start_end_tag_name(source, *node)
4376                    && name.as_ref() == target_name
4377                {
4378                    if depth == 0 {
4379                        return Some(index);
4380                    }
4381                    depth = depth.saturating_sub(1);
4382                }
4383            }
4384            _ => {}
4385        }
4386    }
4387
4388    None
4389}
4390
4391pub(crate) fn parse_modern_expression(source: &str, node: TsNode<'_>) -> Option<Expression> {
4392    let (raw, start) = expression_node_text(source, node)?;
4393    let (line, column) = line_column_at_offset(source, start);
4394    parse_modern_expression_from_text(raw, start, line, column)
4395}
4396
4397fn parse_modern_expression_error(source: &str, node: TsNode<'_>) -> Option<(usize, Arc<str>)> {
4398    let raw = node.utf8_text(source.as_bytes()).ok()?;
4399    if raw.starts_with("{:") {
4400        return None;
4401    }
4402
4403    let (raw, start) = expression_node_text(source, node)?;
4404    let (line, column) = line_column_at_offset(source, start);
4405    parse_modern_expression_error_from_text(raw, start, line, column)
4406}
4407
4408fn expression_node_text<'a>(source: &'a str, node: TsNode<'_>) -> Option<(&'a str, usize)> {
4409    if node.kind() == "expression" {
4410        if let Some(content) = node.child_by_field_name("content") {
4411            let raw = content.utf8_text(source.as_bytes()).ok()?;
4412            return Some((raw, content.start_byte()));
4413        }
4414        let raw = node.utf8_text(source.as_bytes()).ok()?;
4415        if raw.len() >= 2 && raw.starts_with('{') && raw.ends_with('}') {
4416            return Some((&raw[1..raw.len().saturating_sub(1)], node.start_byte() + 1));
4417        }
4418    }
4419
4420    Some((node.utf8_text(source.as_bytes()).ok()?, node.start_byte()))
4421}
4422
4423pub fn modern_empty_identifier_expression(node: TsNode<'_>) -> Expression {
4424    let start = node.start_byte().saturating_add(1).min(node.end_byte());
4425    modern_empty_identifier_expression_span(start, 0)
4426}
4427
4428fn modern_empty_identifier_expression_span(start: usize, len: usize) -> Expression {
4429    let end = start.saturating_add(len);
4430    Expression::empty(start, end)
4431}
4432
4433fn modern_identifier_expression_with_loc(
4434    name: Arc<str>,
4435    start: usize,
4436    end: usize,
4437    line: usize,
4438    column: usize,
4439) -> Expression {
4440    let _ = (name, line, column);
4441    Expression::empty(start, end)
4442}
4443
4444pub fn parse_modern_expression_from_text(
4445    text: &str,
4446    start_byte: usize,
4447    line: usize,
4448    column: usize,
4449) -> Option<Expression> {
4450    let trimmed = text.trim();
4451    if trimmed.is_empty() {
4452        return None;
4453    }
4454
4455    let leading_ws = text.find(trimmed).unwrap_or(0);
4456    let start = start_byte + leading_ws;
4457    let (start_line, start_col) = offset_to_line_column(text, leading_ws, line, column);
4458    let mut raw =
4459        crate::parse::parse_modern_expression_with_oxc(trimmed, start, start_line, start_col)?;
4460    raw.syntax.parens = leading_parens(trimmed, start, raw.start);
4461    attach_leading_comments_to_expression(&mut raw, trimmed, start);
4462    Some(raw)
4463}
4464
4465fn parse_modern_expression_error_from_text(
4466    text: &str,
4467    start_byte: usize,
4468    line: usize,
4469    column: usize,
4470) -> Option<(usize, Arc<str>)> {
4471    let trimmed = text.trim();
4472    if trimmed.is_empty() {
4473        return None;
4474    }
4475
4476    let leading_ws = text.find(trimmed).unwrap_or(0);
4477    let start = start_byte + leading_ws;
4478    let (start_line, start_col) = offset_to_line_column(text, leading_ws, line, column);
4479    let message = crate::parse::parse_modern_expression_error_with_oxc(
4480        trimmed, start, start_line, start_col,
4481    )?;
4482    Some((start, message))
4483}
4484
4485fn leading_parens(text: &str, start: usize, node_start: usize) -> u16 {
4486    let prefix_len = node_start.saturating_sub(start).min(text.len());
4487    let bytes = text.as_bytes();
4488    let mut i = 0usize;
4489    let mut parens = 0u16;
4490
4491    while i < prefix_len {
4492        match bytes[i] {
4493            b'(' => {
4494                parens = parens.saturating_add(1);
4495                i += 1;
4496            }
4497            b'/' if i + 1 < prefix_len && bytes[i + 1] == b'/' => {
4498                i += 2;
4499                while i < prefix_len && bytes[i] != b'\n' {
4500                    i += 1;
4501                }
4502            }
4503            b'/' if i + 1 < prefix_len && bytes[i + 1] == b'*' => {
4504                i += 2;
4505                while i + 1 < prefix_len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
4506                    i += 1;
4507                }
4508                i = (i + 2).min(prefix_len);
4509            }
4510            b' ' | b'\t' | b'\r' | b'\n' => {
4511                i += 1;
4512            }
4513            _ => {
4514                i += 1;
4515            }
4516        }
4517    }
4518
4519    parens
4520}
4521
4522pub fn attach_leading_comments_to_expression(
4523    _expression: &mut Expression,
4524    _source: &str,
4525    _global_start: usize,
4526) {
4527}
4528
4529pub fn attach_trailing_comments_to_expression(
4530    _expression: &mut Expression,
4531    _source: &str,
4532    _global_start: usize,
4533) {
4534}
4535
4536pub fn modern_node_span(node: &Node) -> (usize, usize) {
4537    (node.start(), node.end())
4538}
4539
4540pub fn expression_identifier_name(expression: &Expression) -> Option<Arc<str>> {
4541    crate::parse::oxc_query::expression_identifier_name(expression)
4542}
4543
4544pub fn expression_literal_string(expression: &Expression) -> Option<Arc<str>> {
4545    crate::parse::oxc_query::expression_literal_string(expression)
4546}
4547
4548pub fn expression_literal_bool(expression: &Expression) -> Option<bool> {
4549    crate::parse::oxc_query::expression_literal_bool(expression)
4550}
4551
4552fn offset_to_line_column(
4553    text: &str,
4554    offset: usize,
4555    base_line: usize,
4556    base_column: usize,
4557) -> (usize, usize) {
4558    let mut line = base_line;
4559    let mut column = base_column;
4560    let bytes = text.as_bytes();
4561    let limit = offset.min(bytes.len());
4562
4563    for byte in bytes.iter().take(limit) {
4564        if *byte == b'\n' {
4565            line += 1;
4566            column = 0;
4567        } else {
4568            column += 1;
4569        }
4570    }
4571
4572    (line, column)
4573}
4574
4575pub fn legacy_expression_from_modern_expression(
4576    expression: Expression,
4577    include_character: bool,
4578) -> Option<LegacyExpression> {
4579    super::legacy::legacy_expression_from_modern(expression, include_character)
4580}
4581
4582pub fn named_children_vec(node: TsNode<'_>) -> Vec<TsNode<'_>> {
4583    let mut cursor = node.walk();
4584    node.named_children(&mut cursor).collect()
4585}