yew_html_ext/
lib.rs

1//! This crate provides handy extensions to [Yew](https://yew.rs)'s
2//! [HTML macros](https://docs.rs/yew/latest/yew/macro.html.html).
3//! It provides [`html!`] and [`html_nested!`] macros that are fully backwards-compatible with the
4//! original ones defined in Yew, meaning all one has to do to start using this crate is
5//! just change the uses/imports of `yew::html{_nested}` to `yew_html_ext::html{_nested}`.
6//! # New syntax
7//! ## `for` loops
8//! The syntax is the same as of Rust's `for` loops, the body of the loop can contain 0 or more
9//! nodes.
10//! ```rust
11//! use yew_html_ext::html;
12//! use yew::{Properties, function_component, html::Html};
13//!
14//! #[derive(PartialEq, Properties)]
15//! struct CountdownProps {
16//!     n: usize,
17//! }
18//!
19//! #[function_component]
20//! fn Countdown(props: &CountdownProps) -> Html {
21//!     html! {
22//!         <div>
23//!             for i in (0 .. props.n).rev() {
24//!                 <h2>{ i }</h2>
25//!                 <br />
26//!             }
27//!         </div>
28//!     }
29//! }
30//! ```
31//! In a list of nodes all nodes must have unique keys or have no key, which is why using a
32//! constant to specify a key of a node in a loop is dangerous: if the loop iterates more than
33//! once, the generated list will have repeated keys; as a best-effort attempt to prevent such
34//! cases, the macro disallows specifying literals or constants as keys
35//! ```rust,compile_fail
36//! # use yew::{Properties, function_component, html::Html};
37//! # use yew_html_ext::html;
38//! #
39//! # #[derive(PartialEq, Properties)]
40//! # struct CountdownProps {
41//! #     n: usize,
42//! # }
43//! #
44//! # #[function_component]
45//! # fn Countdown(props: &CountdownProps) -> Html {
46//! html! {
47//!     <div>
48//!         for i in (0 .. props.n).rev() {
49//!             <h2 key="number" /* nuh-uh */>{ i }</h2>
50//!             <br />
51//!         }
52//!     </div>
53//! }
54//! # }
55//! ```
56//! ## `match` nodes
57//! The syntax is the same as of Rust's `match` expressions; the body of a match arm must have
58//! exactly 1 node. That node may be just `{}`, which will expand to nothing.
59//! ```rust
60//! use yew_html_ext::html;
61//! use yew::{Properties, function_component, html::Html};
62//! use std::cmp::Ordering;
63//!
64//! #[derive(PartialEq, Properties)]
65//! struct ComparisonProps {
66//!     int1: usize,
67//!     int2: usize,
68//! }
69//!
70//! #[function_component]
71//! fn Comparison(props: &ComparisonProps) -> Html {
72//!     html! {
73//!         match props.int1.cmp(&props.int2) {
74//!             Ordering::Less => { '<' },
75//!             Ordering::Equal => { '=' },
76//!             Ordering::Greater => { '>' },
77//!             _ => {},
78//!         }
79//!     }
80//! }
81//! ```
82//! ## `let` bindings
83//! Normal Rust's `let` bindings, including `let-else` structures, are supported with the same
84//! syntax.
85//! ```rust
86//! use yew_html_ext::html;
87//! use yew::{Properties, function_component, html::Html};
88//! use std::{fs::read_dir, path::PathBuf};
89//!
90//! #[derive(PartialEq, Properties)]
91//! struct DirProps {
92//!     path: PathBuf,
93//! }
94//!
95//! #[function_component]
96//! fn Dir(props: &DirProps) -> Html {
97//!     html! {
98//!         <ul>
99//!             let Ok(iter) = read_dir(&props.path) else {
100//!                 return html!("oops :P")
101//!             };
102//!             for entry in iter {
103//!                 let Ok(entry) = entry else {
104//!                     return html!("oops :p")
105//!                 };
106//!                 <li>{ format!("{:?}", entry.path()) }</li>
107//!             }
108//!         </ul>
109//!     }
110//! }
111//! ```
112//! ## `#[cfg]` on props of elements & components
113//! Any number of `#[cfg]` attributes can be applied to any prop of an element or component.
114//!
115//! ```rust
116//! use yew_html_ext::html;
117//! use yew::{function_component, Html};
118//!
119//! #[function_component]
120//! fn DebugStmt() -> Html {
121//!     html! {
122//!         <code #[cfg(debug_assertions)] style="color: green;">
123//!             { "Make sure this is not green" }
124//!         </code>
125//!     }
126//! }
127//! ```
128//! ## Any number of top-level nodes is allowed
129//! The limitation of only 1 top-level node per macro invocation of standard Yew is lifted.
130//!
131//! ```rust
132//! use yew_html_ext::html;
133//! use yew::{function_component, Html};
134//!
135//! #[function_component]
136//! fn Main() -> Html {
137//!     html! {
138//!         <h1>{"Node 1"}</h1>
139//!         <h2>{"Node 2"}</h2> // standard Yew would fail right around here
140//!     }
141//! }
142//! ```
143//! ## Optimisation: minified inline CSS
144//! If the `style` attribute of an HTML element is set to a string literal, that string's contents
145//! are interpreted as CSS & minified, namely, the whitespace between the rules & between the key &
146//! value of a rule is removed, and a trailing semicolon is stripped.
147//! ```rust
148//! use yew_html_ext::html;
149//! use yew::{function_component, Html};
150//!
151//! #[function_component]
152//! fn DebugStmt() -> Html {
153//!     html! {
154//!         // the assigned style will be just `"color:green"`
155//!         <strong style="
156//!             color: green;
157//!         ">{"Hackerman"}</strong>
158//!     }
159//! }
160//! ```
161
162mod html_tree;
163mod props;
164mod stringify;
165
166use html_tree::{AsVNode, HtmlRoot};
167use proc_macro::TokenStream;
168use quote::ToTokens;
169use syn::buffer::Cursor;
170use syn::parse_macro_input;
171
172trait OptionExt<T, U> {
173    fn unzip_ref(&self) -> (Option<&T>, Option<&U>);
174}
175
176impl<T, U> OptionExt<T, U> for Option<(T, U)> {
177    fn unzip_ref(&self) -> (Option<&T>, Option<&U>) {
178        if let Some((x, y)) = self {
179            (Some(x), Some(y))
180        } else {
181            (None, None)
182        }
183    }
184}
185
186trait Peek<'a, T> {
187    fn peek(cursor: Cursor<'a>) -> Option<(T, Cursor<'a>)>;
188}
189
190trait PeekValue<T> {
191    fn peek(cursor: Cursor) -> Option<T>;
192}
193
194fn non_capitalized_ascii(string: &str) -> bool {
195    if !string.is_ascii() {
196        false
197    } else if let Some(c) = string.bytes().next() {
198        c.is_ascii_lowercase()
199    } else {
200        false
201    }
202}
203
204/// Combine multiple `syn` errors into a single one.
205/// Returns `Result::Ok` if the given iterator is empty
206fn join_errors(mut it: impl Iterator<Item = syn::Error>) -> syn::Result<()> {
207    it.next().map_or(Ok(()), |mut err| {
208        for other in it {
209            err.combine(other);
210        }
211        Err(err)
212    })
213}
214
215fn is_ide_completion() -> bool {
216    match std::env::var_os("RUST_IDE_PROC_MACRO_COMPLETION_DUMMY_IDENTIFIER") {
217        None => false,
218        Some(dummy_identifier) => !dummy_identifier.is_empty(),
219    }
220}
221
222#[proc_macro_error2::proc_macro_error]
223#[proc_macro]
224pub fn html_nested(input: TokenStream) -> TokenStream {
225    let root = parse_macro_input!(input as HtmlRoot);
226    TokenStream::from(root.into_token_stream())
227}
228
229#[proc_macro_error2::proc_macro_error]
230#[proc_macro]
231pub fn html(input: TokenStream) -> TokenStream {
232    let root = parse_macro_input!(input as AsVNode<HtmlRoot>);
233    TokenStream::from(root.into_token_stream())
234}