asteracea_proc-macro-definitions 0.0.2

Proc macro definitions for Asteracea. They are unlikely to work correctly unless used through the asteracea crate.
Documentation
use super::{GenerateContext, Part, PartKind};
use crate::{
	asteracea_ident,
	parse_with_context::{ParseContext, ParseWithContext},
	Configuration,
};
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{
	braced,
	parse::{ParseStream, Result},
	spanned::Spanned,
	token::{Brace, Dot},
	Ident, LitStr, Token,
};

mod kw {
	use syn::custom_keyword;
	custom_keyword!(with);
	custom_keyword!(scope);
	custom_keyword!(attribute);
}

enum AttributeDefinition {
	LiteralWithBlock(Dot, LitStr, TokenStream),
	LiteralWithLiteral(Dot, LitStr, LitStr),
	Expression(TokenStream),
}

pub struct HtmlDefinition<C> {
	lt: Token![<],
	name: Ident,
	scope_definitions: Vec<TokenStream>,
	attributes: Vec<AttributeDefinition>,
	pub parts: Vec<Part<C>>,
}

impl<C: Configuration> ParseWithContext for HtmlDefinition<C> {
	type Output = Self;
	fn parse_with_context(input: ParseStream<'_>, cx: &mut ParseContext) -> Result<Self> {
		let asteracea = asteracea_ident(Span::call_site());

		let lt = input.parse::<Token![<]>()?;
		let name = input.parse()?;

		cx.imply_bump = true;

		let mut scope_definitions = Vec::new();
		while input.peek(kw::with) {
			input.parse::<kw::with>()?;
			input.parse::<kw::scope>()?;
			input.parse::<kw::attribute>()?;

			let scope_lookahead = input.lookahead1();
			if scope_lookahead.peek(LitStr) {
				let name: LitStr = input.parse()?;
				scope_definitions.push(quote!(
					#asteracea::lignin_schema::lignin::Attribute{
						name: #name,
						value: "",
					}
				))
			} else if scope_lookahead.peek(Brace) {
				let block_contents;
				let brace = braced!(block_contents in input);
				let block_contents: TokenStream = block_contents.parse()?;
				scope_definitions.push(quote_spanned!(brace.span=> {#block_contents}))
			} else {
				return Err(scope_lookahead.error());
			}
		}

		let attributes = {
			let mut attributes = Vec::new();
			while let Ok(dot) = input.parse::<Token![.]>() {
				let attribute_lookahead = input.lookahead1();
				use AttributeDefinition::*;
				attributes.push(if attribute_lookahead.peek(LitStr) {
					let name = input.parse()?;
					input.parse::<Token![=]>()?;
					if let Ok(text) = input.parse() {
						LiteralWithLiteral(dot, name, text)
					} else {
						LiteralWithBlock(dot, name, {
							let content;
							let brace = braced!(content in input);
							let content: TokenStream = content.parse()?;
							quote_spanned! (brace.span=> {#content})
						})
					}
				} else if attribute_lookahead.peek(Brace) {
					let content;
					let brace = braced!(content in input);
					let content: TokenStream = content.parse()?;
					Expression(quote_spanned!(brace.span=> #content))
				} else {
					return Err(attribute_lookahead.error());
				});
			}
			attributes
		};

		let mut parts = Vec::new();
		while !input.peek(Token![>]) {
			if let Some(part) = Part::parse_with_context(input, cx)? {
				parts.push(part);
			}
		}

		input.parse::<Token![>]>()?;

		Ok(Self {
			lt,
			name,
			scope_definitions,
			attributes,
			parts,
		})
	}
}

impl<C> HtmlDefinition<C> {
	pub fn part_tokens(&self, cx: &GenerateContext) -> Result<TokenStream> {
		let Self {
			lt,
			name,
			scope_definitions,
			attributes,
			parts,
		} = self;

		let asteracea = asteracea_ident(name.span());

		let bump = Ident::new("bump", name.span());

		let cx = GenerateContext {
			scope_definitions: if !scope_definitions.is_empty() {
				cx.scope_definitions
					.iter()
					.copied()
					.chain(scope_definitions.iter())
					.collect()
			} else {
				cx.scope_definitions.clone()
			},
		};

		let mut attributes_stream = TokenStream::new();
		for scope_body in &cx.scope_definitions {
			attributes_stream.extend(quote! {
				#scope_body,
			});
		}

		for attribute in attributes.iter() {
			use AttributeDefinition::*;
			match attribute {
				LiteralWithBlock(dot, attr_name, attr_value) => {
					attributes_stream.extend(quote_spanned! {dot.span=>
						#asteracea::lignin_schema::lignin::Attribute {
							name: #attr_name,
							value: #attr_value,
						},
					})
				}
				LiteralWithLiteral(dot, attr_name, attr_text) => {
					attributes_stream.extend(quote_spanned! {dot.span=>
						#asteracea::lignin_schema::lignin::Attribute {
							name: #attr_name,
							value: #attr_text,
						},
					})
				}
				Expression(expression) => attributes_stream.extend(quote! {
					#expression,
				}),
			}
		}

		let (children, parts): (Vec<_>, Vec<_>) = parts
			.iter()
			.partition(|part| (*part).kind() == PartKind::Child);
		let mut child_stream = TokenStream::new();
		for child in children.into_iter() {
			let child = child.part_tokens(&cx)?;
			child_stream.extend(quote_spanned! {child.span()=>
				#child,
			});
		}
		let children = quote_spanned! {child_stream.span()=>
			&*#bump.alloc_with(|| [#child_stream])
		};

		let (event_bindings, parts): (Vec<&Part<C>>, Vec<_>) = parts
			.iter()
			.partition(|part| part.kind() == PartKind::EventBinding);
		let mut event_stream = TokenStream::new();
		for event_binding in event_bindings.into_iter() {
			let event_binding = event_binding.part_tokens(&cx)?;
			event_stream.extend(quote_spanned! {event_binding.span()=>
				#event_binding,
			})
		}
		let event_bindings = quote_spanned! {event_stream.span()=>
			&*#bump.alloc_with(|| [#event_stream])
		};

		assert_eq!(parts.len(), 0);
		Ok(quote_spanned! {lt.span=>
			#asteracea::lignin_schema::lignin::Node::Element(
				#bump.alloc_with(||
					#asteracea::lignin_schema::#name(
						&*#bump.alloc_with(|| [#attributes_stream]),
						#children,
						#event_bindings,
					)
				)
			)
		})
	}
}