syn_rsx/lib.rs
1//! [`syn`]-powered parser for JSX-like [`TokenStream`]s, aka RSX. The parsed
2//! result is a nested [`Node`] structure, similar to the browser DOM, where
3//! node name and value are syn expressions to support building proc macros.
4//!
5//! ```rust
6//! # fn main() -> eyre::Result<()> {
7//! use std::convert::TryFrom;
8//!
9//! use eyre::bail;
10//! use quote::quote;
11//! use syn_rsx::{parse2, Node, NodeAttribute, NodeElement, NodeText};
12//!
13//! // Create HTML `TokenStream`.
14//! let tokens = quote! { <hello world>"hi"</hello> };
15//!
16//! // Parse the tokens into a tree of `Node`s.
17//! let nodes = parse2(tokens)?;
18//!
19//! // Extract some specific nodes from the tree.
20//! let Node::Element(element) = &nodes[0] else { bail!("element") };
21//! let Node::Attribute(attribute) = &element.attributes[0] else { bail!("attribute") };
22//! let Node::Text(text) = &element.children[0] else { bail!("text") };
23//!
24//! // Work with the nodes.
25//! assert_eq!(element.name.to_string(), "hello");
26//! assert_eq!(attribute.key.to_string(), "world");
27//! assert_eq!(String::try_from(&text.value)?, "hi");
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! You might want to check out the [html-to-string-macro example] as well.
33//!
34//! ## Features
35//!
36//! - **Not opinionated**
37//!
38//! Every tag or attribute name is valid
39//!
40//! ```rust
41//! # use quote::quote;
42//! # use syn_rsx::parse2;
43//! # parse2(quote! {
44//! <hello world />
45//! # }).unwrap();
46//! ```
47//!
48//! - **Text nodes**
49//!
50//! Support for [unquoted text is planned].
51//!
52//! ```rust
53//! # use quote::quote;
54//! # use syn_rsx::parse2;
55//! # parse2(quote! {
56//! <div>"String literal"</div>
57//! # }).unwrap();
58//! ```
59//!
60//! - **Node names separated by dash, colon or double colon**
61//!
62//! ```rust
63//! # use quote::quote;
64//! # use syn_rsx::parse2;
65//! # parse2(quote! {
66//! <tag-name attribute-key="value" />
67//! <tag:name attribute:key="value" />
68//! <tag::name attribute::key="value" />
69//! # }).unwrap();
70//! ```
71//!
72//! - **Node names with reserved keywords**
73//!
74//! ```rust
75//! # use quote::quote;
76//! # use syn_rsx::parse2;
77//! # parse2(quote! {
78//! <input type="submit" />
79//! # }).unwrap();
80//! ```
81//!
82//! - **Doctypes, Comments and Fragments**
83//!
84//! ```rust
85//! # use quote::quote;
86//! # use syn_rsx::parse2;
87//! # parse2(quote! {
88//! <!DOCTYPE html>
89//! <!-- "comment" -->
90//! <></>
91//! # }).unwrap();
92//! ```
93//!
94//! - **Braced blocks are parsed as arbitrary Rust code**
95//!
96//! ```rust
97//! # use quote::quote;
98//! # use syn_rsx::parse2;
99//! # parse2(quote! {
100//! <{ let block = "in node name position"; } />
101//! <div>{ let block = "in node position"; }</div>
102//! <div { let block = "in attribute position"; } />
103//! <div key={ let block = "in attribute value position"; } />
104//! # }).unwrap();
105//! ```
106//!
107//! - **Attribute values can be any valid syn expression without requiring
108//! braces**
109//!
110//! ```rust
111//! # use quote::quote;
112//! # use syn_rsx::parse2;
113//! # parse2(quote! {
114//! <div key=some::value() />
115//! # }).unwrap();
116//! ```
117//!
118//! - **Helpful error reporting out of the box**
119//!
120//! ```ignore
121//! error: open tag has no corresponding close tag and is not self-closing
122//! --> examples/html-to-string-macro/tests/lib.rs:5:24
123//! |
124//! 5 | html_to_string! { <div> };
125//! | ^^^
126//! ```
127//!
128//! - **Customization**
129//!
130//! A [`ParserConfig`] to customize parsing behavior is available, so if you
131//! have slightly different requirements for parsing and it's not yet
132//! customizable feel free to open an issue or pull request to extend the
133//! configuration.
134//!
135//! One highlight with regards to customization is the [`transform_block`]
136//! configuration, which takes a closure that receives raw block content as
137//! `ParseStream` and lets you optionally convert it to a `TokenStream`. That
138//! makes it possible to have custom syntax in blocks. More details in [#9]
139//!
140//!
141//! [`syn`]: /syn
142//! [`TokenStream`]: https://doc.rust-lang.org/proc_macro/struct.TokenStream.html
143//! [`Node`]: enum.Node.html
144//! [`ParserConfig`]: struct.ParserConfig.html
145//! [mod style path]: https://docs.rs/syn/1.0.40/syn/struct.Path.html#method.parse_mod_style
146//! [unquoted text is planned]: https://github.com/stoically/syn-rsx/issues/2
147//! [`transform_block`]: struct.ParserConfig.html#method.transform_block
148//! [#9]: https://github.com/stoically/syn-rsx/issues/9
149//! [html-to-string-macro example]: https://github.com/stoically/syn-rsx/tree/main/examples/html-to-string-macro
150
151extern crate proc_macro;
152
153use syn::{
154 parse::{ParseStream, Parser as _},
155 Result,
156};
157
158mod config;
159mod error;
160mod node;
161mod parser;
162
163pub mod punctuation {
164 //! Custom syn punctuations
165 use syn::custom_punctuation;
166
167 custom_punctuation!(Dash, -);
168}
169
170pub use config::ParserConfig;
171pub use error::Error;
172pub use node::*;
173pub use parser::Parser;
174
175/// Parse the given [`proc-macro::TokenStream`] into a [`Node`] tree.
176///
177/// [`proc-macro::TokenStream`]: https://doc.rust-lang.org/proc_macro/struct.TokenStream.html
178/// [`Node`]: struct.Node.html
179pub fn parse(tokens: proc_macro::TokenStream) -> Result<Vec<Node>> {
180 let parser = move |input: ParseStream| Parser::new(ParserConfig::default()).parse(input);
181
182 parser.parse(tokens)
183}
184
185/// Parse the given [`proc-macro::TokenStream`] into a [`Node`] tree with custom
186/// [`ParserConfig`].
187///
188/// [`proc-macro::TokenStream`]: https://doc.rust-lang.org/proc_macro/struct.TokenStream.html
189/// [`Node`]: struct.Node.html
190/// [`ParserConfig`]: struct.ParserConfig.html
191pub fn parse_with_config(
192 tokens: proc_macro::TokenStream,
193 config: ParserConfig,
194) -> Result<Vec<Node>> {
195 let parser = move |input: ParseStream| Parser::new(config).parse(input);
196
197 parser.parse(tokens)
198}
199
200/// Parse the given [`proc-macro2::TokenStream`] into a [`Node`] tree.
201///
202/// [`proc-macro2::TokenStream`]: https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html
203/// [`Node`]: struct.Node.html
204pub fn parse2(tokens: proc_macro2::TokenStream) -> Result<Vec<Node>> {
205 let parser = move |input: ParseStream| Parser::new(ParserConfig::default()).parse(input);
206
207 parser.parse2(tokens)
208}
209
210/// Parse the given [`proc-macro2::TokenStream`] into a [`Node`] tree with
211/// custom [`ParserConfig`].
212///
213/// [`proc-macro2::TokenStream`]: https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html
214/// [`Node`]: struct.Node.html
215/// [`ParserConfig`]: struct.ParserConfig.html
216pub fn parse2_with_config(
217 tokens: proc_macro2::TokenStream,
218 config: ParserConfig,
219) -> Result<Vec<Node>> {
220 let parser = move |input: ParseStream| Parser::new(config).parse(input);
221
222 parser.parse2(tokens)
223}