ludtwig_parser/syntax/
typed.rs

1//! This module contains all abstract syntax tree (AST) types.
2//! All of them implement the [AstNode] trait.
3//!
4//! Some of them come with extra utility methods, to quickly access some data
5//! (e.g. [TwigBlock::name]).
6//!
7//! An overview of the syntax tree concept can be found
8//! at the [crate level documentation](crate#syntax-trees).
9
10pub use rowan::ast::support;
11pub use rowan::ast::AstChildren;
12pub use rowan::ast::AstNode;
13use rowan::NodeOrToken;
14use std::fmt::{Debug, Display, Formatter};
15
16use crate::T;
17
18use super::untyped::{
19    debug_tree, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TemplateLanguage,
20};
21
22/// So far, we've been working with a homogeneous untyped tree.
23/// It's nice to provide generic tree operations, like traversals,
24/// but it's a bad fit for semantic analysis.
25/// The rowan crate itself does not provide AST facilities directly,
26/// but it is possible to layer AST on top of `SyntaxNode` API.
27///
28/// Let's define AST nodes.
29/// It'll be quite a bunch of repetitive code, so we'll use a macro.
30///
31/// For a real language, you'd want to generate an AST. I find a
32/// combination of `serde`, `ron` and `tera` crates invaluable for that!
33macro_rules! ast_node {
34    ($ast:ident, $kind:path) => {
35        #[derive(Clone, PartialEq, Eq, Hash)]
36        pub struct $ast {
37            pub(crate) syntax: SyntaxNode,
38        }
39
40        impl AstNode for $ast {
41            type Language = TemplateLanguage;
42
43            fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool
44            where
45                Self: Sized,
46            {
47                kind == $kind
48            }
49
50            fn cast(node: rowan::SyntaxNode<Self::Language>) -> Option<Self>
51            where
52                Self: Sized,
53            {
54                if Self::can_cast(node.kind()) {
55                    Some(Self { syntax: node })
56                } else {
57                    None
58                }
59            }
60
61            fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
62                &self.syntax
63            }
64        }
65
66        impl Display for $ast {
67            fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
68                write!(f, "{}", self.syntax)?;
69                Ok(())
70            }
71        }
72
73        impl Debug for $ast {
74            fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75                write!(f, "{}", debug_tree(&self.syntax))?;
76                Ok(())
77            }
78        }
79    };
80}
81
82ast_node!(TwigBlock, SyntaxKind::TWIG_BLOCK);
83impl TwigBlock {
84    /// Name of the twig block
85    #[must_use]
86    pub fn name(&self) -> Option<SyntaxToken> {
87        match self.starting_block() {
88            None => None,
89            Some(n) => n.name(),
90        }
91    }
92
93    #[must_use]
94    pub fn starting_block(&self) -> Option<TwigStartingBlock> {
95        support::child(&self.syntax)
96    }
97
98    #[must_use]
99    pub fn body(&self) -> Option<Body> {
100        support::child(&self.syntax)
101    }
102
103    #[must_use]
104    pub fn ending_block(&self) -> Option<TwigEndingBlock> {
105        support::child(&self.syntax)
106    }
107}
108
109ast_node!(TwigStartingBlock, SyntaxKind::TWIG_STARTING_BLOCK);
110impl TwigStartingBlock {
111    /// Name of the twig block
112    #[must_use]
113    pub fn name(&self) -> Option<SyntaxToken> {
114        support::token(&self.syntax, T![word])
115    }
116
117    /// Parent complete twig block
118    #[must_use]
119    pub fn twig_block(&self) -> Option<TwigBlock> {
120        match self.syntax.parent() {
121            Some(p) => TwigBlock::cast(p),
122            None => None,
123        }
124    }
125}
126
127ast_node!(TwigEndingBlock, SyntaxKind::TWIG_ENDING_BLOCK);
128impl TwigEndingBlock {
129    /// Parent complete twig block
130    #[must_use]
131    pub fn twig_block(&self) -> Option<TwigBlock> {
132        match self.syntax.parent() {
133            Some(p) => TwigBlock::cast(p),
134            None => None,
135        }
136    }
137}
138
139ast_node!(HtmlTag, SyntaxKind::HTML_TAG);
140impl HtmlTag {
141    /// Name of the tag
142    #[must_use]
143    pub fn name(&self) -> Option<SyntaxToken> {
144        match self.starting_tag() {
145            Some(n) => n.name(),
146            None => None,
147        }
148    }
149
150    /// Returns true if the tag doesn't have an ending tag
151    #[must_use]
152    pub fn is_self_closing(&self) -> bool {
153        self.ending_tag().is_none()
154    }
155
156    /// Attributes of the tag
157    #[must_use]
158    pub fn attributes(&self) -> AstChildren<HtmlAttribute> {
159        match self.starting_tag() {
160            Some(n) => n.attributes(),
161            // create an iterator for HtmlAttribute over the tag itself, which should yield no results
162            None => support::children(&self.syntax),
163        }
164    }
165
166    #[must_use]
167    pub fn starting_tag(&self) -> Option<HtmlStartingTag> {
168        support::child(&self.syntax)
169    }
170
171    #[must_use]
172    pub fn body(&self) -> Option<Body> {
173        support::child(&self.syntax)
174    }
175
176    #[must_use]
177    pub fn ending_tag(&self) -> Option<HtmlEndingTag> {
178        support::child(&self.syntax)
179    }
180}
181
182ast_node!(HtmlStartingTag, SyntaxKind::HTML_STARTING_TAG);
183impl HtmlStartingTag {
184    /// Name of the tag
185    #[must_use]
186    pub fn name(&self) -> Option<SyntaxToken> {
187        support::token(&self.syntax, T![word])
188    }
189
190    /// Attributes of the tag
191    #[must_use]
192    pub fn attributes(&self) -> AstChildren<HtmlAttribute> {
193        match support::child::<HtmlAttributeList>(&self.syntax) {
194            Some(list) => support::children(&list.syntax),
195            // create an iterator for HtmlAttribute over the startingTag itself, which should yield no results
196            None => support::children(&self.syntax),
197        }
198    }
199
200    /// Parent complete html tag
201    #[must_use]
202    pub fn html_tag(&self) -> Option<HtmlTag> {
203        match self.syntax.parent() {
204            Some(p) => HtmlTag::cast(p),
205            None => None,
206        }
207    }
208}
209
210ast_node!(HtmlAttribute, SyntaxKind::HTML_ATTRIBUTE);
211impl HtmlAttribute {
212    /// Name of the attribute (left side of the equal sign)
213    #[must_use]
214    pub fn name(&self) -> Option<SyntaxToken> {
215        support::token(&self.syntax, T![word])
216    }
217
218    /// Value of the attribute
219    #[must_use]
220    pub fn value(&self) -> Option<HtmlString> {
221        support::child(&self.syntax)
222    }
223
224    /// Parent starting html tag
225    #[must_use]
226    pub fn html_tag(&self) -> Option<HtmlStartingTag> {
227        match self.syntax.parent() {
228            Some(p) => HtmlStartingTag::cast(p),
229            None => None,
230        }
231    }
232}
233
234ast_node!(HtmlEndingTag, SyntaxKind::HTML_ENDING_TAG);
235impl HtmlEndingTag {
236    /// Name of the tag
237    #[must_use]
238    pub fn name(&self) -> Option<SyntaxToken> {
239        support::token(&self.syntax, T![word])
240    }
241
242    /// Parent complete html tag
243    #[must_use]
244    pub fn html_tag(&self) -> Option<HtmlTag> {
245        match self.syntax.parent() {
246            Some(p) => HtmlTag::cast(p),
247            None => None,
248        }
249    }
250}
251
252ast_node!(TwigBinaryExpression, SyntaxKind::TWIG_BINARY_EXPRESSION);
253impl TwigBinaryExpression {
254    #[must_use]
255    pub fn operator(&self) -> Option<SyntaxToken> {
256        self.syntax
257            .children_with_tokens()
258            .find_map(|element| match element {
259                SyntaxElement::Token(t) if !t.kind().is_trivia() => Some(t),
260                _ => None,
261            })
262    }
263
264    #[must_use]
265    pub fn lhs_expression(&self) -> Option<TwigExpression> {
266        self.syntax.children().find_map(TwigExpression::cast)
267    }
268
269    #[must_use]
270    pub fn rhs_expression(&self) -> Option<TwigExpression> {
271        self.syntax
272            .children()
273            .filter_map(TwigExpression::cast)
274            .nth(1)
275    }
276}
277
278ast_node!(
279    LudtwigDirectiveRuleList,
280    SyntaxKind::LUDTWIG_DIRECTIVE_RULE_LIST
281);
282impl LudtwigDirectiveRuleList {
283    #[must_use]
284    pub fn get_rule_names(&self) -> Vec<String> {
285        self.syntax
286            .children_with_tokens()
287            .filter_map(|element| match element {
288                NodeOrToken::Token(t) if t.kind() == SyntaxKind::TK_WORD => {
289                    Some(t.text().to_string())
290                }
291                _ => None,
292            })
293            .collect()
294    }
295}
296
297ast_node!(
298    LudtwigDirectiveFileIgnore,
299    SyntaxKind::LUDTWIG_DIRECTIVE_FILE_IGNORE
300);
301impl LudtwigDirectiveFileIgnore {
302    #[must_use]
303    pub fn get_rules(&self) -> Vec<String> {
304        match support::child::<LudtwigDirectiveRuleList>(&self.syntax) {
305            Some(rule_list) => rule_list.get_rule_names(),
306            None => vec![],
307        }
308    }
309}
310
311ast_node!(LudtwigDirectiveIgnore, SyntaxKind::LUDTWIG_DIRECTIVE_IGNORE);
312impl LudtwigDirectiveIgnore {
313    #[must_use]
314    pub fn get_rules(&self) -> Vec<String> {
315        match support::child::<LudtwigDirectiveRuleList>(&self.syntax) {
316            Some(rule_list) => rule_list.get_rule_names(),
317            None => vec![],
318        }
319    }
320}
321
322ast_node!(TwigLiteralString, SyntaxKind::TWIG_LITERAL_STRING);
323impl TwigLiteralString {
324    #[must_use]
325    pub fn get_inner(&self) -> Option<TwigLiteralStringInner> {
326        support::child(&self.syntax)
327    }
328
329    #[must_use]
330    pub fn get_opening_quote(&self) -> Option<SyntaxToken> {
331        self.syntax
332            .children_with_tokens()
333            .take_while(|element| {
334                if element.as_node().is_some() {
335                    return false; // found inner string node
336                }
337
338                true
339            })
340            .find_map(|element| match element {
341                // first non trivia token should be a quote
342                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
343                _ => None,
344            })
345    }
346
347    #[must_use]
348    pub fn get_closing_quote(&self) -> Option<SyntaxToken> {
349        self.syntax
350            .children_with_tokens()
351            .skip_while(|element| {
352                if element.as_node().is_some() {
353                    return false; // found inner string node, stop skipping
354                }
355
356                true
357            })
358            .find_map(|element| match element {
359                // first non trivia token should be a quote
360                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
361                _ => None,
362            })
363    }
364}
365
366ast_node!(
367    TwigLiteralStringInner,
368    SyntaxKind::TWIG_LITERAL_STRING_INNER
369);
370impl TwigLiteralStringInner {
371    #[must_use]
372    pub fn get_interpolations(&self) -> AstChildren<TwigLiteralStringInterpolation> {
373        support::children(&self.syntax)
374    }
375}
376
377ast_node!(HtmlString, SyntaxKind::HTML_STRING);
378impl HtmlString {
379    #[must_use]
380    pub fn get_inner(&self) -> Option<HtmlStringInner> {
381        support::child(&self.syntax)
382    }
383
384    #[must_use]
385    pub fn get_opening_quote(&self) -> Option<SyntaxToken> {
386        self.syntax
387            .children_with_tokens()
388            .take_while(|element| {
389                if element.as_node().is_some() {
390                    return false; // found inner string node
391                }
392
393                true
394            })
395            .find_map(|element| match element {
396                // first non trivia token should be a quote
397                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
398                _ => None,
399            })
400    }
401
402    #[must_use]
403    pub fn get_closing_quote(&self) -> Option<SyntaxToken> {
404        self.syntax
405            .children_with_tokens()
406            .skip_while(|element| {
407                if element.as_node().is_some() {
408                    return false; // found inner string node, stop skipping
409                }
410
411                true
412            })
413            .find_map(|element| match element {
414                // first non trivia token should be a quote
415                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
416                _ => None,
417            })
418    }
419}
420
421ast_node!(TwigExtends, SyntaxKind::TWIG_EXTENDS);
422impl TwigExtends {
423    #[must_use]
424    pub fn get_extends_keyword(&self) -> Option<SyntaxToken> {
425        support::token(&self.syntax, T!["extends"])
426    }
427}
428
429ast_node!(TwigVar, SyntaxKind::TWIG_VAR);
430impl TwigVar {
431    #[must_use]
432    pub fn get_expression(&self) -> Option<TwigExpression> {
433        support::child(&self.syntax)
434    }
435}
436
437ast_node!(TwigLiteralName, SyntaxKind::TWIG_LITERAL_NAME);
438impl TwigLiteralName {
439    #[must_use]
440    pub fn get_name(&self) -> Option<SyntaxToken> {
441        support::token(&self.syntax, SyntaxKind::TK_WORD)
442    }
443}
444
445ast_node!(Body, SyntaxKind::BODY);
446ast_node!(TwigExpression, SyntaxKind::TWIG_EXPRESSION);
447ast_node!(TwigUnaryExpression, SyntaxKind::TWIG_UNARY_EXPRESSION);
448ast_node!(
449    TwigParenthesesExpression,
450    SyntaxKind::TWIG_PARENTHESES_EXPRESSION
451);
452ast_node!(
453    TwigConditionalExpression,
454    SyntaxKind::TWIG_CONDITIONAL_EXPRESSION
455);
456ast_node!(TwigOperand, SyntaxKind::TWIG_OPERAND);
457ast_node!(TwigAccessor, SyntaxKind::TWIG_ACCESSOR);
458ast_node!(TwigFilter, SyntaxKind::TWIG_FILTER);
459ast_node!(TwigIndexLookup, SyntaxKind::TWIG_INDEX_LOOKUP);
460ast_node!(TwigIndex, SyntaxKind::TWIG_INDEX);
461ast_node!(TwigIndexRange, SyntaxKind::TWIG_INDEX_RANGE);
462ast_node!(TwigFunctionCall, SyntaxKind::TWIG_FUNCTION_CALL);
463ast_node!(TwigArrowFunction, SyntaxKind::TWIG_ARROW_FUNCTION);
464ast_node!(TwigArguments, SyntaxKind::TWIG_ARGUMENTS);
465ast_node!(TwigNamedArgument, SyntaxKind::TWIG_NAMED_ARGUMENT);
466ast_node!(
467    TwigLiteralStringInterpolation,
468    SyntaxKind::TWIG_LITERAL_STRING_INTERPOLATION
469);
470ast_node!(TwigLiteralNumber, SyntaxKind::TWIG_LITERAL_NUMBER);
471ast_node!(TwigLiteralArray, SyntaxKind::TWIG_LITERAL_ARRAY);
472ast_node!(TwigLiteralArrayInner, SyntaxKind::TWIG_LITERAL_ARRAY_INNER);
473ast_node!(TwigLiteralNull, SyntaxKind::TWIG_LITERAL_NULL);
474ast_node!(TwigLiteralBoolean, SyntaxKind::TWIG_LITERAL_BOOLEAN);
475ast_node!(TwigLiteralHash, SyntaxKind::TWIG_LITERAL_HASH);
476ast_node!(TwigLiteralHashItems, SyntaxKind::TWIG_LITERAL_HASH_ITEMS);
477ast_node!(TwigLiteralHashPair, SyntaxKind::TWIG_LITERAL_HASH_PAIR);
478ast_node!(TwigLiteralHashKey, SyntaxKind::TWIG_LITERAL_HASH_KEY);
479ast_node!(TwigLiteralHashValue, SyntaxKind::TWIG_LITERAL_HASH_VALUE);
480ast_node!(TwigComment, SyntaxKind::TWIG_COMMENT);
481ast_node!(TwigIf, SyntaxKind::TWIG_IF);
482ast_node!(TwigIfBlock, SyntaxKind::TWIG_IF_BLOCK);
483ast_node!(TwigElseIfBlock, SyntaxKind::TWIG_ELSE_IF_BLOCK);
484ast_node!(TwigElseBlock, SyntaxKind::TWIG_ELSE_BLOCK);
485ast_node!(TwigEndIfBlock, SyntaxKind::TWIG_ENDIF_BLOCK);
486ast_node!(TwigSet, SyntaxKind::TWIG_SET);
487ast_node!(TwigSetBlock, SyntaxKind::TWIG_SET_BLOCK);
488ast_node!(TwigEndSetBlock, SyntaxKind::TWIG_ENDSET_BLOCK);
489ast_node!(TwigAssignment, SyntaxKind::TWIG_ASSIGNMENT);
490ast_node!(TwigFor, SyntaxKind::TWIG_FOR);
491ast_node!(TwigForBlock, SyntaxKind::TWIG_FOR_BLOCK);
492ast_node!(TwigForElseBlock, SyntaxKind::TWIG_FOR_ELSE_BLOCK);
493ast_node!(TwigEndForBlock, SyntaxKind::TWIG_ENDFOR_BLOCK);
494ast_node!(TwigInclude, SyntaxKind::TWIG_INCLUDE);
495ast_node!(TwigIncludeWith, SyntaxKind::TWIG_INCLUDE_WITH);
496ast_node!(TwigUse, SyntaxKind::TWIG_USE);
497ast_node!(TwigOverride, SyntaxKind::TWIG_OVERRIDE);
498ast_node!(TwigApply, SyntaxKind::TWIG_APPLY);
499ast_node!(
500    TwigApplyStartingBlock,
501    SyntaxKind::TWIG_APPLY_STARTING_BLOCK
502);
503ast_node!(TwigApplyEndingBlock, SyntaxKind::TWIG_APPLY_ENDING_BLOCK);
504ast_node!(TwigAutoescape, SyntaxKind::TWIG_AUTOESCAPE);
505ast_node!(
506    TwigAutoescapeStartingBlock,
507    SyntaxKind::TWIG_AUTOESCAPE_STARTING_BLOCK
508);
509ast_node!(
510    TwigAutoescapeEndingBlock,
511    SyntaxKind::TWIG_AUTOESCAPE_ENDING_BLOCK
512);
513ast_node!(TwigDeprecated, SyntaxKind::TWIG_DEPRECATED);
514ast_node!(TwigDo, SyntaxKind::TWIG_DO);
515ast_node!(TwigEmbed, SyntaxKind::TWIG_EMBED);
516ast_node!(
517    TwigEmbedStartingBlock,
518    SyntaxKind::TWIG_EMBED_STARTING_BLOCK
519);
520ast_node!(TwigEmbedEndingBlock, SyntaxKind::TWIG_EMBED_ENDING_BLOCK);
521ast_node!(TwigFlush, SyntaxKind::TWIG_FLUSH);
522ast_node!(TwigFrom, SyntaxKind::TWIG_FROM);
523ast_node!(TwigImport, SyntaxKind::TWIG_IMPORT);
524ast_node!(TwigSandbox, SyntaxKind::TWIG_SANDBOX);
525ast_node!(
526    TwigSandboxStartingBlock,
527    SyntaxKind::TWIG_SANDBOX_STARTING_BLOCK
528);
529ast_node!(
530    TwigSandboxEndingBlock,
531    SyntaxKind::TWIG_SANDBOX_ENDING_BLOCK
532);
533ast_node!(TwigVerbatim, SyntaxKind::TWIG_VERBATIM);
534ast_node!(
535    TwigVerbatimStartingBlock,
536    SyntaxKind::TWIG_VERBATIM_STARTING_BLOCK
537);
538ast_node!(
539    TwigVerbatimEndingBlock,
540    SyntaxKind::TWIG_VERBATIM_ENDING_BLOCK
541);
542ast_node!(TwigMacro, SyntaxKind::TWIG_MACRO);
543ast_node!(
544    TwigMacroStartingBlock,
545    SyntaxKind::TWIG_MACRO_STARTING_BLOCK
546);
547ast_node!(TwigMacroEndingBlock, SyntaxKind::TWIG_MACRO_ENDING_BLOCK);
548ast_node!(TwigWith, SyntaxKind::TWIG_WITH);
549ast_node!(TwigWithStartingBlock, SyntaxKind::TWIG_WITH_STARTING_BLOCK);
550ast_node!(TwigWithEndingBlock, SyntaxKind::TWIG_WITH_ENDING_BLOCK);
551ast_node!(TwigCache, SyntaxKind::TWIG_CACHE);
552ast_node!(TwigCacheTTL, SyntaxKind::TWIG_CACHE_TTL);
553ast_node!(TwigCacheTags, SyntaxKind::TWIG_CACHE_TAGS);
554ast_node!(
555    TwigCacheStartingBlock,
556    SyntaxKind::TWIG_CACHE_STARTING_BLOCK
557);
558ast_node!(TwigCacheEndingBlock, SyntaxKind::TWIG_CACHE_ENDING_BLOCK);
559ast_node!(TwigProps, SyntaxKind::TWIG_PROPS);
560ast_node!(TwigPropDeclaration, SyntaxKind::TWIG_PROP_DECLARATION);
561ast_node!(TwigComponent, SyntaxKind::TWIG_COMPONENT);
562ast_node!(
563    TwigComponentStartingBlock,
564    SyntaxKind::TWIG_COMPONENT_STARTING_BLOCK
565);
566ast_node!(
567    TwigComponentEndingBlock,
568    SyntaxKind::TWIG_COMPONENT_ENDING_BLOCK
569);
570ast_node!(ShopwareTwigExtends, SyntaxKind::SHOPWARE_TWIG_SW_EXTENDS);
571ast_node!(ShopwareTwigInclude, SyntaxKind::SHOPWARE_TWIG_SW_INCLUDE);
572ast_node!(
573    ShopwareSilentFeatureCall,
574    SyntaxKind::SHOPWARE_SILENT_FEATURE_CALL
575);
576ast_node!(
577    ShopwareSilentFeatureCallStartingBlock,
578    SyntaxKind::SHOPWARE_SILENT_FEATURE_CALL_STARTING_BLOCK
579);
580ast_node!(
581    ShopwareSilentFeatureCallEndingBlock,
582    SyntaxKind::SHOPWARE_SILENT_FEATURE_CALL_ENDING_BLOCK
583);
584ast_node!(ShopwareReturn, SyntaxKind::SHOPWARE_RETURN);
585ast_node!(ShopwareIcon, SyntaxKind::SHOPWARE_ICON);
586ast_node!(ShopwareIconStyle, SyntaxKind::SHOPWARE_ICON_STYLE);
587ast_node!(ShopwareThumbnails, SyntaxKind::SHOPWARE_THUMBNAILS);
588ast_node!(ShopwareThumbnailsWith, SyntaxKind::SHOPWARE_THUMBNAILS_WITH);
589ast_node!(HtmlDoctype, SyntaxKind::HTML_DOCTYPE);
590ast_node!(HtmlAttributeList, SyntaxKind::HTML_ATTRIBUTE_LIST);
591ast_node!(HtmlStringInner, SyntaxKind::HTML_STRING_INNER);
592ast_node!(HtmlText, SyntaxKind::HTML_TEXT);
593ast_node!(HtmlRawText, SyntaxKind::HTML_RAW_TEXT);
594ast_node!(HtmlComment, SyntaxKind::HTML_COMMENT);
595ast_node!(Error, SyntaxKind::ERROR);
596ast_node!(Root, SyntaxKind::ROOT);
597ast_node!(TwigTrans, SyntaxKind::TWIG_TRANS);
598ast_node!(
599    TwigTransStartingBlock,
600    SyntaxKind::TWIG_TRANS_STARTING_BLOCK
601);
602ast_node!(TwigTransEndingBlock, SyntaxKind::TWIG_TRANS_ENDING_BLOCK);
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use crate::parse;
608    use expect_test::expect;
609
610    fn parse_and_extract<T: AstNode<Language = TemplateLanguage>>(input: &str) -> T {
611        let (tree, errors) = parse(input).split();
612        assert_eq!(errors, vec![]);
613        support::child(&tree).unwrap()
614    }
615
616    #[test]
617    fn simple_html_tag() {
618        let raw = r#"<div class="hello">world {{ 42 }}</div>"#;
619        let html_tag: HtmlTag = parse_and_extract(raw);
620
621        assert_eq!(format!("{html_tag}"), raw.to_string());
622        expect![[r#"
623            HTML_TAG@0..39
624              HTML_STARTING_TAG@0..19
625                TK_LESS_THAN@0..1 "<"
626                TK_WORD@1..4 "div"
627                HTML_ATTRIBUTE_LIST@4..18
628                  HTML_ATTRIBUTE@4..18
629                    TK_WHITESPACE@4..5 " "
630                    TK_WORD@5..10 "class"
631                    TK_EQUAL@10..11 "="
632                    HTML_STRING@11..18
633                      TK_DOUBLE_QUOTES@11..12 "\""
634                      HTML_STRING_INNER@12..17
635                        TK_WORD@12..17 "hello"
636                      TK_DOUBLE_QUOTES@17..18 "\""
637                TK_GREATER_THAN@18..19 ">"
638              BODY@19..33
639                HTML_TEXT@19..24
640                  TK_WORD@19..24 "world"
641                TWIG_VAR@24..33
642                  TK_WHITESPACE@24..25 " "
643                  TK_OPEN_CURLY_CURLY@25..27 "{{"
644                  TWIG_EXPRESSION@27..30
645                    TWIG_LITERAL_NUMBER@27..30
646                      TK_WHITESPACE@27..28 " "
647                      TK_NUMBER@28..30 "42"
648                  TK_WHITESPACE@30..31 " "
649                  TK_CLOSE_CURLY_CURLY@31..33 "}}"
650              HTML_ENDING_TAG@33..39
651                TK_LESS_THAN_SLASH@33..35 "</"
652                TK_WORD@35..38 "div"
653                TK_GREATER_THAN@38..39 ">""#]]
654        .assert_eq(&format!("{html_tag:?}"));
655
656        assert!(!html_tag.is_self_closing());
657        assert_eq!(
658            html_tag.name().map(|t| t.to_string()),
659            Some("div".to_string())
660        );
661        assert_eq!(
662            html_tag.starting_tag().map(|t| t.to_string()),
663            Some(r#"<div class="hello">"#.to_string())
664        );
665        assert_eq!(
666            html_tag.body().map(|t| t.to_string()),
667            Some("world {{ 42 }}".to_string())
668        );
669        assert_eq!(
670            html_tag.ending_tag().map(|t| t.to_string()),
671            Some("</div>".to_string())
672        );
673        assert_eq!(html_tag.attributes().count(), 1);
674        assert_eq!(
675            html_tag
676                .attributes()
677                .next()
678                .and_then(|t| t.name())
679                .map(|t| t.to_string()),
680            Some("class".to_string())
681        );
682        assert_eq!(
683            html_tag
684                .attributes()
685                .next()
686                .and_then(|t| t.value())
687                .and_then(|t| t.get_inner())
688                .map(|t| t.to_string()),
689            Some("hello".to_string())
690        );
691    }
692}