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
10use rowan::NodeOrToken;
11pub use rowan::ast::AstChildren;
12pub use rowan::ast::AstNode;
13pub use rowan::ast::support;
14use std::fmt::{Debug, Display, Formatter};
15
16use crate::T;
17
18use super::untyped::{
19    SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TemplateLanguage, debug_tree,
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    /// if the tag is a twig component, e.g. '<twig:my:component />'
167    #[must_use]
168    pub fn is_twig_component(&self) -> bool {
169        match self.starting_tag() {
170            Some(n) => n.is_twig_component(),
171            None => false,
172        }
173    }
174
175    #[must_use]
176    pub fn starting_tag(&self) -> Option<HtmlStartingTag> {
177        support::child(&self.syntax)
178    }
179
180    #[must_use]
181    pub fn body(&self) -> Option<Body> {
182        support::child(&self.syntax)
183    }
184
185    #[must_use]
186    pub fn ending_tag(&self) -> Option<HtmlEndingTag> {
187        support::child(&self.syntax)
188    }
189}
190
191ast_node!(HtmlStartingTag, SyntaxKind::HTML_STARTING_TAG);
192impl HtmlStartingTag {
193    /// Name of the tag
194    #[must_use]
195    pub fn name(&self) -> Option<SyntaxToken> {
196        self.syntax
197            .children_with_tokens()
198            .filter_map(NodeOrToken::into_token)
199            .find(|it| it.kind() == T![word] || it.kind() == T![twig component name])
200    }
201
202    /// Attributes of the tag
203    #[must_use]
204    pub fn attributes(&self) -> AstChildren<HtmlAttribute> {
205        match support::child::<HtmlAttributeList>(&self.syntax) {
206            Some(list) => support::children(&list.syntax),
207            // create an iterator for HtmlAttribute over the startingTag itself, which should yield no results
208            None => support::children(&self.syntax),
209        }
210    }
211
212    /// Parent complete html tag
213    #[must_use]
214    pub fn html_tag(&self) -> Option<HtmlTag> {
215        match self.syntax.parent() {
216            Some(p) => HtmlTag::cast(p),
217            None => None,
218        }
219    }
220
221    /// if the tag is a twig component, e.g. '<twig:my:component />'
222    #[must_use]
223    pub fn is_twig_component(&self) -> bool {
224        support::token(&self.syntax, T![twig component name]).is_some()
225    }
226}
227
228ast_node!(HtmlAttribute, SyntaxKind::HTML_ATTRIBUTE);
229impl HtmlAttribute {
230    /// Name of the attribute (left side of the equal sign)
231    #[must_use]
232    pub fn name(&self) -> Option<SyntaxToken> {
233        support::token(&self.syntax, T![word])
234    }
235
236    /// Value of the attribute
237    #[must_use]
238    pub fn value(&self) -> Option<HtmlString> {
239        support::child(&self.syntax)
240    }
241
242    /// Parent starting html tag
243    #[must_use]
244    pub fn html_tag(&self) -> Option<HtmlStartingTag> {
245        // first parent is HtmlAttributeList, the parent of that is the tag itself
246        match self.syntax.parent()?.parent() {
247            Some(p) => HtmlStartingTag::cast(p),
248            None => None,
249        }
250    }
251}
252
253ast_node!(HtmlEndingTag, SyntaxKind::HTML_ENDING_TAG);
254impl HtmlEndingTag {
255    /// Name of the tag
256    #[must_use]
257    pub fn name(&self) -> Option<SyntaxToken> {
258        self.syntax
259            .children_with_tokens()
260            .filter_map(NodeOrToken::into_token)
261            .find(|it| it.kind() == T![word] || it.kind() == T![twig component name])
262    }
263
264    /// Parent complete html tag
265    #[must_use]
266    pub fn html_tag(&self) -> Option<HtmlTag> {
267        match self.syntax.parent() {
268            Some(p) => HtmlTag::cast(p),
269            None => None,
270        }
271    }
272
273    /// if the tag is a twig component, e.g. '</twig:my:component>'
274    #[must_use]
275    pub fn is_twig_component(&self) -> bool {
276        support::token(&self.syntax, T![twig component name]).is_some()
277    }
278}
279
280ast_node!(TwigBinaryExpression, SyntaxKind::TWIG_BINARY_EXPRESSION);
281impl TwigBinaryExpression {
282    #[must_use]
283    pub fn operator(&self) -> Option<SyntaxToken> {
284        self.syntax
285            .children_with_tokens()
286            .find_map(|element| match element {
287                SyntaxElement::Token(t) if !t.kind().is_trivia() => Some(t),
288                _ => None,
289            })
290    }
291
292    #[must_use]
293    pub fn lhs_expression(&self) -> Option<TwigExpression> {
294        self.syntax.children().find_map(TwigExpression::cast)
295    }
296
297    #[must_use]
298    pub fn rhs_expression(&self) -> Option<TwigExpression> {
299        self.syntax
300            .children()
301            .filter_map(TwigExpression::cast)
302            .nth(1)
303    }
304}
305
306ast_node!(
307    LudtwigDirectiveRuleList,
308    SyntaxKind::LUDTWIG_DIRECTIVE_RULE_LIST
309);
310impl LudtwigDirectiveRuleList {
311    #[must_use]
312    pub fn get_rule_names(&self) -> Vec<String> {
313        self.syntax
314            .children_with_tokens()
315            .filter_map(|element| match element {
316                NodeOrToken::Token(t) if t.kind() == SyntaxKind::TK_WORD => {
317                    Some(t.text().to_string())
318                }
319                _ => None,
320            })
321            .collect()
322    }
323}
324
325ast_node!(
326    LudtwigDirectiveFileIgnore,
327    SyntaxKind::LUDTWIG_DIRECTIVE_FILE_IGNORE
328);
329impl LudtwigDirectiveFileIgnore {
330    #[must_use]
331    pub fn get_rules(&self) -> Vec<String> {
332        match support::child::<LudtwigDirectiveRuleList>(&self.syntax) {
333            Some(rule_list) => rule_list.get_rule_names(),
334            None => vec![],
335        }
336    }
337}
338
339ast_node!(LudtwigDirectiveIgnore, SyntaxKind::LUDTWIG_DIRECTIVE_IGNORE);
340impl LudtwigDirectiveIgnore {
341    #[must_use]
342    pub fn get_rules(&self) -> Vec<String> {
343        match support::child::<LudtwigDirectiveRuleList>(&self.syntax) {
344            Some(rule_list) => rule_list.get_rule_names(),
345            None => vec![],
346        }
347    }
348}
349
350ast_node!(TwigLiteralString, SyntaxKind::TWIG_LITERAL_STRING);
351impl TwigLiteralString {
352    #[must_use]
353    pub fn get_inner(&self) -> Option<TwigLiteralStringInner> {
354        support::child(&self.syntax)
355    }
356
357    #[must_use]
358    pub fn get_opening_quote(&self) -> Option<SyntaxToken> {
359        self.syntax
360            .children_with_tokens()
361            .take_while(|element| {
362                if element.as_node().is_some() {
363                    return false; // found inner string node
364                }
365
366                true
367            })
368            .find_map(|element| match element {
369                // first non trivia token should be a quote
370                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
371                _ => None,
372            })
373    }
374
375    #[must_use]
376    pub fn get_closing_quote(&self) -> Option<SyntaxToken> {
377        self.syntax
378            .children_with_tokens()
379            .skip_while(|element| {
380                if element.as_node().is_some() {
381                    return false; // found inner string node, stop skipping
382                }
383
384                true
385            })
386            .find_map(|element| match element {
387                // first non trivia token should be a quote
388                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
389                _ => None,
390            })
391    }
392}
393
394ast_node!(
395    TwigLiteralStringInner,
396    SyntaxKind::TWIG_LITERAL_STRING_INNER
397);
398impl TwigLiteralStringInner {
399    #[must_use]
400    pub fn get_interpolations(&self) -> AstChildren<TwigLiteralStringInterpolation> {
401        support::children(&self.syntax)
402    }
403}
404
405ast_node!(HtmlString, SyntaxKind::HTML_STRING);
406impl HtmlString {
407    #[must_use]
408    pub fn get_inner(&self) -> Option<HtmlStringInner> {
409        support::child(&self.syntax)
410    }
411
412    #[must_use]
413    pub fn get_opening_quote(&self) -> Option<SyntaxToken> {
414        self.syntax
415            .children_with_tokens()
416            .take_while(|element| {
417                if element.as_node().is_some() {
418                    return false; // found inner string node
419                }
420
421                true
422            })
423            .find_map(|element| match element {
424                // first non trivia token should be a quote
425                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
426                _ => None,
427            })
428    }
429
430    #[must_use]
431    pub fn get_closing_quote(&self) -> Option<SyntaxToken> {
432        self.syntax
433            .children_with_tokens()
434            .skip_while(|element| {
435                if element.as_node().is_some() {
436                    return false; // found inner string node, stop skipping
437                }
438
439                true
440            })
441            .find_map(|element| match element {
442                // first non trivia token should be a quote
443                NodeOrToken::Token(t) if !t.kind().is_trivia() => Some(t),
444                _ => None,
445            })
446    }
447}
448
449ast_node!(TwigExtends, SyntaxKind::TWIG_EXTENDS);
450impl TwigExtends {
451    #[must_use]
452    pub fn get_extends_keyword(&self) -> Option<SyntaxToken> {
453        support::token(&self.syntax, T!["extends"])
454    }
455}
456
457ast_node!(TwigVar, SyntaxKind::TWIG_VAR);
458impl TwigVar {
459    #[must_use]
460    pub fn get_expression(&self) -> Option<TwigExpression> {
461        support::child(&self.syntax)
462    }
463}
464
465ast_node!(TwigLiteralName, SyntaxKind::TWIG_LITERAL_NAME);
466impl TwigLiteralName {
467    #[must_use]
468    pub fn get_name(&self) -> Option<SyntaxToken> {
469        support::token(&self.syntax, SyntaxKind::TK_WORD)
470    }
471}
472
473ast_node!(Body, SyntaxKind::BODY);
474ast_node!(TwigExpression, SyntaxKind::TWIG_EXPRESSION);
475ast_node!(TwigUnaryExpression, SyntaxKind::TWIG_UNARY_EXPRESSION);
476ast_node!(
477    TwigParenthesesExpression,
478    SyntaxKind::TWIG_PARENTHESES_EXPRESSION
479);
480ast_node!(
481    TwigConditionalExpression,
482    SyntaxKind::TWIG_CONDITIONAL_EXPRESSION
483);
484ast_node!(TwigOperand, SyntaxKind::TWIG_OPERAND);
485ast_node!(TwigAccessor, SyntaxKind::TWIG_ACCESSOR);
486ast_node!(TwigFilter, SyntaxKind::TWIG_FILTER);
487impl TwigFilter {
488    /// The expression on the left side of the pipe `|`.
489    #[must_use]
490    pub fn operand(&self) -> Option<TwigOperand> {
491        support::child(&self.syntax)
492    }
493
494    /// The filter on the right side of the pipe `|`.
495    /// This can be a `TwigLiteralName` or a `TwigFunctionCall` inside the returned `TwigOperand`.
496    #[must_use]
497    pub fn filter(&self) -> Option<TwigOperand> {
498        support::children(&self.syntax).nth(1)
499    }
500}
501ast_node!(TwigIndexLookup, SyntaxKind::TWIG_INDEX_LOOKUP);
502ast_node!(TwigIndex, SyntaxKind::TWIG_INDEX);
503ast_node!(TwigIndexRange, SyntaxKind::TWIG_INDEX_RANGE);
504ast_node!(TwigFunctionCall, SyntaxKind::TWIG_FUNCTION_CALL);
505impl TwigFunctionCall {
506    /// The name of the function being called.
507    /// This is an operand which should contain a `TwigLiteralName`.
508    #[must_use]
509    pub fn name_operand(&self) -> Option<TwigOperand> {
510        support::child(&self.syntax)
511    }
512
513    /// The arguments of the function call.
514    #[must_use]
515    pub fn arguments(&self) -> Option<TwigArguments> {
516        support::child(&self.syntax)
517    }
518}
519ast_node!(TwigArrowFunction, SyntaxKind::TWIG_ARROW_FUNCTION);
520ast_node!(TwigArguments, SyntaxKind::TWIG_ARGUMENTS);
521ast_node!(TwigNamedArgument, SyntaxKind::TWIG_NAMED_ARGUMENT);
522ast_node!(
523    TwigLiteralStringInterpolation,
524    SyntaxKind::TWIG_LITERAL_STRING_INTERPOLATION
525);
526ast_node!(TwigLiteralNumber, SyntaxKind::TWIG_LITERAL_NUMBER);
527ast_node!(TwigLiteralArray, SyntaxKind::TWIG_LITERAL_ARRAY);
528ast_node!(TwigLiteralArrayInner, SyntaxKind::TWIG_LITERAL_ARRAY_INNER);
529ast_node!(TwigLiteralNull, SyntaxKind::TWIG_LITERAL_NULL);
530ast_node!(TwigLiteralBoolean, SyntaxKind::TWIG_LITERAL_BOOLEAN);
531ast_node!(TwigLiteralHash, SyntaxKind::TWIG_LITERAL_HASH);
532ast_node!(TwigLiteralHashItems, SyntaxKind::TWIG_LITERAL_HASH_ITEMS);
533ast_node!(TwigLiteralHashPair, SyntaxKind::TWIG_LITERAL_HASH_PAIR);
534ast_node!(TwigLiteralHashKey, SyntaxKind::TWIG_LITERAL_HASH_KEY);
535ast_node!(TwigLiteralHashValue, SyntaxKind::TWIG_LITERAL_HASH_VALUE);
536ast_node!(TwigComment, SyntaxKind::TWIG_COMMENT);
537ast_node!(TwigIf, SyntaxKind::TWIG_IF);
538ast_node!(TwigIfBlock, SyntaxKind::TWIG_IF_BLOCK);
539ast_node!(TwigElseIfBlock, SyntaxKind::TWIG_ELSE_IF_BLOCK);
540ast_node!(TwigElseBlock, SyntaxKind::TWIG_ELSE_BLOCK);
541ast_node!(TwigEndIfBlock, SyntaxKind::TWIG_ENDIF_BLOCK);
542ast_node!(TwigSet, SyntaxKind::TWIG_SET);
543ast_node!(TwigSetBlock, SyntaxKind::TWIG_SET_BLOCK);
544ast_node!(TwigEndSetBlock, SyntaxKind::TWIG_ENDSET_BLOCK);
545ast_node!(TwigAssignment, SyntaxKind::TWIG_ASSIGNMENT);
546ast_node!(TwigFor, SyntaxKind::TWIG_FOR);
547ast_node!(TwigForBlock, SyntaxKind::TWIG_FOR_BLOCK);
548ast_node!(TwigForElseBlock, SyntaxKind::TWIG_FOR_ELSE_BLOCK);
549ast_node!(TwigEndForBlock, SyntaxKind::TWIG_ENDFOR_BLOCK);
550ast_node!(TwigInclude, SyntaxKind::TWIG_INCLUDE);
551ast_node!(TwigIncludeWith, SyntaxKind::TWIG_INCLUDE_WITH);
552ast_node!(TwigUse, SyntaxKind::TWIG_USE);
553ast_node!(TwigOverride, SyntaxKind::TWIG_OVERRIDE);
554ast_node!(TwigApply, SyntaxKind::TWIG_APPLY);
555ast_node!(
556    TwigApplyStartingBlock,
557    SyntaxKind::TWIG_APPLY_STARTING_BLOCK
558);
559ast_node!(TwigApplyEndingBlock, SyntaxKind::TWIG_APPLY_ENDING_BLOCK);
560ast_node!(TwigAutoescape, SyntaxKind::TWIG_AUTOESCAPE);
561ast_node!(
562    TwigAutoescapeStartingBlock,
563    SyntaxKind::TWIG_AUTOESCAPE_STARTING_BLOCK
564);
565ast_node!(
566    TwigAutoescapeEndingBlock,
567    SyntaxKind::TWIG_AUTOESCAPE_ENDING_BLOCK
568);
569ast_node!(TwigDeprecated, SyntaxKind::TWIG_DEPRECATED);
570ast_node!(TwigDo, SyntaxKind::TWIG_DO);
571ast_node!(TwigEmbed, SyntaxKind::TWIG_EMBED);
572ast_node!(
573    TwigEmbedStartingBlock,
574    SyntaxKind::TWIG_EMBED_STARTING_BLOCK
575);
576ast_node!(TwigEmbedEndingBlock, SyntaxKind::TWIG_EMBED_ENDING_BLOCK);
577ast_node!(TwigFlush, SyntaxKind::TWIG_FLUSH);
578ast_node!(TwigFrom, SyntaxKind::TWIG_FROM);
579ast_node!(TwigImport, SyntaxKind::TWIG_IMPORT);
580ast_node!(TwigSandbox, SyntaxKind::TWIG_SANDBOX);
581ast_node!(
582    TwigSandboxStartingBlock,
583    SyntaxKind::TWIG_SANDBOX_STARTING_BLOCK
584);
585ast_node!(
586    TwigSandboxEndingBlock,
587    SyntaxKind::TWIG_SANDBOX_ENDING_BLOCK
588);
589ast_node!(TwigVerbatim, SyntaxKind::TWIG_VERBATIM);
590ast_node!(
591    TwigVerbatimStartingBlock,
592    SyntaxKind::TWIG_VERBATIM_STARTING_BLOCK
593);
594ast_node!(
595    TwigVerbatimEndingBlock,
596    SyntaxKind::TWIG_VERBATIM_ENDING_BLOCK
597);
598ast_node!(TwigMacro, SyntaxKind::TWIG_MACRO);
599ast_node!(
600    TwigMacroStartingBlock,
601    SyntaxKind::TWIG_MACRO_STARTING_BLOCK
602);
603ast_node!(TwigMacroEndingBlock, SyntaxKind::TWIG_MACRO_ENDING_BLOCK);
604ast_node!(TwigWith, SyntaxKind::TWIG_WITH);
605ast_node!(TwigWithStartingBlock, SyntaxKind::TWIG_WITH_STARTING_BLOCK);
606ast_node!(TwigWithEndingBlock, SyntaxKind::TWIG_WITH_ENDING_BLOCK);
607ast_node!(TwigCache, SyntaxKind::TWIG_CACHE);
608ast_node!(TwigCacheTTL, SyntaxKind::TWIG_CACHE_TTL);
609ast_node!(TwigCacheTags, SyntaxKind::TWIG_CACHE_TAGS);
610ast_node!(
611    TwigCacheStartingBlock,
612    SyntaxKind::TWIG_CACHE_STARTING_BLOCK
613);
614ast_node!(TwigCacheEndingBlock, SyntaxKind::TWIG_CACHE_ENDING_BLOCK);
615ast_node!(TwigProps, SyntaxKind::TWIG_PROPS);
616ast_node!(TwigPropDeclaration, SyntaxKind::TWIG_PROP_DECLARATION);
617ast_node!(TwigComponent, SyntaxKind::TWIG_COMPONENT);
618ast_node!(
619    TwigComponentStartingBlock,
620    SyntaxKind::TWIG_COMPONENT_STARTING_BLOCK
621);
622ast_node!(
623    TwigComponentEndingBlock,
624    SyntaxKind::TWIG_COMPONENT_ENDING_BLOCK
625);
626ast_node!(ShopwareTwigExtends, SyntaxKind::SHOPWARE_TWIG_SW_EXTENDS);
627ast_node!(ShopwareTwigInclude, SyntaxKind::SHOPWARE_TWIG_SW_INCLUDE);
628ast_node!(
629    ShopwareSilentFeatureCall,
630    SyntaxKind::SHOPWARE_SILENT_FEATURE_CALL
631);
632ast_node!(
633    ShopwareSilentFeatureCallStartingBlock,
634    SyntaxKind::SHOPWARE_SILENT_FEATURE_CALL_STARTING_BLOCK
635);
636ast_node!(
637    ShopwareSilentFeatureCallEndingBlock,
638    SyntaxKind::SHOPWARE_SILENT_FEATURE_CALL_ENDING_BLOCK
639);
640ast_node!(ShopwareReturn, SyntaxKind::SHOPWARE_RETURN);
641ast_node!(ShopwareIcon, SyntaxKind::SHOPWARE_ICON);
642ast_node!(ShopwareIconStyle, SyntaxKind::SHOPWARE_ICON_STYLE);
643ast_node!(ShopwareThumbnails, SyntaxKind::SHOPWARE_THUMBNAILS);
644ast_node!(ShopwareThumbnailsWith, SyntaxKind::SHOPWARE_THUMBNAILS_WITH);
645ast_node!(HtmlDoctype, SyntaxKind::HTML_DOCTYPE);
646ast_node!(HtmlAttributeList, SyntaxKind::HTML_ATTRIBUTE_LIST);
647ast_node!(HtmlStringInner, SyntaxKind::HTML_STRING_INNER);
648ast_node!(HtmlText, SyntaxKind::HTML_TEXT);
649ast_node!(HtmlRawText, SyntaxKind::HTML_RAW_TEXT);
650ast_node!(HtmlComment, SyntaxKind::HTML_COMMENT);
651ast_node!(Error, SyntaxKind::ERROR);
652ast_node!(Root, SyntaxKind::ROOT);
653ast_node!(TwigTrans, SyntaxKind::TWIG_TRANS);
654ast_node!(
655    TwigTransStartingBlock,
656    SyntaxKind::TWIG_TRANS_STARTING_BLOCK
657);
658ast_node!(TwigTransEndingBlock, SyntaxKind::TWIG_TRANS_ENDING_BLOCK);
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::parse;
664    use expect_test::expect;
665
666    fn parse_and_extract<T: AstNode<Language = TemplateLanguage>>(input: &str) -> T {
667        let (tree, errors) = parse(input).split();
668        assert_eq!(errors, vec![]);
669        support::child(&tree).unwrap()
670    }
671
672    #[test]
673    fn simple_html_tag() {
674        let raw = r#"<div class="hello">world {{ 42 }}</div>"#;
675        let html_tag: HtmlTag = parse_and_extract(raw);
676
677        assert_eq!(format!("{html_tag}"), raw.to_string());
678        expect![[r#"
679            HTML_TAG@0..39
680              HTML_STARTING_TAG@0..19
681                TK_LESS_THAN@0..1 "<"
682                TK_WORD@1..4 "div"
683                HTML_ATTRIBUTE_LIST@4..18
684                  HTML_ATTRIBUTE@4..18
685                    TK_WHITESPACE@4..5 " "
686                    TK_WORD@5..10 "class"
687                    TK_EQUAL@10..11 "="
688                    HTML_STRING@11..18
689                      TK_DOUBLE_QUOTES@11..12 "\""
690                      HTML_STRING_INNER@12..17
691                        TK_WORD@12..17 "hello"
692                      TK_DOUBLE_QUOTES@17..18 "\""
693                TK_GREATER_THAN@18..19 ">"
694              BODY@19..33
695                HTML_TEXT@19..24
696                  TK_WORD@19..24 "world"
697                TWIG_VAR@24..33
698                  TK_WHITESPACE@24..25 " "
699                  TK_OPEN_CURLY_CURLY@25..27 "{{"
700                  TWIG_EXPRESSION@27..30
701                    TWIG_LITERAL_NUMBER@27..30
702                      TK_WHITESPACE@27..28 " "
703                      TK_NUMBER@28..30 "42"
704                  TK_WHITESPACE@30..31 " "
705                  TK_CLOSE_CURLY_CURLY@31..33 "}}"
706              HTML_ENDING_TAG@33..39
707                TK_LESS_THAN_SLASH@33..35 "</"
708                TK_WORD@35..38 "div"
709                TK_GREATER_THAN@38..39 ">""#]]
710        .assert_eq(&format!("{html_tag:?}"));
711
712        assert!(!html_tag.is_self_closing());
713        assert_eq!(
714            html_tag.name().map(|t| t.to_string()),
715            Some("div".to_string())
716        );
717        assert_eq!(
718            html_tag.starting_tag().map(|t| t.to_string()),
719            Some(r#"<div class="hello">"#.to_string())
720        );
721        assert_eq!(
722            html_tag.body().map(|t| t.to_string()),
723            Some("world {{ 42 }}".to_string())
724        );
725        assert_eq!(
726            html_tag.ending_tag().map(|t| t.to_string()),
727            Some("</div>".to_string())
728        );
729        assert_eq!(html_tag.attributes().count(), 1);
730        assert_eq!(
731            html_tag
732                .attributes()
733                .next()
734                .and_then(|t| t.name())
735                .map(|t| t.to_string()),
736            Some("class".to_string())
737        );
738        assert_eq!(
739            html_tag
740                .attributes()
741                .next()
742                .and_then(|t| t.value())
743                .and_then(|t| t.get_inner())
744                .map(|t| t.to_string()),
745            Some("hello".to_string())
746        );
747    }
748}