rstml/node/
parse.rs

1//!
2//! Implementation of ToTokens and Spanned for node related structs
3
4use proc_macro2::{extra::DelimSpan, Delimiter, TokenStream};
5use proc_macro2_diagnostics::{Diagnostic, Level};
6use quote::ToTokens;
7use syn::{
8    braced,
9    parse::{discouraged::Speculative, Parse, ParseStream, Parser},
10    spanned::Spanned,
11    token::Brace,
12    Block, Ident, LitStr, Token,
13};
14
15use super::{
16    atoms::{
17        tokens::{self, DocStart},
18        CloseTag, FragmentClose, FragmentOpen, OpenTag,
19    },
20    raw_text::RawText,
21    CustomNode, Node, NodeBlock, NodeDoctype, NodeFragment,
22};
23use crate::{
24    atoms::CloseTagStart,
25    config::TransformBlockFn,
26    node::{NodeAttribute, NodeElement},
27    parser::recoverable::{ParseRecoverable, RecoverableContext},
28};
29
30impl ParseRecoverable for NodeBlock {
31    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
32        let fork = input.fork();
33
34        let block = match parse_valid_block_expr(parser, &fork) {
35            Ok(value) => {
36                input.advance_to(&fork);
37                NodeBlock::ValidBlock(value)
38            }
39            Err(e) if parser.config().recover_block => {
40                parser.push_diagnostic(e);
41                NodeBlock::Invalid(parser.parse_simple(input)?)
42            }
43            Err(e) => {
44                parser.push_diagnostic(e);
45                return None;
46            }
47        };
48        Some(block)
49    }
50}
51
52impl<C: CustomNode> ParseRecoverable for NodeFragment<C> {
53    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
54        let tag_open: FragmentOpen = parser.parse_simple(input)?;
55
56        let is_raw = |name| parser.config().raw_text_elements.contains(name);
57
58        let (children, tag_close) = if is_raw("") {
59            let (child, closed_tag) =
60                parser.parse_with_ending(input, |_, t| RawText::from(t), FragmentClose::parse);
61
62            (vec![Node::<C>::RawText(child)], closed_tag)
63        } else {
64            let (child, close_tag_start) =
65                parser.parse_tokens_until_call::<Node<C>, _, _>(input, CloseTagStart::parse);
66            (
67                child,
68                FragmentClose::parse_with_start_tag(parser, input, close_tag_start),
69            )
70        };
71        let open_tag_end = tag_open.token_gt.span();
72        let close_tag_start = tag_close.as_ref().map(|v| v.start_tag.token_lt.span());
73
74        let children = RawText::vec_set_context(open_tag_end, close_tag_start, children);
75
76        Some(NodeFragment {
77            tag_open,
78            children,
79            tag_close,
80        })
81    }
82}
83
84impl ParseRecoverable for NodeDoctype {
85    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
86        let token_start = parser.parse_simple::<DocStart>(input)?;
87        let doctype_keyword = parser.parse_simple::<Ident>(input)?;
88        if doctype_keyword.to_string().to_lowercase() != "doctype" {
89            parser.push_diagnostic(Diagnostic::spanned(
90                doctype_keyword.span(),
91                Level::Error,
92                "expected DOCTYPE keyword",
93            ));
94            return None;
95        }
96        let (value, token_end) =
97            parser.parse_with_ending(input, |_, t| RawText::from(t), <Token![>]>::parse);
98
99        let token_end = token_end?;
100        Some(Self {
101            token_start,
102            token_doctype: doctype_keyword,
103            value,
104            token_end,
105        })
106    }
107}
108
109impl OpenTag {
110    /// Parses the opening `<` of an open tag, providing handling of the
111    /// unexpected `</` of a close tag.
112    ///
113    /// **Note:** This is an internal function exported to make parsing of
114    /// custom nodes easier. It is not considered stable.
115    pub fn parse_start_tag(
116        parser: &mut RecoverableContext,
117        input: ParseStream,
118    ) -> Option<Token![<]> {
119        let token_lt = parser.parse_simple::<Token![<]>(input)?;
120        // Found closing tag when open tag was expected
121        // keep parsing it as open tag.
122        if input.peek(Token![/]) {
123            let span = if let Ok(solidus) = input.parse::<Token![/]>() {
124                solidus.span()
125            } else {
126                token_lt.span()
127            };
128            parser.push_diagnostic(Diagnostic::spanned(
129                span,
130                Level::Error,
131                "close tag was parsed while waiting for open tag",
132            ));
133        }
134        Some(token_lt)
135    }
136}
137
138impl ParseRecoverable for OpenTag {
139    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
140        let token_lt = Self::parse_start_tag(parser, input)?;
141        let name = parser.parse_simple(input)?;
142        let generics = parser.parse_simple(input)?;
143
144        let (attributes, end_tag) = parser
145            .parse_tokens_with_conflicted_ending::<NodeAttribute, _, _>(
146                input,
147                tokens::OpenTagEnd::parse,
148            );
149
150        if end_tag.is_none() {
151            parser.push_diagnostic(Diagnostic::new(Level::Error, "expected end of tag '>'"));
152        }
153        end_tag.map(|end_tag| OpenTag {
154            token_lt,
155            name,
156            generics,
157            attributes,
158            end_tag,
159        })
160    }
161}
162
163impl<C: CustomNode> NodeElement<C> {
164    /// Parses the children of a node, stopping at the first matching closing
165    /// tag, following the behavior specified in the [`ParserConfig`].
166    ///
167    /// **Note:** This is an internal function exported to make parsing of
168    /// custom nodes easier. It is not considered stable.
169    ///
170    /// [`ParserConfig`]: crate::ParserConfig
171    pub fn parse_children(
172        parser: &mut RecoverableContext,
173        input: ParseStream,
174        raw: bool,
175        open_tag: &OpenTag,
176    ) -> Option<(Vec<Node<C>>, Option<CloseTag>)> {
177        let (children, close_tag) = if raw {
178            let (child, closed_tag) =
179                parser.parse_with_ending(input, |_, t| RawText::from(t), CloseTag::parse);
180            // don't keep empty RawText
181            let children = if !child.is_empty() {
182                vec![Node::RawText(child)]
183            } else {
184                vec![]
185            };
186            (children, closed_tag)
187        } else {
188            // If node is not raw use any closing tag as separator, to early report about
189            // invalid closing tags.
190            // Also parse only </ part to recover parser as soon as user types </
191            let (children, close_tag) =
192                parser.parse_tokens_until_call::<Node<C>, _, _>(input, CloseTagStart::parse);
193
194            let close_tag = CloseTag::parse_with_start_tag(parser, input, close_tag);
195
196            (children, close_tag)
197        };
198
199        let open_tag_end = open_tag.end_tag.token_gt.span();
200        let close_tag_start = close_tag.as_ref().map(|c| c.start_tag.token_lt.span());
201        let children = RawText::vec_set_context(open_tag_end, close_tag_start, children);
202
203        let Some(close_tag) = close_tag else {
204            let mut diagnostic = Diagnostic::spanned(
205                open_tag.span(),
206                Level::Error,
207                "open tag has no corresponding close tag",
208            );
209            if !children.is_empty() {
210                let mut note_span = TokenStream::new();
211                children.iter().for_each(|v| v.to_tokens(&mut note_span));
212                diagnostic = diagnostic.span_note(
213                    note_span.span(),
214                    "treating all inputs after open tag as it content",
215                );
216            }
217
218            parser.push_diagnostic(diagnostic);
219            return Some((children, None));
220        };
221
222        if close_tag.name != open_tag.name {
223            match parser.config().element_close_wildcard.as_deref() {
224                Some(is_wildcard) if is_wildcard(open_tag, &close_tag) => {}
225                _ => {
226                    let diagnostic = Diagnostic::spanned(
227                        close_tag.span(),
228                        Level::Error,
229                        "wrong close tag found",
230                    )
231                    .spanned_child(
232                        open_tag.span(),
233                        Level::Help,
234                        "open tag that should be closed; it's started here",
235                    );
236
237                    parser.push_diagnostic(diagnostic)
238                }
239            }
240        }
241        if close_tag.generics != open_tag.generics {
242            let diagnostic = Diagnostic::spanned(
243                close_tag.span(),
244                Level::Error,
245                "close tag generics missmatch",
246            )
247            .spanned_child(
248                open_tag.span(),
249                Level::Help,
250                "open tag generics should match close tag generics",
251            );
252            parser.push_diagnostic(diagnostic)
253        }
254        Some((children, Some(close_tag)))
255    }
256}
257
258impl<C: CustomNode> ParseRecoverable for NodeElement<C> {
259    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
260        let open_tag: OpenTag = parser.parse_recoverable(input)?;
261        let is_known_self_closed =
262            |name| parser.config().always_self_closed_elements.contains(name);
263        let is_raw = |name| parser.config().raw_text_elements.contains(name);
264
265        let tag_name_str = &*open_tag.name.to_string();
266        if open_tag.is_self_closed() || is_known_self_closed(tag_name_str) {
267            return Some(NodeElement {
268                open_tag,
269                children: vec![],
270                close_tag: None,
271            });
272        }
273        let (children, close_tag) =
274            Self::parse_children(parser, input, is_raw(tag_name_str), &open_tag)?;
275        let element = NodeElement {
276            open_tag,
277            children,
278            close_tag,
279        };
280        Some(element)
281    }
282}
283
284impl<C: CustomNode> ParseRecoverable for Node<C> {
285    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
286        let node = if C::peek_element(&input.fork()) {
287            Node::Custom(C::parse_recoverable(parser, input)?)
288        } else if input.peek(Token![<]) {
289            if input.peek2(Token![!]) {
290                if input.peek3(Ident) {
291                    Node::Doctype(parser.parse_recoverable(input)?)
292                } else {
293                    Node::Comment(parser.parse_simple(input)?)
294                }
295            } else if input.peek2(Token![>]) {
296                Node::Fragment(parser.parse_recoverable(input)?)
297            } else {
298                Node::Element(parser.parse_recoverable(input)?)
299            }
300        } else if input.peek(Brace) {
301            Node::Block(parser.parse_recoverable(input)?)
302        } else if input.peek(LitStr) {
303            Node::Text(parser.parse_simple(input)?)
304        } else if !input.is_empty() {
305            // Parse any input except of any other Node starting
306            Node::RawText(parser.parse_recoverable(input)?)
307        } else {
308            return None;
309        };
310        Some(node)
311    }
312}
313
314// This method couldn't be const generic until https://github.com/rust-lang/rust/issues/63569
315/// Parse array of tokens with
316pub(super) fn parse_array_of2_tokens<T: Parse>(input: ParseStream) -> syn::Result<[T; 2]> {
317    Ok([input.parse()?, input.parse()?])
318}
319
320pub(super) fn to_tokens_array<I>(input: &mut TokenStream, iter: I)
321where
322    I: IntoIterator,
323    I::Item: ToTokens,
324{
325    use quote::TokenStreamExt;
326    input.append_all(iter)
327}
328
329/// Replace the next [`TokenTree::Group`] in the given parse stream with a
330/// token stream returned by a user callback, or parse as original block if
331/// no token stream is returned.
332fn block_transform(input: ParseStream, transform_fn: &TransformBlockFn) -> syn::Result<Block> {
333    input.step(|cursor| {
334        let (block_group, block_span, next) = cursor
335            .group(Delimiter::Brace)
336            .ok_or_else(|| cursor.error("unexpected: no Group found"))?;
337        let parser = move |block_content: ParseStream| {
338            let forked_block_content = block_content.fork();
339
340            match transform_fn(&forked_block_content) {
341                Ok(transformed_tokens) => match transformed_tokens {
342                    Some(tokens) => {
343                        let parser = move |input: ParseStream| {
344                            Ok(block_expr_with_extern_span(input, block_span))
345                        };
346                        let transformed_content = parser.parse2(tokens)?;
347                        block_content.advance_to(&forked_block_content);
348                        transformed_content
349                    }
350                    None => block_expr_with_extern_span(block_content, block_span),
351                },
352                Err(error) => Err(error),
353            }
354        };
355
356        Ok((parser.parse2(block_group.token_stream())?, next))
357    })
358}
359
360#[allow(clippy::needless_pass_by_ref_mut)]
361pub(crate) fn parse_valid_block_expr(
362    parser: &mut RecoverableContext,
363    input: syn::parse::ParseStream,
364) -> syn::Result<Block> {
365    let transform_block = parser.config().transform_block.clone();
366    let value = if let Some(transform_fn) = transform_block {
367        block_transform(input, &*transform_fn)?
368    } else {
369        block_expr(input)?
370    };
371    Ok(value)
372}
373/// Parse the given stream and span as [`Expr::Block`].
374fn block_expr_with_extern_span(input: ParseStream, span: DelimSpan) -> syn::Result<Block> {
375    Ok(Block {
376        brace_token: Brace { span },
377        stmts: Block::parse_within(input)?,
378    })
379}
380
381/// Parse the given stream as [`Expr::Block`].
382pub(crate) fn block_expr(input: syn::parse::ParseStream) -> syn::Result<Block> {
383    let content;
384    let brace_token = braced!(content in input);
385    Ok(Block {
386        brace_token,
387        stmts: Block::parse_within(&content)?,
388    })
389}