Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 198 additions & 80 deletions crates/hypertext-macros/src/html/component.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::{
Ident, Lit, Token,
Ident, LitStr,
parse::{Parse, ParseStream},
spanned::Spanned,
token::{Brace, Paren},
};

use super::{AttributeValue, ElementBody, Generate, Generator, ParenExpr, Syntax};
use crate::html::Node;
use super::{
Attribute, AttributeKind, AttributeValue, Class, ElementBody, Generate, Generator, Syntax,
generate::AnyBlock,
};
use crate::html::{Node, basics::Literal};

pub struct Component<S: Syntax> {
pub name: Ident,
Expand All @@ -19,27 +23,16 @@ impl<S: Syntax> Generate for Component<S> {
type Context = Node<S>;

fn generate(&self, g: &mut Generator) {
let props = self.attrs.iter().map(|attr| {
let name = &attr.name;
attr.value_expr()
.map_or_else(|| quote!(.#name(#name)), |value| quote!(.#name(#value)))
});
let mut owned: Vec<TokenStream> = vec![];
let props: Vec<TokenStream> = self
.attrs
.iter()
.map(|attr| attr.kind_expr(g, &mut owned))
.collect();

let children = match &self.body {
ElementBody::Normal { children, .. } => {
let buffer_ident = Generator::buffer_ident();

let block = g.block_with(Brace::default(), |g| {
g.push(children);
});

let lazy = quote! {
::hypertext::Lazy::dangerously_create(
|#buffer_ident: &mut ::hypertext::Buffer|
#block
)
};

let lazy = lazy_block(&children.block(g, Brace::default()));
let children_ident = Ident::new("children", self.name.span());

quote!(
Expand All @@ -51,80 +44,205 @@ impl<S: Syntax> Generate for Component<S> {

let name = &self.name;

let init = quote! {
#name::builder()
#(#props)*
#children
.build()
};

g.push_expr::<Self::Context>(Paren::default(), &init);
g.push_in_block(Brace::default(), |g| {
g.push_stmt(quote! { #(#owned)* });
g.push_expr::<Self::Context>(
Paren::default(),
quote! {
#name::builder()
#(#props)*
#children
.build()
},
);
});
}
}

pub struct ComponentAttribute {
name: Ident,
value: Option<ComponentAttributeValue>,
fn lazy_block(block: &AnyBlock) -> TokenStream {
let buffer_ident = Generator::buffer_ident();
quote! {
::hypertext::Lazy::dangerously_create(
|#buffer_ident: &mut ::hypertext::Buffer|
#block
)
}
}

pub struct ComponentAttribute(pub Attribute);

impl ComponentAttribute {
fn value_expr(&self) -> Option<TokenStream> {
self.value.as_ref().map(|value| match value {
ComponentAttributeValue::Literal(lit) => lit.to_token_stream(),
ComponentAttributeValue::Ident(ident) => ident.to_token_stream(),
ComponentAttributeValue::Expr(expr) => {
let mut tokens = TokenStream::new();

expr.paren_token.surround(&mut tokens, |tokens| {
expr.expr.to_tokens(tokens);
});
fn kind_expr(&self, g: &mut Generator, owned: &mut Vec<TokenStream>) -> TokenStream {
let lits = self.0.name.lits();
let name_str = lits.iter().map(syn::LitStr::value).collect::<String>();
let span = lits.first().map_or_else(Span::call_site, Spanned::span);
let name = Ident::new(&name_str, span);
let maybe_name = format_ident!("maybe_{name}", span = span);
match &self.0.kind {
AttributeKind::Value { value, toggle } => {
let value_expr = Self::value_expr(value, g, owned);
toggle.as_ref().map_or_else(
|| quote!(.#name(#value_expr)),
|toggle| {
let toggle_expr = &toggle.expr;
quote!(.#maybe_name(if #toggle_expr { Some(#value_expr) } else { None }))
},
)
}
AttributeKind::Option(toggle) => {
let toggle_expr = &toggle.expr;
quote!(.#maybe_name(#toggle_expr))
}
AttributeKind::Empty(None) => {
quote!(.#name(true))
}
AttributeKind::Empty(Some(toggle)) => {
let toggle_expr = &toggle.expr;
quote!(.#maybe_name(if #toggle_expr { Some(true) } else { None }))
}
AttributeKind::ClassList(classes) => {
let to_litstr = |class: &Class| {
let Class::Value {
value,
toggle: None,
} = class
else {
return None;
};

let AttributeValue::Literal(literal) = value else {
return None;
};

Some(literal.lit_str())
};

quote! {
{
#[allow(unused_parens)]
#tokens
let to_class = |static_str: &str| {
let literal = Literal::Str(LitStr::new(static_str, span));
Class::Value {
value: AttributeValue::Literal(literal),
toggle: None,
}
};

let mut current_static: String = String::new();
let mut class_exprs: Vec<TokenStream> = vec![];

// Collect consecutive static classes into a single string
for class in classes {
if let Some(stat) = to_litstr(class) {
if !current_static.is_empty() {
current_static.push(' ');
}
current_static.push_str(&stat.value());
} else {
let static_class = to_class(&current_static);
class_exprs.push(Self::class_expr(&static_class, g, owned));
class_exprs.push(Self::class_expr(class, g, owned));
current_static.clear();
}
}

// If no expressions were pushed, the whole class expression is just a literal:
if class_exprs.is_empty() {
let literal_class = Literal::Str(LitStr::new(&current_static, span));
quote!(.class(#literal_class))
} else {
if !current_static.is_empty() {
let static_class = to_class(&current_static);
class_exprs.push(Self::class_expr(&static_class, g, owned));
}

let ident = Self::push_owned(
owned,
&quote! {
[#(#class_exprs),*]
.into_iter()
.flatten()
.fold(String::new(), |mut classes, class| {
if !classes.is_empty() { classes.push(' '); }
classes.push_str(class);
classes
});
},
span,
);

quote!(.class(&#ident))
}
}
})
}
}
}

impl Parse for ComponentAttribute {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
name: input.parse()?,
value: {
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
fn value_expr(
value: &AttributeValue,
g: &mut Generator,
owned: &mut Vec<TokenStream>,
) -> TokenStream {
match value {
AttributeValue::Literal(literal) => literal.into_token_stream(),
AttributeValue::Group(group) => {
let block = g.block_with(Brace::default(), |g| g.push_all(&group.0.0));
let lazy = lazy_block(&block);
let ident =
Self::push_owned(owned, &quote! { #lazy.render().into_inner() }, block.span());
quote!(&(#ident))
}
AttributeValue::Control(control) => {
let block = g.block_with(Brace::default(), |g| {
g.push(control);
});
quote! { #block }
}
AttributeValue::Expr(paren_expr) => {
let expr_tokens = paren_expr.to_token_stream();
quote!(#[allow(unused_parens)] #expr_tokens)
}
AttributeValue::DisplayExpr(expr) => {
let expr_tokens = expr.wrapped_expr();
quote! { #expr_tokens }
}
AttributeValue::DebugExpr(expr) => {
let expr_tokens = expr.wrapped_expr();
quote! { #expr_tokens }
}
AttributeValue::Ident(ident) => quote! { #ident },
}
}

Some(input.parse()?)
} else {
None
}
},
})
fn class_expr(class: &Class, g: &mut Generator, owned: &mut Vec<TokenStream>) -> TokenStream {
match class {
Class::Value {
value,
toggle: None,
} => {
let value_expr = Self::value_expr(value, g, owned);
quote! { Some(#value_expr) }
}
Class::Value {
value,
toggle: Some(toggle),
} => {
let value_expr = Self::value_expr(value, g, owned);
let toggle_expr = &toggle.expr;
quote! { if #toggle_expr { Some(#value_expr) } else { None } }
}
Class::Option(toggle) => {
let toggle_expr = &toggle.expr;
quote! { #toggle_expr }
}
}
}
}

pub enum ComponentAttributeValue {
Literal(Lit),
Ident(Ident),
Expr(ParenExpr<AttributeValue>),
fn push_owned(alloc: &mut Vec<TokenStream>, expr: &TokenStream, span: Span) -> Ident {
let ident = format_ident!("owned_{}", alloc.len(), span = span);
alloc.push(quote!(let #ident = #expr;));
ident
}
}

impl Parse for ComponentAttributeValue {
impl Parse for ComponentAttribute {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();

if lookahead.peek(Lit) {
input.parse().map(Self::Literal)
} else if lookahead.peek(Ident) {
input.parse().map(Self::Ident)
} else if lookahead.peek(Paren) {
input.parse().map(Self::Expr)
} else {
Err(lookahead.error())
}
Ok(Self(input.parse()?))
}
}
23 changes: 10 additions & 13 deletions crates/hypertext-macros/src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,12 @@ pub struct Attribute {
impl Attribute {
fn parse_id(input: ParseStream) -> syn::Result<Self> {
let pound_token = input.parse::<Token![#]>()?;
let name = parse_quote_spanned!(pound_token.span()=> id);
let value = input.call(AttributeValue::parse_unquoted)?;
let toggle = input.call(Toggle::parse_optional)?;
Ok(Self {
name: parse_quote_spanned!(pound_token.span()=> id),
kind: AttributeKind::Value {
value: input.call(AttributeValue::parse_unquoted)?,
toggle: None,
},
name,
kind: AttributeKind::Value { value, toggle },
})
}

Expand Down Expand Up @@ -781,14 +781,11 @@ pub enum AttributeValue {
impl AttributeValue {
fn parse_unquoted(input: ParseStream) -> syn::Result<Self> {
if input.peek(Ident::peek_any) || input.peek(LitInt) {
Ok(Self::Group(Group(Many(
input
.call(UnquotedName::parse_attr_value)?
.lits()
.into_iter()
.map(|lit| Self::Literal(Literal::Str(lit)))
.collect(),
))))
let lits = UnquotedName::parse_attr_value(input)?.lits();
let span = lits.first().map_or_else(Span::call_site, LitStr::span);
let name = lits.into_iter().map(|lit| lit.value()).collect::<String>();

Ok(Self::Literal(Literal::Str(LitStr::new(&name, span))))
} else {
input.parse()
}
Expand Down
14 changes: 13 additions & 1 deletion crates/hypertext-macros/src/html/syntaxes/maud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use syn::{

use crate::html::{
Attribute, Component, Doctype, Element, ElementBody, Group, Node, Syntax, UnquotedName,
XmlDecl, kw,
XmlDecl, component::ComponentAttribute, kw,
};

pub struct Maud;
Expand Down Expand Up @@ -149,6 +149,18 @@ impl Parse for Component<Maud> {
attrs: {
let mut attrs = Vec::new();

if input.peek(Token![#]) {
attrs.push(input.call(Attribute::parse_id).map(ComponentAttribute)?);
}

if input.peek(Token![.]) {
attrs.push(
input
.call(Attribute::parse_class_list)
.map(ComponentAttribute)?,
);
}

while !(input.peek(Token![..]) || input.peek(Token![;]) || input.peek(Brace)) {
attrs.push(input.parse()?);
}
Expand Down
Loading