1
0
Эх сурвалжийг харах

Improve prop docs (#1565)

* Do the stuff

* More sanitizing + fix warnings and format
Leonard 1 жил өмнө
parent
commit
cea9563e25

+ 1 - 0
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]

+ 42 - 30
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<Attribute> {
                     });
 
                 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<Attribute> {
         #[doc = #header]
     });
 
-    for (arg_name, arg_type, attrs, input_arg_doc) in arg_docs {
+    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);
+
         let input_arg_doc =
-            keep_up_to_two_consecutive_chars(input_arg_doc.trim_matches('\n'), '\n')
-                .replace('\n', "<br>");
+            keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n').replace('\n', "<br/>");
         let prop_def_link = format!("{props_def_link}::{arg_name}");
-        let mut attribute_list = String::new();
-        let mut is_deprecated = false;
+        let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
 
-        for attr in attrs {
-            if attr.path() == &parse_quote!(deprecated) {
-                is_deprecated = true;
-                continue;
-            }
+        if let Some(deprecation) = deprecation {
+            arg_doc.push_str("<br/>👎 Deprecated");
 
-            attribute_list.push('`');
-            attribute_list.push_str(&quote!(#attr).into_token_stream().to_string());
-            attribute_list.push_str("`, ");
-        }
-
-        let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link})");
+            if let Some(since) = deprecation.since {
+                arg_doc.push_str(&format!(" since {since}"));
+            }
 
-        if is_deprecated {
-            arg_doc.push_str(" 👎 Deprecated");
-        }
+            if let Some(note) = deprecation.note {
+                let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
+                let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
 
-        arg_doc.push_str(&format!("<br>Type: `{arg_type}`"));
+                arg_doc.push_str(&format!(": {note}"));
+            }
 
-        if !attribute_list.is_empty() {
-            // Truncate the last `, `
-            attribute_list.truncate(attribute_list.len() - 2);
-            arg_doc.push_str(&format!("<br>Attributes: {attribute_list}"));
+            if !input_arg_doc.is_empty() {
+                arg_doc.push_str("<hr/>");
+            }
+        } else {
+            arg_doc.push_str("<br/>");
         }
 
-        arg_doc.push_str(&format!("<hr>{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<char> = 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;
                 }

+ 1 - 0
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;

+ 129 - 0
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<String> {
+    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<String>,
+    pub note: Option<String>,
+}
+
+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<Self> {
+        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::<DeprecatedAttributeArgsParser>()?;
+
+                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<String>,
+    note: Option<String>,
+}
+
+impl Parse for DeprecatedAttributeArgsParser {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut since: Option<String> = None;
+        let mut note: Option<String> = None;
+
+        if input.peek(kw::since) {
+            input.parse::<kw::since>()?;
+            input.parse::<Token![=]>()?;
+
+            since = lit_to_string(input.parse()?);
+        }
+
+        if input.peek(Token![,]) && input.peek2(kw::note) {
+            input.parse::<Token![,]>()?;
+            input.parse::<kw::note>()?;
+            input.parse::<Token![=]>()?;
+
+            note = lit_to_string(input.parse()?);
+        }
+
+        Ok(Self { since, note })
+    }
+}