diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index d928d5fec..1898d8bd1 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] } dioxus-rsx = { workspace = true } dioxus-core = { workspace = true } constcat = "0.3.0" +prettyplease = "0.2.15" # testing [dev-dependencies] diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs index a6e0a07d2..670a14d88 100644 --- a/packages/core-macro/src/component_body_deserializers/inline_props.rs +++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs @@ -153,9 +153,20 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec { }); Some(( - pt.pat.to_token_stream().to_string(), - pt.ty.to_token_stream().to_string(), - pt.attrs.iter().filter(|a| !is_attr_doc(a)), + &pt.pat, + &pt.ty, + pt.attrs.iter().find_map(|attr| { + if attr.path() != &parse_quote!(deprecated) { + return None; + } + + let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); + + match res { + Err(e) => panic!("{}", e.to_string()), + Ok(v) => Some(v), + } + }), arg_doc, )) } @@ -171,40 +182,37 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec { #[doc = #header] }); - for (arg_name, arg_type, attrs, input_arg_doc) in arg_docs { - let input_arg_doc = - keep_up_to_two_consecutive_chars(input_arg_doc.trim_matches('\n'), '\n') - .replace('\n', "
"); - let prop_def_link = format!("{props_def_link}::{arg_name}"); - let mut attribute_list = String::new(); - let mut is_deprecated = false; + for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs { + let arg_name = arg_name.into_token_stream().to_string(); + let arg_type = crate::utils::format_type_string(arg_type); - for attr in attrs { - if attr.path() == &parse_quote!(deprecated) { - is_deprecated = true; - continue; + let input_arg_doc = + keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n').replace('\n', "
"); + let prop_def_link = format!("{props_def_link}::{arg_name}"); + let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`"); + + if let Some(deprecation) = deprecation { + arg_doc.push_str("
👎 Deprecated"); + + if let Some(since) = deprecation.since { + arg_doc.push_str(&format!(" since {since}")); } - attribute_list.push('`'); - attribute_list.push_str("e!(#attr).into_token_stream().to_string()); - attribute_list.push_str("`, "); + if let Some(note) = deprecation.note { + let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " "); + let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " "); + + arg_doc.push_str(&format!(": {note}")); + } + + if !input_arg_doc.is_empty() { + arg_doc.push_str("
"); + } + } else { + arg_doc.push_str("
"); } - let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link})"); - - if is_deprecated { - arg_doc.push_str(" 👎 Deprecated"); - } - - arg_doc.push_str(&format!("
Type: `{arg_type}`")); - - if !attribute_list.is_empty() { - // Truncate the last `, ` - attribute_list.truncate(attribute_list.len() - 2); - arg_doc.push_str(&format!("
Attributes: {attribute_list}")); - } - - arg_doc.push_str(&format!("
{input_arg_doc}")); + arg_doc.push_str(&input_arg_doc); props_docs.push(parse_quote! { #[doc = #arg_doc] @@ -302,7 +310,11 @@ fn is_attr_doc(attr: &Attribute) -> bool { attr.path() == &parse_quote!(doc) } -fn keep_up_to_two_consecutive_chars(input: &str, target_char: char) -> String { +fn keep_up_to_n_consecutive_chars( + input: &str, + n_of_consecutive_chars_allowed: usize, + target_char: char, +) -> String { let mut output = String::new(); let mut prev_char: Option = None; let mut consecutive_count = 0; @@ -310,7 +322,7 @@ fn keep_up_to_two_consecutive_chars(input: &str, target_char: char) -> String { for c in input.chars() { match prev_char { Some(prev) if c == target_char && prev == target_char => { - if consecutive_count < 2 { + if consecutive_count < n_of_consecutive_chars_allowed { output.push(c); consecutive_count += 1; } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 477c05d76..4686bc797 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -12,6 +12,7 @@ use syn::{parse_macro_input, Path, Token}; mod component_body; mod component_body_deserializers; mod props; +mod utils; // mod rsx; use crate::component_body::ComponentBody; diff --git a/packages/core-macro/src/utils.rs b/packages/core-macro/src/utils.rs new file mode 100644 index 000000000..7e066f773 --- /dev/null +++ b/packages/core-macro/src/utils.rs @@ -0,0 +1,129 @@ +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{parse_quote, Expr, Lit, Meta, Token, Type}; + +const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:"; +const FORMATTED_TYPE_END: &str = "= todo!();"; + +/// Attempts to convert the given literal to a string. +/// Converts ints and floats to their base 10 counterparts. +/// +/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`] +/// and the byte string could not be converted to UTF-8. +pub fn lit_to_string(lit: Lit) -> Option { + match lit { + Lit::Str(l) => Some(l.value()), + Lit::ByteStr(l) => String::from_utf8(l.value()).ok(), + Lit::Byte(l) => Some(String::from(l.value() as char)), + Lit::Char(l) => Some(l.value().to_string()), + Lit::Int(l) => Some(l.base10_digits().to_string()), + Lit::Float(l) => Some(l.base10_digits().to_string()), + Lit::Bool(l) => Some(l.value().to_string()), + Lit::Verbatim(_) => None, + _ => None, + } +} + +pub fn format_type_string(ty: &Type) -> String { + let ty_unformatted = ty.into_token_stream().to_string(); + let ty_unformatted = ty_unformatted.trim(); + + // This should always be valid syntax. + // Not Rust code, but syntax, which is the only thing that `syn` cares about. + let Ok(file_unformatted) = syn::parse_file(&format!( + "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}" + )) else { + return ty_unformatted.to_string(); + }; + + let file_formatted = prettyplease::unparse(&file_unformatted); + + let file_trimmed = file_formatted.trim(); + let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START); + let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END); + let ty_formatted = end_removed.trim(); + + ty_formatted.to_string() +} + +/// Represents the `#[deprecated]` attribute. +/// +/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct. +#[derive(Default)] +pub struct DeprecatedAttribute { + pub since: Option, + pub note: Option, +} + +impl DeprecatedAttribute { + /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute. + pub fn from_meta(meta: &Meta) -> syn::Result { + if meta.path() != &parse_quote!(deprecated) { + return Err(syn::Error::new( + meta.span(), + "attribute path is not `deprecated`", + )); + } + + match &meta { + Meta::Path(_) => Ok(Self::default()), + Meta::NameValue(name_value) => { + let Expr::Lit(expr_lit) = &name_value.value else { + return Err(syn::Error::new( + name_value.span(), + "literal in `deprecated` value must be a string", + )); + }; + + Ok(Self { + since: None, + note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()), + }) + } + Meta::List(list) => { + let parsed = list.parse_args::()?; + + Ok(Self { + since: parsed.since.map(|s| s.trim().to_string()), + note: parsed.note.map(|s| s.trim().to_string()), + }) + } + } + } +} + +mod kw { + use syn::custom_keyword; + custom_keyword!(since); + custom_keyword!(note); +} + +struct DeprecatedAttributeArgsParser { + since: Option, + note: Option, +} + +impl Parse for DeprecatedAttributeArgsParser { + fn parse(input: ParseStream) -> syn::Result { + let mut since: Option = None; + let mut note: Option = None; + + if input.peek(kw::since) { + input.parse::()?; + input.parse::()?; + + since = lit_to_string(input.parse()?); + } + + if input.peek(Token![,]) && input.peek2(kw::note) { + input.parse::()?; + input.parse::()?; + input.parse::()?; + + note = lit_to_string(input.parse()?); + } + + Ok(Self { since, note }) + } +}