Skip to main content

svelte_syntax/
cst.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use self_cell::self_cell;
6use tree_sitter::{InputEdit, Node, Parser, Point, Tree, TreeCursor};
7
8use crate::ast::modern::Expression;
9use crate::error::CompileError;
10use crate::parse::is_component_name;
11use crate::primitives::{BytePos, Span};
12use crate::source::SourceText;
13
14/// Languages supported by the CST parser.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Language {
17    /// The Svelte component language.
18    Svelte,
19}
20
21// ---------------------------------------------------------------------------
22// ExpressionCache — pre-parsed OXC expressions indexed by byte offset
23// ---------------------------------------------------------------------------
24
25/// Cache of pre-parsed OXC expressions, keyed by node start byte offset.
26#[derive(Debug, Default)]
27pub struct ExpressionCache {
28    expressions: HashMap<usize, Expression>,
29}
30
31impl ExpressionCache {
32    /// Build the cache by walking the tree and parsing all expression nodes.
33    pub fn from_tree(source: &str, tree: &Tree) -> Self {
34        let mut cache = Self::default();
35        cache.walk_and_parse(source, tree.root_node());
36        cache
37    }
38
39    /// Look up a pre-parsed expression by its node's start byte offset.
40    pub fn get(&self, start_byte: usize) -> Option<&Expression> {
41        self.expressions.get(&start_byte)
42    }
43
44    /// Number of cached expressions.
45    pub fn len(&self) -> usize {
46        self.expressions.len()
47    }
48
49    /// Whether the cache is empty.
50    pub fn is_empty(&self) -> bool {
51        self.expressions.is_empty()
52    }
53
54    fn walk_and_parse(&mut self, source: &str, node: Node<'_>) {
55        match node.kind() {
56            "expression" | "expression_value" => {
57                self.parse_and_insert(source, node);
58            }
59            _ => {}
60        }
61
62        let mut cursor = node.walk();
63        for child in node.children(&mut cursor) {
64            self.walk_and_parse(source, child);
65        }
66    }
67
68    fn parse_and_insert(&mut self, source: &str, node: Node<'_>) {
69        if let Some(expr) = parse_expression_from_node(source, node) {
70            self.expressions.insert(node.start_byte(), expr);
71        }
72    }
73}
74
75/// Parse a tree-sitter expression/expression_value node into an OXC `Expression`.
76fn parse_expression_from_node(source: &str, node: Node<'_>) -> Option<Expression> {
77    let raw = node.utf8_text(source.as_bytes()).ok()?;
78
79    // For expression nodes with `{...}` delimiters, extract the inner content
80    let (text, start_byte) = if node.kind() == "expression" {
81        if let Some(content) = node.child_by_field_name("content") {
82            let t = content.utf8_text(source.as_bytes()).ok()?;
83            (t, content.start_byte())
84        } else if raw.len() >= 2 && raw.starts_with('{') && raw.ends_with('}') {
85            (&raw[1..raw.len() - 1], node.start_byte() + 1)
86        } else {
87            (raw, node.start_byte())
88        }
89    } else {
90        (raw, node.start_byte())
91    };
92
93    let trimmed = text.trim();
94    if trimmed.is_empty() {
95        return None;
96    }
97
98    let leading = text.find(trimmed).unwrap_or(0);
99    let abs = start_byte + leading;
100    let (line, column) = crate::parse::line_column_at_offset(source, abs);
101    crate::parse::parse_modern_expression_from_text(trimmed, abs, line, column)
102}
103
104// ---------------------------------------------------------------------------
105// ParsedDocument — self-contained owning document (source + tree + expressions)
106// ---------------------------------------------------------------------------
107
108struct ParsedDocumentOwner {
109    source: Arc<str>,
110    tree: Tree,
111    expressions: ExpressionCache,
112}
113
114/// The dependent data borrowing from the owner.
115struct ParsedDocumentDependent<'a> {
116    root: Root<'a>,
117}
118
119self_cell! {
120    /// A fully parsed, self-contained document.
121    ///
122    /// Owns the source text, tree-sitter tree, and pre-parsed expression cache.
123    /// Provides zero-copy wrapper access through `root()`.
124    pub struct ParsedDocument {
125        owner: ParsedDocumentOwner,
126
127        #[covariant]
128        dependent: ParsedDocumentDependent,
129    }
130}
131
132impl std::fmt::Debug for ParsedDocument {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        f.debug_struct("ParsedDocument")
135            .field("source_len", &self.source().len())
136            .field("expressions", &self.expressions().len())
137            .finish()
138    }
139}
140
141// SAFETY: ParsedDocument owns all its data (Arc<str>, Tree, ExpressionCache).
142// The !Send/!Sync comes from self_cell's self-referential borrow, but the
143// underlying data is fully owned and not shared across threads unsafely.
144unsafe impl Send for ParsedDocument {}
145unsafe impl Sync for ParsedDocument {}
146
147impl ParsedDocument {
148    /// Parse source into a fully self-contained document.
149    pub fn parse(source: &str) -> Result<Self, CompileError> {
150        let tree = {
151            let mut parser = CstParser::new().configure(Language::Svelte)?;
152            let st = SourceText::new(crate::primitives::SourceId::new(0), source, None);
153            let doc = parser.parse(st)?;
154            doc.tree
155        };
156        let expressions = ExpressionCache::from_tree(source, &tree);
157        let source_arc: Arc<str> = Arc::from(source);
158
159        Ok(ParsedDocument::new(
160            ParsedDocumentOwner {
161                source: source_arc,
162                tree,
163                expressions,
164            },
165            |owner| ParsedDocumentDependent {
166                root: Root::new(&owner.source, owner.tree.root_node()),
167            },
168        ))
169    }
170
171    /// The source text.
172    pub fn source(&self) -> &str {
173        &self.borrow_owner().source
174    }
175
176    /// The root wrapper node.
177    pub fn root(&self) -> &Root<'_> {
178        &self.borrow_dependent().root
179    }
180
181    /// The pre-parsed expression cache.
182    pub fn expressions(&self) -> &ExpressionCache {
183        &self.borrow_owner().expressions
184    }
185
186    /// The underlying tree-sitter tree.
187    pub fn tree(&self) -> &Tree {
188        &self.borrow_owner().tree
189    }
190}
191
192// ---------------------------------------------------------------------------
193// Document — legacy borrowed document (kept for incremental parsing support)
194// ---------------------------------------------------------------------------
195
196/// A parsed tree-sitter document holding the source text, language, and
197/// concrete syntax tree.
198///
199/// Use [`parse_svelte`] to create a `Document` from source text, or
200/// [`CstParser`] for more control over parser reuse.
201#[derive(Debug)]
202pub struct Document<'src> {
203    /// The language this document was parsed as.
204    pub language: Language,
205    /// The original source text.
206    pub source: SourceText<'src>,
207    /// The tree-sitter syntax tree.
208    pub tree: Tree,
209}
210
211impl<'src> Document<'src> {
212    /// Return the root tree-sitter node.
213    pub fn root_node(&self) -> Node<'_> {
214        self.tree.root_node()
215    }
216
217    /// Return the root node kind.
218    pub fn root_kind(&self) -> &str {
219        self.root_node().kind()
220    }
221
222    /// Return `true` if the CST contains parse errors.
223    pub fn has_error(&self) -> bool {
224        self.root_node().has_error()
225    }
226
227    /// Return the root node span in byte offsets.
228    pub fn root_span(&self) -> Span {
229        node_span(self.root_node())
230    }
231
232    /// Apply an edit to the stored tree so it can be reused for incremental reparsing.
233    pub fn apply_edit(&mut self, edit: CstEdit) {
234        self.tree.edit(&edit.into_input_edit());
235    }
236
237    /// Clone the tree for incremental parsing. The source text reference is
238    /// preserved but the tree is cloned so `apply_edit` can be called on the
239    /// copy without mutating the original.
240    pub fn clone_for_incremental(&self) -> Document<'src> {
241        Document {
242            language: self.language,
243            source: self.source,
244            tree: self.tree.clone(),
245        }
246    }
247
248    /// Return byte ranges that differ structurally between this document and a
249    /// previously parsed document. Wraps [`Tree::changed_ranges`].
250    pub fn changed_ranges(&self, old: &Document<'_>) -> Vec<std::ops::Range<usize>> {
251        old.tree
252            .changed_ranges(&self.tree)
253            .map(|r| r.start_byte..r.end_byte)
254            .collect()
255    }
256}
257
258/// A row/column position in source text, used by [`CstEdit`].
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub struct CstPoint {
261    /// Zero-based line number.
262    pub row: usize,
263    /// Zero-based byte column within the line.
264    pub column: usize,
265}
266
267/// Describes a text edit for incremental reparsing.
268///
269/// Records the byte range that was replaced and the resulting positions after
270/// the edit. Use the convenience constructors [`CstEdit::replace`],
271/// [`CstEdit::insert`], and [`CstEdit::delete`] to build edits from the old
272/// source text.
273///
274/// # Example
275///
276/// ```
277/// use svelte_syntax::CstEdit;
278///
279/// let old = "<div>Hello</div>";
280/// let edit = CstEdit::replace(old, 5, 10, "World");
281///
282/// assert_eq!(edit.start_byte, 5);
283/// assert_eq!(edit.new_end_byte, 10); // 5 + "World".len()
284/// ```
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct CstEdit {
287    /// Byte offset where the edit begins.
288    pub start_byte: usize,
289    /// Byte offset where the old text ended (before the edit).
290    pub old_end_byte: usize,
291    /// Byte offset where the new text ends (after the edit).
292    pub new_end_byte: usize,
293    /// Row/column position where the edit begins.
294    pub start_position: CstPoint,
295    /// Row/column position where the old text ended.
296    pub old_end_position: CstPoint,
297    /// Row/column position where the new text ends.
298    pub new_end_position: CstPoint,
299}
300
301impl CstEdit {
302    /// Create an edit that replaces `old_source[start_byte..old_end_byte]` with
303    /// `new_text`. Positions are computed automatically from the old source.
304    pub fn replace(
305        old_source: &str,
306        start_byte: usize,
307        old_end_byte: usize,
308        new_text: &str,
309    ) -> Self {
310        let start_position = byte_point_at_offset(old_source, start_byte);
311        let old_end_position = byte_point_at_offset(old_source, old_end_byte);
312        let new_end_byte = start_byte.saturating_add(new_text.len());
313        let new_end_position = advance_point(start_position, new_text);
314
315        Self {
316            start_byte,
317            old_end_byte,
318            new_end_byte,
319            start_position,
320            old_end_position,
321            new_end_position,
322        }
323    }
324
325    /// Create an edit that inserts `new_text` at `start_byte` without removing
326    /// any existing text.
327    pub fn insert(old_source: &str, start_byte: usize, new_text: &str) -> Self {
328        Self::replace(old_source, start_byte, start_byte, new_text)
329    }
330
331    /// Create an edit that deletes `old_source[start_byte..old_end_byte]`.
332    pub fn delete(old_source: &str, start_byte: usize, old_end_byte: usize) -> Self {
333        Self::replace(old_source, start_byte, old_end_byte, "")
334    }
335
336    fn into_input_edit(self) -> InputEdit {
337        InputEdit {
338            start_byte: self.start_byte,
339            old_end_byte: self.old_end_byte,
340            new_end_byte: self.new_end_byte,
341            start_position: self.start_position.into_point(),
342            old_end_position: self.old_end_position.into_point(),
343            new_end_position: self.new_end_position.into_point(),
344        }
345    }
346}
347
348impl CstPoint {
349    fn into_point(self) -> Point {
350        Point {
351            row: self.row,
352            column: self.column,
353        }
354    }
355}
356
357/// Typestate marker for a parser before a language has been selected.
358pub struct Unconfigured;
359/// Typestate marker for a parser after a language has been selected.
360pub struct Configured {
361    language: Language,
362}
363
364/// Tree-sitter-backed CST parser with typestate for language selection.
365///
366/// Create a parser with [`CstParser::new`], configure it with
367/// [`CstParser::configure`], then call [`parse`](CstParser::parse) or
368/// [`parse_incremental`](CstParser::parse_incremental).
369///
370/// For a simpler one-shot API, use the free function [`parse_svelte`].
371///
372/// # Example
373///
374/// ```
375/// use svelte_syntax::cst::{CstParser, Language};
376/// use svelte_syntax::{SourceId, SourceText};
377///
378/// let mut parser = CstParser::new().configure(Language::Svelte)?;
379/// let source = SourceText::new(SourceId::new(0), "<p>hi</p>", None);
380/// let doc = parser.parse(source)?;
381///
382/// assert_eq!(doc.root_kind(), "document");
383/// # Ok::<(), svelte_syntax::CompileError>(())
384/// ```
385pub struct CstParser<State> {
386    parser: Parser,
387    state: State,
388}
389
390impl CstParser<Unconfigured> {
391    /// Create a parser with no configured language.
392    pub fn new() -> Self {
393        Self {
394            parser: Parser::new(),
395            state: Unconfigured,
396        }
397    }
398
399    /// Configure the parser for a supported language.
400    pub fn configure(mut self, language: Language) -> Result<CstParser<Configured>, CompileError> {
401        let ts_lang = match language {
402            Language::Svelte => tree_sitter_svelte::language(),
403        };
404
405        self.parser
406            .set_language(&ts_lang)
407            .map_err(|_| CompileError::internal("failed to configure tree-sitter language"))?;
408
409        Ok(CstParser {
410            parser: self.parser,
411            state: Configured { language },
412        })
413    }
414}
415
416impl Default for CstParser<Unconfigured> {
417    fn default() -> Self {
418        Self::new()
419    }
420}
421
422impl CstParser<Configured> {
423    /// Parse source text into a CST document.
424    pub fn parse<'src>(
425        &mut self,
426        source: SourceText<'src>,
427    ) -> Result<Document<'src>, CompileError> {
428        let tree = self
429            .parser
430            .parse(source.text, None)
431            .ok_or_else(|| CompileError::internal("tree-sitter parser returned no syntax tree"))?;
432
433        Ok(Document {
434            language: self.state.language,
435            source,
436            tree,
437        })
438    }
439
440    /// Parse source text using a previous tree plus edit information for incremental reparsing.
441    pub fn parse_incremental<'src>(
442        &mut self,
443        source: SourceText<'src>,
444        previous: &Document<'_>,
445        edit: CstEdit,
446    ) -> Result<Document<'src>, CompileError> {
447        let mut previous_tree = previous.tree.clone();
448        previous_tree.edit(&edit.into_input_edit());
449
450        let tree = self
451            .parser
452            .parse(source.text, Some(&previous_tree))
453            .ok_or_else(|| CompileError::internal("tree-sitter parser returned no syntax tree"))?;
454
455        Ok(Document {
456            language: self.state.language,
457            source,
458            tree,
459        })
460    }
461}
462
463/// Parse Svelte source into a tree-sitter CST document.
464///
465/// This is the simplest way to obtain a concrete syntax tree. For parser
466/// reuse across multiple files, use [`CstParser`] directly.
467///
468/// # Example
469///
470/// ```
471/// use svelte_syntax::{SourceId, SourceText, parse_svelte};
472///
473/// let source = SourceText::new(SourceId::new(0), "<div>hello</div>", None);
474/// let cst = parse_svelte(source)?;
475///
476/// assert_eq!(cst.root_kind(), "document");
477/// # Ok::<(), svelte_syntax::CompileError>(())
478/// ```
479pub fn parse_svelte<'src>(source: SourceText<'src>) -> Result<Document<'src>, CompileError> {
480    let mut parser = CstParser::new().configure(Language::Svelte)?;
481    parser.parse(source)
482}
483
484/// Parse Svelte source using an already-edited old tree for incremental reparsing.
485/// Unlike `parse_svelte_incremental`, this expects the caller to have already
486/// called `apply_edit` on the old document.
487pub fn parse_svelte_with_old_tree<'src>(
488    source: SourceText<'src>,
489    edited_old: &Document<'_>,
490) -> Result<Document<'src>, CompileError> {
491    let ts_lang = match edited_old.language {
492        Language::Svelte => tree_sitter_svelte::language(),
493    };
494    let mut parser = Parser::new();
495    parser
496        .set_language(&ts_lang)
497        .map_err(|_| CompileError::internal("failed to configure tree-sitter language"))?;
498    let tree = parser
499        .parse(source.text, Some(&edited_old.tree))
500        .ok_or_else(|| CompileError::internal("tree-sitter parser returned no syntax tree"))?;
501    Ok(Document {
502        language: edited_old.language,
503        source,
504        tree,
505    })
506}
507
508/// Parse Svelte source into a tree-sitter CST using a previous CST and edit for incremental reparsing.
509pub fn parse_svelte_incremental<'src>(
510    source: SourceText<'src>,
511    previous: &Document<'_>,
512    edit: CstEdit,
513) -> Result<Document<'src>, CompileError> {
514    let mut parser = CstParser::new().configure(Language::Svelte)?;
515    parser.parse_incremental(source, previous, edit)
516}
517
518fn node_span(node: Node<'_>) -> Span {
519    let start = byte_pos_saturating(node.start_byte());
520    let end = byte_pos_saturating(node.end_byte());
521    Span::new(start, end)
522}
523
524fn byte_pos_saturating(offset: usize) -> BytePos {
525    u32::try_from(offset)
526        .map(BytePos::from)
527        .unwrap_or_else(|_| BytePos::from(u32::MAX))
528}
529
530fn byte_point_at_offset(source: &str, offset: usize) -> CstPoint {
531    let bounded = offset.min(source.len());
532    let mut row = 0usize;
533    let mut column = 0usize;
534
535    for byte in source.as_bytes().iter().take(bounded) {
536        if *byte == b'\n' {
537            row += 1;
538            column = 0;
539        } else {
540            column += 1;
541        }
542    }
543
544    CstPoint { row, column }
545}
546
547fn advance_point(start: CstPoint, inserted_text: &str) -> CstPoint {
548    let mut point = start;
549
550    for byte in inserted_text.as_bytes() {
551        if *byte == b'\n' {
552            point.row += 1;
553            point.column = 0;
554        } else {
555            point.column += 1;
556        }
557    }
558
559    point
560}
561
562// ---------------------------------------------------------------------------
563// Thin wrapper types — zero-copy views over tree-sitter nodes
564// ---------------------------------------------------------------------------
565
566/// Extract the text of a tree-sitter node from source.
567fn node_text<'src>(source: &'src str, node: Node<'_>) -> &'src str {
568    &source[node.start_byte()..node.end_byte()]
569}
570
571/// A thin wrapper around a tree-sitter node and source text.
572/// All wrapper types share this layout: `(&str, Node)`.
573macro_rules! define_wrapper {
574    ($($(#[$meta:meta])* $name:ident),* $(,)?) => {
575        $(
576            $(#[$meta])*
577            #[derive(Clone, Copy)]
578            pub struct $name<'src> {
579                source: &'src str,
580                node: Node<'src>,
581            }
582
583            impl<'src> $name<'src> {
584                /// Create from source text and a matching tree-sitter node.
585                pub fn new(source: &'src str, node: Node<'src>) -> Self {
586                    Self { source, node }
587                }
588
589                /// The underlying tree-sitter node.
590                pub fn ts_node(&self) -> Node<'src> {
591                    self.node
592                }
593
594                /// Byte offset of the start of this node.
595                pub fn start(&self) -> usize {
596                    self.node.start_byte()
597                }
598
599                /// Byte offset of the end of this node.
600                pub fn end(&self) -> usize {
601                    self.node.end_byte()
602                }
603
604                /// Span covering this node.
605                pub fn span(&self) -> Span {
606                    node_span(self.node)
607                }
608
609                /// The raw source text of this node.
610                pub fn text(&self) -> &'src str {
611                    node_text(self.source, self.node)
612                }
613
614                /// Whether this node has parse errors.
615                pub fn has_error(&self) -> bool {
616                    self.node.has_error()
617                }
618            }
619
620            impl std::fmt::Debug for $name<'_> {
621                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622                    f.debug_struct(stringify!($name))
623                        .field("kind", &self.node.kind())
624                        .field("range", &(self.start()..self.end()))
625                        .finish()
626                }
627            }
628        )*
629    };
630}
631
632define_wrapper!(
633    /// Root document node.
634    Root,
635    /// An HTML/Svelte element.
636    Element,
637    /// A text node.
638    TextNode,
639    /// An HTML comment.
640    CommentNode,
641    /// `{#if}...{:else if}...{:else}...{/if}`
642    IfBlock,
643    /// `{#each items as item}...{/each}`
644    EachBlock,
645    /// `{#await promise}...{:then}...{:catch}...{/await}`
646    AwaitBlock,
647    /// `{#key expression}...{/key}`
648    KeyBlock,
649    /// `{#snippet name(params)}...{/snippet}`
650    SnippetBlock,
651    /// `{expression}`
652    ExpressionTag,
653    /// `{@html expression}`
654    HtmlTag,
655    /// `{@const assignment}`
656    ConstTag,
657    /// `{@debug vars}`
658    DebugTag,
659    /// `{@render snippet()}`
660    RenderTag,
661    /// `{@attach handler}`
662    AttachTag,
663    /// An attribute on an element.
664    AttributeNode,
665    /// A start tag `<name ...>`.
666    StartTag,
667);
668
669// --- Root accessors ---
670
671impl<'src> Root<'src> {
672    /// Iterate over top-level child nodes as `TemplateNode`s.
673    pub fn children(&self) -> ChildIter<'src> {
674        ChildIter::new(self.source, self.node)
675    }
676}
677
678// --- Element accessors ---
679
680impl<'src> Element<'src> {
681    /// The element's tag name (e.g., "div", "Button", "svelte:head").
682    pub fn name(&self) -> &'src str {
683        // First child is start_tag or self_closing_tag
684        let tag = self.node.child(0).expect("element must have a tag");
685        if let Some(name_node) = tag.child_by_field_name("name") {
686            node_text(self.source, name_node)
687        } else {
688            // Fallback: find first tag_name child
689            let mut cursor = tag.walk();
690            for child in tag.children(&mut cursor) {
691                if child.kind() == "tag_name" {
692                    return node_text(self.source, child);
693                }
694            }
695            ""
696        }
697    }
698
699    /// Whether this element uses self-closing syntax (`<br />`).
700    pub fn is_self_closing(&self) -> bool {
701        self.node.child(0)
702            .is_some_and(|tag| tag.kind() == "self_closing_tag")
703    }
704
705    /// Whether this element has an explicit end tag.
706    pub fn has_end_tag(&self) -> bool {
707        let mut cursor = self.node.walk();
708        self.node.children(&mut cursor).any(|c| c.kind() == "end_tag")
709    }
710
711    /// The start tag node.
712    pub fn start_tag(&self) -> Option<StartTag<'src>> {
713        let first = self.node.child(0)?;
714        match first.kind() {
715            "start_tag" | "self_closing_tag" => Some(StartTag::new(self.source, first)),
716            _ => None,
717        }
718    }
719
720    /// Iterate over attributes on this element.
721    pub fn attributes(&self) -> AttributeIter<'src> {
722        let tag = self.node.child(0).expect("element must have a tag");
723        AttributeIter {
724            source: self.source,
725            cursor: tag.walk(),
726            started: false,
727        }
728    }
729
730    /// Iterate over child content nodes (between start and end tags).
731    pub fn children(&self) -> ChildIter<'src> {
732        ChildIter::new(self.source, self.node)
733    }
734
735    /// Whether this element's name indicates a component.
736    pub fn is_component(&self) -> bool {
737        is_component_name(self.name())
738    }
739
740    /// Classify this element into a `TemplateNode` variant based on its name.
741    pub fn classify(&self) -> TemplateNode<'src> {
742        classify_element(self.source, self.node)
743    }
744}
745
746// --- StartTag accessors ---
747
748impl<'src> StartTag<'src> {
749    /// The tag name.
750    pub fn name(&self) -> &'src str {
751        if let Some(name_node) = self.node.child_by_field_name("name") {
752            node_text(self.source, name_node)
753        } else {
754            ""
755        }
756    }
757
758    /// Iterate over attributes on this start tag.
759    pub fn attributes(&self) -> AttributeIter<'src> {
760        AttributeIter {
761            source: self.source,
762            cursor: self.node.walk(),
763            started: false,
764        }
765    }
766}
767
768// --- TextNode accessors ---
769
770impl<'src> TextNode<'src> {
771    /// The raw text content.
772    pub fn raw(&self) -> &'src str {
773        node_text(self.source, self.node)
774    }
775
776    /// Decoded text content (HTML entities resolved).
777    pub fn data(&self) -> Cow<'src, str> {
778        // For now, return raw. Entity decoding can be added later.
779        Cow::Borrowed(self.raw())
780    }
781}
782
783// --- CommentNode accessors ---
784
785impl<'src> CommentNode<'src> {
786    /// The comment content (without `<!--` and `-->`).
787    pub fn data(&self) -> &'src str {
788        let raw = self.raw();
789        raw.strip_prefix("<!--")
790            .and_then(|s| s.strip_suffix("-->"))
791            .unwrap_or(raw)
792    }
793
794    fn raw(&self) -> &'src str {
795        node_text(self.source, self.node)
796    }
797}
798
799// --- Block accessors ---
800
801impl<'src> IfBlock<'src> {
802    /// The expression node for the test condition.
803    pub fn test_node(&self) -> Option<Node<'src>> {
804        self.node.child_by_field_name("expression")
805    }
806
807    /// The raw test expression text.
808    pub fn test_text(&self) -> &'src str {
809        self.test_node()
810            .map(|n| node_text(self.source, n))
811            .unwrap_or("")
812    }
813
814    /// The pre-parsed test expression from the cache.
815    pub fn test_expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
816        self.test_node().and_then(|n| cache.get(n.start_byte()))
817    }
818
819    /// Iterate over consequent (then-branch) child nodes.
820    pub fn consequent(&self) -> ChildIter<'src> {
821        ChildIter::new(self.source, self.node)
822    }
823
824    /// The else clause, if any. Returns either an else fragment or an else-if block.
825    pub fn alternate(&self) -> Option<Alternate<'src>> {
826        if self.node.kind() == "else_if_clause" {
827            // When wrapping an else_if_clause, the alternate is the next
828            // sibling clause from the parent if_block, not a child of this node.
829            let mut sibling = self.node.next_named_sibling();
830            while let Some(s) = sibling {
831                match s.kind() {
832                    "else_clause" => return Some(Alternate::Else(ElseClause::new(self.source, s))),
833                    "else_if_clause" => return Some(Alternate::ElseIf(IfBlock::new(self.source, s))),
834                    _ => {}
835                }
836                sibling = s.next_named_sibling();
837            }
838            return None;
839        }
840        let mut cursor = self.node.walk();
841        for child in self.node.children(&mut cursor) {
842            match child.kind() {
843                "else_clause" => return Some(Alternate::Else(ElseClause::new(self.source, child))),
844                "else_if_clause" => return Some(Alternate::ElseIf(IfBlock::new(self.source, child))),
845                _ => {}
846            }
847        }
848        None
849    }
850}
851
852impl<'src> EachBlock<'src> {
853    /// The iterable expression node.
854    pub fn expression_node(&self) -> Option<Node<'src>> {
855        self.node.child_by_field_name("expression")
856    }
857
858    /// The pre-parsed iterable expression from the cache.
859    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
860        self.expression_node().and_then(|n| cache.get(n.start_byte()))
861    }
862
863    /// The binding pattern node.
864    pub fn binding_node(&self) -> Option<Node<'src>> {
865        self.node.child_by_field_name("binding")
866    }
867
868    /// The key expression node, if any.
869    pub fn key_node(&self) -> Option<Node<'src>> {
870        self.node.child_by_field_name("key")
871    }
872
873    /// The index identifier node, if any.
874    pub fn index_node(&self) -> Option<Node<'src>> {
875        self.node.child_by_field_name("index")
876    }
877
878    /// Iterate over body child nodes.
879    pub fn body(&self) -> ChildIter<'src> {
880        ChildIter::new(self.source, self.node)
881    }
882
883    /// The else (fallback) clause, if any.
884    pub fn fallback(&self) -> Option<ElseClause<'src>> {
885        let mut cursor = self.node.walk();
886        for child in self.node.children(&mut cursor) {
887            if child.kind() == "else_clause" {
888                return Some(ElseClause::new(self.source, child));
889            }
890        }
891        None
892    }
893}
894
895impl<'src> AwaitBlock<'src> {
896    /// The promise expression node.
897    pub fn expression_node(&self) -> Option<Node<'src>> {
898        self.node.child_by_field_name("expression")
899    }
900
901    /// The pre-parsed promise expression from the cache.
902    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
903        self.expression_node().and_then(|n| cache.get(n.start_byte()))
904    }
905
906    /// The pending (loading) body, if any.
907    pub fn pending(&self) -> Option<ChildIter<'src>> {
908        let mut cursor = self.node.walk();
909        for child in self.node.children(&mut cursor) {
910            if child.kind() == "await_pending" {
911                return Some(ChildIter::new(self.source, child));
912            }
913        }
914        None
915    }
916
917    /// The then branch children, if any.
918    pub fn then_children(&self) -> Option<ChildIter<'src>> {
919        // Check for shorthand form: {#await expr then value}...{/await}
920        if let Some(shorthand) = self.node.child_by_field_name("shorthand") {
921            if node_text(self.source, shorthand) == "then" {
922                if let Some(children) = self.node.child_by_field_name("shorthand_children") {
923                    return Some(ChildIter::new(self.source, children));
924                }
925            }
926        }
927        self.branch_children("then")
928    }
929
930    /// The catch branch children, if any.
931    pub fn catch_children(&self) -> Option<ChildIter<'src>> {
932        // Check for shorthand form: {#await expr catch error}...{/await}
933        if let Some(shorthand) = self.node.child_by_field_name("shorthand") {
934            if node_text(self.source, shorthand) == "catch" {
935                if let Some(children) = self.node.child_by_field_name("shorthand_children") {
936                    return Some(ChildIter::new(self.source, children));
937                }
938            }
939        }
940        self.branch_children("catch")
941    }
942
943    /// The then binding pattern text (e.g., "value" in `{:then value}`).
944    pub fn then_binding_text(&self) -> Option<&'src str> {
945        // Shorthand form: {#await expr then value}...{/await}
946        if let Some(shorthand) = self.node.child_by_field_name("shorthand") {
947            if node_text(self.source, shorthand) == "then" {
948                return self.node.child_by_field_name("binding")
949                    .map(|n| node_text(self.source, n));
950            }
951        }
952        self.branch_binding("then")
953    }
954
955    /// The catch binding pattern text (e.g., "error" in `{:catch error}`).
956    pub fn catch_binding_text(&self) -> Option<&'src str> {
957        // Shorthand form: {#await expr catch error}...{/await}
958        if let Some(shorthand) = self.node.child_by_field_name("shorthand") {
959            if node_text(self.source, shorthand) == "catch" {
960                return self.node.child_by_field_name("binding")
961                    .map(|n| node_text(self.source, n));
962            }
963        }
964        self.branch_binding("catch")
965    }
966
967    fn branch_binding(&self, kind_name: &str) -> Option<&'src str> {
968        let mut cursor = self.node.walk();
969        for child in self.node.children(&mut cursor) {
970            if child.kind() == "await_branch" {
971                let mut inner = child.walk();
972                for c in child.children(&mut inner) {
973                    if c.kind() == "branch_kind" && node_text(self.source, c) == kind_name {
974                        return child.child_by_field_name("binding")
975                            .or_else(|| child.child_by_field_name("expression"))
976                            .map(|n| node_text(self.source, n));
977                    }
978                }
979            }
980        }
981        None
982    }
983
984    fn branch_children(&self, kind_name: &str) -> Option<ChildIter<'src>> {
985        let mut cursor = self.node.walk();
986        for child in self.node.children(&mut cursor) {
987            if child.kind() == "await_branch" {
988                // Check branch_kind
989                let mut inner = child.walk();
990                for c in child.children(&mut inner) {
991                    if c.kind() == "branch_kind" && node_text(self.source, c) == kind_name {
992                        // Find await_branch_children
993                        let mut inner2 = child.walk();
994                        for c2 in child.children(&mut inner2) {
995                            if c2.kind() == "await_branch_children" {
996                                return Some(ChildIter::new(self.source, c2));
997                            }
998                        }
999                    }
1000                }
1001            }
1002        }
1003        None
1004    }
1005}
1006
1007impl<'src> KeyBlock<'src> {
1008    /// The key expression node.
1009    pub fn expression_node(&self) -> Option<Node<'src>> {
1010        self.node.child_by_field_name("expression")
1011    }
1012
1013    /// The pre-parsed key expression from the cache.
1014    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1015        self.expression_node().and_then(|n| cache.get(n.start_byte()))
1016    }
1017
1018    /// Iterate over body child nodes.
1019    pub fn body(&self) -> ChildIter<'src> {
1020        ChildIter::new(self.source, self.node)
1021    }
1022}
1023
1024impl<'src> SnippetBlock<'src> {
1025    /// The snippet name.
1026    pub fn name(&self) -> &'src str {
1027        let mut cursor = self.node.walk();
1028        for child in self.node.children(&mut cursor) {
1029            if child.kind() == "snippet_name" {
1030                return node_text(self.source, child);
1031            }
1032        }
1033        ""
1034    }
1035
1036    /// The snippet parameters node, if any.
1037    pub fn parameters_node(&self) -> Option<Node<'src>> {
1038        let mut cursor = self.node.walk();
1039        for child in self.node.children(&mut cursor) {
1040            if child.kind() == "snippet_parameters" {
1041                return Some(child);
1042            }
1043        }
1044        None
1045    }
1046
1047    /// The raw text of the parameters (e.g., "a, b" from `{#snippet name(a, b)}`).
1048    pub fn parameters_text(&self) -> Option<&'src str> {
1049        self.parameters_node().map(|n| node_text(self.source, n))
1050    }
1051
1052    /// Iterate over body child nodes.
1053    pub fn body(&self) -> ChildIter<'src> {
1054        ChildIter::new(self.source, self.node)
1055    }
1056}
1057
1058// --- Tag accessors ---
1059
1060impl<'src> ExpressionTag<'src> {
1061    /// The expression content node (js or ts).
1062    pub fn content_node(&self) -> Option<Node<'src>> {
1063        self.node.child_by_field_name("content")
1064    }
1065
1066    /// The raw expression text.
1067    pub fn content_text(&self) -> &'src str {
1068        self.content_node()
1069            .map(|n| node_text(self.source, n))
1070            .unwrap_or("")
1071    }
1072
1073    /// The pre-parsed expression from the cache (keyed on the expression node itself).
1074    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1075        cache.get(self.node.start_byte())
1076    }
1077}
1078
1079impl<'src> HtmlTag<'src> {
1080    /// The expression node.
1081    pub fn expression_node(&self) -> Option<Node<'src>> {
1082        self.node.child_by_field_name("expression")
1083            .or_else(|| {
1084                let mut cursor = self.node.walk();
1085                self.node.children(&mut cursor)
1086                    .find(|c| c.kind() == "expression_value")
1087            })
1088    }
1089
1090    /// The pre-parsed expression from the cache.
1091    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1092        self.expression_node().and_then(|n| cache.get(n.start_byte()))
1093    }
1094}
1095
1096impl<'src> ConstTag<'src> {
1097    /// The expression/declaration node.
1098    pub fn expression_node(&self) -> Option<Node<'src>> {
1099        let mut cursor = self.node.walk();
1100        self.node.children(&mut cursor)
1101            .find(|c| c.kind() == "expression_value")
1102    }
1103
1104    /// The pre-parsed expression from the cache.
1105    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1106        self.expression_node().and_then(|n| cache.get(n.start_byte()))
1107    }
1108}
1109
1110impl<'src> DebugTag<'src> {
1111    /// The expression value node containing debug identifiers.
1112    pub fn expression_node(&self) -> Option<Node<'src>> {
1113        let mut cursor = self.node.walk();
1114        self.node.children(&mut cursor)
1115            .find(|c| c.kind() == "expression_value")
1116    }
1117
1118    /// The pre-parsed expression from the cache.
1119    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1120        self.expression_node().and_then(|n| cache.get(n.start_byte()))
1121    }
1122}
1123
1124impl<'src> RenderTag<'src> {
1125    /// The expression node.
1126    pub fn expression_node(&self) -> Option<Node<'src>> {
1127        self.node.child_by_field_name("expression")
1128            .or_else(|| {
1129                let mut cursor = self.node.walk();
1130                self.node.children(&mut cursor)
1131                    .find(|c| c.kind() == "expression_value")
1132            })
1133    }
1134
1135    /// The pre-parsed expression from the cache.
1136    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1137        self.expression_node().and_then(|n| cache.get(n.start_byte()))
1138    }
1139}
1140
1141impl<'src> AttachTag<'src> {
1142    /// The expression node.
1143    pub fn expression_node(&self) -> Option<Node<'src>> {
1144        let mut cursor = self.node.walk();
1145        self.node.children(&mut cursor)
1146            .find(|c| c.kind() == "expression_value" || c.kind() == "expression")
1147    }
1148
1149    /// The pre-parsed expression from the cache.
1150    pub fn expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1151        self.expression_node().and_then(|n| cache.get(n.start_byte()))
1152    }
1153}
1154
1155// --- AttributeNode accessors ---
1156
1157impl<'src> AttributeNode<'src> {
1158    /// The attribute name.
1159    pub fn name(&self) -> &'src str {
1160        if let Some(name_node) = self.node.child_by_field_name("name") {
1161            node_text(self.source, name_node)
1162        } else {
1163            // Shorthand attribute — content is the name
1164            node_text(self.source, self.node)
1165        }
1166    }
1167
1168    /// The attribute value node, if any.
1169    pub fn value_node(&self) -> Option<Node<'src>> {
1170        self.node.child_by_field_name("value")
1171    }
1172
1173    /// Whether this is a shorthand attribute (`{identifier}`).
1174    pub fn is_shorthand(&self) -> bool {
1175        self.node.kind() == "shorthand_attribute"
1176    }
1177
1178    /// Whether this is a spread attribute (`{...expr}`).
1179    pub fn is_spread(&self) -> bool {
1180        self.is_shorthand() && self.text().starts_with("{...")
1181    }
1182
1183    /// Whether this is a directive (e.g., `bind:value`, `on:click`).
1184    pub fn is_directive(&self) -> bool {
1185        self.directive_prefix().is_some()
1186    }
1187
1188    /// The directive prefix (e.g., "class" for `class:active`, "bind" for `bind:value`).
1189    pub fn directive_prefix(&self) -> Option<&'src str> {
1190        let name_node = self.node.child_by_field_name("name")?;
1191        let mut cursor = name_node.walk();
1192        for child in name_node.children(&mut cursor) {
1193            if child.kind() == "attribute_directive" {
1194                return Some(node_text(self.source, child));
1195            }
1196        }
1197        None
1198    }
1199
1200    /// For directives, the identifier after the colon (e.g., "active" in `class:active`).
1201    pub fn directive_name(&self) -> Option<&'src str> {
1202        let name_node = self.node.child_by_field_name("name")?;
1203        let mut cursor = name_node.walk();
1204        for child in name_node.children(&mut cursor) {
1205            if child.kind() == "attribute_identifier" {
1206                return Some(node_text(self.source, child));
1207            }
1208        }
1209        None
1210    }
1211
1212    /// Whether this is a `class:name` directive.
1213    pub fn is_class_directive(&self) -> bool {
1214        self.directive_prefix() == Some("class")
1215    }
1216
1217    /// Whether this is a `bind:name` directive.
1218    pub fn is_bind_directive(&self) -> bool {
1219        self.directive_prefix() == Some("bind")
1220    }
1221
1222    /// Whether this is a `style:name` directive.
1223    pub fn is_style_directive(&self) -> bool {
1224        self.directive_prefix() == Some("style")
1225    }
1226
1227    /// Whether this wraps a shorthand_attribute child node (both `{name}` and `{...spread}`).
1228    pub fn has_shorthand_child(&self) -> bool {
1229        let mut cursor = self.node.walk();
1230        self.node.children(&mut cursor).any(|c| c.kind() == "shorthand_attribute")
1231    }
1232
1233    /// The static text value of this attribute, if it's a simple quoted or bare value.
1234    pub fn static_value(&self) -> Option<&'src str> {
1235        let value = self.value_node()?;
1236        match value.kind() {
1237            "quoted_attribute_value" => {
1238                let mut cursor = value.walk();
1239                for child in value.children(&mut cursor) {
1240                    if child.kind() == "attribute_value" {
1241                        return Some(node_text(self.source, child));
1242                    }
1243                }
1244                None
1245            }
1246            "attribute_value" => Some(node_text(self.source, value)),
1247            _ => None,
1248        }
1249    }
1250
1251    /// The value expression from the cache, if the value is an expression tag.
1252    pub fn value_expression<'c>(&self, cache: &'c ExpressionCache) -> Option<&'c Expression> {
1253        let value = self.value_node()?;
1254        match value.kind() {
1255            "expression" => cache.get(value.start_byte()),
1256            "quoted_attribute_value" => {
1257                let mut cursor = value.walk();
1258                for child in value.children(&mut cursor) {
1259                    if child.kind() == "expression" {
1260                        return cache.get(child.start_byte());
1261                    }
1262                }
1263                None
1264            }
1265            _ => None,
1266        }
1267    }
1268
1269    /// Whether the value contains any expression (dynamic value).
1270    pub fn has_expression_value(&self) -> bool {
1271        let Some(value) = self.value_node() else { return false };
1272        match value.kind() {
1273            "expression" => true,
1274            "quoted_attribute_value" => {
1275                let mut cursor = value.walk();
1276                value.children(&mut cursor).any(|c| c.kind() == "expression")
1277            }
1278            _ => false,
1279        }
1280    }
1281
1282    /// Whether the value contains mixed text and expressions.
1283    pub fn has_mixed_value(&self) -> bool {
1284        let Some(value) = self.value_node() else { return false };
1285        if value.kind() != "quoted_attribute_value" { return false; }
1286        let mut has_text = false;
1287        let mut has_expr = false;
1288        let mut cursor = value.walk();
1289        for child in value.children(&mut cursor) {
1290            match child.kind() {
1291                "attribute_value" => has_text = true,
1292                "expression" => has_expr = true,
1293                _ => {}
1294            }
1295        }
1296        has_text && has_expr
1297    }
1298
1299    /// The source text that this attribute references.
1300    pub fn source_text(&self) -> &'src str {
1301        self.source
1302    }
1303
1304    /// Iterate over value parts as `(is_expression: bool, text: &str, start_byte: usize)`.
1305    /// For quoted values like `"foo {bar} baz"`, yields text and expression parts.
1306    /// For expression values like `{expr}`, yields one expression part.
1307    /// For boolean (no value), yields nothing.
1308    pub fn value_parts(&self) -> Vec<AttributeValuePart<'src>> {
1309        let Some(value) = self.value_node() else { return vec![] };
1310        match value.kind() {
1311            "expression" => {
1312                vec![AttributeValuePart::Expression(
1313                    value.start_byte(),
1314                    node_text(self.source, value),
1315                )]
1316            }
1317            "quoted_attribute_value" => {
1318                let mut parts = Vec::new();
1319                let mut cursor = value.walk();
1320                for child in value.children(&mut cursor) {
1321                    match child.kind() {
1322                        "attribute_value" => {
1323                            parts.push(AttributeValuePart::Text(
1324                                node_text(self.source, child),
1325                            ));
1326                        }
1327                        "expression" => {
1328                            parts.push(AttributeValuePart::Expression(
1329                                child.start_byte(),
1330                                node_text(self.source, child),
1331                            ));
1332                        }
1333                        _ => {}
1334                    }
1335                }
1336                parts
1337            }
1338            _ => vec![],
1339        }
1340    }
1341}
1342
1343/// A part of an attribute value (text or expression).
1344#[derive(Debug, Clone, Copy)]
1345pub enum AttributeValuePart<'src> {
1346    /// Static text content.
1347    Text(&'src str),
1348    /// An expression `{...}`. The usize is the start byte offset for cache lookup.
1349    Expression(usize, &'src str),
1350}
1351
1352// --- ElseClause (not in define_wrapper since it's structural) ---
1353
1354define_wrapper!(
1355    /// An else clause in a block.
1356    ElseClause,
1357);
1358
1359impl<'src> ElseClause<'src> {
1360    /// Iterate over child nodes of this else clause.
1361    pub fn children(&self) -> ChildIter<'src> {
1362        ChildIter::new(self.source, self.node)
1363    }
1364}
1365
1366// --- Alternate enum ---
1367
1368/// The alternate branch of an if block.
1369#[derive(Debug, Clone, Copy)]
1370pub enum Alternate<'src> {
1371    Else(ElseClause<'src>),
1372    ElseIf(IfBlock<'src>),
1373}
1374
1375// --- TemplateNode enum ---
1376
1377/// A template node — discriminated by tree-sitter node kind.
1378#[derive(Debug, Clone, Copy)]
1379pub enum TemplateNode<'src> {
1380    Text(TextNode<'src>),
1381    Comment(CommentNode<'src>),
1382    ExpressionTag(ExpressionTag<'src>),
1383    HtmlTag(HtmlTag<'src>),
1384    ConstTag(ConstTag<'src>),
1385    DebugTag(DebugTag<'src>),
1386    RenderTag(RenderTag<'src>),
1387    AttachTag(AttachTag<'src>),
1388    IfBlock(IfBlock<'src>),
1389    EachBlock(EachBlock<'src>),
1390    AwaitBlock(AwaitBlock<'src>),
1391    KeyBlock(KeyBlock<'src>),
1392    SnippetBlock(SnippetBlock<'src>),
1393    // Element variants — all wrap Element but carry classification
1394    RegularElement(Element<'src>),
1395    Component(Element<'src>),
1396    SlotElement(Element<'src>),
1397    SvelteHead(Element<'src>),
1398    SvelteBody(Element<'src>),
1399    SvelteWindow(Element<'src>),
1400    SvelteDocument(Element<'src>),
1401    SvelteComponent(Element<'src>),
1402    SvelteElement(Element<'src>),
1403    SvelteSelf(Element<'src>),
1404    SvelteFragment(Element<'src>),
1405    SvelteBoundary(Element<'src>),
1406    TitleElement(Element<'src>),
1407}
1408
1409impl<'src> TemplateNode<'src> {
1410    /// Byte offset of the start of this node.
1411    pub fn start(&self) -> usize {
1412        self.ts_node().start_byte()
1413    }
1414
1415    /// Byte offset of the end of this node.
1416    pub fn end(&self) -> usize {
1417        self.ts_node().end_byte()
1418    }
1419
1420    /// The underlying tree-sitter node.
1421    pub fn ts_node(&self) -> Node<'src> {
1422        match self {
1423            Self::Text(n) => n.ts_node(),
1424            Self::Comment(n) => n.ts_node(),
1425            Self::ExpressionTag(n) => n.ts_node(),
1426            Self::HtmlTag(n) => n.ts_node(),
1427            Self::ConstTag(n) => n.ts_node(),
1428            Self::DebugTag(n) => n.ts_node(),
1429            Self::RenderTag(n) => n.ts_node(),
1430            Self::AttachTag(n) => n.ts_node(),
1431            Self::IfBlock(n) => n.ts_node(),
1432            Self::EachBlock(n) => n.ts_node(),
1433            Self::AwaitBlock(n) => n.ts_node(),
1434            Self::KeyBlock(n) => n.ts_node(),
1435            Self::SnippetBlock(n) => n.ts_node(),
1436            Self::RegularElement(n)
1437            | Self::Component(n)
1438            | Self::SlotElement(n)
1439            | Self::SvelteHead(n)
1440            | Self::SvelteBody(n)
1441            | Self::SvelteWindow(n)
1442            | Self::SvelteDocument(n)
1443            | Self::SvelteComponent(n)
1444            | Self::SvelteElement(n)
1445            | Self::SvelteSelf(n)
1446            | Self::SvelteFragment(n)
1447            | Self::SvelteBoundary(n)
1448            | Self::TitleElement(n) => n.ts_node(),
1449        }
1450    }
1451
1452    /// If this is any element variant, return the inner `Element`.
1453    pub fn as_element(&self) -> Option<&Element<'src>> {
1454        match self {
1455            Self::RegularElement(e)
1456            | Self::Component(e)
1457            | Self::SlotElement(e)
1458            | Self::SvelteHead(e)
1459            | Self::SvelteBody(e)
1460            | Self::SvelteWindow(e)
1461            | Self::SvelteDocument(e)
1462            | Self::SvelteComponent(e)
1463            | Self::SvelteElement(e)
1464            | Self::SvelteSelf(e)
1465            | Self::SvelteFragment(e)
1466            | Self::SvelteBoundary(e)
1467            | Self::TitleElement(e) => Some(e),
1468            _ => None,
1469        }
1470    }
1471
1472    /// Whether this node is a component-like element (Component, SvelteComponent, etc.).
1473    pub fn is_component_like(&self) -> bool {
1474        matches!(
1475            self,
1476            Self::Component(_)
1477                | Self::SvelteComponent(_)
1478                | Self::SvelteSelf(_)
1479                | Self::SvelteFragment(_)
1480                | Self::SvelteBoundary(_)
1481                | Self::SvelteHead(_)
1482                | Self::SvelteBody(_)
1483                | Self::SvelteWindow(_)
1484                | Self::SvelteDocument(_)
1485                | Self::TitleElement(_)
1486        )
1487    }
1488
1489    /// Whether this is a SvelteElement (`<svelte:element>`).
1490    pub fn is_svelte_element(&self) -> bool {
1491        matches!(self, Self::SvelteElement(_))
1492    }
1493
1494    /// Visit all direct child fragments of this node.
1495    /// Calls `f` with a `ChildIter` for each child fragment (body, fallback, branches, etc.).
1496    pub fn for_each_child_iter<F>(&self, mut f: F)
1497    where
1498        F: FnMut(ChildIter<'src>),
1499    {
1500        match self {
1501            Self::RegularElement(el)
1502            | Self::Component(el)
1503            | Self::SlotElement(el)
1504            | Self::SvelteHead(el)
1505            | Self::SvelteBody(el)
1506            | Self::SvelteWindow(el)
1507            | Self::SvelteDocument(el)
1508            | Self::SvelteComponent(el)
1509            | Self::SvelteElement(el)
1510            | Self::SvelteSelf(el)
1511            | Self::SvelteFragment(el)
1512            | Self::SvelteBoundary(el)
1513            | Self::TitleElement(el) => {
1514                f(el.children());
1515            }
1516            Self::IfBlock(block) => {
1517                f(block.consequent());
1518                match block.alternate() {
1519                    Some(Alternate::Else(clause)) => f(clause.children()),
1520                    Some(Alternate::ElseIf(nested)) => {
1521                        TemplateNode::IfBlock(nested).for_each_child_iter(f);
1522                    }
1523                    None => {}
1524                }
1525            }
1526            Self::EachBlock(block) => {
1527                f(block.body());
1528                if let Some(clause) = block.fallback() {
1529                    f(clause.children());
1530                }
1531            }
1532            Self::AwaitBlock(block) => {
1533                if let Some(iter) = block.pending() {
1534                    f(iter);
1535                }
1536                if let Some(iter) = block.then_children() {
1537                    f(iter);
1538                }
1539                if let Some(iter) = block.catch_children() {
1540                    f(iter);
1541                }
1542            }
1543            Self::KeyBlock(block) => {
1544                f(block.body());
1545            }
1546            Self::SnippetBlock(block) => {
1547                f(block.body());
1548            }
1549            _ => {}
1550        }
1551    }
1552
1553    /// Recursively walk all descendant template nodes depth-first.
1554    pub fn walk<F>(&self, f: &mut F)
1555    where
1556        F: FnMut(TemplateNode<'src>),
1557    {
1558        self.for_each_child_iter(|iter| {
1559            for child in iter {
1560                f(child);
1561                child.walk(f);
1562            }
1563        });
1564    }
1565}
1566
1567impl<'src> Root<'src> {
1568    /// Recursively walk all descendant template nodes depth-first.
1569    pub fn walk<F>(&self, f: &mut F)
1570    where
1571        F: FnMut(TemplateNode<'src>),
1572    {
1573        for child in self.children() {
1574            f(child);
1575            child.walk(f);
1576        }
1577    }
1578
1579    /// Check if any descendant matches a predicate.
1580    pub fn any<F>(&self, mut f: F) -> bool
1581    where
1582        F: FnMut(TemplateNode<'src>) -> bool,
1583    {
1584        let mut found = false;
1585        self.walk(&mut |node| {
1586            if !found && f(node) {
1587                found = true;
1588            }
1589        });
1590        found
1591    }
1592}
1593
1594// --- Classify functions ---
1595
1596/// Classify an `element` tree-sitter node into the appropriate `TemplateNode` variant.
1597fn classify_element<'src>(source: &'src str, node: Node<'src>) -> TemplateNode<'src> {
1598    let el = Element::new(source, node);
1599    let name = el.name();
1600
1601    match name {
1602        "slot" => TemplateNode::SlotElement(el),
1603        "title" => TemplateNode::TitleElement(el),
1604        _ if name.starts_with("svelte:") => {
1605            match &name[7..] {
1606                "head" => TemplateNode::SvelteHead(el),
1607                "body" => TemplateNode::SvelteBody(el),
1608                "window" => TemplateNode::SvelteWindow(el),
1609                "document" => TemplateNode::SvelteDocument(el),
1610                "component" => TemplateNode::SvelteComponent(el),
1611                "element" => TemplateNode::SvelteElement(el),
1612                "self" => TemplateNode::SvelteSelf(el),
1613                "fragment" => TemplateNode::SvelteFragment(el),
1614                "boundary" => TemplateNode::SvelteBoundary(el),
1615                _ => TemplateNode::RegularElement(el),
1616            }
1617        }
1618        _ if is_component_name(name) => TemplateNode::Component(el),
1619        _ => TemplateNode::RegularElement(el),
1620    }
1621}
1622
1623/// Classify any named tree-sitter node into a `TemplateNode`.
1624pub fn classify_node<'src>(source: &'src str, node: Node<'src>) -> Option<TemplateNode<'src>> {
1625    match node.kind() {
1626        "text" => Some(TemplateNode::Text(TextNode::new(source, node))),
1627        "comment" => Some(TemplateNode::Comment(CommentNode::new(source, node))),
1628        "expression" => Some(TemplateNode::ExpressionTag(ExpressionTag::new(source, node))),
1629        "html_tag" => Some(TemplateNode::HtmlTag(HtmlTag::new(source, node))),
1630        "const_tag" => Some(TemplateNode::ConstTag(ConstTag::new(source, node))),
1631        "debug_tag" => Some(TemplateNode::DebugTag(DebugTag::new(source, node))),
1632        "render_tag" => Some(TemplateNode::RenderTag(RenderTag::new(source, node))),
1633        "attach_tag" => Some(TemplateNode::AttachTag(AttachTag::new(source, node))),
1634        "if_block" => Some(TemplateNode::IfBlock(IfBlock::new(source, node))),
1635        "each_block" => Some(TemplateNode::EachBlock(EachBlock::new(source, node))),
1636        "await_block" => Some(TemplateNode::AwaitBlock(AwaitBlock::new(source, node))),
1637        "key_block" => Some(TemplateNode::KeyBlock(KeyBlock::new(source, node))),
1638        "snippet_block" => Some(TemplateNode::SnippetBlock(SnippetBlock::new(source, node))),
1639        "element" => Some(classify_element(source, node)),
1640        _ => None,
1641    }
1642}
1643
1644// --- ChildIter ---
1645
1646/// Iterator over child nodes that are template content (skipping structural nodes).
1647pub struct ChildIter<'src> {
1648    source: &'src str,
1649    cursor: TreeCursor<'src>,
1650    started: bool,
1651}
1652
1653impl<'src> ChildIter<'src> {
1654    fn new(source: &'src str, parent: Node<'src>) -> Self {
1655        Self {
1656            source,
1657            cursor: parent.walk(),
1658            started: false,
1659        }
1660    }
1661}
1662
1663/// Node kinds that are structural (not template content).
1664fn is_structural_kind(kind: &str) -> bool {
1665    matches!(
1666        kind,
1667        "block_open"
1668            | "block_close"
1669            | "block_end"
1670            | "block_keyword"
1671            | "block_sigil"
1672            | "branch_kind"
1673            | "start_tag"
1674            | "end_tag"
1675            | "self_closing_tag"
1676            | "else_clause"
1677            | "else_if_clause"
1678            | "await_branch"
1679            | "await_pending"
1680            | "await_branch_children"
1681            | "snippet_name"
1682            | "snippet_parameters"
1683            | "snippet_type_parameters"
1684            | "pattern"
1685            | "expression_value"
1686            | "shorthand_kind"
1687            | "raw_text"
1688    )
1689}
1690
1691impl<'src> Iterator for ChildIter<'src> {
1692    type Item = TemplateNode<'src>;
1693
1694    fn next(&mut self) -> Option<TemplateNode<'src>> {
1695        loop {
1696            let moved = if self.started {
1697                self.cursor.goto_next_sibling()
1698            } else {
1699                self.started = true;
1700                self.cursor.goto_first_child()
1701            };
1702
1703            if !moved {
1704                return None;
1705            }
1706
1707            let node = self.cursor.node();
1708            if !node.is_named() {
1709                continue;
1710            }
1711
1712            // Skip children that are field-named (structural parts of parent:
1713            // expression, binding, key, etc.)
1714            if self.cursor.field_name().is_some() {
1715                continue;
1716            }
1717
1718            let kind = node.kind();
1719            if is_structural_kind(kind) {
1720                continue;
1721            }
1722
1723            if let Some(template_node) = classify_node(self.source, node) {
1724                return Some(template_node);
1725            }
1726        }
1727    }
1728}
1729
1730// --- AttributeIter ---
1731
1732/// Iterator over attribute nodes on a start tag.
1733pub struct AttributeIter<'src> {
1734    source: &'src str,
1735    cursor: TreeCursor<'src>,
1736    started: bool,
1737}
1738
1739impl<'src> Iterator for AttributeIter<'src> {
1740    type Item = AttributeNode<'src>;
1741
1742    fn next(&mut self) -> Option<AttributeNode<'src>> {
1743        loop {
1744            let moved = if self.started {
1745                self.cursor.goto_next_sibling()
1746            } else {
1747                self.started = true;
1748                self.cursor.goto_first_child()
1749            };
1750
1751            if !moved {
1752                return None;
1753            }
1754
1755            let node = self.cursor.node();
1756            match node.kind() {
1757                "attribute" | "shorthand_attribute" | "attach_tag" => {
1758                    return Some(AttributeNode::new(self.source, node));
1759                }
1760                _ => continue,
1761            }
1762        }
1763    }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768    use super::*;
1769    use crate::primitives::SourceId;
1770
1771    #[test]
1772    fn parses_svelte_cst_document() {
1773        let source = SourceText::new(SourceId::new(1), "<div>Hello</div>", None);
1774        let cst = parse_svelte(source).expect("expected tree-sitter CST parse to succeed");
1775
1776        assert!(!cst.root_kind().is_empty());
1777        assert!(cst.root_span().end.as_usize() >= cst.source.len());
1778    }
1779
1780    #[test]
1781    fn cst_contains_attribute_nodes() {
1782        let source = SourceText::new(SourceId::new(2), "<div class='foo'></div>", None);
1783        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1784        let sexp = cst.root_node().to_sexp();
1785
1786        assert!(sexp.contains("(attribute"));
1787        assert!(sexp.contains("(attribute_name"));
1788    }
1789
1790    #[test]
1791    fn cst_style_directive_shape() {
1792        let source = SourceText::new(SourceId::new(3), "<div style:color={myColor}></div>", None);
1793        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1794        let sexp = cst.root_node().to_sexp();
1795
1796        assert!(sexp.contains("attribute_directive"));
1797        assert!(sexp.contains("attribute_identifier"));
1798    }
1799
1800    #[test]
1801    fn cst_if_block_shape() {
1802        let source = SourceText::new(SourceId::new(4), "{#if foo}bar{/if}", None);
1803        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1804        let sexp = cst.root_node().to_sexp();
1805
1806        assert!(sexp.contains("if_block"));
1807        assert!(sexp.contains("block_end"));
1808    }
1809
1810    #[test]
1811    fn cst_breaks_unterminated_tags_before_block_branches() {
1812        let source = SourceText::new(
1813            SourceId::new(5),
1814            "{#if true}\n\t<input>\n{:else}\n{/if}\n\n{#await true}\n\t<input>\n{:then f}\n{/await}",
1815            None,
1816        );
1817        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1818        let sexp = cst.root_node().to_sexp();
1819
1820        assert!(sexp.matches("(else_clause").count() + sexp.matches("(await_branch").count() >= 2);
1821    }
1822
1823    #[test]
1824    fn cst_directive_and_debug_tag_shapes() {
1825        let source = SourceText::new(
1826            SourceId::new(6),
1827            "<div let:x style:color={c} transition:fade={t} animate:flip={a} use:act={u}></div>{@debug x, y}",
1828            None,
1829        );
1830        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1831        let sexp = cst.root_node().to_sexp();
1832
1833        assert!(sexp.contains("attribute_name"));
1834        assert!(sexp.contains("debug_tag"));
1835        assert!(sexp.contains("expression_value"));
1836    }
1837
1838    #[test]
1839    fn cst_malformed_snippet_headers_report_error_shape() {
1840        let source = SourceText::new(SourceId::new(7), "{#snippet children()hi{/snippet}", None);
1841        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1842        let sexp = cst.root_node().to_sexp();
1843        assert!(
1844            cst.has_error(),
1845            "expected malformed snippet header CST error"
1846        );
1847        assert!(sexp.contains("(snippet_name"));
1848
1849        let source = SourceText::new(SourceId::new(8), "{#snippet children(hi{/snippet}", None);
1850        let cst = parse_svelte(source).expect("expected cst parse to succeed");
1851        let sexp = cst.root_node().to_sexp();
1852        assert!(sexp.contains("(snippet_name"));
1853        assert!(sexp.contains("(snippet_parameters"));
1854    }
1855
1856    #[test]
1857    fn incremental_parse_matches_fresh_parse_after_insert() {
1858        let before_text = "<div>Hello</div>";
1859        let after_text = "<div>Hello {name}</div>";
1860        let before = SourceText::new(SourceId::new(9), before_text, None);
1861        let after = SourceText::new(SourceId::new(10), after_text, None);
1862
1863        let mut parser = CstParser::new()
1864            .configure(Language::Svelte)
1865            .expect("parser");
1866        let previous = parser.parse(before).expect("initial parse");
1867        let edit = CstEdit::insert(before_text, "<div>Hello".len(), " {name}");
1868
1869        let incremental = parser
1870            .parse_incremental(after, &previous, edit)
1871            .expect("incremental parse");
1872        let fresh = parse_svelte(after).expect("fresh parse");
1873
1874        assert_eq!(
1875            incremental.root_node().to_sexp(),
1876            fresh.root_node().to_sexp()
1877        );
1878    }
1879
1880    #[test]
1881    fn document_apply_edit_keeps_tree_reusable() {
1882        let before_text = "<div>Hello</div>";
1883        let after_text = "<div>Hi</div>";
1884        let before = SourceText::new(SourceId::new(11), before_text, None);
1885        let after = SourceText::new(SourceId::new(12), after_text, None);
1886
1887        let mut parser = CstParser::new()
1888            .configure(Language::Svelte)
1889            .expect("parser");
1890        let mut previous = parser.parse(before).expect("initial parse");
1891        let edit = CstEdit::replace(before_text, "<div>".len(), "<div>Hello".len(), "Hi");
1892        previous.apply_edit(edit.clone());
1893
1894        let incremental = parser
1895            .parse_incremental(after, &previous, edit)
1896            .expect("incremental parse");
1897        let fresh = parse_svelte(after).expect("fresh parse");
1898
1899        assert_eq!(
1900            incremental.root_node().to_sexp(),
1901            fresh.root_node().to_sexp()
1902        );
1903    }
1904
1905    // --- Wrapper type tests ---
1906
1907    fn parse_source(text: &str) -> Document<'_> {
1908        let source = SourceText::new(SourceId::new(100), text, None);
1909        parse_svelte(source).expect("parse")
1910    }
1911
1912    #[test]
1913    fn wrapper_element_name_via_field() {
1914        let text = r#"<div class="foo">hello</div>"#;
1915        let doc = parse_source(text);
1916        let root = Root::new(text, doc.root_node());
1917        let children: Vec<_> = root.children().collect();
1918        assert_eq!(children.len(), 1);
1919        let TemplateNode::RegularElement(el) = &children[0] else {
1920            panic!("expected RegularElement");
1921        };
1922        assert_eq!(el.name(), "div");
1923        assert!(!el.is_self_closing());
1924        assert!(el.has_end_tag());
1925    }
1926
1927    #[test]
1928    fn wrapper_self_closing_element() {
1929        let text = r#"<br />"#;
1930        let doc = parse_source(text);
1931        let root = Root::new(text, doc.root_node());
1932        let children: Vec<_> = root.children().collect();
1933        assert_eq!(children.len(), 1);
1934        let TemplateNode::RegularElement(el) = &children[0] else {
1935            panic!("expected RegularElement");
1936        };
1937        assert_eq!(el.name(), "br");
1938        assert!(el.is_self_closing());
1939    }
1940
1941    #[test]
1942    fn wrapper_component_classification() {
1943        let text = r#"<Button>Click</Button>"#;
1944        let doc = parse_source(text);
1945        let root = Root::new(text, doc.root_node());
1946        let children: Vec<_> = root.children().collect();
1947        assert!(matches!(&children[0], TemplateNode::Component(el) if el.name() == "Button"));
1948    }
1949
1950    #[test]
1951    fn wrapper_svelte_element_classification() {
1952        let text = r#"<svelte:head><title>Hi</title></svelte:head>"#;
1953        let doc = parse_source(text);
1954        let root = Root::new(text, doc.root_node());
1955        let children: Vec<_> = root.children().collect();
1956        assert!(matches!(&children[0], TemplateNode::SvelteHead(_)));
1957    }
1958
1959    #[test]
1960    fn wrapper_if_block() {
1961        let text = r#"{#if visible}<p>Hello</p>{/if}"#;
1962        let doc = parse_source(text);
1963        let root = Root::new(text, doc.root_node());
1964        let children: Vec<_> = root.children().collect();
1965        assert_eq!(children.len(), 1);
1966        let TemplateNode::IfBlock(block) = &children[0] else {
1967            panic!("expected IfBlock");
1968        };
1969        assert!(block.test_node().is_some());
1970        let consequent: Vec<_> = block.consequent().collect();
1971        assert!(!consequent.is_empty());
1972    }
1973
1974    #[test]
1975    fn wrapper_each_block() {
1976        let text = r#"{#each items as item}<li>{item}</li>{/each}"#;
1977        let doc = parse_source(text);
1978        let root = Root::new(text, doc.root_node());
1979        let children: Vec<_> = root.children().collect();
1980        assert_eq!(children.len(), 1);
1981        let TemplateNode::EachBlock(block) = &children[0] else {
1982            panic!("expected EachBlock");
1983        };
1984        assert!(block.expression_node().is_some());
1985        let body: Vec<_> = block.body().collect();
1986        assert!(!body.is_empty());
1987    }
1988
1989    #[test]
1990    fn wrapper_text_node() {
1991        let text = r#"<div>hello world</div>"#;
1992        let doc = parse_source(text);
1993        let root = Root::new(text, doc.root_node());
1994        let children: Vec<_> = root.children().collect();
1995        let TemplateNode::RegularElement(el) = &children[0] else {
1996            panic!("expected element");
1997        };
1998        let inner: Vec<_> = el.children().collect();
1999        assert_eq!(inner.len(), 1);
2000        let TemplateNode::Text(t) = &inner[0] else {
2001            panic!("expected text");
2002        };
2003        assert_eq!(t.raw(), "hello world");
2004    }
2005
2006    #[test]
2007    fn wrapper_attributes() {
2008        let text = r#"<div class="foo" id="bar">x</div>"#;
2009        let doc = parse_source(text);
2010        let root = Root::new(text, doc.root_node());
2011        let children: Vec<_> = root.children().collect();
2012        let TemplateNode::RegularElement(el) = &children[0] else {
2013            panic!("expected element");
2014        };
2015        let attrs: Vec<_> = el.attributes().collect();
2016        assert_eq!(attrs.len(), 2);
2017        assert_eq!(attrs[0].name(), "class");
2018        assert_eq!(attrs[1].name(), "id");
2019    }
2020
2021    #[test]
2022    fn wrapper_expression_tag() {
2023        let text = r#"<p>{count}</p>"#;
2024        let doc = parse_source(text);
2025        let root = Root::new(text, doc.root_node());
2026        let children: Vec<_> = root.children().collect();
2027        let TemplateNode::RegularElement(el) = &children[0] else {
2028            panic!("expected element");
2029        };
2030        let inner: Vec<_> = el.children().collect();
2031        assert!(matches!(&inner[0], TemplateNode::ExpressionTag(_)));
2032    }
2033
2034    #[test]
2035    fn wrapper_snippet_block() {
2036        let text = r#"{#snippet btn(text)}<button>{text}</button>{/snippet}"#;
2037        let doc = parse_source(text);
2038        let root = Root::new(text, doc.root_node());
2039        let children: Vec<_> = root.children().collect();
2040        let TemplateNode::SnippetBlock(block) = &children[0] else {
2041            panic!("expected SnippetBlock");
2042        };
2043        assert_eq!(block.name(), "btn");
2044        assert!(block.parameters_node().is_some());
2045    }
2046
2047    #[test]
2048    fn wrapper_child_iter_skips_structural_nodes() {
2049        // Ensure ChildIter doesn't yield start_tag, end_tag, block_open, etc.
2050        let text = r#"{#if x}<div>A</div>{:else}<span>B</span>{/if}"#;
2051        let doc = parse_source(text);
2052        let root = Root::new(text, doc.root_node());
2053        let children: Vec<_> = root.children().collect();
2054        let TemplateNode::IfBlock(block) = &children[0] else {
2055            panic!("expected IfBlock");
2056        };
2057        // Consequent should have just the div
2058        let consequent: Vec<_> = block.consequent().collect();
2059        assert!(consequent.iter().all(|n| matches!(n, TemplateNode::RegularElement(_))));
2060    }
2061
2062    // --- ParsedDocument tests ---
2063
2064    #[test]
2065    fn attribute_directive_accessors() {
2066        let text = r#"<div class:active={isActive} bind:value={name} style:color="red" />"#;
2067        let doc = parse_source(text);
2068        let root = Root::new(text, doc.root_node());
2069        let children: Vec<_> = root.children().collect();
2070        let TemplateNode::RegularElement(el) = &children[0] else {
2071            panic!("expected element");
2072        };
2073        let attrs: Vec<_> = el.attributes().collect();
2074        assert_eq!(attrs.len(), 3);
2075
2076        // class:active
2077        assert!(attrs[0].is_class_directive());
2078        assert_eq!(attrs[0].directive_prefix(), Some("class"));
2079        assert_eq!(attrs[0].directive_name(), Some("active"));
2080
2081        // bind:value
2082        assert!(attrs[1].is_bind_directive());
2083        assert_eq!(attrs[1].directive_prefix(), Some("bind"));
2084        assert_eq!(attrs[1].directive_name(), Some("value"));
2085
2086        // style:color
2087        assert!(attrs[2].is_style_directive());
2088        assert_eq!(attrs[2].directive_prefix(), Some("style"));
2089        assert_eq!(attrs[2].directive_name(), Some("color"));
2090        assert_eq!(attrs[2].static_value(), Some("red"));
2091    }
2092
2093    #[test]
2094    fn attribute_static_and_expression_values() {
2095        let text = r#"<div class="foo" id={myId} />"#;
2096        let doc = ParsedDocument::parse(text).unwrap();
2097        let children: Vec<_> = doc.root().children().collect();
2098        let TemplateNode::RegularElement(el) = &children[0] else {
2099            panic!("expected element");
2100        };
2101        let attrs: Vec<_> = el.attributes().collect();
2102        assert_eq!(attrs.len(), 2);
2103
2104        // class="foo" — static value
2105        assert_eq!(attrs[0].static_value(), Some("foo"));
2106        assert!(!attrs[0].has_expression_value());
2107
2108        // id={myId} — expression value
2109        assert!(attrs[1].has_expression_value());
2110        let expr = attrs[1].value_expression(doc.expressions());
2111        assert!(expr.is_some(), "id expression should be cached");
2112    }
2113
2114    #[test]
2115    fn attribute_spread_detection() {
2116        let text = r#"<div {...props} {shorthand} />"#;
2117        let doc = parse_source(text);
2118        let root = Root::new(text, doc.root_node());
2119        let children: Vec<_> = root.children().collect();
2120        let TemplateNode::RegularElement(el) = &children[0] else {
2121            panic!("expected element");
2122        };
2123        let attrs: Vec<_> = el.attributes().collect();
2124        assert_eq!(attrs.len(), 2);
2125
2126        // {...props} is within an attribute wrapping a shorthand
2127        assert!(attrs[0].has_shorthand_child());
2128        // {shorthand} is also within an attribute wrapping a shorthand
2129        assert!(attrs[1].has_shorthand_child());
2130    }
2131
2132    #[test]
2133    fn walk_visits_all_descendants() {
2134        let text = r#"<div><p>A</p><span>{x}</span></div>{#if y}<b>B</b>{/if}"#;
2135        let doc = parse_source(text);
2136        let root = Root::new(text, doc.root_node());
2137        let mut count = 0;
2138        root.walk(&mut |_| count += 1);
2139        // div, p, Text(A), span, ExpressionTag(x), if_block, b, Text(B)
2140        assert!(count >= 7, "expected at least 7 descendants, got {count}");
2141    }
2142
2143    // --- ParsedDocument tests ---
2144
2145    #[test]
2146    fn parsed_document_basic() {
2147        let doc = ParsedDocument::parse("<div>{count}</div>").unwrap();
2148        assert_eq!(doc.root().children().count(), 1);
2149        assert!(!doc.expressions().is_empty());
2150    }
2151
2152    #[test]
2153    fn parsed_document_expression_cache() {
2154        let doc = ParsedDocument::parse("{#if visible}<p>Hello</p>{/if}").unwrap();
2155        let children: Vec<_> = doc.root().children().collect();
2156        let TemplateNode::IfBlock(block) = &children[0] else {
2157            panic!("expected IfBlock");
2158        };
2159        let expr = block.test_expression(doc.expressions());
2160        assert!(expr.is_some(), "test expression should be cached");
2161    }
2162
2163    #[test]
2164    fn parsed_document_each_expression() {
2165        let doc = ParsedDocument::parse("{#each items as item}<li>{item}</li>{/each}").unwrap();
2166        let children: Vec<_> = doc.root().children().collect();
2167        let TemplateNode::EachBlock(block) = &children[0] else {
2168            panic!("expected EachBlock");
2169        };
2170        let expr = block.expression(doc.expressions());
2171        assert!(expr.is_some(), "each expression should be cached");
2172    }
2173
2174    #[test]
2175    fn parsed_document_expression_tag() {
2176        let doc = ParsedDocument::parse("<p>{count + 1}</p>").unwrap();
2177        let children: Vec<_> = doc.root().children().collect();
2178        let TemplateNode::RegularElement(el) = &children[0] else {
2179            panic!("expected element");
2180        };
2181        let inner: Vec<_> = el.children().collect();
2182        let TemplateNode::ExpressionTag(tag) = &inner[0] else {
2183            panic!("expected ExpressionTag");
2184        };
2185        let expr = tag.expression(doc.expressions());
2186        assert!(expr.is_some(), "expression tag should be cached");
2187    }
2188}