use std::{collections::BTreeMap, iter};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{ToTokens, format_ident, quote, quote_spanned};
use syn::{
Error, LitStr, braced,
parse::Parse,
token::{Brace, Paren},
};
use super::{AttributeValueNode, DataModifierPart, DataModifiers, SyntaxStatic, UnquotedName};
fn escape_script_source_literal(value: &str) -> std::borrow::Cow<'_, str> {
let bytes = value.as_bytes();
let script_end = b"</script";
let mut i = 0;
let mut start = 0;
let mut escaped = None::<String>;
while i + script_end.len() <= bytes.len() {
if bytes[i] == b'<'
&& bytes[i + 1] == b'/'
&& bytes[i + 2..i + script_end.len()].eq_ignore_ascii_case(b"script")
{
let escaped = escaped.get_or_insert_with(|| String::with_capacity(value.len() + 1));
escaped.push_str(&value[start..i]);
escaped.push_str("<\\/");
escaped.push_str(&value[i + 2..i + script_end.len()]);
i += script_end.len();
start = i;
} else {
i += 1;
}
}
if let Some(mut escaped) = escaped {
escaped.push_str(&value[start..]);
std::borrow::Cow::Owned(escaped)
} else {
std::borrow::Cow::Borrowed(value)
}
}
fn pinned_stream_tokens_expr(stream: &TokenStream) -> TokenStream {
quote! {
::std::boxed::Box::pin(#stream) as ::std::pin::Pin<::std::boxed::Box<dyn ::cheers::__internal::futures::stream::Stream<Item = ::cheers::Rendered<::std::string::String>> + ::std::marker::Send>>
}
}
pub fn lazy<T: Parse + Generate + SyntaxStatic>(tokens: TokenStream) -> Result<TokenStream, Error> {
lazy_with_flavour::<T>(tokens, NodeFlavour::Html)
}
pub fn lazy_with_flavour<T: Parse + Generate + SyntaxStatic>(
tokens: TokenStream,
flavour: NodeFlavour,
) -> Result<TokenStream, Error> {
let mut borrow_state = BorrowState::new();
let mut g = Generator::new_closure(T::CONTEXT, flavour, &mut borrow_state);
let mut input = syn::parse2::<T>(tokens)?;
let syntax_static = input.is_static();
g.push(&mut input);
let block = g.finish();
let borrow_captures = borrow_state.captures;
let buffer_ident = Generator::buffer_ident();
let marker_ident = T::CONTEXT.marker_type();
let lazy = if !syntax_static {
quote! {
::cheers::prelude::Lazy::<_, #marker_ident>::dangerously_create(
move |#buffer_ident: &mut ::cheers::prelude::Buffer<#marker_ident>| {
::cheers::__internal::subsecond::call(|| {
#block
})
}
)
}
} else {
quote! {
{
let __cheers_subsecond_hot_render: fn(&mut ::cheers::prelude::Buffer<#marker_ident>) = |#buffer_ident| {
#block
};
::cheers::prelude::Lazy::<_, #marker_ident>::dangerously_create(
move |#buffer_ident: &mut ::cheers::prelude::Buffer<#marker_ident>| {
::cheers::__internal::subsecond::hot_call(
__cheers_subsecond_hot_render,
(#buffer_ident,),
);
}
)
}
}
};
let rendered = if block.async_stmts.is_empty() {
quote! {
{
use ::cheers::validation::attributes::*;
#(#borrow_captures)*
#lazy
}
}
} else {
let streams = &block.async_stmts;
let streams = streams.iter().map(pinned_stream_tokens_expr);
quote! {
{
use ::cheers::validation::attributes::*;
#(#borrow_captures)*
let lazy = #lazy;
let stream = ::cheers::__internal::futures::stream::select_all([
#(#streams),*
]);
::cheers::prelude::AsyncLazy::__select_all(lazy, stream)
}
}
};
Ok(rendered)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeFlavour {
Html,
Xml(XmlFlavour),
}
impl NodeFlavour {
pub const fn void_close(self) -> &'static str {
match self {
Self::Html => ">",
Self::Xml(_) => "/>",
}
}
pub const fn elements_module(self) -> ValidationModule {
match self {
Self::Html => ValidationModule::Html,
Self::Xml(XmlFlavour::Svg) => ValidationModule::Svg,
Self::Xml(XmlFlavour::MathMl) => ValidationModule::MathMl,
}
}
pub const fn element_kind(self, is_void: bool) -> ElementKind {
match self {
Self::Html => {
if is_void {
ElementKind::Void
} else {
ElementKind::Normal
}
}
Self::Xml(_) => ElementKind::Xml,
}
}
pub fn child_flavour(self, element_name: &UnquotedName) -> Self {
match self {
Self::Html => match element_name {
name if name == &"svg" => Self::Xml(XmlFlavour::Svg),
name if name == &"math" => Self::Xml(XmlFlavour::MathMl),
_ => self,
},
Self::Xml(XmlFlavour::Svg) => match element_name {
name if name == &"foreignObject" => Self::Html,
_ => self,
},
Self::Xml(XmlFlavour::MathMl) => self,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XmlFlavour {
Svg,
MathMl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ValidationModule {
Html,
Svg,
MathMl,
}
impl ToTokens for ValidationModule {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Html => quote!(::cheers::validation::elements),
Self::Svg => quote!(::cheers::validation::svg::elements),
Self::MathMl => quote!(::cheers::validation::mathml::elements),
}
.to_tokens(tokens);
}
}
struct BorrowState {
captures: Vec<TokenStream>,
counter: usize,
}
impl BorrowState {
fn new() -> Self {
Self {
captures: Vec::new(),
counter: 0,
}
}
fn hoist_ref_expr(&mut self, paren_token: Paren, expr: impl ToTokens) -> Ident {
let ref_idx = self.counter;
self.counter += 1;
let ref_ident = format_ident!("__cheers_ref_{ref_idx}", span = Span::mixed_site());
let mut ref_expr = TokenStream::new();
paren_token.surround(&mut ref_expr, |tokens| expr.to_tokens(tokens));
let reference = quote_spanned!(paren_token.span=> &);
self.captures.push(quote! {
let #ref_ident = #reference #ref_expr;
});
ref_ident
}
}
pub struct Generator<'a> {
context: Context,
flavour: NodeFlavour,
brace_token: Brace,
parts: Vec<Part>,
checks: Checks,
async_stmts: Vec<TokenStream>,
collect_async_stmts_into_buffer: bool,
context_override: Option<Context>,
borrow_state: &'a mut BorrowState,
}
impl<'a> Generator<'a> {
pub fn buffer_ident() -> Ident {
Ident::new("__hypertext_buffer", Span::mixed_site())
}
fn new_closure(
context: Context,
flavour: NodeFlavour,
borrow_state: &'a mut BorrowState,
) -> Self {
Self::new_root_with_brace(context, Brace::default(), flavour, borrow_state)
}
fn new_root_with_brace(
context: Context,
brace_token: Brace,
flavour: NodeFlavour,
borrow_state: &'a mut BorrowState,
) -> Self {
Self {
context,
flavour,
brace_token,
parts: Vec::new(),
checks: Checks::new(),
async_stmts: Vec::new(),
collect_async_stmts_into_buffer: false,
context_override: None,
borrow_state,
}
}
fn new_child_with_brace<'b>(
&'b mut self,
brace_token: Brace,
flavour: NodeFlavour,
) -> Generator<'b> {
Generator {
context: self.context,
flavour,
brace_token,
parts: Vec::new(),
checks: Checks::new(),
async_stmts: Vec::new(),
collect_async_stmts_into_buffer: self.collect_async_stmts_into_buffer,
context_override: self.context_override,
borrow_state: &mut *self.borrow_state,
}
}
fn finish(self) -> AnyBlock {
let buffer_ident = Self::buffer_ident();
let mut stmts = TokenStream::new();
let mut parts = self.parts.into_iter();
let mut size_estimate = 0;
while let Some(part) = parts.next() {
match part {
Part::Static(lit) => {
let mut dynamic_stmt = None;
let static_parts = iter::once(lit)
.chain(parts.by_ref().map_while(|part| match part {
Part::Static(lit) => Some(lit),
Part::Dynamic(stmt) => {
dynamic_stmt = Some(stmt);
None
}
}))
.inspect(|static_part| {
size_estimate += static_part.value().len();
});
stmts.extend(quote! {
#buffer_ident.dangerously_get_string().push_str(::core::concat!(#(#static_parts),*));
});
stmts.extend(dynamic_stmt);
}
Part::Dynamic(stmt) => {
stmts.extend(stmt);
}
}
}
let render = quote! {
#buffer_ident.dangerously_get_string().reserve(#size_estimate);
#stmts
};
let checks = self.checks;
AnyBlock {
brace_token: self.brace_token,
stmts: quote! {
#checks
#render
},
async_stmts: self.async_stmts,
}
}
pub fn block_with(
&mut self,
brace_token: Brace,
f: impl for<'b> FnOnce(&mut Generator<'b>),
append_async: bool,
) -> AnyBlock {
self.block_with_flavour(brace_token, self.flavour, f, append_async)
}
pub fn block_with_flavour(
&mut self,
brace_token: Brace,
flavour: NodeFlavour,
f: impl for<'b> FnOnce(&mut Generator<'b>),
append_async: bool,
) -> AnyBlock {
let (mut child_checks, mut block) = {
let mut g = self.new_child_with_brace(brace_token, flavour);
f(&mut g);
let child_checks = std::mem::replace(&mut g.checks, Checks::new());
let block = g.finish();
(child_checks, block)
};
self.checks.append(&mut child_checks);
if append_async {
self.async_stmts.append(&mut block.async_stmts);
}
block
}
pub fn push_with_flavour(
&mut self,
flavour: NodeFlavour,
f: impl for<'b> FnOnce(&mut Generator<'b>),
) {
let block = self.block_with_flavour(Brace::default(), flavour, f, true);
self.push_stmt(block);
}
pub fn push_in_block(
&mut self,
brace_token: Brace,
f: impl for<'b> FnOnce(&mut Generator<'b>),
) {
let block = self.block_with(brace_token, f, true);
self.push_stmt(block);
}
pub fn push_str(&mut self, s: &'static str) {
self.push_spanned_str(s, Span::mixed_site());
}
pub fn push_spanned_str(&mut self, s: &'static str, span: Span) {
self.parts.push(Part::Static(LitStr::new(s, span)));
}
pub fn push_escaped_literal(&mut self, context: Context, lit: &LitStr) {
let value = lit.value();
let effective_context = self.context_override.unwrap_or(context);
let escaped_value = match effective_context {
Context::Element => html_escape::encode_text(&value),
Context::AttributeValue | Context::DatastarSource => {
html_escape::encode_double_quoted_attribute(&value)
}
Context::ScriptSource => escape_script_source_literal(&value),
};
self.parts
.push(Part::Static(LitStr::new(&escaped_value, lit.span())));
}
pub fn push_literals(&mut self, literals: Vec<LitStr>) {
for lit in literals {
self.parts.push(Part::Static(lit));
}
}
pub fn push_literal(&mut self, lit: LitStr) {
self.parts.push(Part::Static(lit));
}
#[cfg(feature = "pi-extension")]
pub fn push_element_source_hint(&mut self, source: LitStr) {
let buffer_ident = Self::buffer_ident();
self.push_stmt(quote! {
#[cfg(debug_assertions)]
{
::cheers::__internal::pi_extension::__push_element_source_hint(
#buffer_ident,
#source,
);
}
});
}
pub fn with_context_override<R>(
&mut self,
context: Context,
f: impl FnOnce(&mut Self) -> R,
) -> R {
let prev = self.context_override.replace(context);
let result = f(self);
self.context_override = prev;
result
}
pub fn push_expr(&mut self, paren_token: Paren, context: Context, expr: impl ToTokens) {
let effective_context = self.context_override.unwrap_or(context);
let buffer_ident = Self::buffer_ident();
let buffer_expr = match (self.context, effective_context) {
(Context::Element, Context::Element)
| (Context::AttributeValue, Context::AttributeValue)
| (Context::DatastarSource, Context::DatastarSource)
| (Context::ScriptSource, Context::ScriptSource) => {
quote!(#buffer_ident)
}
(Context::Element, Context::AttributeValue) => {
quote!(#buffer_ident.as_attribute_buffer())
}
(Context::Element, Context::DatastarSource) => {
quote!(#buffer_ident.as_datastar_buffer())
}
(Context::Element, Context::ScriptSource) => {
quote!(#buffer_ident.as_script_buffer())
}
(Context::AttributeValue, Context::DatastarSource) => {
quote!(#buffer_ident.as_datastar_buffer())
}
(Context::AttributeValue, Context::ScriptSource) => unreachable!(),
(Context::DatastarSource, Context::Element) => unreachable!(),
(Context::DatastarSource, Context::AttributeValue) => {
quote!(#buffer_ident.as_attribute_buffer())
}
(Context::DatastarSource, Context::ScriptSource) => unreachable!(),
(Context::AttributeValue, Context::Element) => unreachable!(),
(Context::ScriptSource, Context::Element)
| (Context::ScriptSource, Context::AttributeValue)
| (Context::ScriptSource, Context::DatastarSource) => unreachable!(),
};
let mut paren_expr = TokenStream::new();
paren_token.surround(&mut paren_expr, |tokens| expr.to_tokens(tokens));
let reference = quote_spanned!(paren_token.span=> &);
self.push_stmt(quote! {
::cheers::prelude::Render::render_to(
#reference #paren_expr,
#buffer_expr
);
});
}
pub fn push_js_value_node(&mut self, node: &mut AttributeValueNode) {
self.with_context_override(Context::DatastarSource, |g| g.push(node));
}
pub fn hoist_ref_expr(&mut self, paren_token: Paren, expr: impl ToTokens) -> Ident {
self.borrow_state.hoist_ref_expr(paren_token, expr)
}
pub fn push_ref_expr(&mut self, paren_token: Paren, context: Context, expr: impl ToTokens) {
let ref_ident = self.hoist_ref_expr(paren_token, expr);
self.push_expr(Paren::default(), context, ref_ident);
}
pub fn push_async_stmt(&mut self, async_stmt: impl ToTokens) {
let async_stmt = async_stmt.to_token_stream();
if self.collect_async_stmts_into_buffer {
let buffer_ident = Self::buffer_ident();
let async_stmt = pinned_stream_tokens_expr(&async_stmt);
self.push_stmt(quote! {
::cheers::__internal::async_streams::push(&mut *#buffer_ident, #async_stmt);
});
} else {
self.async_stmts.push(async_stmt);
}
}
pub fn with_async_stream_collection<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
let prev = self.collect_async_stmts_into_buffer;
self.collect_async_stmts_into_buffer = true;
let result = f(self);
self.collect_async_stmts_into_buffer = prev;
result
}
pub fn push_stmt(&mut self, stmt: impl ToTokens) {
self.parts.push(Part::Dynamic(stmt.to_token_stream()));
}
pub fn push_conditional(
&mut self,
cond: impl ToTokens,
f: impl for<'b> FnOnce(&mut Generator<'b>),
) {
let then_block = self.block_with(Brace::default(), f, true);
self.push_stmt(quote! {
if #cond #then_block
});
}
pub fn push(&mut self, mut value: impl Generate) {
value.generate(self);
}
pub fn record_element(&mut self, el_checks: ElementCheck) {
self.checks.push_element(el_checks);
}
pub fn push_diagnostic(&mut self, diagnostic: impl ToTokens) {
self.checks.push_diagnostic(diagnostic.to_token_stream());
}
pub const fn node_flavour(&self) -> NodeFlavour {
self.flavour
}
pub fn push_all(&mut self, values: impl IntoIterator<Item = impl Generate>) {
for value in values {
self.push(value);
}
}
}
enum Part {
Static(LitStr),
Dynamic(TokenStream),
}
#[derive(Debug, Clone, Copy)]
pub enum Context {
Element,
AttributeValue,
DatastarSource,
ScriptSource,
}
impl Context {
pub fn marker_type(self) -> TokenStream {
let ident = match self {
Self::Element => Ident::new("Element", Span::mixed_site()),
Self::AttributeValue => Ident::new("AttributeValue", Span::mixed_site()),
Self::DatastarSource => Ident::new("DatastarSource", Span::mixed_site()),
Self::ScriptSource => Ident::new("ScriptSource", Span::mixed_site()),
};
quote!(::cheers::prelude::#ident)
}
}
pub trait Generate {
const CONTEXT: Context;
fn generate(&mut self, g: &mut Generator<'_>);
}
impl<T: Generate> Generate for &mut T {
const CONTEXT: Context = T::CONTEXT;
fn generate(&mut self, g: &mut Generator<'_>) {
(*self).generate(g);
}
}
struct Checks {
elements: Vec<ElementCheck>,
recovered_errors: Vec<TokenStream>,
}
impl Checks {
const fn new() -> Self {
Self {
elements: Vec::new(),
recovered_errors: Vec::new(),
}
}
fn append(&mut self, other: &mut Self) {
self.elements.append(&mut other.elements);
self.recovered_errors.append(&mut other.recovered_errors);
}
fn is_empty(&self) -> bool {
self.elements.is_empty() && self.recovered_errors.is_empty()
}
fn push_element(&mut self, element: ElementCheck) {
self.elements.push(element);
}
fn push_diagnostic(&mut self, diagnostic: TokenStream) {
self.recovered_errors.push(diagnostic);
}
}
impl ToTokens for Checks {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.is_empty() {
return;
}
for diagnostic in &self.recovered_errors {
diagnostic.to_tokens(tokens);
}
let mut by_module: BTreeMap<ValidationModule, Vec<&ElementCheck>> = BTreeMap::new();
for check in &self.elements {
by_module.entry(check.module).or_default().push(check);
}
for (module, checks) in by_module {
quote! {
const _: fn() = || {
#[allow(unused_imports)]
use #module::*;
#[doc(hidden)]
fn check_element<
K: ::cheers::validation::ElementKind
>(_: impl ::cheers::validation::Element<Kind = K>) {}
#(#checks)*
};
}
.to_tokens(tokens);
}
}
}
pub struct ElementCheck {
module: ValidationModule,
ident: UnquotedName,
kind: ElementKind,
attributes: Vec<AttributeNameCheck>,
}
impl ElementCheck {
pub fn new(
el_name: &UnquotedName,
element_kind: ElementKind,
module: ValidationModule,
) -> Self {
Self {
module,
ident: el_name.clone(),
kind: element_kind,
attributes: Vec::new(),
}
}
pub fn push_attribute(&mut self, attr: AttributeNameCheck) {
self.attributes.push(attr);
}
}
impl ToTokens for ElementCheck {
fn to_tokens(&self, tokens: &mut TokenStream) {
let el = &self.ident;
let kind = self.kind;
let el_check = {
quote! {
check_element::<#kind>(#el);
}
};
let attr_checks = self
.attributes
.iter()
.map(|attr| attr.to_token_stream_with_el(el));
quote! {
#el_check
#(#attr_checks)*
}
.to_tokens(tokens);
}
}
#[derive(Debug, Clone, Copy)]
pub enum ElementKind {
Normal,
Void,
Xml,
}
impl ToTokens for ElementKind {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Normal => quote!(::cheers::validation::Normal),
Self::Void => quote!(::cheers::validation::Void),
Self::Xml => quote!(::cheers::validation::Xml),
}
.to_tokens(tokens);
}
}
pub struct AttributeNameCheck {
kind: AttributeNameCheckKind,
ident: UnquotedName,
data: bool,
data_modifiers: Vec<UnquotedName>,
}
struct DataModifierNameCheck<'a>(&'a UnquotedName);
impl ToTokens for DataModifierNameCheck<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.0 == &"self" {
format_ident!("self_", span = self.0.span()).to_tokens(tokens);
} else {
self.0.to_tokens(tokens);
}
}
}
impl AttributeNameCheck {
pub fn new(kind: AttributeNameCheckKind, ident: UnquotedName, data: bool) -> Self {
Self {
kind,
ident,
data,
data_modifiers: Vec::new(),
}
}
pub fn push_data_modifiers(&mut self, modifiers: Option<&DataModifiers>) {
if let Some(modifiers) = modifiers {
self.data_modifiers
.extend(
modifiers
.modifiers
.iter()
.filter_map(|modifier| match &modifier.name {
DataModifierPart::Ident(ident) => Some(ident.clone()),
DataModifierPart::Literal(_) => None,
}),
);
}
}
fn data_modifier_checks(&self) -> TokenStream {
if !self.data || self.data_modifiers.is_empty() {
return TokenStream::new();
}
let plugin = match &self.kind {
AttributeNameCheckKind::Normal => &self.ident,
AttributeNameCheckKind::Namespace(namespace) => namespace,
};
let modifiers = self.data_modifiers.iter().map(DataModifierNameCheck);
quote! {
#(
let _: ::cheers::validation::data::Modifier = ::cheers::validation::data::modifiers::#plugin::#modifiers;
)*
}
}
fn to_token_stream_with_el(&self, el: &UnquotedName) -> TokenStream {
let data_modifier_checks = self.data_modifier_checks();
match &self.kind {
AttributeNameCheckKind::Namespace(namespace) => {
let ident = &self.ident;
if self.data {
quote! {
{
let _: ::cheers::validation::data::#namespace::Namespace = ::cheers::validation::data::#namespace::Namespace;
#[allow(unused_imports)]
use ::cheers::validation::data::#namespace::*;
let _: ::cheers::validation::Attribute = #ident;
#data_modifier_checks
}
}
} else {
quote! {
let _: ::cheers::validation::#namespace::Namespace = <#el>::#namespace;
let _: ::cheers::validation::Attribute = ::cheers::validation::#namespace::#ident;
}
}
}
AttributeNameCheckKind::Normal => {
let ident = &self.ident;
if self.data {
quote! {
let _: ::cheers::validation::Attribute = ::cheers::validation::data::#ident;
#data_modifier_checks
}
} else {
quote! {
let _: ::cheers::validation::Attribute = <#el>::#ident;
}
}
}
}
}
}
pub enum AttributeNameCheckKind {
Normal,
Namespace(UnquotedName),
}
pub struct AnyBlock {
pub brace_token: Brace,
pub stmts: TokenStream,
pub async_stmts: Vec<TokenStream>,
}
impl Parse for AnyBlock {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let content;
Ok(Self {
brace_token: braced!(content in input),
stmts: content.parse()?,
async_stmts: Vec::new(),
})
}
}
impl ToTokens for AnyBlock {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.brace_token.surround(tokens, |tokens| {
self.stmts.to_tokens(tokens);
});
}
}