dioxus_rsx/
literal.rs

1use proc_macro2::Span;
2use quote::quote;
3use quote::ToTokens;
4use std::fmt::Display;
5use std::ops::Deref;
6use syn::{
7    parse::{Parse, ParseStream},
8    Lit, LitBool, LitFloat, LitInt, LitStr,
9};
10
11use crate::{location::DynIdx, IfmtInput, Segment};
12use proc_macro2::TokenStream as TokenStream2;
13
14/// A literal value in the rsx! macro
15///
16/// These get hotreloading super powers, making them a bit more complex than a normal literal.
17/// In debug mode we need to generate a bunch of extra code to support hotreloading.
18///
19/// Eventually we want to remove this notion of hot literals since we're generating different code
20/// in debug than in release, which is harder to maintain and can lead to bugs.
21#[derive(PartialEq, Eq, Clone, Debug, Hash)]
22pub enum HotLiteral {
23    /// A *formatted* string literal
24    /// We know this will generate a String, not an &'static str
25    ///
26    /// The raw str type will generate a &'static str, but we need to distinguish the two for component props
27    ///
28    /// "hello {world}"
29    Fmted(HotReloadFormattedSegment),
30
31    /// A float literal
32    ///
33    /// 1.0
34    Float(LitFloat),
35
36    /// An int literal
37    ///
38    /// 1
39    Int(LitInt),
40
41    /// A bool literal
42    ///
43    /// true
44    Bool(LitBool),
45}
46
47impl HotLiteral {
48    pub fn quote_as_hot_reload_literal(&self) -> TokenStream2 {
49        match &self {
50            HotLiteral::Fmted(f) => quote! { dioxus_core::internal::HotReloadLiteral::Fmted(#f) },
51            HotLiteral::Float(f) => {
52                quote! { dioxus_core::internal::HotReloadLiteral::Float(#f as _) }
53            }
54            HotLiteral::Int(f) => quote! { dioxus_core::internal::HotReloadLiteral::Int(#f as _) },
55            HotLiteral::Bool(f) => quote! { dioxus_core::internal::HotReloadLiteral::Bool(#f) },
56        }
57    }
58}
59
60impl Parse for HotLiteral {
61    fn parse(input: ParseStream) -> syn::Result<Self> {
62        let raw = input.parse::<Lit>()?;
63
64        let value = match raw.clone() {
65            Lit::Int(a) => HotLiteral::Int(a),
66            Lit::Bool(a) => HotLiteral::Bool(a),
67            Lit::Float(a) => HotLiteral::Float(a),
68            Lit::Str(a) => HotLiteral::Fmted(IfmtInput::new_litstr(a)?.into()),
69            _ => {
70                return Err(syn::Error::new(
71                    raw.span(),
72                    "Only string, int, float, and bool literals are supported",
73                ))
74            }
75        };
76
77        Ok(value)
78    }
79}
80
81impl ToTokens for HotLiteral {
82    fn to_tokens(&self, out: &mut proc_macro2::TokenStream) {
83        match &self {
84            HotLiteral::Fmted(f) => {
85                f.formatted_input.to_tokens(out);
86            }
87            HotLiteral::Float(f) => f.to_tokens(out),
88            HotLiteral::Int(f) => f.to_tokens(out),
89            HotLiteral::Bool(f) => f.to_tokens(out),
90        }
91    }
92}
93
94impl HotLiteral {
95    pub fn span(&self) -> Span {
96        match self {
97            HotLiteral::Fmted(f) => f.span(),
98            HotLiteral::Float(f) => f.span(),
99            HotLiteral::Int(f) => f.span(),
100            HotLiteral::Bool(f) => f.span(),
101        }
102    }
103}
104
105impl HotLiteral {
106    // We can only handle a few types of literals - the rest need to be expressions
107    // todo on adding more of course - they're not hard to support, just work
108    pub fn peek(input: ParseStream) -> bool {
109        if input.peek(Lit) {
110            let lit = input.fork().parse::<Lit>().unwrap();
111
112            matches!(
113                lit,
114                Lit::Str(_) | Lit::Int(_) | Lit::Float(_) | Lit::Bool(_)
115            )
116        } else {
117            false
118        }
119    }
120
121    pub fn is_static(&self) -> bool {
122        match &self {
123            HotLiteral::Fmted(fmt) => fmt.is_static(),
124            _ => false,
125        }
126    }
127
128    pub fn from_raw_text(text: &str) -> Self {
129        HotLiteral::Fmted(HotReloadFormattedSegment::from(IfmtInput {
130            source: LitStr::new(text, Span::call_site()),
131            segments: vec![],
132        }))
133    }
134}
135
136impl Display for HotLiteral {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match &self {
139            HotLiteral::Fmted(l) => l.to_string_with_quotes().fmt(f),
140            HotLiteral::Float(l) => l.fmt(f),
141            HotLiteral::Int(l) => l.fmt(f),
142            HotLiteral::Bool(l) => l.value().fmt(f),
143        }
144    }
145}
146
147/// A formatted segment that can be hot reloaded
148#[derive(PartialEq, Eq, Clone, Debug, Hash)]
149pub struct HotReloadFormattedSegment {
150    pub formatted_input: IfmtInput,
151    pub dynamic_node_indexes: Vec<DynIdx>,
152}
153
154impl HotReloadFormattedSegment {
155    /// This method is very important!
156    /// Deref + Spanned + .span() methods leads to name collisions
157    pub fn span(&self) -> Span {
158        self.formatted_input.span()
159    }
160}
161
162impl Deref for HotReloadFormattedSegment {
163    type Target = IfmtInput;
164
165    fn deref(&self) -> &Self::Target {
166        &self.formatted_input
167    }
168}
169
170impl From<IfmtInput> for HotReloadFormattedSegment {
171    fn from(input: IfmtInput) -> Self {
172        let mut dynamic_node_indexes = Vec::new();
173        for segment in &input.segments {
174            if let Segment::Formatted { .. } = segment {
175                dynamic_node_indexes.push(DynIdx::default());
176            }
177        }
178        Self {
179            formatted_input: input,
180            dynamic_node_indexes,
181        }
182    }
183}
184
185impl Parse for HotReloadFormattedSegment {
186    fn parse(input: ParseStream) -> syn::Result<Self> {
187        let ifmt: IfmtInput = input.parse()?;
188        Ok(Self::from(ifmt))
189    }
190}
191
192impl ToTokens for HotReloadFormattedSegment {
193    fn to_tokens(&self, tokens: &mut TokenStream2) {
194        let mut idx = 0_usize;
195        let segments = self.segments.iter().map(|s| match s {
196            Segment::Literal(lit) => quote! {
197                dioxus_core::internal::FmtSegment::Literal { value: #lit }
198            },
199            Segment::Formatted(_fmt) => {
200                // increment idx for the dynamic segment so we maintain the mapping
201                let _idx = self.dynamic_node_indexes[idx].get();
202                idx += 1;
203                quote! {
204                   dioxus_core::internal::FmtSegment::Dynamic { id: #_idx }
205                }
206            }
207        });
208
209        // The static segments with idxs for locations
210        tokens.extend(quote! {
211            dioxus_core::internal::FmtedSegments::new( vec![ #(#segments),* ], )
212        });
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use prettier_please::PrettyUnparse;
220
221    #[test]
222    fn parses_lits() {
223        let _ = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
224        let _ = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
225        let _ = syn::parse2::<HotLiteral>(quote! { 1 }).unwrap();
226        let _ = syn::parse2::<HotLiteral>(quote! { 1.0 }).unwrap();
227        let _ = syn::parse2::<HotLiteral>(quote! { false }).unwrap();
228        let _ = syn::parse2::<HotLiteral>(quote! { true }).unwrap();
229
230        // Refuses the other unsupported types - we could add them if we wanted to
231        assert!(syn::parse2::<HotLiteral>(quote! { b"123" }).is_err());
232        assert!(syn::parse2::<HotLiteral>(quote! { 'a' }).is_err());
233
234        let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
235        assert!(matches!(lit, HotLiteral::Fmted(_)));
236
237        let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
238        assert!(matches!(lit, HotLiteral::Fmted(_)));
239    }
240
241    #[test]
242    fn outputs_a_signal() {
243        // Should output a type of f64 which we convert into whatever the expected type is via "into"
244        // todo: hmmmmmmmmmmmm might not always work
245        let lit = syn::parse2::<HotLiteral>(quote! { 1.0 }).unwrap();
246        println!("{}", lit.to_token_stream().pretty_unparse());
247
248        let lit = syn::parse2::<HotLiteral>(quote! { "hi" }).unwrap();
249        println!("{}", lit.to_token_stream().pretty_unparse());
250
251        let lit = syn::parse2::<HotLiteral>(quote! { "hi {world}" }).unwrap();
252        println!("{}", lit.to_token_stream().pretty_unparse());
253    }
254
255    #[test]
256    fn static_str_becomes_str() {
257        let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
258        let HotLiteral::Fmted(segments) = &lit else {
259            panic!("expected a formatted string");
260        };
261        assert!(segments.is_static());
262        assert_eq!(r##""hello""##, segments.to_string_with_quotes());
263        println!("{}", lit.to_token_stream().pretty_unparse());
264    }
265
266    #[test]
267    fn formatted_prints_as_formatted() {
268        let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
269        let HotLiteral::Fmted(segments) = &lit else {
270            panic!("expected a formatted string");
271        };
272        assert!(!segments.is_static());
273        assert_eq!(r##""hello {world}""##, segments.to_string_with_quotes());
274        println!("{}", lit.to_token_stream().pretty_unparse());
275    }
276}