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}