This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/html_tree/
tag.rs

1use proc_macro2::{Span, TokenStream, TokenTree};
2use quote::{quote, ToTokens};
3use syn::parse::{ParseStream, Parser};
4use syn::Token;
5
6/// Check whether two spans are equal.
7/// The implementation is really silly but I couldn't find another way to do it on stable.
8/// This check isn't required to be fully accurate so it's not the end of the world if it breaks.
9fn span_eq_hack(a: &Span, b: &Span) -> bool {
10    format!("{a:?}") == format!("{b:?}")
11}
12
13/// Change all occurrences of span `from` to `to` in the given error.
14fn error_replace_span(err: syn::Error, from: Span, to: impl ToTokens) -> syn::Error {
15    let err_it = err.into_iter().map(|err| {
16        if span_eq_hack(&err.span(), &from) {
17            syn::Error::new_spanned(&to, err.to_string())
18        } else {
19            err
20        }
21    });
22
23    // SAFETY: all errors have at least one message
24    crate::join_errors(err_it).unwrap_err()
25}
26
27/// Helper type for parsing HTML tags.
28/// The struct only stores the associated tokens, not the content of the tag.
29/// This is meant to mirror the design of delimiters in `syn`.
30pub struct TagTokens {
31    pub lt: Token![<],
32    pub div: Option<Token![/]>,
33    pub gt: Token![>],
34}
35impl TagTokens {
36    /// Parse the content of a start tag.
37    /// The given parse function is called with a `ParseStream`
38    /// containing only the contents of the tag and surrounding `TagTokens`.
39    pub fn parse_start_content<T>(
40        input: ParseStream,
41        parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
42    ) -> syn::Result<T> {
43        Self::parse_content(Self::parse_start(input)?, parse)
44    }
45
46    /// Same as `parse_start_content` but for end tags.
47    pub fn parse_end_content<T>(
48        input: ParseStream,
49        parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
50    ) -> syn::Result<T> {
51        Self::parse_content(Self::parse_end(input)?, parse)
52    }
53
54    fn parse_content<T>(
55        (tag, content): (Self, TokenStream),
56        parse: impl FnOnce(ParseStream, Self) -> syn::Result<T>,
57    ) -> syn::Result<T> {
58        let scope_spanned = tag.to_spanned();
59        let content_parser = |input: ParseStream| {
60            parse(input, tag).map_err(|err| {
61                // we can't modify the scope span used by `ParseStream`. It just uses the call site
62                // by default. The scope span is used when an error can't be
63                // attributed to a token tree (ex. when the input is empty).
64                // We rewrite all spans to point at the tag which at least narrows down the correct
65                // location. It's not ideal, but it'll have to do until `syn` gives
66                // us more access.
67                error_replace_span(err, Span::call_site(), &scope_spanned)
68            })
69        };
70        content_parser.parse2(content)
71    }
72
73    /// Parse a start tag
74    fn parse_start(input: ParseStream) -> syn::Result<(Self, TokenStream)> {
75        let lt = input.parse()?;
76        let (content, div, gt) = Self::parse_until_end(input)?;
77
78        Ok((Self { lt, div, gt }, content))
79    }
80
81    /// Parse an end tag.
82    /// `div` will always be `Some` for end tags.
83    fn parse_end(input: ParseStream) -> syn::Result<(Self, TokenStream)> {
84        let lt = input.parse()?;
85        let div = Some(input.parse()?);
86
87        let (content, end_div, gt) = Self::parse_until_end(input)?;
88        if end_div.is_some() {
89            return Err(syn::Error::new_spanned(
90                end_div,
91                "unexpected `/` in this end tag",
92            ));
93        }
94
95        Ok((Self { lt, div, gt }, content))
96    }
97
98    fn parse_until_end(
99        input: ParseStream,
100    ) -> syn::Result<(TokenStream, Option<Token![/]>, Token![>])> {
101        let mut inner_trees = Vec::new();
102        let mut angle_count: usize = 1;
103        let mut div: Option<Token![/]> = None;
104        let gt: Token![>];
105
106        loop {
107            let next = input.parse()?;
108            if let TokenTree::Punct(punct) = &next {
109                match punct.as_char() {
110                    '/' => {
111                        if angle_count == 1 && input.peek(Token![>]) {
112                            div = Some(syn::token::Slash {
113                                spans: [punct.span()],
114                            });
115                            gt = input.parse()?;
116                            break;
117                        }
118                    }
119                    '>' => {
120                        angle_count = angle_count.checked_sub(1).ok_or_else(|| {
121                            syn::Error::new_spanned(
122                                punct,
123                                "this tag close has no corresponding tag open",
124                            )
125                        })?;
126                        if angle_count == 0 {
127                            gt = syn::token::Gt {
128                                spans: [punct.span()],
129                            };
130                            break;
131                        }
132                    }
133                    '<' => angle_count += 1,
134                    _ => {}
135                };
136            }
137
138            inner_trees.push(next);
139        }
140
141        Ok((inner_trees.into_iter().collect(), div, gt))
142    }
143
144    /// Generate tokens which can be used in `syn::Error::new_spanned` to span the entire tag.
145    /// This is to work around the limitation of being unable to manually join spans on stable.
146    pub fn to_spanned(&self) -> impl ToTokens {
147        let Self { lt, gt, .. } = self;
148        quote! {#lt #gt}
149    }
150}