Skip to content

Commit ea7a94a

Browse files
committed
feat: parsing utilities for custom nodes
1 parent a7d2377 commit ea7a94a

File tree

4 files changed

+162
-25
lines changed

4 files changed

+162
-25
lines changed

src/node/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod attribute;
1111
mod node_name;
1212
mod node_value;
1313
pub mod parse;
14+
mod parser_ext;
1415
mod raw_text;
1516

1617
pub use attribute::{

src/node/parse.rs

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,11 @@ impl ParseRecoverable for NodeDoctype {
114114
}
115115
}
116116

117-
impl ParseRecoverable for OpenTag {
118-
fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
117+
impl OpenTag {
118+
pub fn parse_start_tag(
119+
parser: &mut RecoverableContext,
120+
input: ParseStream,
121+
) -> Option<Token![<]> {
119122
let token_lt = parser.parse_simple::<Token![<]>(input)?;
120123
// Found closing tag when open tag was expected
121124
// keep parsing it as open tag.
@@ -131,6 +134,13 @@ impl ParseRecoverable for OpenTag {
131134
"close tag was parsed while waiting for open tag",
132135
));
133136
}
137+
Some(token_lt)
138+
}
139+
}
140+
141+
impl ParseRecoverable for OpenTag {
142+
fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
143+
let token_lt = Self::parse_start_tag(parser, input)?;
134144
let name = parser.parse_simple(input)?;
135145
let generics = parser.parse_simple(input)?;
136146

@@ -150,23 +160,14 @@ impl ParseRecoverable for OpenTag {
150160
}
151161
}
152162

153-
impl<C: CustomNode> ParseRecoverable for NodeElement<C> {
154-
fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
155-
let open_tag: OpenTag = parser.parse_recoverable(input)?;
156-
let is_known_self_closed =
157-
|name| parser.config().always_self_closed_elements.contains(name);
158-
let is_raw = |name| parser.config().raw_text_elements.contains(name);
159-
160-
let tag_name_str = &*open_tag.name.to_string();
161-
if open_tag.is_self_closed() || is_known_self_closed(tag_name_str) {
162-
return Some(NodeElement {
163-
open_tag,
164-
children: vec![],
165-
close_tag: None,
166-
});
167-
}
168-
169-
let (children, close_tag) = if is_raw(tag_name_str) {
163+
impl<C: CustomNode> NodeElement<C> {
164+
pub fn parse_children(
165+
parser: &mut RecoverableContext,
166+
input: ParseStream,
167+
raw: bool,
168+
open_tag: &OpenTag,
169+
) -> Option<(Vec<Node<C>>, Option<CloseTag>)> {
170+
let (children, close_tag) = if raw {
170171
let (child, closed_tag) =
171172
parser.parse_with_ending(input, |_, t| RawText::from(t), CloseTag::parse);
172173
// don't keep empty RawText
@@ -208,11 +209,7 @@ impl<C: CustomNode> ParseRecoverable for NodeElement<C> {
208209
}
209210

210211
parser.push_diagnostic(diagnostic);
211-
return Some(NodeElement {
212-
open_tag,
213-
children,
214-
close_tag: None,
215-
});
212+
return Some(( children, None, ));
216213
};
217214

218215
if close_tag.name != open_tag.name {
@@ -247,10 +244,31 @@ impl<C: CustomNode> ParseRecoverable for NodeElement<C> {
247244
);
248245
parser.push_diagnostic(diagnostic)
249246
}
247+
Some((children, Some(close_tag)))
248+
}
249+
}
250+
251+
impl<C: CustomNode> ParseRecoverable for NodeElement<C> {
252+
fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
253+
let open_tag: OpenTag = parser.parse_recoverable(input)?;
254+
let is_known_self_closed =
255+
|name| parser.config().always_self_closed_elements.contains(name);
256+
let is_raw = |name| parser.config().raw_text_elements.contains(name);
257+
258+
let tag_name_str = &*open_tag.name.to_string();
259+
if open_tag.is_self_closed() || is_known_self_closed(tag_name_str) {
260+
return Some(NodeElement {
261+
open_tag,
262+
children: vec![],
263+
close_tag: None,
264+
});
265+
}
266+
let (children, close_tag) =
267+
Self::parse_children(parser, input, is_raw(tag_name_str), &open_tag)?;
250268
let element = NodeElement {
251269
open_tag,
252270
children,
253-
close_tag: Some(close_tag),
271+
close_tag,
254272
};
255273
Some(element)
256274
}

src/node/parser_ext.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use proc_macro2::{TokenStream, TokenTree};
2+
use syn::parse::{discouraged::Speculative, Parse, ParseStream};
3+
4+
use crate::recoverable::RecoverableContext;
5+
6+
impl RecoverableContext {
7+
/// Like [`parse_simple`], but splits the tokenstream at `E` first only
8+
/// parsing the tokens before it as `T`.
9+
///
10+
/// [`parse_simple`]: #method.parse_simple
11+
pub fn parse_simple_with_ending<T: Parse, E: Parse>(
12+
&mut self,
13+
input: ParseStream,
14+
) -> Option<(T, E)> {
15+
let mut tokens = TokenStream::new();
16+
let res = loop {
17+
// Use fork, because we can't limit separator to be only Peekable for custom
18+
// tokens but we also need to parse complex expressions like
19+
// "foo=x/y" or "/>"
20+
let fork = input.fork();
21+
if let Ok(end) = fork.parse() {
22+
input.advance_to(&fork);
23+
break Some(end);
24+
}
25+
26+
if input.is_empty() {
27+
break None;
28+
}
29+
30+
let next: TokenTree = self
31+
.parse_simple(input)
32+
.expect("TokenTree should always be parsable");
33+
tokens.extend([next]);
34+
};
35+
res.and_then(|res| {
36+
self.save_diagnostics(syn::parse2(tokens))
37+
.map(|val| (val, res))
38+
})
39+
}
40+
}

tests/custom_node.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use quote::{ToTokens, TokenStreamExt};
2+
use rstml::{
3+
atoms::{self, OpenTag, OpenTagEnd},
4+
node::{CustomNode, Node, NodeElement},
5+
recoverable::Recoverable,
6+
};
7+
use syn::{parse_quote, Expr, Token};
8+
9+
#[derive(Debug, syn_derive::ToTokens)]
10+
struct If {
11+
token_lt: Token![<],
12+
token_if: Token![if],
13+
condition: Expr,
14+
open_tag_end: OpenTagEnd,
15+
#[to_tokens(TokenStreamExt::append_all)]
16+
body: Vec<Node>,
17+
close_tag: Option<atoms::CloseTag>,
18+
}
19+
20+
impl CustomNode for If {
21+
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
22+
ToTokens::to_tokens(&self, tokens)
23+
}
24+
25+
fn peek_element(input: syn::parse::ParseStream) -> bool {
26+
input.peek(Token![<]) && input.peek2(Token![if])
27+
}
28+
29+
fn parse_element(
30+
parser: &mut rstml::recoverable::RecoverableContext,
31+
input: syn::parse::ParseStream,
32+
) -> Option<Self> {
33+
let token_lt = OpenTag::parse_start_tag(parser, input)?;
34+
let token_if = parser.parse_simple(input)?;
35+
let (condition, open_tag_end): (_, OpenTagEnd) = parser.parse_simple_with_ending(input)?;
36+
let (body, close_tag) = if open_tag_end.token_solidus.is_none() {
37+
// Passed to allow parsing of close_tag
38+
NodeElement::parse_children(
39+
parser,
40+
input,
41+
false,
42+
&OpenTag {
43+
token_lt,
44+
name: parse_quote!(#token_if),
45+
generics: Default::default(),
46+
attributes: Default::default(),
47+
end_tag: open_tag_end.clone(),
48+
},
49+
)?
50+
} else {
51+
(Vec::new(), None)
52+
};
53+
Some(Self {
54+
token_lt,
55+
token_if,
56+
condition,
57+
open_tag_end,
58+
body,
59+
close_tag,
60+
})
61+
}
62+
}
63+
64+
#[test]
65+
fn custom_node() {
66+
let actual: Recoverable<Node<If>> = parse_quote! {
67+
<if just && an || expression>
68+
<a regular="html" component/>
69+
<div>
70+
</div>
71+
</if>
72+
};
73+
let Node::Custom(actual) = actual.inner() else {
74+
panic!()
75+
};
76+
77+
assert_eq!(actual.condition, parse_quote!(just && an || expression));
78+
}

0 commit comments

Comments
 (0)