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}