html_to_bevy/
lib.rs

1use proc_macro::{token_stream, Delimiter, TokenStream, TokenTree};
2use std::iter::Peekable;
3
4#[macro_use]
5mod macros;
6
7extern crate proc_macro;
8
9type StructName = String;
10#[derive(Default)]
11struct StructInfo {
12    visibility: Option<String>,
13    node: Option<String>,
14    attributes: Vec<String>,
15    vars: Vec<VarInfo>,
16}
17type ClassName = String;
18struct ClassInfo {
19    visibility: Option<String>,
20    attributes: Vec<String>,
21    vars: Vec<VarInfo>,
22}
23type StyleInfo = (Vec<(StructName, StructInfo)>, Vec<(ClassName, ClassInfo)>);
24type AttributeName = String;
25type AttributeValue = String;
26
27/// Creates bevy ui from an html like syntax.
28///
29/// Tag names are optional, and allow you to style elements and query those elements in other parts of your bevy code. Classes allow you to apply the same style to multiple elements. Also, styles made from a macro call can be used in other macro calls.
30///
31/// ## Example
32/// ```rust
33/// html!(
34///     <Container>
35///         <>"Hello, World!"</>
36///     </Container>
37/// );
38/// App.add_systems(Startup, Container::spawn_as_root);
39/// ```
40/// # Reusing Roots
41/// Root elements can be used as elements in other macro calls, by formatting it as a self closing tag.
42///
43/// ## Example
44/// ```rust
45/// html!(
46///     <Info>
47///         <>"Hello,"</>
48///         <>"World"</>
49///     </Info>
50/// );
51/// html!(
52///     <head>
53///     <style>
54///         .odd {
55///             Node {
56///                 flex_direction: FlexDirection::Column,
57///                 ..default()
58///             };
59///         }
60///         .even {
61///             Node {
62///                 flex_direction: FlexDirection::Row,
63///                 ..default()
64///             };
65///         }
66///     </style>
67///     </head>
68///
69///     <Container>
70///         <Info class="odd" />
71///         <Info class="even" />
72///         <Info class="odd" />
73///     </Container>
74/// );
75/// App.add_systems(Startup, Container::spawn_as_root);
76/// ```
77/// # Style Variables
78/// String variables can be used when styling elements to make them more reusable. Elements inherit their parent's variable values, unless the value is reset.
79/// ## Example
80/// In this example, a generic `User` tag is made, with children `FName` and `LName`. `FName` and `LName` use variables in their text content, but in other parts of the code, these value can be set on the `User` tag.
81/// ```rust
82/// html!(
83/// <head>
84/// <style>
85///     User {
86///     }
87///     Name {
88///         Node {
89///             column_gap: Val::Px(10.),
90///             ..default()
91///         };
92///     }
93///     FName {
94///         $fname = "Error";
95///
96///         Text::from(String::from($fname));
97///     }
98///     LName {
99///         $lname = "Error";
100///
101///         Text::from(String::from($lname));
102///     }
103/// </style>
104/// </head>
105/// <User>
106///     <Name>
107///         <FName></FName><LName></LName>
108///     </Name>
109/// </User>
110/// );
111/// html!(
112/// <head>
113/// <style>
114/// Container {
115///     Node {
116///         flex_direction: FlexDirection::Column,
117///         ..default()
118///     };
119/// }
120/// </style>
121/// </head>
122///
123/// <Container>
124///     <User fname="John" lname="Smith" />
125///     <User fname="Jane" lname="Smith" />
126/// </Container>
127/// );
128/// ```
129///
130#[proc_macro]
131pub fn html(input: TokenStream) -> TokenStream {
132    let mut tokens = input.into_iter().peekable();
133
134    let mut styles: (Vec<(String, StructInfo)>, Vec<(String, ClassInfo)>) =
135        match parse_head(&mut tokens) {
136            Ok(styles) => styles,
137            Err(err) => return err,
138        };
139
140    let root = match get_root_tag(&mut tokens) {
141        Ok(root_tag) => root_tag,
142        Err(err) => return err,
143    };
144
145    if let Some((root_tag, _root_attributes)) = &root {
146        if !styles
147            .0
148            .iter()
149            .any(|struct_style| &struct_style.0 == root_tag)
150        {
151            styles.0.push((root_tag.clone(), StructInfo::default()));
152        }
153    }
154
155    let mut result = implement_styles(&styles);
156
157    if let Some((root_tag, mut root_attributes)) = root {
158        let root_visibility = styles
159            .0
160            .iter()
161            .find(|struct_style| struct_style.0 == root_tag)
162            .map(|struct_style| struct_style.1.visibility.clone())
163            .unwrap_or_default();
164
165        match parse_root(
166            &mut tokens,
167            &root_tag,
168            &mut root_attributes,
169            root_visibility.as_deref(),
170            &styles.1.iter().map(|class| class.0.as_str()).collect(),
171        ) {
172            Ok(body_result) => result.push_str(&body_result),
173            Err(err) => return err,
174        };
175    }
176
177    match result.parse() {
178        Ok(result) => result,
179        Err(err) => format_compile_error!("Could not parse result: {err}"),
180    }
181}
182#[allow(clippy::too_many_lines)]
183fn parse_head(tokens: &mut Peekable<token_stream::IntoIter>) -> Result<StyleInfo, TokenStream> {
184    let mut structs: Vec<(StructName, StructInfo)> = Vec::new();
185    let mut classes: Vec<(ClassName, ClassInfo)> = Vec::new();
186    let mut visibility = None;
187
188    if !peek_matches_tag(tokens.clone(), "head") {
189        return Ok((structs, classes));
190    }
191    assert_next_tag(tokens, "head")?;
192    if peek_matches_tag(tokens.clone(), "style") {
193        tokens.nth(2);
194
195        while let Some(token) = tokens.peek() {
196            match token {
197                TokenTree::Punct(p) => match p.as_char() {
198                    '<' => {
199                        break;
200                    }
201                    // Class
202                    '.' => {
203                        tokens.next();
204                        let class_name = assert_next_token!(tokens, Ident, Err).to_string();
205
206                        let TokenTree::Group(group) =
207                            assert_next_token!(tokens, Group, Delimiter::Brace, Err)
208                        else {
209                            return Err(format_compile_error!("Not reachable"));
210                        };
211                        let mut group_tokens = group.stream().into_iter().peekable();
212                        let mut attributes = vec![];
213                        let mut vars = vec![];
214                        while group_tokens.peek().is_some() {
215                            if peek_matches_token!(group_tokens, Punct, "$") {
216                                vars.push(parse_local_var(&mut group_tokens)?);
217                            } else {
218                                let attribute = parse_style_attribute(&mut group_tokens, "$")?;
219                                assert_next_token!(group_tokens, Punct, ";", Err);
220                                attributes.push(attribute);
221                            }
222                        }
223                        classes.push((
224                            class_name,
225                            ClassInfo {
226                                visibility: visibility.take(),
227                                attributes,
228                                vars,
229                            },
230                        ));
231                    }
232                    unexpected => {
233                        return Err(format_compile_error!(
234                            "Unexpected value {unexpected} in style"
235                        ))
236                    }
237                },
238                // Visibility
239                TokenTree::Ident(ident) if ident.to_string() == "pub" => {
240                    let ident = ident.to_string();
241                    tokens.next();
242                    visibility = match tokens.peek() {
243                        Some(TokenTree::Group(group))
244                            if group.delimiter() == Delimiter::Parenthesis =>
245                        {
246                            let group = group.to_string();
247                            tokens.next();
248                            Some(format!("{ident}{group}"))
249                        }
250                        _ => Some(ident),
251                    }
252                }
253                // Tag
254                TokenTree::Ident(struct_name) => {
255                    let struct_name = struct_name.to_string();
256                    tokens.next();
257
258                    let TokenTree::Group(group) =
259                        assert_next_token!(tokens, Group, Delimiter::Brace, Err)
260                    else {
261                        return Err(format_compile_error!("Not reachable"));
262                    };
263                    let mut group_tokens = group.stream().into_iter().peekable();
264                    let mut node = None;
265                    let mut attributes = vec![];
266                    let mut vars = vec![];
267                    while group_tokens.peek().is_some() {
268                        if peek_matches_token!(group_tokens, Punct, "$") {
269                            vars.push(parse_local_var(&mut group_tokens)?);
270                        } else {
271                            let attribute = parse_style_attribute(&mut group_tokens, "")?;
272                            assert_next_token!(group_tokens, Punct, ";", Err);
273                            if &attribute[.."Node".len()] == "Node" {
274                                node = Some(attribute);
275                            } else {
276                                attributes.push(attribute);
277                            }
278                        }
279                    }
280                    structs.push((
281                        struct_name,
282                        StructInfo {
283                            visibility: visibility.take(),
284                            node,
285                            attributes,
286                            vars,
287                        },
288                    ));
289                }
290                unexpected => {
291                    return Err(format_compile_error!(
292                        "Unexpected value {unexpected} in style"
293                    ))
294                }
295            }
296        }
297
298        assert_next_end_tag(tokens, "style")?;
299    }
300    assert_next_end_tag(tokens, "head")?;
301
302    Ok((structs, classes))
303}
304struct VarInfo {
305    name: String,
306    value: String,
307}
308fn parse_local_var(tokens: &mut Peekable<token_stream::IntoIter>) -> Result<VarInfo, TokenStream> {
309    assert_next_token!(tokens, Punct, "$", Err);
310    let name = assert_next_token!(tokens, Ident, Err).to_string();
311    assert_next_token!(tokens, Punct, "=", Err);
312    let value = assert_next_string_lit!(tokens, Err);
313    assert_next_token!(tokens, Punct, ";", Err);
314
315    Ok(VarInfo { name, value })
316}
317const fn delimiter_to_chars(delimiter: Delimiter) -> (char, char) {
318    match delimiter {
319        Delimiter::Brace => ('{', '}'),
320        Delimiter::Parenthesis => ('(', ')'),
321        Delimiter::Bracket => ('[', ']'),
322        Delimiter::None => (' ', ' '),
323    }
324}
325fn parse_style_attribute(
326    group_tokens: &mut Peekable<token_stream::IntoIter>,
327    attributes_prefix: &str,
328) -> Result<String, TokenStream> {
329    let mut token_stack = vec![];
330    let mut group_delimiter_stack = vec![];
331    let attribute = {
332        let mut collected = String::new();
333        let mut last_was_ident = false;
334        while token_stack.last().is_some()
335            || (group_tokens.peek().is_some() && !peek_matches_token!(group_tokens, Punct, ";"))
336        {
337            let tokens = token_stack.last_mut().unwrap_or(group_tokens);
338            let Some(next_token) = tokens.peek() else {
339                unreachable!()
340            };
341            match next_token {
342                TokenTree::Group(group) => {
343                    let delimiter = group.delimiter();
344                    group_delimiter_stack.push(delimiter);
345                    collected.push(delimiter_to_chars(delimiter).0);
346
347                    let group_tokens = group.stream().into_iter().peekable();
348                    tokens.next();
349                    token_stack.push(group_tokens);
350                }
351                TokenTree::Punct(punct) if punct.as_char() == '$' => {
352                    tokens.next();
353                    let var = assert_next_token!(tokens, Ident, Err);
354                    collected.push_str(&format!(
355                        "{attributes_prefix}attributes.get(\"{var}\").unwrap_or(&{var})"
356                    ));
357                }
358                token => {
359                    if let TokenTree::Ident(_) = token {
360                        if last_was_ident {
361                            collected.push(' ');
362                        }
363                        last_was_ident = true;
364                    } else {
365                        last_was_ident = false;
366                    }
367
368                    let token = token.to_string();
369                    tokens.next();
370                    collected.push_str(&token);
371                }
372            }
373            while token_stack
374                .last_mut()
375                .is_some_and(|tokens| tokens.peek().is_none())
376            {
377                if let Some(delimiter) = group_delimiter_stack.pop() {
378                    collected.push(delimiter_to_chars(delimiter).1);
379                }
380
381                token_stack.pop();
382            }
383        }
384        collected
385    };
386    Ok(attribute)
387}
388fn implement_styles(styles: &StyleInfo) -> String {
389    let mut result = String::new();
390    let (structs, classes) = styles;
391
392    for (struct_name, struct_info) in structs {
393        let visibility = struct_info
394            .visibility
395            .clone()
396            .map_or_else(<_>::default, |visibility| format!("{visibility} "));
397
398        let node = struct_info
399            .node
400            .as_ref()
401            .map_or_else(|| "Node::default()".to_owned(), ToOwned::to_owned);
402
403        let mut attributes = String::new();
404
405        if !struct_info.attributes.is_empty() {
406            attributes.push_str("me");
407            for attribute in &struct_info.attributes {
408                attributes.push_str(&format!(".insert({attribute})"));
409            }
410            attributes.push(';');
411        }
412
413        let vars = {
414            let mut vars = String::new();
415            for var in &struct_info.vars {
416                vars.push_str(&format!(
417                    "let {}: String = String::from(\"{}\");\n",
418                    var.name, var.value
419                ));
420            }
421            vars
422        };
423
424        // Attributes (Variables) need to be passed into apply_attributes method
425        result.push_str(&format!(
426            "
427            #[derive(Component)]\n
428            #[allow(dead_code)]\n
429            {visibility}struct {struct_name};\n
430            impl {struct_name} {{\n
431                #![allow(unused_variables)]\n
432
433                {visibility}fn spawn_as_child<'a>(parent: &'a mut ChildBuilder<'_>, attributes: &std::collections::HashMap<String,String>) -> EntityCommands<'a> {{\n
434                    parent.spawn((Self, Self::get_node(attributes)))\n
435                }}\n
436
437                {visibility}fn get_node(attributes: &std::collections::HashMap<String,String>) -> Node {{\n
438                    {vars}\n
439                    {node}\n
440                }}\n
441
442                {visibility}fn apply_attributes<'a>(mut me: EntityCommands<'a>, asset_server: &'a Res<AssetServer>, attributes: &std::collections::HashMap<String,String>) -> EntityCommands<'a> {{\n
443                    {vars}\n
444                    {attributes}\n
445                    me\n
446                }}\n
447            }}"
448        ));
449    }
450    for (class_name, class_info) in classes {
451        let macro_name = format!("apply_{class_name}_class");
452
453        let (visibility, attributes, vars) = {
454            let visibility = class_info
455                .visibility
456                .clone()
457                .map_or_else(<_>::default, |visibility| {
458                    format!("{visibility} use {macro_name};")
459                });
460
461            let mut attributes = String::new();
462
463            if !class_info.attributes.is_empty() {
464                attributes.push_str("element");
465                for attribute in &class_info.attributes {
466                    attributes.push_str(&format!(".insert({attribute})"));
467                }
468                attributes.push(';');
469            }
470
471            let vars = {
472                let mut vars = String::new();
473                for var in &class_info.vars {
474                    vars.push_str(&format!(
475                        "let {}: String = String::from(\"{}\");\n",
476                        var.name, var.value
477                    ));
478                }
479                vars
480            };
481
482            (visibility, attributes, vars)
483        };
484
485        result.push_str(&format!(
486            "
487            #[allow(unused_macros)]
488            macro_rules! {macro_name} {{\n
489                ($element:expr, $asset_server:ident, $attributes:ident) => {{{{\n
490                    #[allow(unused_variables)]
491                    let asset_server = &$asset_server;
492                    {vars}
493                    #[allow(unused_mut)]
494                    let mut element = $element;\n
495                    {attributes}
496                    element\n
497                }}}}\n
498            }}\n
499            {visibility}"
500        ));
501    }
502
503    result
504}
505fn get_root_tag(
506    tokens: &mut Peekable<token_stream::IntoIter>,
507) -> Result<Option<(String, Vec<(AttributeName, AttributeValue)>)>, TokenStream> {
508    if tokens.peek().is_some() {
509        assert_next_token!(tokens, Punct, "<", Err);
510        let Some(tag_name) = tokens.next().map(|token| token.to_string()) else {
511            return Err(format_compile_error!("Expected root tag name"));
512        };
513        let attribute_info = parse_attributes(tokens)?;
514        assert_next_token!(tokens, Punct, ">", Err);
515        Ok(Some((tag_name, attribute_info)))
516    } else {
517        Ok(None)
518    }
519}
520fn parse_root(
521    tokens: &mut Peekable<token_stream::IntoIter>,
522    root_tag: &str,
523    root_attributes: &mut Vec<(AttributeName, AttributeValue)>,
524    root_visibility: Option<&str>,
525    implemented_classes: &Vec<&str>,
526) -> Result<String, TokenStream> {
527    let root_visibility = root_visibility.unwrap_or_default();
528    let classes = classes_from_attributes(root_attributes);
529    let (apply_classes, apply_classes_end) = get_apply_classes_code(implemented_classes, classes)?;
530    let attribute_tuples = get_attribute_tuples(root_attributes);
531
532    let is_literal = peek_matches_token!(tokens, Literal);
533    let literal_value = if is_literal {
534        Some(assert_next_string_lit!(tokens, Err))
535    } else {
536        None
537    };
538    let literal_value_code = literal_value
539        .map(|literal_value| format!(", Text::from(\"{literal_value}\")"))
540        .unwrap_or_default();
541
542    let mut result = format!("
543    impl {root_tag}{{\n
544        {root_visibility}fn spawn_as_root(mut commands: Commands, asset_server: Res<AssetServer>) {{\n
545            let attributes: std::collections::HashMap<String, String> = std::collections::HashMap::from([{attribute_tuples}]);\n
546            let entity = commands.spawn((Self, Self::get_node(&attributes){literal_value_code})).id();\n
547            let mut me = commands.entity(entity);\n
548            Self::spawn_children(&mut me, &asset_server, &attributes);\n
549            {apply_classes}Self::apply_attributes(me, &asset_server, &attributes){apply_classes_end};\n
550        }}\n
551        {root_visibility}fn spawn<'a>(parent: &'a mut ChildBuilder<'_>, asset_server: &'a Res<AssetServer>, new_attributes: &std::collections::HashMap<String,String>) -> EntityCommands<'a> {{\n
552            let mut attributes: std::collections::HashMap<String, String> = std::collections::HashMap::from([{attribute_tuples}]);\n
553            for (k, v) in new_attributes {{\n
554                attributes.insert(k.clone(), v.clone());\n
555            }}\n
556            let mut me = {apply_classes}Self::apply_attributes(\n
557                parent.spawn((Self, Self::get_node(&attributes){literal_value_code})),\n
558                asset_server,\n
559                &attributes,\n
560            ){apply_classes_end};\n
561            Self::spawn_children(&mut me, asset_server, &attributes);\n
562            me\n
563        }}\n
564        {root_visibility}fn spawn_children(me: &mut EntityCommands<'_>, asset_server: &Res<AssetServer>, new_attributes: &std::collections::HashMap<String,String>) {{\n
565            me.with_children(|parent| {{\n");
566    if !is_literal {
567        while tokens
568            .clone()
569            .nth(1)
570            .is_some_and(|token| token.to_string() != "/")
571        {
572            let tag_result = parse_tag(tokens, implemented_classes)?;
573            result.push_str(&tag_result);
574        }
575    }
576    assert_next_end_tag(tokens, root_tag)?;
577    result.push_str("});\n}\n}");
578
579    Ok(result)
580}
581
582fn get_attribute_tuples(attributes: &[(String, String)]) -> String {
583    let attribute_tuples = attributes
584        .iter()
585        .fold(String::new(), |mut result, attribute| {
586            result.push_str(&format!(
587                "(String::from(\"{}\"), String::from(\"{}\")),",
588                attribute.0, attribute.1
589            ));
590            result
591        });
592    attribute_tuples
593}
594#[allow(clippy::too_many_lines)]
595fn parse_tag(
596    tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
597    implemented_classes: &Vec<&str>,
598) -> Result<String, TokenStream> {
599    let mut result = String::new();
600
601    assert_next_token!(tokens, Punct, "<", Err);
602    let (struct_name, classes, attributes) = if let Some(TokenTree::Ident(ident)) = tokens.peek() {
603        let ident = ident.to_string();
604        let struct_name = if tokens
605            .clone()
606            .nth(1)
607            .is_some_and(|token| token.to_string() == "=")
608        {
609            None
610        } else {
611            tokens.next();
612            Some(ident)
613        };
614        let mut attributes = parse_attributes(tokens)?;
615        let classes = classes_from_attributes(&mut attributes);
616
617        (struct_name, classes, attributes)
618    } else {
619        (None, None, vec![])
620    };
621    let is_self_closing = match tokens.next() {
622        Some(TokenTree::Punct(punct)) if [">", "/"].contains(&punct.to_string().as_ref()) => {
623            punct.to_string() == "/"
624        }
625        _ => {
626            return Err(format_compile_error!(
627                "Expected > or /{}.",
628                struct_name.map_or_else(String::new, |struct_name| format!(" after {struct_name}"))
629            ));
630        }
631    };
632
633    let (apply_classes, apply_classes_end) = get_apply_classes_code(implemented_classes, classes)?;
634
635    let attribute_tuples = get_attribute_tuples(&attributes);
636    if is_self_closing {
637        // Has no children
638        let Some(struct_name) = struct_name else {
639            return Err(format_compile_error!("Self closing tag must have a name"));
640        };
641        result.push_str(&format!("{{\n
642            let mut attributes: std::collections::HashMap<String, String> = std::collections::HashMap::<String, String>::from([{attribute_tuples}]);\n
643            for (k, v) in new_attributes {{\n
644                attributes.insert(k.clone(), v.clone());\n
645            }}\n
646            {apply_classes}{struct_name}::apply_attributes({struct_name}::spawn(parent, asset_server, &attributes), asset_server, &attributes){apply_classes_end}\n
647        }};"));
648    } else {
649        // Potentially has children
650        if !(peek_matches_token!(tokens, Literal) || peek_matches_token!(tokens, Punct, "<")) {
651            return Err(format_compile_error!("Expected end tag"));
652        }
653
654        result.push_str(&struct_name.as_ref().map_or_else(
655            || format!("{{{apply_classes}parent.spawn(Node::default()){apply_classes_end}"),
656            |struct_name| {
657                format!("{{\n
658                    let mut attributes = std::collections::HashMap::<String, String>::from([{attribute_tuples}]);\n
659                    for (k, v) in new_attributes {{\n
660                        attributes.insert(k.clone(), v.clone());\n
661                    }}\n
662                    {apply_classes}{struct_name}::apply_attributes({struct_name}::spawn_as_child(parent, &attributes), asset_server, &attributes){apply_classes_end}")
663            },
664        ));
665        if let Some(TokenTree::Literal(literal)) = tokens.peek() {
666            let literal = literal.to_string();
667            tokens.next();
668            result.push_str(&format!(".insert(Text::from({literal}));"));
669        } else {
670            result.push_str(".with_children(|parent| {\n");
671            while tokens
672                .clone()
673                .nth(1)
674                .is_some_and(|token| token.to_string() != "/")
675            {
676                match parse_tag(tokens, implemented_classes) {
677                    Ok(child_result) => {
678                        result.push_str(&child_result);
679                    }
680                    error => {
681                        return error;
682                    }
683                }
684            }
685            result.push_str("});");
686        }
687        result.push('}');
688        assert_next_token!(tokens, Punct, "<", Err);
689        assert_next_token!(tokens, Punct, "/", Err);
690        if peek_matches_token!(tokens, Ident) {
691            let tag = collect_until_token!(tokens, Punct, ">");
692            if struct_name
693                .clone()
694                .is_none_or(|struct_name| tag != struct_name)
695            {
696                return Err(format_compile_error!(
697                    "Expected </{}>",
698                    struct_name.unwrap_or_default()
699                ));
700            }
701        }
702    }
703    assert_next_token!(tokens, Punct, ">", Err);
704
705    Ok(result)
706}
707
708fn classes_from_attributes(attributes: &mut Vec<(String, String)>) -> Option<Vec<String>> {
709    let class_info = attributes
710        .iter()
711        .position(|attribute| attribute.0 == "class");
712    let classes = class_info.map(|class_info| {
713        attributes
714            .swap_remove(class_info)
715            .1
716            .split_whitespace()
717            .map(str::to_string)
718            .collect::<Vec<_>>()
719    });
720    classes
721}
722fn get_apply_classes_code(
723    implemented_classes: &Vec<&str>,
724    classes: Option<Vec<String>>,
725) -> Result<(String, String), TokenStream> {
726    let (apply_classes, apply_classes_end) = if let Some(classes) = classes {
727        let (mut apply_classes, mut apply_classes_end) = (String::new(), String::new());
728        for class in classes {
729            if !implemented_classes.contains(&class.as_ref()) {
730                return Err(format_compile_error!(
731                    "Class \\\"{class}\\\" does not exist"
732                ));
733            }
734
735            apply_classes.push_str(&format!("apply_{class}_class!("));
736            apply_classes_end.push_str(", asset_server, attributes)");
737        }
738
739        (apply_classes, apply_classes_end)
740    } else {
741        (String::new(), String::new())
742    };
743    Ok((apply_classes, apply_classes_end))
744}
745fn peek_matches_tag(mut tokens: impl Iterator<Item = TokenTree>, expected: &str) -> bool {
746    if let (Some(TokenTree::Punct(p1)), Some(TokenTree::Ident(tag)), Some(TokenTree::Punct(p2))) =
747        (tokens.next(), tokens.next(), tokens.next())
748    {
749        p1.as_char() == '<' && p2.as_char() == '>' && tag.to_string() == expected
750    } else {
751        false
752    }
753}
754fn assert_next_end_tag(
755    tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
756    expected: &str,
757) -> Result<(), TokenStream> {
758    let first = tokens.peek().map(ToString::to_string);
759    let second = tokens.clone().nth(1).map(|token| token.to_string());
760    match (tokens.next(), tokens.next(), tokens.next(), tokens.next()) {
761        (
762            Some(TokenTree::Punct(p1)),
763            Some(TokenTree::Punct(p2)),
764            Some(TokenTree::Ident(tag)),
765            Some(TokenTree::Punct(p3)),
766        ) if p1.as_char() == '<' && p2.as_char() == '/' && p3.as_char() == '>' => {
767            if tag.to_string() != expected {
768                return Err(format_compile_error!(
769                    "Expected </{expected}>, but received </{tag}>",
770                ));
771            }
772            Ok(())
773        }
774        _ => {
775            if let Some(first) = first {
776                return Err(format_compile_error!(
777                    "Expected </{expected}>, but received {first}{}",
778                    second.unwrap_or_default()
779                ));
780            }
781            Err(format_compile_error!("Expected </{expected}>",))
782        }
783    }
784}
785fn assert_next_tag(
786    tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
787    expected: &str,
788) -> Result<(), TokenStream> {
789    match (tokens.next(), tokens.next(), tokens.next()) {
790        (Some(TokenTree::Punct(p1)), Some(TokenTree::Ident(tag)), Some(TokenTree::Punct(p2)))
791            if p1.as_char() == '<' && p2.as_char() == '>' =>
792        {
793            if tag.to_string() != expected {
794                return Err(format_compile_error!(
795                    "Expected <{expected}>, but received <{tag}>",
796                ));
797            }
798            Ok(())
799        }
800        _ => Err(format_compile_error!("Expected <{expected}>",)),
801    }
802}
803fn parse_attributes(
804    tokens: &mut Peekable<impl Iterator<Item = TokenTree> + Clone>,
805) -> Result<Vec<(AttributeName, AttributeValue)>, TokenStream> {
806    let mut result = vec![];
807    while !(peek_matches_token!(tokens, Punct, ">") || peek_matches_token!(tokens, Punct, "/")) {
808        let attribute_name = assert_next_token!(tokens, Ident, Err).to_string();
809        assert_next_token!(tokens, Punct, "=", Err);
810        let attribute_value = assert_next_string_lit!(tokens, Err);
811        result.push((attribute_name, attribute_value));
812    }
813    Ok(result)
814}