Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/
lib.rs

1//! This crate provides Yew's procedural macro `html!` which allows using JSX-like syntax
2//! for generating html and the `Properties` derive macro for deriving the `Properties` trait
3//! for components.
4//!
5//! ```
6//! use yew::prelude::*;
7//!
8//! struct Component;
9//!
10//! #[derive(Properties, PartialEq)]
11//! struct Props {
12//!     prop: String,
13//! }
14//!
15//! # enum Msg { Submit }
16//! #
17//! # impl yew::Component for Component {
18//! #     type Message = Msg;
19//! #     type Properties = Props;
20//! #     fn create(_ctx: &Context<Self>) -> Self {
21//! #         unimplemented!()
22//! #     }
23//! #
24//! #
25//! #     fn view(&self, ctx: &Context<Self>) -> Html {
26//! #
27//! // ...
28//!
29//! html! {
30//!   <div>
31//!     <button onclick={ctx.link().callback(|_| Msg::Submit)}>
32//!       { "Submit" }
33//!     </button>
34//!     <>
35//!       <Component prop="first" />
36//!       <Component prop="second" />
37//!     </>
38//!   </div>
39//! }
40//! #
41//! #     }
42//! # }
43//! #
44//! # fn main() {}
45//! ```
46//!
47//! Please refer to [https://github.com/yewstack/yew](https://github.com/yewstack/yew) for how to set this up.
48
49mod classes;
50mod derive_props;
51mod function_component;
52mod hook;
53mod html_tree;
54mod props;
55mod stringify;
56mod use_prepared_state;
57mod use_transitive_state;
58
59use std::fmt::{Display, Write};
60
61use derive_props::DerivePropsInput;
62use function_component::{function_component_impl, FunctionComponent, FunctionComponentName};
63use hook::{hook_impl, HookFn};
64use html_tree::{HtmlRoot, HtmlRootVNode};
65use proc_macro::TokenStream;
66use quote::ToTokens;
67use syn::buffer::Cursor;
68use syn::parse_macro_input;
69use use_prepared_state::PreparedState;
70use use_transitive_state::TransitiveState;
71
72trait Peek<'a, T> {
73    fn peek(cursor: Cursor<'a>) -> Option<(T, Cursor<'a>)>;
74}
75
76trait PeekValue<T> {
77    fn peek(cursor: Cursor) -> Option<T>;
78}
79
80/// Extension methods for treating `Display`able values like strings, without allocating the
81/// strings.
82///
83/// Needed to check the plentiful token-like values in the impl of the macros, which are
84/// `Display`able but which either correspond to multiple source code tokens, or are themselves
85/// tokens that don't provide a reference to their repr.
86pub(crate) trait DisplayExt: Display {
87    /// Equivalent to [`str::eq_ignore_ascii_case`], but works for anything that's `Display` without
88    /// allocations
89    fn eq_str_ignore_ascii_case(&self, other: &str) -> bool {
90        /// Writer that only succeeds if all of the input is a __prefix__ of the contained string,
91        /// ignoring ASCII case.
92        ///
93        /// It cannot verify that `other` is not longer than the input.
94        struct X<'src> {
95            other: &'src str,
96        }
97
98        impl Write for X<'_> {
99            fn write_str(&mut self, self_chunk: &str) -> std::fmt::Result {
100                if self.other.len() < self_chunk.len() {
101                    return Err(std::fmt::Error); // `other` is shorter than `self`.
102                }
103                let other_chunk;
104                // Chop off a chunk from `other` the size of `self_chunk` to compare them
105                (other_chunk, self.other) = self.other.split_at(self_chunk.len());
106                // Check if the chunks match
107                self_chunk
108                    .eq_ignore_ascii_case(other_chunk)
109                    .then_some(())
110                    .ok_or(std::fmt::Error)
111            }
112        }
113
114        let mut writer = X { other };
115        // The `is_ok_and` call ensures that there's nothing left over.
116        // If the remainder of `other` is not empty, it means `other` is longer than
117        // `self`.
118        write!(writer, "{self}").is_ok_and(|_| writer.other.is_empty())
119    }
120
121    /// Equivalent of `s1.to_string() == s2` but without allocations
122    fn eq_str(&self, other: &str) -> bool {
123        /// Writer that only succeeds if all of the input is a __prefix__ of the contained string.
124        ///
125        /// It cannot verify that `other` is not longer than the input.
126        struct X<'src> {
127            other: &'src str,
128        }
129
130        impl Write for X<'_> {
131            fn write_str(&mut self, chunk: &str) -> std::fmt::Result {
132                self.other
133                    .strip_prefix(chunk) // Try to chop off a chunk of `self` from `other`
134                    .map(|rest| self.other = rest) // If it matched, reassign the rest of `other`
135                    .ok_or(std::fmt::Error) // Otherwise, break out signifying a mismatch
136            }
137        }
138
139        let mut writer = X { other };
140        // The `is_ok_and` call ensures that there's nothing left over.
141        // If the remainder of `other` is not empty, it means `other` is longer than
142        // `self`.
143        write!(writer, "{self}").is_ok_and(|_| writer.other.is_empty())
144    }
145
146    /// Equivalent of [`str::starts_with`], but works for anything that's `Display` without
147    /// allocations
148    fn starts_with(&self, prefix: &str) -> bool {
149        /// Writer that only succeeds if `prefix` is a prefix of the input
150        struct X<'src> {
151            prefix: &'src str,
152        }
153
154        impl Write for X<'_> {
155            fn write_str(&mut self, chunk: &str) -> std::fmt::Result {
156                match self.prefix.strip_prefix(chunk) {
157                    // Try to chop off a chunk from `prefix`
158                    Some(rest) => self.prefix = rest, // Reassign the rest of `prefix` on success
159                    None => {
160                        // Check if `prefix` became shorter than the rest of input, but can still be
161                        // found in the input
162                        chunk.strip_prefix(self.prefix).ok_or(std::fmt::Error)?;
163                        self.prefix = ""; // All of `prefix` was found, ignore the rest of input
164                    }
165                }
166
167                Ok(())
168            }
169        }
170
171        let mut writer = X { prefix };
172        write!(writer, "{self}").is_ok()
173    }
174
175    /// Returns `true` if `s` only displays ASCII chars & doesn't start with a capital letter
176    fn is_non_capitalized_ascii(&self) -> bool {
177        /// Writer that succeeds only if the input is non-capitalised ASCII _or is empty_
178        ///
179        /// The case of empty input should be checked afterwards by the checking `self.empty`
180        struct X {
181            /// Whether there was any non-empty input
182            empty: bool,
183        }
184
185        impl Write for X {
186            fn write_str(&mut self, s: &str) -> std::fmt::Result {
187                if self.empty {
188                    // Executed if there was no input before that
189                    self.empty = s.is_empty();
190                    // Inspecting the 1st char
191                    if s.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
192                        // The 1st char is A-Z, the input is capitalised
193                        return Err(std::fmt::Error);
194                    }
195                }
196
197                // Check if everything is ASCII
198                s.is_ascii().then_some(()).ok_or(std::fmt::Error)
199            }
200        }
201
202        let mut writer = X { empty: true };
203        // The `is_ok_and` call ensures that empty input is _NOT_ considered non-capitalised ASCII
204        write!(writer, "{self}").is_ok_and(|_| !writer.empty)
205    }
206}
207
208impl<T: Display> DisplayExt for T {}
209
210/// Combine multiple `syn` errors into a single one.
211/// Returns `Result::Ok` if the given iterator is empty
212fn join_errors(mut it: impl Iterator<Item = syn::Error>) -> syn::Result<()> {
213    it.next().map_or(Ok(()), |mut err| {
214        for other in it {
215            err.combine(other);
216        }
217        Err(err)
218    })
219}
220
221fn is_ide_completion() -> bool {
222    match std::env::var_os("RUST_IDE_PROC_MACRO_COMPLETION_DUMMY_IDENTIFIER") {
223        None => false,
224        Some(dummy_identifier) => !dummy_identifier.is_empty(),
225    }
226}
227
228#[proc_macro_derive(Properties, attributes(prop_or, prop_or_else, prop_or_default))]
229pub fn derive_props(input: TokenStream) -> TokenStream {
230    let mut input = parse_macro_input!(input as DerivePropsInput);
231    input.normalise();
232    TokenStream::from(input.into_token_stream())
233}
234
235#[proc_macro_error::proc_macro_error]
236#[proc_macro]
237pub fn html_nested(input: TokenStream) -> TokenStream {
238    let root = parse_macro_input!(input as HtmlRoot);
239    TokenStream::from(root.into_token_stream())
240}
241
242#[proc_macro_error::proc_macro_error]
243#[proc_macro]
244pub fn html(input: TokenStream) -> TokenStream {
245    let root = parse_macro_input!(input as HtmlRootVNode);
246    TokenStream::from(root.into_token_stream())
247}
248
249#[proc_macro]
250pub fn props(input: TokenStream) -> TokenStream {
251    let props = parse_macro_input!(input as props::PropsMacroInput);
252    TokenStream::from(props.into_token_stream())
253}
254
255#[proc_macro]
256pub fn classes(input: TokenStream) -> TokenStream {
257    let classes = parse_macro_input!(input as classes::Classes);
258    TokenStream::from(classes.into_token_stream())
259}
260
261#[proc_macro_error::proc_macro_error]
262#[proc_macro_attribute]
263pub fn function_component(attr: TokenStream, item: TokenStream) -> TokenStream {
264    let item = parse_macro_input!(item as FunctionComponent);
265    let attr = parse_macro_input!(attr as FunctionComponentName);
266
267    function_component_impl(attr, item)
268        .unwrap_or_else(|err| err.to_compile_error())
269        .into()
270}
271
272#[proc_macro_error::proc_macro_error]
273#[proc_macro_attribute]
274pub fn hook(attr: TokenStream, item: TokenStream) -> TokenStream {
275    let item = parse_macro_input!(item as HookFn);
276
277    if let Some(m) = proc_macro2::TokenStream::from(attr).into_iter().next() {
278        return syn::Error::new_spanned(m, "hook attribute does not accept any arguments")
279            .into_compile_error()
280            .into();
281    }
282
283    hook_impl(item)
284        .unwrap_or_else(|err| err.to_compile_error())
285        .into()
286}
287
288#[proc_macro]
289pub fn use_prepared_state_with_closure(input: TokenStream) -> TokenStream {
290    let prepared_state = parse_macro_input!(input as PreparedState);
291    prepared_state.to_token_stream_with_closure().into()
292}
293
294#[proc_macro]
295pub fn use_prepared_state_without_closure(input: TokenStream) -> TokenStream {
296    let prepared_state = parse_macro_input!(input as PreparedState);
297    prepared_state.to_token_stream_without_closure().into()
298}
299
300#[proc_macro]
301pub fn use_transitive_state_with_closure(input: TokenStream) -> TokenStream {
302    let transitive_state = parse_macro_input!(input as TransitiveState);
303    transitive_state.to_token_stream_with_closure().into()
304}
305
306#[proc_macro]
307pub fn use_transitive_state_without_closure(input: TokenStream) -> TokenStream {
308    let transitive_state = parse_macro_input!(input as TransitiveState);
309    transitive_state.to_token_stream_without_closure().into()
310}
311
312#[cfg(test)]
313mod tests {
314    use std::fmt::{Display, Formatter, Write};
315
316    use rand::rngs::SmallRng;
317    use rand::{Rng, SeedableRng};
318
319    use crate::DisplayExt;
320
321    const N_ITERS: usize = 0x4000;
322    const STR_LEN: usize = 32;
323
324    /// Implements `Display` by feeding the formatter 1 `char` at a time.
325    ///
326    /// Tests the ability of [`DisplayExt`] to handle disparate chunks of strings
327    struct DisplayObfuscator<'a>(&'a str);
328
329    impl Display for DisplayObfuscator<'_> {
330        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
331            for ch in self.0.chars() {
332                f.write_char(ch)?;
333            }
334            Ok(())
335        }
336    }
337
338    /// Does the same thing as [`DisplayObfuscator`] but also lowercases all chars.
339    struct Lowercaser<'a>(&'a str);
340
341    impl Display for Lowercaser<'_> {
342        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
343            for ch in self.0.chars() {
344                f.write_char(ch.to_ascii_lowercase())?;
345            }
346            Ok(())
347        }
348    }
349
350    #[test]
351    fn display_ext_works() {
352        let rng = &mut SmallRng::from_os_rng();
353        let mut s = String::with_capacity(STR_LEN);
354
355        for i in 0..N_ITERS {
356            s.clear();
357            // Generate `STR_LEN` ASCII chars
358            s.extend(
359                rng.random_iter::<u8>()
360                    .take(STR_LEN)
361                    .map(|b| (b & 127) as char),
362            );
363
364            assert!(Lowercaser(&s).eq_str_ignore_ascii_case(&s));
365            assert!(DisplayObfuscator(&s).eq_str(&s));
366            assert!(DisplayObfuscator(&s).starts_with(&s[..i % STR_LEN]));
367            assert_eq!(
368                DisplayObfuscator(&s).is_non_capitalized_ascii(),
369                s.is_non_capitalized_ascii()
370            );
371        }
372    }
373}