use proc_macro2::Span;
use quote::quote;
use quote::ToTokens;
use std::fmt::Display;
use std::ops::Deref;
use syn::{
parse::{Parse, ParseStream},
Lit, LitBool, LitFloat, LitInt, LitStr,
};
use crate::{location::DynIdx, IfmtInput, Segment};
use proc_macro2::TokenStream as TokenStream2;
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum HotLiteral {
Fmted(HotReloadFormattedSegment),
Float(LitFloat),
Int(LitInt),
Bool(LitBool),
}
impl HotLiteral {
pub fn quote_as_hot_reload_literal(&self) -> TokenStream2 {
match &self {
HotLiteral::Fmted(f) => quote! { dioxus_core::internal::HotReloadLiteral::Fmted(#f) },
HotLiteral::Float(f) => {
quote! { dioxus_core::internal::HotReloadLiteral::Float(#f as _) }
}
HotLiteral::Int(f) => quote! { dioxus_core::internal::HotReloadLiteral::Int(#f as _) },
HotLiteral::Bool(f) => quote! { dioxus_core::internal::HotReloadLiteral::Bool(#f) },
}
}
}
impl Parse for HotLiteral {
fn parse(input: ParseStream) -> syn::Result<Self> {
let raw = input.parse::<Lit>()?;
let value = match raw.clone() {
Lit::Int(a) => HotLiteral::Int(a),
Lit::Bool(a) => HotLiteral::Bool(a),
Lit::Float(a) => HotLiteral::Float(a),
Lit::Str(a) => HotLiteral::Fmted(IfmtInput::new_litstr(a)?.into()),
_ => {
return Err(syn::Error::new(
raw.span(),
"Only string, int, float, and bool literals are supported",
))
}
};
Ok(value)
}
}
impl ToTokens for HotLiteral {
fn to_tokens(&self, out: &mut proc_macro2::TokenStream) {
match &self {
HotLiteral::Fmted(f) => {
f.formatted_input.to_tokens(out);
}
HotLiteral::Float(f) => f.to_tokens(out),
HotLiteral::Int(f) => f.to_tokens(out),
HotLiteral::Bool(f) => f.to_tokens(out),
}
}
}
impl HotLiteral {
pub fn span(&self) -> Span {
match self {
HotLiteral::Fmted(f) => f.span(),
HotLiteral::Float(f) => f.span(),
HotLiteral::Int(f) => f.span(),
HotLiteral::Bool(f) => f.span(),
}
}
}
impl HotLiteral {
pub fn peek(input: ParseStream) -> bool {
if input.peek(Lit) {
let lit = input.fork().parse::<Lit>().unwrap();
matches!(
lit,
Lit::Str(_) | Lit::Int(_) | Lit::Float(_) | Lit::Bool(_)
)
} else {
false
}
}
pub fn is_static(&self) -> bool {
match &self {
HotLiteral::Fmted(fmt) => fmt.is_static(),
_ => false,
}
}
pub fn from_raw_text(text: &str) -> Self {
HotLiteral::Fmted(HotReloadFormattedSegment::from(IfmtInput {
source: LitStr::new(text, Span::call_site()),
segments: vec![],
}))
}
}
impl Display for HotLiteral {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
HotLiteral::Fmted(l) => l.to_string_with_quotes().fmt(f),
HotLiteral::Float(l) => l.fmt(f),
HotLiteral::Int(l) => l.fmt(f),
HotLiteral::Bool(l) => l.value().fmt(f),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct HotReloadFormattedSegment {
pub formatted_input: IfmtInput,
pub dynamic_node_indexes: Vec<DynIdx>,
}
impl HotReloadFormattedSegment {
pub fn span(&self) -> Span {
self.formatted_input.span()
}
}
impl Deref for HotReloadFormattedSegment {
type Target = IfmtInput;
fn deref(&self) -> &Self::Target {
&self.formatted_input
}
}
impl From<IfmtInput> for HotReloadFormattedSegment {
fn from(input: IfmtInput) -> Self {
let mut dynamic_node_indexes = Vec::new();
for segment in &input.segments {
if let Segment::Formatted { .. } = segment {
dynamic_node_indexes.push(DynIdx::default());
}
}
Self {
formatted_input: input,
dynamic_node_indexes,
}
}
}
impl Parse for HotReloadFormattedSegment {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ifmt: IfmtInput = input.parse()?;
Ok(Self::from(ifmt))
}
}
impl ToTokens for HotReloadFormattedSegment {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let mut idx = 0_usize;
let segments = self.segments.iter().map(|s| match s {
Segment::Literal(lit) => quote! {
dioxus_core::internal::FmtSegment::Literal { value: #lit }
},
Segment::Formatted(_fmt) => {
let _idx = self.dynamic_node_indexes[idx].get();
idx += 1;
quote! {
dioxus_core::internal::FmtSegment::Dynamic { id: #_idx }
}
}
});
tokens.extend(quote! {
dioxus_core::internal::FmtedSegments::new( vec![ #(#segments),* ], )
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use prettier_please::PrettyUnparse;
#[test]
fn parses_lits() {
let _ = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
let _ = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
let _ = syn::parse2::<HotLiteral>(quote! { 1 }).unwrap();
let _ = syn::parse2::<HotLiteral>(quote! { 1.0 }).unwrap();
let _ = syn::parse2::<HotLiteral>(quote! { false }).unwrap();
let _ = syn::parse2::<HotLiteral>(quote! { true }).unwrap();
assert!(syn::parse2::<HotLiteral>(quote! { b"123" }).is_err());
assert!(syn::parse2::<HotLiteral>(quote! { 'a' }).is_err());
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
assert!(matches!(lit, HotLiteral::Fmted(_)));
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
assert!(matches!(lit, HotLiteral::Fmted(_)));
}
#[test]
fn outputs_a_signal() {
let lit = syn::parse2::<HotLiteral>(quote! { 1.0 }).unwrap();
println!("{}", lit.to_token_stream().pretty_unparse());
let lit = syn::parse2::<HotLiteral>(quote! { "hi" }).unwrap();
println!("{}", lit.to_token_stream().pretty_unparse());
let lit = syn::parse2::<HotLiteral>(quote! { "hi {world}" }).unwrap();
println!("{}", lit.to_token_stream().pretty_unparse());
}
#[test]
fn static_str_becomes_str() {
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
let HotLiteral::Fmted(segments) = &lit else {
panic!("expected a formatted string");
};
assert!(segments.is_static());
assert_eq!(r##""hello""##, segments.to_string_with_quotes());
println!("{}", lit.to_token_stream().pretty_unparse());
}
#[test]
fn formatted_prints_as_formatted() {
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
let HotLiteral::Fmted(segments) = &lit else {
panic!("expected a formatted string");
};
assert!(!segments.is_static());
assert_eq!(r##""hello {world}""##, segments.to_string_with_quotes());
println!("{}", lit.to_token_stream().pretty_unparse());
}
}