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}