#![allow(clippy::to_string_trait_impl)]
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{
Error, Expr, ExprMacro, Ident, Lit, LitStr, Macro, Path, Result, Token,
ext::IdentExt,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
use crate::known::{is_known_tag, is_void_tag};
pub(crate) enum AttrName {
Ident(Ident),
LitStr(LitStr),
}
impl Parse for AttrName {
fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Ident) {
Ok(Self::Ident(input.parse()?))
} else if lookahead.peek(LitStr) {
Ok(Self::LitStr(input.parse()?))
} else {
Err(lookahead.error())
}
}
}
impl ToTokens for AttrName {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Ident(ident) => ident.to_tokens(tokens),
Self::LitStr(lit_str) => lit_str.to_tokens(tokens),
}
}
}
impl ToString for AttrName {
fn to_string(&self) -> String {
match self {
Self::Ident(ident) => ident.unraw().to_string(),
Self::LitStr(lit_str) => lit_str.value(),
}
}
}
pub(crate) enum AttrValue {
Expr(Expr),
Bool(bool),
}
impl Parse for AttrValue {
fn parse(input: ParseStream) -> Result<Self> {
if let Ok(Lit::Bool(b)) = input.fork().parse::<Lit>() {
input.parse::<Lit>()?;
return Ok(Self::Bool(b.value));
}
Ok(Self::Expr(input.parse()?))
}
}
impl From<AttrValue> for Expr {
fn from(value: AttrValue) -> Self {
match value {
AttrValue::Expr(expr) => expr,
AttrValue::Bool(b) => Self::Lit(syn::ExprLit {
attrs: Vec::new(),
lit: Lit::Bool(syn::LitBool::new(b, proc_macro2::Span::call_site())),
}),
}
}
}
impl ToTokens for AttrValue {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Expr(expr) => expr.to_tokens(tokens),
Self::Bool(b) => {
let lit = Lit::Bool(syn::LitBool::new(*b, proc_macro2::Span::call_site()));
lit.to_tokens(tokens);
}
}
}
}
pub(crate) struct Attr {
pub name: AttrName,
pub question_token: Option<Token![?]>,
pub eq_token: Token![=],
pub value: AttrValue,
}
impl Attr {
pub(crate) const fn is_optional(&self) -> bool {
self.question_token.is_some()
}
}
impl Parse for Attr {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
name: input.parse()?,
question_token: input.parse()?,
eq_token: input.parse()?,
value: input.parse()?,
})
}
}
impl ToTokens for Attr {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.name.to_tokens(tokens);
self.eq_token.to_tokens(tokens);
self.value.to_tokens(tokens);
}
}
pub(crate) struct ElementBody {
pub attrs: Vec<Attr>,
pub nodes: Vec<Node>,
}
impl Parse for ElementBody {
fn parse(input: ParseStream) -> Result<Self> {
let mut parts = Punctuated::<AttrOrNode, Token![,]>::parse_terminated(input)?
.into_iter()
.peekable();
let mut attrs = Vec::new();
let mut nodes = Vec::new();
while let Some(AttrOrNode::Attr(_)) = parts.peek() {
let AttrOrNode::Attr(attr) = parts.next().unwrap() else {
unreachable!();
};
attrs.push(attr);
}
for part in parts {
match part {
AttrOrNode::Attr(attr) => {
return Err(Error::new_spanned(
attr,
"attributes must be at the beginning",
));
}
AttrOrNode::Node(node) => nodes.push(node),
};
}
Ok(Self { attrs, nodes })
}
}
impl ToTokens for ElementBody {
fn to_tokens(&self, tokens: &mut TokenStream) {
for attr in &self.attrs {
attr.to_tokens(tokens);
}
for node in &self.nodes {
node.to_tokens(tokens);
}
}
}
pub(crate) enum ElementHead {
Known {
ident: Ident,
name: String,
void: bool,
},
Custom {
tag: String,
},
}
impl ElementHead {
pub(crate) fn known(ident: Ident) -> Result<Self> {
let name = ident.to_string();
if !is_known_tag(&name) {
return Err(Error::new_spanned(
ident,
format!(
"unknown HTML tag `{name}`; use the `custom!` macro \
to render arbitrary tag names (e.g. web components)"
),
));
}
let void = is_void_tag(&name);
Ok(Self::Known { ident, name, void })
}
pub(crate) fn custom(tag: String) -> Self {
Self::Custom { tag }
}
pub(crate) fn name(&self) -> &str {
match self {
Self::Known { name, .. } => name,
Self::Custom { tag } => tag,
}
}
pub(crate) const fn is_void(&self) -> bool {
match self {
Self::Known { void, .. } => *void,
Self::Custom { .. } => false,
}
}
pub(crate) fn import_ident(&self) -> Option<Ident> {
match self {
Self::Known { ident, .. } => Some(ident.clone()),
Self::Custom { .. } => None,
}
}
}
pub(crate) struct Element {
pub head: ElementHead,
pub body: ElementBody,
}
impl Element {
pub(crate) fn new(head: ElementHead, body: ElementBody) -> Result<Self> {
if head.is_void() && !body.nodes.is_empty() {
return Err(Error::new_spanned(
body.nodes.first().unwrap(),
"void tags cannot contain content",
));
}
Ok(Self { head, body })
}
pub(crate) fn from_known_macro(
Macro {
path: Path { mut segments, .. },
tokens,
..
}: Macro,
) -> Result<Self> {
let ident = segments.pop().unwrap().into_value().ident;
let head = ElementHead::known(ident)?;
let body = syn::parse2(tokens)?;
Self::new(head, body)
}
}
impl ToTokens for Element {
fn to_tokens(&self, tokens: &mut TokenStream) {
match &self.head {
ElementHead::Known { ident, .. } => ident.to_tokens(tokens),
ElementHead::Custom { tag } => {
LitStr::new(tag, proc_macro2::Span::call_site()).to_tokens(tokens);
}
}
self.body.to_tokens(tokens);
}
}
pub(crate) enum Node {
Element(Element),
Expr(Expr),
}
impl Parse for Node {
fn parse(input: ParseStream) -> Result<Self> {
let expr = input.parse()?;
if let Expr::Macro(ExprMacro { mac, .. }) = &expr
&& let Ok(el) = Element::from_known_macro(mac.clone())
{
return Ok(Self::Element(el));
}
Ok(Self::Expr(expr))
}
}
impl ToTokens for Node {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Element(element) => element.to_tokens(tokens),
Self::Expr(expr) => expr.to_tokens(tokens),
}
}
}
enum AttrOrNode {
Attr(Attr),
Node(Node),
}
impl Parse for AttrOrNode {
fn parse(input: ParseStream) -> Result<Self> {
if (input.peek(Ident) || input.peek(LitStr))
&& (input.peek2(Token![=]) || (input.peek2(Token![?]) && input.peek3(Token![=])))
{
Ok(Self::Attr(input.parse()?))
} else {
Ok(Self::Node(input.parse()?))
}
}
}