(tokens) {
@@ -38,16 +38,14 @@ pub fn rsx(tokens: TokenStream) -> TokenStream {
}
}
-/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
-///
-/// The render macro automatically renders rsx - making it unhygienic.
+/// This macro has been deprecated in favor of [`rsx`].
#[deprecated(note = "Use `rsx!` instead.")]
#[proc_macro]
pub fn render(tokens: TokenStream) -> TokenStream {
rsx(tokens)
}
-/// * Makes the compiler prefer an `UpperCamelCase` function identifier.
+/// * Makes the compiler allow an `UpperCamelCase` function identifier.
/// * Seamlessly creates a props struct if there's more than 1 parameter in the function.
/// * Verifies the validity of your component.
///
@@ -55,6 +53,7 @@ pub fn render(tokens: TokenStream) -> TokenStream {
///
/// * Without props:
/// ```rust
+/// # use dioxus::prelude::*;
/// #[component]
/// fn Greet() -> Element {
/// rsx! { "hello, someone" }
@@ -63,19 +62,20 @@ pub fn render(tokens: TokenStream) -> TokenStream {
///
/// * With props:
/// ```rust
+/// # use dioxus::prelude::*;
/// #[component]
/// fn Greet(person: String) -> Element {
/// rsx! { "hello, " {person} }
/// }
-///
-/// // is roughly equivalent to
-///
+/// ```
+/// Which is roughly equivalent to:
+/// ```rust
+/// # use dioxus::prelude::*;
/// #[derive(PartialEq, Clone, Props)]
/// struct GreetProps {
/// person: String,
/// }
///
-/// #[component]
/// fn Greet(GreetProps { person }: GreetProps) -> Element {
/// rsx! { "hello, " {person} }
/// }
@@ -87,32 +87,7 @@ pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
.into()
}
-/// Derive props for a component within the component definition.
-///
-/// This macro provides a simple transformation from `Scope<{}>` to `Scope`,
-/// removing some boilerplate when defining props.
-///
-/// You don't *need* to use this macro at all, but it can be helpful in cases where
-/// you would be repeating a lot of the usual Rust boilerplate.
-///
-/// # Example
-/// ```rust,ignore
-/// #[inline_props]
-/// fn app(bob: String) -> Element {
-/// rsx! { "hello, {bob}") }
-/// }
-///
-/// // is equivalent to
-///
-/// #[derive(PartialEq, Props)]
-/// struct AppProps {
-/// bob: String,
-/// }
-///
-/// fn app(props: AppProps) -> Element {
-/// rsx! { "hello, {bob}") }
-/// }
-/// ```
+/// This macro has been deprecated in favor of [`component`].
#[proc_macro_attribute]
#[deprecated(note = "Use `#[component]` instead.")]
pub fn inline_props(args: TokenStream, input: TokenStream) -> TokenStream {
From 19e7a3ee7f47b4251e1af1e4b64eaf7a3f68181b Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 8 Aug 2024 09:41:48 -0700
Subject: [PATCH 14/15] revert changes outside of core macro
---
examples/rsx_usage.rs | 24 ++++++++++++++++++++++++
packages/core/src/error_boundary.rs | 6 +++---
packages/core/src/virtual_dom.rs | 8 ++++----
3 files changed, 31 insertions(+), 7 deletions(-)
diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs
index c3be8e9932..361a3839b1 100644
--- a/examples/rsx_usage.rs
+++ b/examples/rsx_usage.rs
@@ -197,6 +197,12 @@ fn app() -> Element {
Label { text: "hello geneirc world!" }
Label { text: 99.9 }
+ // Lowercase components work too, as long as they are access using a path
+ baller::lowercase_component {}
+
+ // For in-scope lowercase components, use the `self` keyword
+ self::lowercase_helper {}
+
// helper functions
// Anything that implements IntoVnode can be dropped directly into Rsx
{helper("hello world!")}
@@ -223,6 +229,16 @@ fn helper(text: &str) -> Element {
}
}
+// no_case_check disables PascalCase checking if you *really* want a snake_case component.
+// This will likely be deprecated/removed in a future update that will introduce a more polished linting system,
+// something like Clippy.
+#[component(no_case_check)]
+fn lowercase_helper() -> Element {
+ rsx! {
+ "asd"
+ }
+}
+
mod baller {
use super::*;
@@ -231,6 +247,14 @@ mod baller {
pub fn Baller() -> Element {
rsx! { "ballin'" }
}
+
+ // no_case_check disables PascalCase checking if you *really* want a snake_case component.
+ // This will likely be deprecated/removed in a future update that will introduce a more polished linting system,
+ // something like Clippy.
+ #[component(no_case_check)]
+ pub fn lowercase_component() -> Element {
+ rsx! { "look ma, no uppercase" }
+ }
}
/// Documention for this component is visible within the rsx macro
diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs
index dbb3440dc6..88359dd1ca 100644
--- a/packages/core/src/error_boundary.rs
+++ b/packages/core/src/error_boundary.rs
@@ -154,7 +154,7 @@ impl ErrorBoundary {
///
/// ```rust, ignore
/// #[component]
-/// fn App(count: String) -> Element {
+/// fn app(count: String) -> Element {
/// let id: i32 = count.parse().throw()?;
///
/// rsx! {
@@ -179,7 +179,7 @@ pub trait Throw: Sized {
///
/// ```rust, ignore
/// #[component]
- /// fn App(count: String) -> Element {
+ /// fn app( count: String) -> Element {
/// let id: i32 = count.parse().throw()?;
///
/// rsx! {
@@ -202,7 +202,7 @@ pub trait Throw: Sized {
///
/// ```rust, ignore
/// #[component]
- /// fn App(count: String) -> Element {
+ /// fn app( count: String) -> Element {
/// let id: i32 = count.parse().throw()?;
///
/// rsx! {
diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs
index 32a6f02476..f223118eca 100644
--- a/packages/core/src/virtual_dom.rs
+++ b/packages/core/src/virtual_dom.rs
@@ -58,16 +58,16 @@ use tracing::instrument;
/// static ROUTES: &str = "";
///
/// #[component]
-/// fn App(props: AppProps) -> Element {
+/// fn app(cx: AppProps) -> Element {
/// rsx!(
/// NavBar { routes: ROUTES }
-/// Title { "{props.title}" }
+/// Title { "{cx.title}" }
/// Footer {}
/// )
/// }
///
/// #[component]
-/// fn NavBar(routes: &'static str) -> Element {
+/// fn NavBar( routes: &'static str) -> Element {
/// rsx! {
/// div { "Routes: {routes}" }
/// }
@@ -130,7 +130,7 @@ use tracing::instrument;
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
/// ```rust, ignore
/// #[component]
-/// fn App() -> Element {
+/// fn app() -> Element {
/// rsx! {
/// div { "Hello World" }
/// }
From 54680186fa8d1c1e3a0bf0bd5c4c8fe5700a1746 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 8 Aug 2024 10:28:26 -0700
Subject: [PATCH 15/15] restore doc changes
---
packages/core-macro/src/component.rs | 183 ++++++++++++++++++++++++++-
packages/core-macro/src/lib.rs | 1 +
packages/core-macro/src/utils.rs | 129 +++++++++++++++++++
3 files changed, 306 insertions(+), 7 deletions(-)
create mode 100644 packages/core-macro/src/utils.rs
diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs
index 9e85682900..fb8cf0c204 100644
--- a/packages/core-macro/src/component.rs
+++ b/packages/core-macro/src/component.rs
@@ -83,13 +83,15 @@ impl ComponentBody {
let Generics { where_clause, .. } = generics;
let (_, impl_generics, _) = generics.split_for_impl();
- let generics_turbofish = ty_generics.as_turbofish();
+ let generics_turbofish = impl_generics.as_turbofish();
// We generate a struct with the same name as the component but called `Props`
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
// We pull in the field names from the original function signature, but need to strip off the mutability
- let struct_field_names = inputs.iter().filter_map(rebind_mutability);
+ let struct_field_names = inputs.iter().map(rebind_mutability);
+
+ let props_docs = self.props_docs(inputs.iter().collect());
let inlined_props_argument = if inputs.is_empty() {
quote! {}
@@ -100,6 +102,7 @@ impl ComponentBody {
// The extra nest is for the snake case warning to kick back in
parse_quote! {
#(#attrs)*
+ #(#props_docs)*
#[allow(non_snake_case)]
#vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause {
{
@@ -140,6 +143,77 @@ impl ComponentBody {
}
}
+ /// Convert a list of function arguments into a list of doc attributes for the props struct
+ ///
+ /// This lets us generate set of attributes that we can apply to the props struct to give it a nice docstring.
+ fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec {
+ let fn_ident = &self.item_fn.sig.ident;
+
+ if inputs.is_empty() {
+ return Vec::new();
+ }
+
+ let arg_docs = inputs
+ .iter()
+ .filter_map(|f| build_doc_fields(f))
+ .collect::>();
+
+ let mut props_docs = Vec::with_capacity(5);
+ let props_def_link = fn_ident.to_string() + "Props";
+ let header =
+ format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
+
+ props_docs.push(parse_quote! {
+ #[doc = #header]
+ });
+
+ for arg in arg_docs {
+ let DocField {
+ arg_name,
+ arg_type,
+ deprecation,
+ input_arg_doc,
+ } = arg;
+
+ let arg_name = strip_pat_mutability(arg_name).to_token_stream().to_string();
+ let arg_type = crate::utils::format_type_string(arg_type);
+
+ let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
+ .replace("\n\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}"));
+ }
+
+ 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}"));
+ }
+
+ arg_doc.push_str("
");
+
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str("
");
+ }
+ }
+
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str(&format!("{input_arg_doc}
"));
+ }
+
+ props_docs.push(parse_quote! { #[doc = #arg_doc] });
+ }
+
+ props_docs
+ }
+
fn is_explicit_props_ident(&self) -> bool {
if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
if let Pat::Ident(ident) = pat.as_ref() {
@@ -186,6 +260,65 @@ impl ComponentBody {
}
}
+struct DocField<'a> {
+ arg_name: &'a Pat,
+ arg_type: &'a Type,
+ deprecation: Option,
+ input_arg_doc: String,
+}
+
+fn build_doc_fields(f: &FnArg) -> Option {
+ let FnArg::Typed(pt) = f else { unreachable!() };
+
+ let arg_doc = pt
+ .attrs
+ .iter()
+ .filter_map(|attr| {
+ // TODO: Error reporting
+ // Check if the path of the attribute is "doc"
+ if !is_attr_doc(attr) {
+ return None;
+ };
+
+ let Meta::NameValue(meta_name_value) = &attr.meta else {
+ return None;
+ };
+
+ let Expr::Lit(doc_lit) = &meta_name_value.value else {
+ return None;
+ };
+
+ let Lit::Str(doc_lit_str) = &doc_lit.lit else {
+ return None;
+ };
+
+ Some(doc_lit_str.value())
+ })
+ .fold(String::new(), |mut doc, next_doc_line| {
+ doc.push('\n');
+ doc.push_str(&next_doc_line);
+ doc
+ });
+
+ Some(DocField {
+ arg_name: &pt.pat,
+ arg_type: &pt.ty,
+ deprecation: pt.attrs.iter().find_map(|attr| {
+ if !attr.path().is_ident("deprecated") {
+ return None;
+ }
+
+ let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
+
+ match res {
+ Err(e) => panic!("{}", e.to_string()),
+ Ok(v) => Some(v),
+ }
+ }),
+ input_arg_doc: arg_doc,
+ })
+}
+
fn validate_component_fn(item_fn: &ItemFn) -> Result<()> {
// Do some validation....
// 1. Ensure the component returns *something*
@@ -266,20 +399,56 @@ fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
}
}
-fn rebind_mutability(f: &FnArg) -> Option {
+fn rebind_mutability(f: &FnArg) -> TokenStream {
// There's no receivers (&self) allowed in the component body
let FnArg::Typed(pt) = f else { unreachable!() };
- let pat = &pt.pat;
+ let immutable = strip_pat_mutability(&pt.pat);
- let mut pat = pat.clone();
+ quote!(mut #immutable)
+}
+fn strip_pat_mutability(pat: &Pat) -> Pat {
+ let mut pat = pat.clone();
// rip off mutability, but still write it out eventually
- if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
+ if let Pat::Ident(ref mut pat_ident) = &mut pat {
pat_ident.mutability = None;
}
- Some(quote!(mut #pat))
+ pat
+}
+
+/// Checks if the attribute is a `#[doc]` attribute.
+fn is_attr_doc(attr: &Attribute) -> bool {
+ attr.path() == &parse_quote!(doc)
+}
+
+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;
+
+ for c in input.chars() {
+ match prev_char {
+ Some(prev) if c == target_char && prev == target_char => {
+ if consecutive_count < n_of_consecutive_chars_allowed {
+ output.push(c);
+ consecutive_count += 1;
+ }
+ }
+ _ => {
+ output.push(c);
+ prev_char = Some(c);
+ consecutive_count = 1;
+ }
+ }
+ }
+
+ output
}
/// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is allowed by the compiler.
diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs
index 7b2c844313..3ff19d9be2 100644
--- a/packages/core-macro/src/lib.rs
+++ b/packages/core-macro/src/lib.rs
@@ -9,6 +9,7 @@ use syn::parse_macro_input;
mod component;
mod props;
+mod utils;
use dioxus_rsx as rsx;
diff --git a/packages/core-macro/src/utils.rs b/packages/core-macro/src/utils.rs
new file mode 100644
index 0000000000..918c8d2935
--- /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 = "= unreachable!();";
+
+/// 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 })
+ }
+}