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 std::fmt::{Display, Write};
170use syn::buffer::Cursor;
171use syn::parse_macro_input;
172
173trait OptionExt<T, U> {
174    fn unzip_ref(&self) -> (Option<&T>, Option<&U>);
175}
176
177impl<T, U> OptionExt<T, U> for Option<(T, U)> {
178    fn unzip_ref(&self) -> (Option<&T>, Option<&U>) {
179        if let Some((x, y)) = self {
180            (Some(x), Some(y))
181        } else {
182            (None, None)
183        }
184    }
185}
186
187trait Peek<'a, T> {
188    fn peek(cursor: Cursor<'a>) -> Option<(T, Cursor<'a>)>;
189}
190
191trait PeekValue<T> {
192    fn peek(cursor: Cursor) -> Option<T>;
193}
194
195/// Extension methods for treating `Display`able values like strings, without allocating the
196/// strings.
197///
198/// Needed to check the plentiful token-like values in the impl of the macros, which are
199/// `Display`able but which either correspond to multiple source code tokens, or are themselves
200/// tokens that don't provide a reference to their repr.
201trait DisplayExt: Display {
202    /// Equivalent to [`str::eq_ignore_ascii_case`], but works for anything that's `Display` without
203    /// allocations
204    fn repr_eq_ignore_ascii_case(&self, other: &str) -> bool {
205        /// Writer that only succeeds if all of the input is a prefix of the contained string.
206        struct X<'src>(&'src str);
207
208        impl Write for X<'_> {
209            fn write_str(&mut self, chunk: &str) -> std::fmt::Result {
210                if !self
211                    .0
212                    .get(..chunk.len())
213                    .is_some_and(|x| x.eq_ignore_ascii_case(chunk))
214                {
215                    return Err(std::fmt::Error);
216                }
217                self.0 = self.0.split_at(chunk.len()).1;
218                Ok(())
219            }
220        }
221
222        // The `is_ok_and` call ensures that there's nothing left over, ensuring
223        // `s1.to_string().eq_ignore_ascii_case(s2)`
224        // without ever allocating `s1`
225        let mut writer = X(other);
226        write!(writer, "{self}").is_ok_and(|_| writer.0.is_empty())
227    }
228
229    /// Equivalent of `s1.to_string() == s2` but without allocations
230    fn repr_eq(&self, other: &str) -> bool {
231        /// Writer that only succeeds if all of the input is a prefix of the contained string.
232        struct X<'src>(&'src str);
233
234        impl Write for X<'_> {
235            fn write_str(&mut self, chunk: &str) -> std::fmt::Result {
236                self.0
237                    .strip_prefix(chunk)
238                    .map(|rest| self.0 = rest)
239                    .ok_or(std::fmt::Error)
240            }
241        }
242
243        // The `is_ok_and` call ensures that there's nothing left over, ensuring `s1.to_string() == s2`
244        // without ever allocating `s1`
245        let mut writer = X(other);
246        write!(writer, "{self}").is_ok_and(|_| writer.0.is_empty())
247    }
248
249    /// Equivalent of [`str::starts_with`], but works for anything that's `Display` without allocations
250    fn starts_with(&self, prefix: &str) -> bool {
251        /// Writer that only succeeds if all of the input is a prefix of the contained string.
252        struct X<'src>(&'src str);
253
254        impl Write for X<'_> {
255            fn write_str(&mut self, s: &str) -> std::fmt::Result {
256                match self.0.strip_prefix(s) {
257                    Some(rest) => self.0 = rest,
258                    None if self.0.len() < s.len() => {
259                        s.strip_prefix(self.0).ok_or(std::fmt::Error)?;
260                        self.0 = "";
261                    }
262                    None => return Err(std::fmt::Error),
263                }
264
265                Ok(())
266            }
267        }
268
269        let mut writer = X(prefix);
270        write!(writer, "{self}").is_ok()
271    }
272
273    /// Returns `true` if `s` only displays ASCII chars & doesn't start with a capital letter
274    fn is_non_capitalized_ascii(&self) -> bool {
275        /// Writer that succeeds only if the input is non-capitalised ASCII
276        struct X {
277            empty: bool,
278        }
279
280        impl Write for X {
281            fn write_str(&mut self, mut s: &str) -> std::fmt::Result {
282                if self.empty {
283                    self.empty = s.is_empty();
284                    let mut iter = s.chars();
285                    if iter.next().is_some_and(|c| c.is_ascii_uppercase()) {
286                        return Err(std::fmt::Error);
287                    }
288                    s = iter.as_str();
289                }
290
291                s.is_ascii().then_some(()).ok_or(std::fmt::Error)
292            }
293        }
294
295        let mut writer = X { empty: true };
296        write!(writer, "{self}").is_ok_and(|_| !writer.empty)
297    }
298}
299
300impl<T: Display> DisplayExt for T {}
301
302/// Combine multiple `syn` errors into a single one.
303/// Returns `Result::Ok` if the given iterator is empty
304fn join_errors(mut it: impl Iterator<Item = syn::Error>) -> syn::Result<()> {
305    it.next().map_or(Ok(()), |mut err| {
306        for other in it {
307            err.combine(other);
308        }
309        Err(err)
310    })
311}
312
313fn is_ide_completion() -> bool {
314    match std::env::var_os("RUST_IDE_PROC_MACRO_COMPLETION_DUMMY_IDENTIFIER") {
315        None => false,
316        Some(dummy_identifier) => !dummy_identifier.is_empty(),
317    }
318}
319
320#[proc_macro_error2::proc_macro_error]
321#[proc_macro]
322pub fn html_nested(input: TokenStream) -> TokenStream {
323    let root = parse_macro_input!(input as HtmlRoot);
324    TokenStream::from(root.into_token_stream())
325}
326
327#[proc_macro_error2::proc_macro_error]
328#[proc_macro]
329pub fn html(input: TokenStream) -> TokenStream {
330    let root = parse_macro_input!(input as AsVNode<HtmlRoot>);
331    TokenStream::from(root.into_token_stream())
332}