mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Merge pull request #2035 from DioxusLabs/jk/clean-core-macro
Chore #2028: radically clean up core-macro
This commit is contained in:
commit
60235ac06b
8 changed files with 456 additions and 872 deletions
20
packages/core-macro/.vscode/settings.json
vendored
Normal file
20
packages/core-macro/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"rust-analyzer.check.workspace": false,
|
||||
// "rust-analyzer.check.command": "check alsjdlaskdjljkasd",
|
||||
"rust-analyzer.check.allTargets": false,
|
||||
"rust-analyzer.cargo.buildScripts.enable": true,
|
||||
"rust-analyzer.cargo.buildScripts.overrideCommand": [
|
||||
"cargo",
|
||||
"check",
|
||||
"--quiet",
|
||||
// "--package",
|
||||
// "dioxus-core-macro",
|
||||
"--message-format",
|
||||
"json",
|
||||
"--all-targets"
|
||||
],
|
||||
"rust-analyzer.cargo.buildScripts.rebuildOnSave": false,
|
||||
"rust-analyzer.cargo.buildScripts.invocationLocation": "root",
|
||||
"rust-analyzer.cargo.buildScripts.invocationStrategy": "once",
|
||||
// "rust-analyzer.check.command": "check --package dioxus-core-macro"
|
||||
}
|
387
packages/core-macro/src/component.rs
Normal file
387
packages/core-macro/src/component.rs
Normal file
|
@ -0,0 +1,387 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::*;
|
||||
|
||||
pub struct ComponentBody {
|
||||
pub item_fn: ItemFn,
|
||||
}
|
||||
|
||||
impl Parse for ComponentBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let item_fn: ItemFn = input.parse()?;
|
||||
validate_component_fn_signature(&item_fn)?;
|
||||
Ok(Self { item_fn })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentBody {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let comp_fn = self.comp_fn();
|
||||
|
||||
// If there's no props declared, we simply omit the props argument
|
||||
// This is basically so you can annotate the App component with #[component] and still be compatible with the
|
||||
// launch signatures that take fn() -> Element
|
||||
let props_struct = match self.item_fn.sig.inputs.is_empty() {
|
||||
// No props declared, so we don't need to generate a props struct
|
||||
true => quote! {},
|
||||
|
||||
// Props declared, so we generate a props struct and thatn also attach the doc attributes to it
|
||||
false => {
|
||||
let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
|
||||
let props_struct = self.props_struct();
|
||||
quote! {
|
||||
#[doc = #doc]
|
||||
#props_struct
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#props_struct
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#comp_fn
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentBody {
|
||||
// build a new item fn, transforming the original item fn
|
||||
fn comp_fn(&self) -> ItemFn {
|
||||
let ComponentBody { item_fn, .. } = self;
|
||||
let ItemFn {
|
||||
attrs,
|
||||
vis,
|
||||
sig,
|
||||
block,
|
||||
} = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
output: fn_output,
|
||||
asyncness,
|
||||
..
|
||||
} = sig;
|
||||
let Generics { where_clause, .. } = generics;
|
||||
let (_, ty_generics, _) = generics.split_for_impl();
|
||||
|
||||
// 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 props_docs = self.props_docs(inputs.iter().skip(1).collect());
|
||||
|
||||
// Don't generate the props argument if there are no inputs
|
||||
// This means we need to skip adding the argument to the function signature, and also skip the expanded struct
|
||||
let props_ident = match inputs.is_empty() {
|
||||
true => quote! {},
|
||||
false => quote! { mut __props: #struct_ident #ty_generics },
|
||||
};
|
||||
let expanded_struct = match inputs.is_empty() {
|
||||
true => quote! {},
|
||||
false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; },
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#(#attrs)*
|
||||
#(#props_docs)*
|
||||
#asyncness #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause {
|
||||
#expanded_struct
|
||||
#block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an associated struct for the props of the component
|
||||
///
|
||||
/// This will expand to the typed-builder implementation that we have vendored in this crate.
|
||||
/// TODO: don't vendor typed-builder and instead transform the tokens we give it before expansion.
|
||||
/// TODO: cache these tokens since this codegen is rather expensive (lots of tokens)
|
||||
///
|
||||
/// We try our best to transfer over any declared doc attributes from the original function signature onto the
|
||||
/// props struct fields.
|
||||
fn props_struct(&self) -> ItemStruct {
|
||||
let ItemFn { vis, sig, .. } = &self.item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident,
|
||||
generics,
|
||||
..
|
||||
} = sig;
|
||||
|
||||
let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
|
||||
let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
|
||||
|
||||
parse_quote! {
|
||||
#[derive(Props, Clone, PartialEq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_ident #generics {
|
||||
#(#struct_fields),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Attribute> {
|
||||
let fn_ident = &self.item_fn.sig.ident;
|
||||
|
||||
if inputs.len() <= 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let arg_docs = inputs
|
||||
.iter()
|
||||
.filter_map(|f| build_doc_fields(f))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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 = arg_name.into_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", "</p><p>");
|
||||
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("<p>👎 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("</p>");
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str("<hr/>");
|
||||
}
|
||||
}
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||
}
|
||||
|
||||
props_docs.push(parse_quote! { #[doc = #arg_doc] });
|
||||
}
|
||||
|
||||
props_docs
|
||||
}
|
||||
}
|
||||
|
||||
struct DocField<'a> {
|
||||
arg_name: &'a Pat,
|
||||
arg_type: &'a Type,
|
||||
deprecation: Option<crate::utils::DeprecatedAttribute>,
|
||||
input_arg_doc: String,
|
||||
}
|
||||
|
||||
fn build_doc_fields(f: &FnArg) -> Option<DocField> {
|
||||
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() != &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),
|
||||
}
|
||||
}),
|
||||
input_arg_doc: arg_doc,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
|
||||
// Do some validation....
|
||||
// 1. Ensure the component returns *something*
|
||||
if item_fn.sig.output == ReturnType::Default {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.output.span(),
|
||||
"Must return a <dioxus_core::Element>".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// 2. make sure there's no lifetimes on the component - we don't know how to handle those
|
||||
if item_fn.sig.generics.lifetimes().count() > 0 {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.generics.span(),
|
||||
"Lifetimes are not supported in components".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// 3. we can't handle async components
|
||||
if item_fn.sig.asyncness.is_some() {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.asyncness.span(),
|
||||
"Async components are not supported".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// 4. we can't handle const components
|
||||
if item_fn.sig.constness.is_some() {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.constness.span(),
|
||||
"Const components are not supported".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// 5. no receiver parameters
|
||||
if item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|f| matches!(f, FnArg::Receiver(_)))
|
||||
{
|
||||
return Err(Error::new(
|
||||
item_fn.sig.inputs.span(),
|
||||
"Receiver parameters are not supported".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert a function arg with a given visibility (provided by the function) and then generate a field for the
|
||||
/// associated props struct.
|
||||
fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
|
||||
// There's no receivers (&self) allowed in the component body
|
||||
let FnArg::Typed(pt) = f else { unreachable!() };
|
||||
|
||||
let arg_pat = match pt.pat.as_ref() {
|
||||
// rip off mutability
|
||||
// todo: we actually don't want any of the extra bits of the field pattern
|
||||
Pat::Ident(f) => {
|
||||
let mut f = f.clone();
|
||||
f.mutability = None;
|
||||
quote! { #f }
|
||||
}
|
||||
a => quote! { #a },
|
||||
};
|
||||
|
||||
let PatType {
|
||||
attrs,
|
||||
ty,
|
||||
colon_token,
|
||||
..
|
||||
} = pt;
|
||||
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#vis #arg_pat #colon_token #ty
|
||||
}
|
||||
}
|
||||
|
||||
fn rebind_mutability(f: &FnArg) -> Option<TokenStream> {
|
||||
// There's no receivers (&self) allowed in the component body
|
||||
let FnArg::Typed(pt) = f else { unreachable!() };
|
||||
|
||||
let pat = &pt.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() {
|
||||
pat_ident.mutability = None;
|
||||
}
|
||||
|
||||
Some(quote!(mut #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<char> = 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
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
//! This module is used for parsing a component function into a struct that is subsequently
|
||||
//! deserialized into something useful using deserializer arguments.
|
||||
//!
|
||||
//! Let's break that down with a term glossary and examples which show usage and implementing.
|
||||
//!
|
||||
//! # Glossary
|
||||
//! * `component body` - The [`ComponentBody`] struct. It's used to parse a component function [`proc_macro::TokenStream`]
|
||||
//! to a reusable struct that deserializers use to modify the token stream.
|
||||
//! * `deserializer` - A struct that deserializes the [`ComponentBody`] into a [`DeserializerOutput`].
|
||||
//! It implements the [`DeserializerArgs`] trait, but as you can see, it's called "DeserializerArgs",
|
||||
//! not "Deserializer". Why?
|
||||
//! Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
|
||||
//! takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
|
||||
//! * `deserializer output` - A struct that implements the [`DeserializerOutput`] trait.
|
||||
//! This struct is what enables deserializers to use each other, since it contains the fields that
|
||||
//! a deserializer needs to turn a token stream to a different token stream.
|
||||
//! This means a deserializer can get the output of another deserializer, and use that output,
|
||||
//! thereby using the functionality of a different deserializer.
|
||||
//! This struct also implements [`ToTokens`], which means that this is the final stage of the whole process.
|
||||
//!
|
||||
//! # Examples
|
||||
//! *Not all imports might be included.*
|
||||
//!
|
||||
//! ## Usage in a procedural macro attribute
|
||||
//! ```rs,ignore
|
||||
//! use proc_macro::TokenStream;
|
||||
//!
|
||||
//! // Some documentation. You can reuse this in your deserializer structs.
|
||||
//! /// This attribute changes the name of a component function to whatever the first argument is.
|
||||
//! #[proc_macro_attribute]
|
||||
//! pub fn name_changer(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
//! // Parse the component body.
|
||||
//! let component_body = parse_macro_input!(input as ComponentBody);
|
||||
//!
|
||||
//! // Parse the first argument, which is going to be the components new name.
|
||||
//! let new_name: String = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
|
||||
//! Err(e) => return e.to_compile_error().into(), // Convert to a compile error and return
|
||||
//! Ok(args) => {
|
||||
//! // If the argument exists, then convert it to a string
|
||||
//! if let Some(first) = args.first() {
|
||||
//! first.to_token_stream().to_string()
|
||||
//! } else {
|
||||
//! // If the argument doesn't exist, return an error with the appropriate message.
|
||||
//! // The "span" is the location of some code.
|
||||
//! // The error occurred in the "args" token stream, so point the error there.
|
||||
//! return Error::new(args.span(), "No new name provided").to_compile_error().into();
|
||||
//! }
|
||||
//! }
|
||||
//! };
|
||||
//!
|
||||
//! let new_name = &*new_name;
|
||||
//!
|
||||
//! // Deserialize the component body to an output with the given args.
|
||||
//! let output = component_body.deserialize(NameChangerDeserializerArgs { new_name });
|
||||
//!
|
||||
//! // Error handling like before, except now you're ready to return the final value.
|
||||
//! match output {
|
||||
//! Err(e) => e.to_compile_error().into(),
|
||||
//! Ok(output) => output.to_token_stream().into(),
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! ## Using the macro in Dioxus code:
|
||||
//! ```rs
|
||||
//! use your_proc_macro_library::name_changer;
|
||||
//! use dioxus::prelude::*;
|
||||
//!
|
||||
//! #[name_changer(CoolName)]
|
||||
//! pub fn LameName() -> Element {
|
||||
//! rsx! { "I want a cool name!" }
|
||||
//! }
|
||||
//!
|
||||
//! pub fn App() -> Element {
|
||||
//! rsx! { CoolName {} } // Renders: "I want a cool name!"
|
||||
//! }
|
||||
//! ```
|
||||
//! ## Implementing a component body deserializer
|
||||
//! ```rs
|
||||
//! use syn::{Result, ItemFn, Signature, Ident};
|
||||
//! use quote::quote;
|
||||
//!
|
||||
//! // Create a list of arguments.
|
||||
//! // If there was no args, just make it empty. The "args" struct is also the deserializer struct.
|
||||
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
|
||||
//! // Although unfortunately, the link does not work
|
||||
//! // Just make sure that your macro is well documented.
|
||||
//! /// The args and deserializing implementation for the [`name_changer`] macro.
|
||||
//! #[derive(Clone)]
|
||||
//! pub struct NameChangerDeserializerArgs<'a> {
|
||||
//! pub new_name: &'a str,
|
||||
//! }
|
||||
//!
|
||||
//! // Create an output struct.
|
||||
//! // The ItemFn represents a modified component function.
|
||||
//! // To read what fields should be here, check out the `DeserializerOutput` struct docs.
|
||||
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
|
||||
//! // Just make sure that your macro is well documented.
|
||||
//! /// The output fields and [`ToTokens`] implementation for the [`name_changer`] macro.
|
||||
//! #[derive(Clone)]
|
||||
//! pub struct NameChangerDeserializerOutput {
|
||||
//! pub comp_fn: ItemFn,
|
||||
//! }
|
||||
//!
|
||||
//! // Implement `ToTokens`, which is forced by `DeserializerOutput`.
|
||||
//! // This will usually be very simple like this, even for complex deserializers.
|
||||
//! // That's because of the way the `DeserializerOutput` is designed.
|
||||
//! impl ToTokens for NameChangerDeserializerOutput {
|
||||
//! fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
//! let comp_fn = &self.comp_fn;
|
||||
//!
|
||||
//! tokens.append_all(quote! {
|
||||
//! #comp_fn
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl DeserializerOutput for NameChangerDeserializerOutput {}
|
||||
//!
|
||||
//! // Implement `DeserializerArgs`. This is the core part of deserializers.
|
||||
//! impl<'a> DeserializerArgs<NameChangerDeserializerOutput> for NameChangerDeserializerArgs<'a> {
|
||||
//! fn to_output(&self, component_body: &ComponentBody) -> Result<NameChangerDeserializerOutput> {
|
||||
//! let old_fn = &component_body.item_fn;
|
||||
//! let old_sig = &old_fn.sig;
|
||||
//!
|
||||
//! // For more complex uses, you will probably use `quote::parse_quote!` in combination with
|
||||
//! // creating the structs manually.
|
||||
//! // However, create the structs manually if you can.
|
||||
//! // It's more reliable, because you only modify a certain struct field
|
||||
//! // and set the others to be the clone of the original component body.
|
||||
//! // That ensures that no information will be accidentally removed.
|
||||
//! let new_sig = Signature {
|
||||
//! ident: Ident::new(self.new_name, old_sig.ident.span()),
|
||||
//! ..old_sig.clone()
|
||||
//! };
|
||||
//! let new_fn = ItemFn {
|
||||
//! sig: new_sig,
|
||||
//! ..old_fn.clone()
|
||||
//! };
|
||||
//!
|
||||
//! Ok(NameChangerDeserializerOutput {
|
||||
//! comp_fn: new_fn
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod utils;
|
||||
|
||||
pub use utils::DeserializerArgs;
|
||||
pub use utils::DeserializerOutput;
|
||||
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::*;
|
||||
|
||||
/// General struct for parsing a component body.
|
||||
/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens).
|
||||
///
|
||||
/// Refer to the [module documentation](crate::component_body) for more.
|
||||
pub struct ComponentBody {
|
||||
/// The component function definition. You can parse this back into a [`ComponentBody`].
|
||||
/// For example, you might modify it, parse it into a [`ComponentBody`], and deserialize that
|
||||
/// using some deserializer. This is how deserializers use other deserializers.
|
||||
///
|
||||
/// **`item_fn.sig.inputs` includes the context argument!**
|
||||
/// Keep this in mind when creating deserializers, because you often might want to ignore it.
|
||||
/// That might be annoying, but it would be bad design for this kind of struct to not be parsable from itself.
|
||||
pub item_fn: ItemFn,
|
||||
/// If the function has any arguments other than the context.
|
||||
pub has_extra_args: bool,
|
||||
}
|
||||
|
||||
impl ComponentBody {
|
||||
/// Deserializes the body into the [`TOutput`] with the specific [`TArgs`].
|
||||
/// Even if the args are empty, the [`TArg`] type still determines what [`TOutput`] will be generated.
|
||||
pub fn deserialize<TOutput, TArgs>(&self, args: TArgs) -> Result<TOutput>
|
||||
where
|
||||
TOutput: DeserializerOutput,
|
||||
TArgs: DeserializerArgs<TOutput>,
|
||||
{
|
||||
args.to_output(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ComponentBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let item_fn: ItemFn = input.parse()?;
|
||||
|
||||
let element_type_path = "dioxus_core::Element";
|
||||
|
||||
if item_fn.sig.output == ReturnType::Default {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.output.span(),
|
||||
format!("Must return a <{}>", element_type_path),
|
||||
));
|
||||
}
|
||||
|
||||
let has_extra_args = !item_fn.sig.inputs.is_empty();
|
||||
|
||||
Ok(Self {
|
||||
item_fn,
|
||||
has_extra_args,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use crate::component_body::ComponentBody;
|
||||
use quote::ToTokens;
|
||||
|
||||
/// The output produced by a deserializer.
|
||||
///
|
||||
/// # For implementors
|
||||
/// Struct field guidelines:
|
||||
/// * Must be public, so that other deserializers can utilize them.
|
||||
/// * Should usually be [`Item`]s that you then simply combine in a [`quote!`]
|
||||
/// in the [`ComponentBodyDeserializer::output_to_token_stream2`] function.
|
||||
/// * If an [`Item`] might not be included, wrap it in an [`Option`].
|
||||
/// * Must be at the component function "level"/"context".
|
||||
/// For example, the [`InlinePropsDeserializer`](crate::component_body_deserializers::inline_props::InlinePropsDeserializer)
|
||||
/// produces two [`Item`]s; the function but with arguments turned into props, and the props struct.
|
||||
/// It does not return any [`Item`]s inside the struct or function.
|
||||
pub trait DeserializerOutput: ToTokens {}
|
||||
|
||||
impl<T: ToTokens> DeserializerOutput for T {}
|
||||
|
||||
/// The args passed to a [`ComponentBody`] when deserializing it.
|
||||
///
|
||||
/// It's also the struct that does the deserializing.
|
||||
/// It's called "DeserializerArgs", not "Deserializer". Why?
|
||||
/// Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
|
||||
/// takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
|
||||
pub trait DeserializerArgs<TOutput>: Clone
|
||||
where
|
||||
TOutput: ToTokens,
|
||||
{
|
||||
// There's a lot of Results out there... let's make sure that this is a syn::Result.
|
||||
// Let's also make sure there's not a warning.
|
||||
/// Creates a [`ToTokens`] struct from the `self` args and a [`ComponentBody`].
|
||||
/// The [`ComponentBody::deserialize`] provides a cleaner way of calling this function.
|
||||
#[allow(unused_qualifications)]
|
||||
fn to_output(&self, component_body: &ComponentBody) -> syn::Result<TOutput>;
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
use crate::component_body::{ComponentBody, DeserializerArgs};
|
||||
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
|
||||
use constcat::concat;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::*;
|
||||
|
||||
pub(crate) const COMPONENT_ARG_CASE_CHECK_ERROR: &str = concat!(
|
||||
"This component does not use PascalCase. \
|
||||
To ignore this check, pass the \"",
|
||||
crate::COMPONENT_ARG_CASE_CHECK_OFF,
|
||||
"\" argument, like so: #[component(",
|
||||
crate::COMPONENT_ARG_CASE_CHECK_OFF,
|
||||
")]"
|
||||
);
|
||||
|
||||
const INNER_FN_NAME: &str = "__dx_inner_comp";
|
||||
|
||||
fn get_out_comp_fn(orig_comp_fn: &ItemFn) -> ItemFn {
|
||||
let inner_comp_ident = Ident::new(INNER_FN_NAME, orig_comp_fn.sig.ident.span());
|
||||
|
||||
let inner_comp_fn = ItemFn {
|
||||
sig: Signature {
|
||||
ident: inner_comp_ident.clone(),
|
||||
..orig_comp_fn.sig.clone()
|
||||
},
|
||||
..orig_comp_fn.clone()
|
||||
};
|
||||
|
||||
let props_ident = match orig_comp_fn.sig.inputs.is_empty() {
|
||||
true => quote! {},
|
||||
false => quote! { __props },
|
||||
};
|
||||
|
||||
ItemFn {
|
||||
block: parse_quote! {
|
||||
{
|
||||
#[warn(non_snake_case)]
|
||||
#[allow(clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
#inner_comp_fn
|
||||
#inner_comp_ident(#props_ident)
|
||||
}
|
||||
},
|
||||
..orig_comp_fn.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// The args and deserializing implementation for the [`crate::component`] macro.
|
||||
#[derive(Clone)]
|
||||
pub struct ComponentDeserializerArgs {
|
||||
pub case_check: bool,
|
||||
}
|
||||
|
||||
/// The output fields and [`ToTokens`] implementation for the [`crate::component`] macro.
|
||||
#[derive(Clone)]
|
||||
pub struct ComponentDeserializerOutput {
|
||||
pub comp_fn: ItemFn,
|
||||
pub props_struct: Option<ItemStruct>,
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentDeserializerOutput {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let comp_fn = &self.comp_fn;
|
||||
let props_struct = &self.props_struct;
|
||||
let fn_ident = &comp_fn.sig.ident;
|
||||
|
||||
let doc = format!("Properties for the [`{fn_ident}`] component.");
|
||||
tokens.append_all(quote! {
|
||||
#[doc = #doc]
|
||||
#props_struct
|
||||
#[allow(non_snake_case)]
|
||||
#comp_fn
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializerArgs<ComponentDeserializerOutput> for ComponentDeserializerArgs {
|
||||
fn to_output(&self, component_body: &ComponentBody) -> Result<ComponentDeserializerOutput> {
|
||||
let Signature { ident, .. } = &component_body.item_fn.sig;
|
||||
|
||||
if self.case_check && !is_pascal_case(&ident.to_string()) {
|
||||
return Err(Error::new(ident.span(), COMPONENT_ARG_CASE_CHECK_ERROR));
|
||||
}
|
||||
|
||||
if component_body.has_extra_args {
|
||||
Self::deserialize_with_props(component_body)
|
||||
} else {
|
||||
Ok(Self::deserialize_no_props(component_body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentDeserializerArgs {
|
||||
fn deserialize_no_props(component_body: &ComponentBody) -> ComponentDeserializerOutput {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
|
||||
let comp_fn = get_out_comp_fn(item_fn);
|
||||
|
||||
ComponentDeserializerOutput {
|
||||
comp_fn,
|
||||
props_struct: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_with_props(
|
||||
component_body: &ComponentBody,
|
||||
) -> Result<ComponentDeserializerOutput> {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
|
||||
let comp_parsed = match parse2::<ComponentBody>(quote!(#item_fn)) {
|
||||
Ok(comp_body) => comp_body,
|
||||
Err(e) => {
|
||||
return Err(Error::new(
|
||||
e.span(),
|
||||
format!(
|
||||
"This is probably a bug in our code, please report it! Error: {}",
|
||||
e
|
||||
),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let inlined_props_output = comp_parsed.deserialize(InlinePropsDeserializerArgs {})?;
|
||||
let props_struct = inlined_props_output.props_struct;
|
||||
let props_fn = inlined_props_output.comp_fn;
|
||||
|
||||
let comp_fn = get_out_comp_fn(&props_fn);
|
||||
|
||||
Ok(ComponentDeserializerOutput {
|
||||
comp_fn,
|
||||
props_struct: Some(props_struct),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_pascal_case(input: &str) -> bool {
|
||||
let mut is_next_lowercase = false;
|
||||
|
||||
for c in input.chars() {
|
||||
let is_upper = c.is_ascii_uppercase();
|
||||
|
||||
if (c == '_') || (is_upper && is_next_lowercase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
is_next_lowercase = is_upper;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
|
@ -1,355 +0,0 @@
|
|||
use crate::component_body::{ComponentBody, DeserializerArgs};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::token::Comma;
|
||||
use syn::{punctuated::Punctuated, *};
|
||||
|
||||
/// The args and deserializing implementation for the [`crate::inline_props`] macro.
|
||||
#[derive(Clone)]
|
||||
pub struct InlinePropsDeserializerArgs;
|
||||
|
||||
/// The output fields and [`ToTokens`] implementation for the [`crate::inline_props`] macro.
|
||||
#[derive(Clone)]
|
||||
pub struct InlinePropsDeserializerOutput {
|
||||
pub comp_fn: ItemFn,
|
||||
pub props_struct: ItemStruct,
|
||||
}
|
||||
|
||||
impl ToTokens for InlinePropsDeserializerOutput {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let function = &self.comp_fn;
|
||||
let props_struct = &self.props_struct;
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#function
|
||||
#props_struct
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
|
||||
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
|
||||
Ok(InlinePropsDeserializerOutput {
|
||||
comp_fn: get_function(component_body),
|
||||
props_struct: get_props_struct(component_body),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
let ItemFn { vis, sig, .. } = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
..
|
||||
} = sig;
|
||||
|
||||
let struct_fields = inputs.iter().map(move |f| {
|
||||
match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
||||
FnArg::Typed(pt) => {
|
||||
let arg_pat = match pt.pat.as_ref() {
|
||||
// rip off mutability
|
||||
Pat::Ident(f) => {
|
||||
let mut f = f.clone();
|
||||
f.mutability = None;
|
||||
quote! { #f }
|
||||
}
|
||||
a => quote! { #a },
|
||||
};
|
||||
|
||||
let arg_colon = &pt.colon_token;
|
||||
let arg_ty = &pt.ty; // Type
|
||||
let arg_attrs = &pt.attrs; // Attributes
|
||||
|
||||
quote! {
|
||||
#(#arg_attrs)
|
||||
*
|
||||
#vis #arg_pat #arg_colon #arg_ty
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let struct_attrs = if first_lifetime.is_some() {
|
||||
quote! { #[derive(Props, Clone)] }
|
||||
} else {
|
||||
quote! { #[derive(Props, Clone, PartialEq)] }
|
||||
};
|
||||
|
||||
let struct_generics = if first_lifetime.is_some() {
|
||||
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
||||
.item_fn
|
||||
.sig
|
||||
.generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.push(parse_quote!( 'a ));
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
quote! { <#struct_generics> }
|
||||
} else {
|
||||
quote! { #generics }
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#struct_attrs
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_ident #struct_generics
|
||||
{
|
||||
#(#struct_fields),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||
if inputs.len() <= 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let arg_docs = inputs
|
||||
.iter()
|
||||
.filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(pt) => {
|
||||
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((
|
||||
&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,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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_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_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
||||
.replace("\n\n", "</p><p>");
|
||||
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("<p>👎 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("</p>");
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str("<hr/>");
|
||||
}
|
||||
}
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||
}
|
||||
|
||||
props_docs.push(parse_quote! {
|
||||
#[doc = #arg_doc]
|
||||
});
|
||||
}
|
||||
|
||||
props_docs
|
||||
}
|
||||
|
||||
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
let ItemFn {
|
||||
attrs: fn_attrs,
|
||||
vis,
|
||||
sig,
|
||||
block: fn_block,
|
||||
} = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
output: fn_output,
|
||||
asyncness,
|
||||
..
|
||||
} = sig;
|
||||
let Generics { where_clause, .. } = generics;
|
||||
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
// Skip first arg since that's the context
|
||||
let struct_field_names = inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(pt) => {
|
||||
let pat = &pt.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() {
|
||||
pat_ident.mutability = None;
|
||||
}
|
||||
|
||||
Some(quote!(mut #pat))
|
||||
}
|
||||
});
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (_scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
|
||||
(quote! { #lt, }, generics.clone())
|
||||
} else {
|
||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
|
||||
(quote! { #lifetime, }, fn_generics)
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
|
||||
|
||||
parse_quote! {
|
||||
#(#fn_attrs)*
|
||||
#(#props_docs)*
|
||||
#asyncness #vis fn #fn_ident #fn_generics (mut __props: #struct_ident #generics_no_bounds) #fn_output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_ident { #(#struct_field_names),* } = __props;
|
||||
#fn_block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<char> = 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
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers.
|
||||
|
||||
pub mod component;
|
||||
pub mod inline_props;
|
|
@ -2,21 +2,15 @@
|
|||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
use component::ComponentBody;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use syn::parse::Parser;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{parse_macro_input, Path, Token};
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod component_body;
|
||||
mod component_body_deserializers;
|
||||
mod component;
|
||||
mod props;
|
||||
mod utils;
|
||||
|
||||
// mod rsx;
|
||||
use crate::component_body::ComponentBody;
|
||||
use crate::component_body_deserializers::component::ComponentDeserializerArgs;
|
||||
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
|
||||
use dioxus_rsx as rsx;
|
||||
|
||||
#[proc_macro]
|
||||
|
@ -54,6 +48,50 @@ pub fn render(tokens: TokenStream) -> TokenStream {
|
|||
rsx(tokens)
|
||||
}
|
||||
|
||||
/// Streamlines component creation.
|
||||
/// This is the recommended way of creating components,
|
||||
/// though you might want lower-level control with more advanced uses.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names.
|
||||
/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.**
|
||||
/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas
|
||||
/// macros are extremely limited.
|
||||
///
|
||||
/// # Features
|
||||
/// This attribute:
|
||||
/// * Enforces that your component uses `PascalCase`.
|
||||
/// No warnings are generated for the `PascalCase`
|
||||
/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`.
|
||||
/// Does not disable warnings anywhere else, so if you, for example,
|
||||
/// accidentally don't use `snake_case`
|
||||
/// for a variable name in the function, the compiler will still warn you.
|
||||
/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function.
|
||||
/// * Verifies the validity of your component.
|
||||
///
|
||||
/// # Examples
|
||||
/// * Without props:
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// fn GreetBob() -> Element {
|
||||
/// rsx! { "hello, bob" }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// * With props:
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// fn GreetBob(bob: String) -> Element {
|
||||
/// rsx! { "hello, {bob}" }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
parse_macro_input!(input as ComponentBody)
|
||||
.into_token_stream()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Derive props for a component within the component definition.
|
||||
///
|
||||
/// This macro provides a simple transformation from `Scope<{}>` to `Scope<P>`,
|
||||
|
@ -82,117 +120,6 @@ pub fn render(tokens: TokenStream) -> TokenStream {
|
|||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
#[deprecated(note = "Use `#[component]` instead.")]
|
||||
pub fn inline_props(_args: TokenStream, s: TokenStream) -> TokenStream {
|
||||
let comp_body = parse_macro_input!(s as ComponentBody);
|
||||
|
||||
match comp_body.deserialize(InlinePropsDeserializerArgs {}) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(output) => output.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
|
||||
|
||||
/// Streamlines component creation.
|
||||
/// This is the recommended way of creating components,
|
||||
/// though you might want lower-level control with more advanced uses.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names.
|
||||
/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.**
|
||||
/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas
|
||||
/// macros are extremely limited.
|
||||
///
|
||||
/// # Features
|
||||
/// This attribute:
|
||||
/// * Enforces that your component uses `PascalCase`.
|
||||
/// No warnings are generated for the `PascalCase`
|
||||
/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`.
|
||||
/// Does not disable warnings anywhere else, so if you, for example,
|
||||
/// accidentally don't use `snake_case`
|
||||
/// for a variable name in the function, the compiler will still warn you.
|
||||
/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function.
|
||||
/// * Verifies the validity of your component.
|
||||
/// E.g. if it has a [`Scope`](dioxus_core::Scope) argument.
|
||||
/// Notes:
|
||||
/// * This doesn't work 100% of the time, because of macro limitations.
|
||||
/// * Provides helpful messages if your component is not correct.
|
||||
/// Possible bugs (please, report these!):
|
||||
/// * There might be bugs where it incorrectly *denies* validity.
|
||||
/// This is bad as it means that you can't use the attribute or you have to change the component.
|
||||
/// * There might be bugs where it incorrectly *confirms* validity.
|
||||
/// You will still know if the component is invalid once you use it,
|
||||
/// but the error might be less helpful.
|
||||
///
|
||||
/// # Examples
|
||||
/// * Without props:
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// fn GreetBob() -> Element {
|
||||
/// rsx! { "hello, bob" }
|
||||
/// }
|
||||
///
|
||||
/// // is equivalent to
|
||||
///
|
||||
/// #[allow(non_snake_case)]
|
||||
/// fn GreetBob() -> Element {
|
||||
/// #[warn(non_snake_case)]
|
||||
/// #[inline(always)]
|
||||
/// fn __dx_inner_comp() -> Element {
|
||||
/// rsx! { "hello, bob" }
|
||||
/// }
|
||||
/// // There's no function call overhead since __dx_inner_comp has the #[inline(always)] attribute,
|
||||
/// // so don't worry about performance.
|
||||
/// __dx_inner_comp(cx)
|
||||
/// }
|
||||
/// ```
|
||||
/// * With props:
|
||||
/// ```rust,ignore
|
||||
/// #[component(no_case_check)]
|
||||
/// fn GreetPerson(person: String) -> Element {
|
||||
/// rsx! { "hello, {person}" }
|
||||
/// }
|
||||
///
|
||||
/// // is equivalent to
|
||||
///
|
||||
/// #[derive(Props, PartialEq)]
|
||||
/// #[allow(non_camel_case_types)]
|
||||
/// struct GreetPersonProps {
|
||||
/// person: String,
|
||||
/// }
|
||||
///
|
||||
/// #[allow(non_snake_case)]
|
||||
/// fn GreetPerson(props: GreetPersonProps>) -> Element {
|
||||
/// #[warn(non_snake_case)]
|
||||
/// #[inline(always)]
|
||||
/// fn __dx_inner_comp(props: GreetPersonProps>e) -> Element {
|
||||
/// let GreetPersonProps { person } = props;
|
||||
/// {
|
||||
/// rsx! { "hello, {person}" }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// __dx_inner_comp(cx)
|
||||
/// }
|
||||
/// ```
|
||||
// TODO: Maybe add an option to input a custom component name through the args.
|
||||
// I think that's unnecessary, but there might be some scenario where it could be useful.
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let component_body = parse_macro_input!(input as ComponentBody);
|
||||
let case_check = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
Ok(args) => {
|
||||
if let Some(first) = args.first() {
|
||||
!first.is_ident(COMPONENT_ARG_CASE_CHECK_OFF)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match component_body.deserialize(ComponentDeserializerArgs { case_check }) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(output) => output.to_token_stream().into(),
|
||||
}
|
||||
pub fn inline_props(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
component(args, input)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue