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}