use crate::{
asteracea_ident,
parse_with_context::{ParseContext, ParseWithContext},
part::{CaptureDefinition, GenerateContext},
warn, Configuration, MapMessage, Part, YankAny,
};
use call2_for_syn::call2;
use heck::KebabCase;
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use syn::{
braced, parenthesized,
parse::{discouraged, Parse, ParseStream, Result},
parse2, parse_quote,
punctuated::Punctuated,
spanned::Spanned,
token::Paren,
Attribute, Error, FnArg, Generics, Ident, Lifetime, LitStr, ReturnType, Token, Type,
Visibility, WherePredicate,
};
use unzip_n::unzip_n;
unzip_n!(5);
unzip_n!(6);
unzip_n!(7);
pub struct ComponentDeclaration {
cx: ParseContext,
attributes: Vec<Attribute>,
visibility: Visibility,
component_generics: Option<Generics>,
component_wheres: Vec<WherePredicate>,
constructor_attributes: Vec<Attribute>,
constructor_generics: Option<Generics>,
constructor_args: Vec<FnArg>,
render_attributes: Vec<Attribute>,
render_generics: Option<Generics>,
render_paren: Paren,
render_args: Vec<FnArg>,
render_type: ReturnType,
static_shared_new_procedure: Vec<TokenStream>,
static_shared_render_procedure: Vec<TokenStream>,
new_procedure: Vec<TokenStream>,
render_procedure: Vec<TokenStream>,
body: Part<ComponentRenderConfiguration>,
rhizome_transform: bool,
rhizome_extractions: Vec<TokenStream>,
}
pub struct TypeLevelFieldTarget(pub Lifetime);
impl Parse for TypeLevelFieldTarget {
fn parse(input: ParseStream) -> Result<Self> {
let target: Lifetime = input.parse()?;
match &target.ident {
x if x == "NEW" => (),
x if x == "RENDER" => (),
_ => return Err(Error::new_spanned(target, "Expected 'NEW or 'RENDER.")),
};
Ok(Self(target))
}
}
impl PartialEq for TypeLevelFieldTarget {
fn eq(&self, rhs: &Self) -> bool {
self.0.ident == rhs.0.ident }
}
impl TypeLevelFieldTarget {
fn is_new(&self) -> bool {
self.0.ident == "NEW"
}
fn is_render(&self) -> bool {
self.0.ident == "RENDER"
}
}
impl ToTokens for TypeLevelFieldTarget {
fn to_tokens(&self, output: &mut TokenStream) {
self.0.to_tokens(output);
}
}
pub struct TypeLevelFieldDefinition {
pub targets: Vec<TypeLevelFieldTarget>,
pub field_definition: FieldDefinition,
}
pub struct FieldDefinition {
pub attributes: Vec<Attribute>,
pub visibility: Visibility,
pub name: Ident,
pub field_type: TokenStream,
pub initial_value: TokenStream,
}
enum ComponentRenderConfiguration {}
impl Configuration for ComponentRenderConfiguration {
const NAME: &'static str = "component! render expression";
const CAN_CAPTURE: bool = true;
}
impl Parse for ComponentDeclaration {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let attributes = input.call(Attribute::parse_outer)?;
let visibility = input.parse()?;
let component_name: Ident = input
.parse()
.map_message("Expected identifier (component name).")?;
let component_generics = if input.peek(Token![<]) {
Some(input.parse()?)
} else {
None
};
let mut component_wheres = Vec::new();
if input.peek(Token![where]) {
input.parse::<Token![where]>()?;
loop {
let forked = input.fork();
if let Ok(predicate) = forked.parse() {
let comma_required = !forked.peek(Token![#]);
if comma_required && forked.parse::<Token![,]>().is_err() {
return Err(Error::new_spanned(
predicate,
"A component's where clause must end with a comma unless it is followed by an attribute.",
));
}
component_wheres.push(predicate);
discouraged::Speculative::advance_to(input, &forked);
if !comma_required {
break;
}
} else {
break;
}
}
if component_wheres.is_empty() {
warn(
input.cursor().span(),
"No where predicate found.
Did you forget to end it with a comma?",
)?;
}
}
let constructor_attributes = input.call(Attribute::parse_outer)?;
let constructor_generics = if input.peek(Token![<]) {
Some(input.parse()?)
} else {
None
};
if !input.peek(Paren) {
let message = "Expected parentheses (constructor arguments).".to_string();
return Err(Error::new(
input.cursor().span(),
if !component_wheres.is_empty() {
message + "\nDid you forget to end the component where clause with a comma?"
} else {
message
},
));
}
let constructor_args;
parenthesized!(constructor_args in input);
let constructor_args = Punctuated::<_, Token![,]>::parse_terminated(&constructor_args)?;
let constructor_args = constructor_args.into_iter().collect();
let render_attributes = input.call(Attribute::parse_outer)?;
let render_generics = if input.peek(Token![<]) {
Some(input.parse()?)
} else {
None
};
let render_args;
let render_paren = parenthesized!(render_args in input); let render_args: Punctuated<FnArg, Token![,]> = Punctuated::parse_terminated(&render_args)?;
let render_args = render_args.into_iter().collect();
let render_type = input.parse()?;
let mut rhizome_transform = false;
let mut rhizome_extractions = Vec::new();
let mut cx = ParseContext {
component_name: Some(component_name.clone()),
..Default::default()
};
let mut render_procedure = Vec::default();
#[cfg(feature = "styles")]
while let Some(use_token) = input.parse::<Token![use]>().ok() {
let css_path: LitStr = input.parse()?;
if !css_path.value().ends_with(".css") {
let message = format!(
"Expected path to .css file, found \"{}\".",
css_path.value()
);
return Err(Error::new_spanned(css_path, message));
}
let as_token: Token![as] = input.parse()?;
let scope_ident: Ident = input.parse()?;
rhizome_transform = true;
let use_statement_span = quote!(#use_token #css_path #as_token #scope_ident).span();
let scope_name_lit = {
use rand::Rng;
LitStr::new(
&format!(
"data--asteracea--scope-{}-{}",
format!("{}", component_name).to_kebab_case(),
rand::thread_rng().gen::<u32>() ),
use_statement_span,
)
};
let asteracea = asteracea_ident(use_statement_span);
rhizome_extractions.push(quote_spanned! {use_statement_span=>
let styles = #asteracea::styles::Styles::extract_from(&node)
.map_err(|error| #asteracea::extractable_resolution_error::ExtractableResolutionError{
component: core::any::type_name::<Self>(),
dependency: core::any::type_name::<dyn #asteracea::styles::Styles>(),
source: error,
})?;
let mut #scope_ident = #scope_ident.lock().unwrap();
if #scope_ident.is_none() {
let scope_name = #scope_name_lit;
*#scope_ident = Some(#asteracea::lignin_schema::lignin::Attribute{
name: scope_name,
value: "", });
}
let scope_name = #scope_ident.unwrap().name;
styles.add(#scope_name_lit, move |bump| {
let selector = ::std::format!("[{}]", scope_name);
let raw_css = ::core::include_str!(#css_path);
let css = #asteracea::topiary::scope_css(raw_css, &selector);
let content = #asteracea::bump_format!("{}", css);
#asteracea::fragment!(<style {content}>)
});
});
call2(
quote_spanned! {use_statement_span=>
|'NEW 'RENDER static #scope_ident: std::sync::Mutex<Option<#asteracea::lignin_schema::lignin::Attribute<'static>>> = {std::sync::Mutex::default()}|;
},
|input| match CaptureDefinition::<ComponentRenderConfiguration>::parse_with_context(
input, &mut cx,
)
.expect("Error parsing internal scope static capture.")
{
None => (),
Some(_) => unreachable!(),
},
);
render_procedure.push(quote_spanned! {use_statement_span=>
let #scope_ident = #scope_ident.lock().unwrap();
let #scope_ident = #scope_ident.as_ref().unwrap();
});
}
#[cfg_attr(not(feature = "rhizome"), allow(clippy::never_loop))]
while let Some(ref_token) = input.parse::<Token![ref]>().ok() {
rhizome_transform = true;
#[cfg(not(feature = "rhizome"))]
{
return Err(Error::new_spanned(
ref_token,
"Dependency extraction is only available with the asteracea feature \"rhizome\"."
));
}
#[cfg(feature = "rhizome")]
{
let rhizome_lookahead = input.lookahead1();
if rhizome_lookahead.peek(Token![;]) {
input.parse::<Token![;]>()?;
continue;
} else if rhizome_lookahead.peek(Token![for]) {
let extracted_for = input.parse::<Token![for]>()?;
let scope: Lifetime = input.parse()?;
if scope.ident != "NEW" {
return Err(Error::new_spanned(scope, "Expected 'NEW."));
}
let extracted_let = quote_spanned!(ref_token.span=> let);
let extracted_name: Ident = input.parse()?;
let extracted_colon: Token![:] = input.parse()?;
let extracted_type: Type = input.parse()?;
let extracted_question: Token![?] = input.parse()?;
let extracted_semi = quote_spanned!(extracted_question.span=> ;);
let ref_statement_span = quote!(#ref_token #extracted_for #scope #extracted_name #extracted_colon #extracted_type #extracted_question).span();
rhizome_extractions.push({
let asteracea = asteracea_ident(ref_statement_span);
quote_spanned! {
ref_statement_span=>
#extracted_let #extracted_name#extracted_colon std::sync::Arc<#extracted_type>
= <#extracted_type>::extract_from(&node)
.map_err(|error| #asteracea::extractable_resolution_error::ExtractableResolutionError{
component: core::any::type_name::<Self>(),
dependency: core::any::type_name::<#extracted_type>(),
source: error,
})#extracted_question#extracted_semi
}
})
} else {
return Err(rhizome_lookahead.error());
}
}
}
let mut static_shared_new_procedure = Vec::default();
let mut static_shared_render_procedure = Vec::default();
let mut new_procedure = Vec::default();
while input.peek(Token![do]) {
input.parse::<Token![do]>()?;
input.parse::<Token![for]>()?;
let lifetime: TypeLevelFieldTarget = input.parse()?;
let static_token = if input.peek(Token![static]) {
Some(input.parse::<Token![static]>()?)
} else {
None
};
let procedure = if lifetime.is_new() {
if static_token.is_some() {
&mut static_shared_new_procedure
} else {
&mut new_procedure
}
} else if lifetime.is_render() {
if static_token.is_some() {
&mut static_shared_render_procedure
} else {
&mut render_procedure
}
} else {
todo!("Refactor into TypeLevelFieldTarget so that this todo! isn't necessary anymore.");
};
{
let contents;
let brace = braced!(contents in input);
let contents: TokenStream = contents.parse()?;
procedure.push(quote_spanned! {brace.span=>
{}
#contents
#[allow(unused_doc_comments)]
{}
});
}
}
let body = loop {
match Part::parse_with_context(input, &mut cx)? {
None => (),
Some(body) => break body,
}
};
if !input.is_empty() {
return Err(input.error(
"Currently, only one root element is supported.
Consider wrapping your elements like so: <div child1 child2 ...>",
));
}
Ok(Self {
cx,
attributes,
visibility,
component_generics,
component_wheres,
constructor_attributes,
constructor_generics,
constructor_args,
render_attributes,
render_generics,
render_paren,
render_args,
render_type,
static_shared_new_procedure,
static_shared_render_procedure,
new_procedure,
render_procedure,
body,
rhizome_transform,
rhizome_extractions,
})
}
}
impl ComponentDeclaration {
#[allow(clippy::cognitive_complexity)]
pub fn into_tokens(self) -> Result<TokenStream> {
let Self {
cx:
ParseContext {
component_name,
static_shared,
allow_non_snake_case_on_structure_workaround,
field_definitions,
imply_bump,
imply_self_outlives_bump,
event_binding_count: _,
},
attributes,
visibility,
component_generics,
component_wheres,
constructor_attributes,
constructor_generics,
mut constructor_args,
render_attributes,
render_generics,
render_paren,
render_args,
render_type,
static_shared_new_procedure,
static_shared_render_procedure,
mut new_procedure,
render_procedure,
body,
rhizome_transform,
rhizome_extractions,
} = self;
let asteracea = asteracea_ident(Span::call_site());
let component_wheres = {
let mut stream = TokenStream::new();
for component_where in component_wheres {
stream.extend(quote! {
#component_where,
})
}
if !stream.is_empty() {
stream = quote! {
where
#stream
};
}
stream
};
let (new_statics, post_new_statics) = static_shared
.into_iter()
.partition::<Vec<_>, _>(|ss| ss.targets.iter().any(TypeLevelFieldTarget::is_new));
let (render_statics, other_statics) = post_new_statics
.into_iter()
.partition::<Vec<_>, _>(|ss| ss.targets.iter().any(TypeLevelFieldTarget::is_render));
assert!(other_statics.is_empty());
let (
new_statics,
borrow_new_statics_for_render_statics_or_in_new,
borrow_new_statics_in_render,
) = if new_statics.is_empty() {
(None, None, None)
} else {
let mut any_render = false;
let (
attributes,
_visibilities, names,
types,
values,
new_pattern_parts,
render_pattern_parts,
) = new_statics
.into_iter()
.map(|mut ss| {
let field_name = ss.field_definition.name;
(
ss.field_definition.attributes,
ss.field_definition.visibility,
field_name.clone(),
ss.field_definition.field_type,
ss.field_definition.initial_value,
if let Some(target) = ss.targets.yank_any(TypeLevelFieldTarget::is_new) {
let mut name = field_name.clone();
name.set_span(target.0.ident.span());
quote!(#name)
} else {
unreachable!();
},
if let Some(target) = ss.targets.iter().find(|x| x.is_render()) {
any_render = true;
let mut field_name = field_name;
field_name.set_span(target.0.ident.span());
Some(quote!(#field_name,))
} else {
None
},
)
})
.unzip_n_vec();
(
Some(quote! {
struct NewStatics {
#(#(#attributes)* #names: #types,)*
}
#asteracea::lazy_static::lazy_static! {
static ref NEW_STATICS: NewStatics = {
#(#static_shared_new_procedure)*
NewStatics {
#(#names: {#values},)*
}
};
}
}),
Some(quote! {
let NewStatics {
#(#new_pattern_parts,)*
} = &*NEW_STATICS;
}),
if !any_render {
None } else {
Some(quote! {
let NewStatics {
#(#render_pattern_parts)*
..
} = &*NEW_STATICS;
})
},
)
};
let (render_statics, borrow_render_statics_in_render) = if render_statics.is_empty() {
(None, None)
} else {
let (
attributes,
_visibilities, names,
types,
values,
render_pattern_parts,
) = render_statics
.into_iter()
.map(|mut ss| {
let field_name = ss.field_definition.name;
(
ss.field_definition.attributes,
ss.field_definition.visibility,
field_name.clone(),
ss.field_definition.field_type,
ss.field_definition.initial_value,
if let Some(target) = ss.targets.yank_any(TypeLevelFieldTarget::is_render) {
let mut field_name = field_name;
field_name.set_span(target.0.ident.span());
quote!(#field_name)
} else {
unreachable!();
},
)
})
.unzip_n_vec();
(
Some(quote! {
struct RenderStatics {
#(#(#attributes)* #names: #types,)*
}
#asteracea::lazy_static::lazy_static! {
static ref RENDER_STATICS: RenderStatics = {
#[allow(unused_variables)]
#borrow_new_statics_for_render_statics_or_in_new
#(#static_shared_render_procedure)*
RenderStatics {
#(#names: {#values},)*
}
};
}
}),
Some(quote! {
let RenderStatics {
#(#render_pattern_parts,)*
} = &*RENDER_STATICS;
}),
)
};
let allow_non_snake_case_on_structure_workaround =
if allow_non_snake_case_on_structure_workaround {
quote! (#[allow(non_snake_case)])
} else {
quote!()
};
let (field_attributes, field_visibilities, field_names, field_types, field_values) =
field_definitions
.into_iter()
.map(|c| {
(
c.attributes,
c.visibility,
c.name,
c.field_type,
c.initial_value,
)
})
.unzip_n_vec();
if rhizome_transform {
constructor_args.insert(
0,
parse_quote!(parent_node: &std::sync::Arc<#asteracea::rhizome::Node>),
);
}
let constructor_type = if rhizome_transform {
quote!(Result<Self, #asteracea::extractable_resolution_error::ExtractableResolutionError>)
} else {
quote!(Self)
};
if rhizome_transform {
new_procedure.insert(
0,
quote! {
let node = #asteracea::rhizome::extensions::TypeTaggedNodeArc::derive_for::<Self>(parent_node);
#(#rhizome_extractions)*
let mut node = node;
},
);
new_procedure.push(quote! {
let node = node.into_arc();
})
}
let field_initializers = quote! {
#(#field_names: (#field_values),)* };
let constructor_result = if rhizome_transform {
quote! {
Ok(Self {
#field_initializers
})
}
} else {
quote! {
Self {
#field_initializers
}
}
};
let render_generics = if imply_bump || render_generics.is_some() {
let (render_generics_lt, render_generics_params, render_generics_gt) =
match render_generics {
Some(Generics {
lt_token,
params,
gt_token,
where_clause,
}) => {
let lt_token = lt_token.unwrap();
let gt_token = gt_token.unwrap();
assert!(where_clause.is_none());
(lt_token, Some(params), gt_token)
}
None => (
parse2(quote_spanned!(render_paren.span=> <)).unwrap(),
None,
parse2(quote_spanned!(render_paren.span=> >)).unwrap(),
),
};
let mut implied = if imply_bump {
quote_spanned!(render_generics_lt.span=> 'bump,)
} else {
quote!()
};
if imply_self_outlives_bump {
assert!(imply_bump);
implied = quote_spanned! (render_generics_lt.span=> 'a: 'bump, #implied);
}
quote!(#render_generics_lt #implied #render_generics_params #render_generics_gt)
} else {
quote!()
};
let render_args = {
let implied_bump = if imply_bump {
quote_spanned! {render_paren.span=>
bump: &'bump #asteracea::lignin_schema::lignin::bumpalo::Bump,
}
} else {
quote!()
};
let implied_self_lifetime = if imply_self_outlives_bump {
quote_spanned! {render_paren.span=>
'a
}
} else {
quote!()
};
quote_spanned! {render_paren.span=>
(&#implied_self_lifetime self, #implied_bump #(#render_args),*)
}
};
let render_type = match render_type {
ReturnType::Default if imply_bump => {
let bump = Lifetime::new("'bump", render_paren.span);
parse_quote!(-> #asteracea::lignin_schema::lignin::Node<#bump>)
}
render_type => render_type,
};
let body = body.part_tokens(&GenerateContext::default())?;
Ok(quote! {
#new_statics
#render_statics
#allow_non_snake_case_on_structure_workaround
#(#attributes)*
#visibility struct #component_name#component_generics
#component_wheres
{
#(
#(#field_attributes)*
#field_visibilities #field_names: #field_types,
)*
}
impl#component_generics #component_name#component_generics
#component_wheres
{
#(#constructor_attributes)*
pub fn new#constructor_generics(#(#constructor_args),*) -> #constructor_type {
#borrow_new_statics_for_render_statics_or_in_new
#(#new_procedure)*
#constructor_result
}
#(#render_attributes)*
pub fn render#render_generics#render_args #render_type {
#borrow_new_statics_in_render
#borrow_render_statics_in_render
#(#render_procedure)*
(#body)
}
}
})
}
}