yew_html_ext/
lib.rs

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