The nastiest element declaration macro.

This commit is contained in:
Bodil Stokke 2018-10-27 03:11:00 +01:00
parent 3bc444068c
commit 4f8b4e2b20
5 changed files with 340 additions and 31 deletions

View File

@ -4,10 +4,26 @@ extern crate proc_macro;
use pom::combinator::*; use pom::combinator::*;
use pom::{Error, Parser}; use pom::{Error, Parser};
use proc_macro2::{Group, Ident, Literal, TokenStream, TokenTree}; use proc_macro2::{Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
use quote::quote; use quote::quote;
use std::collections::HashMap; use std::collections::HashMap;
fn required_children(element: &str) -> &[&str] {
match element {
"html" => &["head", "body"],
"head" => &["title"],
_ => &[],
}
}
fn global_attrs(span: Span) -> HashMap<Ident, TokenStream> {
let mut attrs = HashMap::new();
let mut insert = |key, value: &str| attrs.insert(Ident::new(key, span), value.parse().unwrap());
insert("id", "crate::elements::CssId");
insert("class", "crate::elements::CssClass");
attrs
}
#[derive(Clone)] #[derive(Clone)]
enum Node { enum Node {
Element(Element), Element(Element),
@ -19,8 +35,8 @@ impl Node {
fn into_token_stream(self) -> TokenStream { fn into_token_stream(self) -> TokenStream {
match self { match self {
Node::Element(el) => el.into_token_stream(), Node::Element(el) => el.into_token_stream(),
Node::Text(_) => panic!("top level must be an element"), Node::Text(text) => quote!(typed_html::elements::TextNode::new(#text.to_string())),
Node::Block(_) => panic!("top level must be an element"), Node::Block(_) => panic!("cannot have a block in this position"),
} }
} }
@ -29,16 +45,19 @@ impl Node {
Node::Element(el) => { Node::Element(el) => {
let el = el.into_token_stream(); let el = el.into_token_stream();
quote!( quote!(
element.append_child(#el); element.children.push(Box::new(#el));
)
}
tx @ Node::Text(_) => {
let tx = tx.into_token_stream();
quote!(
element.children.push(Box::new(#tx));
) )
} }
Node::Text(tx) => quote!(
element.append_child(typed_html::Node::Text(#tx.to_string()));
),
Node::Block(group) => quote!({ Node::Block(group) => quote!({
let iter = #group.into_iter(); let iter = #group.into_iter();
for child in iter { for child in iter {
element.append_child(child); element.children.push(Box::new(child));
} }
}), }),
} }
@ -47,13 +66,13 @@ impl Node {
#[derive(Clone)] #[derive(Clone)]
struct Element { struct Element {
name: String, name: Ident,
attributes: HashMap<String, TokenTree>, attributes: HashMap<Ident, TokenTree>,
children: Vec<Node>, children: Vec<Node>,
} }
impl Element { impl Element {
fn new(name: String) -> Self { fn new(name: Ident) -> Self {
Element { Element {
name, name,
attributes: HashMap::new(), attributes: HashMap::new(),
@ -61,19 +80,54 @@ impl Element {
} }
} }
fn into_token_stream(self) -> TokenStream { fn into_token_stream(mut self) -> TokenStream {
let name = self.name; let name = self.name;
let keys: Vec<_> = self.attributes.keys().cloned().collect(); let name_str = name.to_string();
let typename = Ident::new(&format!("Element_{}", &name_str), name.span());
let req_names = required_children(&name_str);
if req_names.len() > self.children.len() {
panic!(
"<{}> requires {} children but found only {}",
name_str,
req_names.len(),
self.children.len()
);
}
let keys: Vec<_> = self
.attributes
.keys()
.map(|key| Ident::new(&format!("attr_{}", key), key.span()))
.collect();
let values: Vec<TokenTree> = self.attributes.values().cloned().collect(); let values: Vec<TokenTree> = self.attributes.values().cloned().collect();
let children = self.children.into_iter().map(Node::into_child_stream); let opt_children = self
.children
.split_off(req_names.len())
.into_iter()
.map(Node::into_child_stream);
for (index, child) in self.children.iter().enumerate() {
match child {
Node::Element(_) => (),
_ => panic!(
"child #{} of {} must be a {} element",
index + 1,
&name,
req_names[index]
),
}
}
let req_children = self.children.into_iter().map(Node::into_token_stream);
quote!( quote!(
{ {
let mut element = typed_html::Element::new(#name); let mut element = typed_html::elements::#typename::new(
#({ #req_children }),*
);
#( #(
element.set_attr(#keys, #values.to_string()); element.#keys = Some(#values.into());
)* )*
#(#children)* #(
typed_html::Node::Element(element) #opt_children
)*
element
} }
) )
} }
@ -83,9 +137,9 @@ fn unit<'a, I: 'a, A: Clone>(value: A) -> Combinator<impl Parser<'a, I, Output =
comb(move |_, start| Ok((value.clone(), start))) comb(move |_, start| Ok((value.clone(), start)))
} }
fn punct<'a>(punct: char) -> Combinator<impl Parser<'a, TokenTree, Output = ()>> { fn punct<'a>(punct: char) -> Combinator<impl Parser<'a, TokenTree, Output = Punct>> {
comb(move |input: &[TokenTree], start| match input.get(start) { comb(move |input: &[TokenTree], start| match input.get(start) {
Some(TokenTree::Punct(p)) if p.as_char() == punct => Ok(((), start + 1)), Some(TokenTree::Punct(p)) if p.as_char() == punct => Ok((p.clone(), start + 1)),
_ => Err(Error::Mismatch { _ => Err(Error::Mismatch {
message: format!("expected {:?}", punct), message: format!("expected {:?}", punct),
position: start, position: start,
@ -143,15 +197,15 @@ fn group<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Group>> {
} }
fn element_start<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> { fn element_start<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
(punct('<') * ident()).map(|i| Element::new(i.to_string())) (punct('<') * ident()).map(Element::new)
} }
fn attr_value<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenTree>> { fn attr_value<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenTree>> {
literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident) literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident)
} }
fn attr<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = (String, TokenTree)>> { fn attr<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = (Ident, TokenTree)>> {
ident().map(|i| i.to_string()) + (punct('=') * attr_value()) ident() + (punct('=') * attr_value())
} }
fn element_with_attrs<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> { fn element_with_attrs<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
@ -171,16 +225,17 @@ fn element_open<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>
element_with_attrs() - punct('>') element_with_attrs() - punct('>')
} }
fn element_close<'a>(name: String) -> Combinator<impl Parser<'a, TokenTree, Output = ()>> { fn element_close<'a>(name: &str) -> Combinator<impl Parser<'a, TokenTree, Output = ()>> {
let name = name.to_lowercase();
// TODO make this return an error message containing the tag name // TODO make this return an error message containing the tag name
punct('<') * punct('/') * ident_match(name) * punct('>') punct('<') * punct('/') * ident_match(name) * punct('>').discard()
} }
fn element_with_children<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> { fn element_with_children<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
(element_open() + comb(node).repeat(0..)).map(|(mut el, children)| { (element_open() + comb(node).repeat(0..)).map(|(mut el, children)| {
el.children.extend(children.into_iter()); el.children.extend(children.into_iter());
el el
}) >> |el: Element| element_close(el.name.clone()).expect("closing tag") * unit(el) }) >> |el: Element| element_close(&el.name.to_string()).expect("closing tag") * unit(el)
} }
fn node(input: &[TokenTree], start: usize) -> pom::Result<(Node, usize)> { fn node(input: &[TokenTree], start: usize) -> pom::Result<(Node, usize)> {
@ -192,15 +247,214 @@ fn node(input: &[TokenTree], start: usize) -> pom::Result<(Node, usize)> {
.parse(input, start) .parse(input, start)
} }
fn macro_expand(input: &[TokenTree]) -> pom::Result<TokenStream> { fn expand_html(input: &[TokenTree]) -> pom::Result<TokenStream> {
comb(node).parse(input).map(|el| el.into_token_stream()) comb(node).parse(input).map(|el| el.into_token_stream())
} }
struct Declare {
name: Ident,
attrs: HashMap<Ident, TokenStream>,
req_children: Vec<Ident>,
opt_children: Option<TokenStream>,
traits: Vec<TokenStream>,
}
impl Declare {
fn new(name: Ident) -> Self {
Declare {
attrs: global_attrs(name.span()),
req_children: Vec::new(),
opt_children: None,
traits: Vec::new(),
name,
}
}
fn into_token_stream(self) -> TokenStream {
let elem_name = Ident::new(
&format!("Element_{}", self.name.to_string()),
self.name.span(),
);
let name = self.name.to_string();
let attr_name: Vec<Ident> = self
.attrs
.keys()
.map(|k| Ident::new(&format!("attr_{}", k.to_string()), k.span()))
.collect();
let attr_name_2 = attr_name.clone();
let attr_name_3 = attr_name.clone();
let attr_name_str = self.attrs.keys().map(|k| k.to_string());
let attr_type = self.attrs.values().cloned();
let req_child_name: Vec<Ident> = self
.req_children
.iter()
.map(|c| Ident::new(&format!("child_{}", c.to_string()), c.span()))
.collect();
let req_child_name_2 = req_child_name.clone();
let req_child_name_3 = req_child_name.clone();
let req_child_name_4 = req_child_name.clone();
let req_child_type: Vec<Ident> = self
.req_children
.iter()
.map(|c| Ident::new(&format!("Element_{}", c.to_string()), c.span()))
.collect();
let req_child_type_2 = req_child_type.clone();
let construct_children = match self.opt_children {
Some(_) => quote!(children: Vec::new()),
None => TokenStream::new(),
};
let print_opt_children = if self.opt_children.is_some() {
quote!(for child in &self.children {
child.fmt(f)?;
})
} else {
TokenStream::new()
};
let print_children = if req_child_name_2.is_empty() {
if self.opt_children.is_some() {
quote!(if self.children.is_empty() {
write!(f, "/>")
} else {
write!(f, ">")?;
#print_opt_children
write!(f, "</{}>", #name)
})
} else {
quote!(write!(f, "/>"))
}
} else {
quote!(
write!(f, ">")?;
#(
self.#req_child_name_2.fmt(f)?;
)*
#print_opt_children
write!(f, "</{}>", #name)
)
};
let children = match self.opt_children {
Some(child_constraint) => quote!(pub children: Vec<Box<#child_constraint>>),
None => TokenStream::new(),
};
let trait_for = std::iter::repeat(elem_name.clone());
let trait_name = self.traits.into_iter();
quote!(
pub struct #elem_name {
#( pub #attr_name: Option<#attr_type>, )*
#( pub #req_child_name: #req_child_type, )*
#children
}
impl #elem_name {
pub fn new(#(#req_child_name_3: #req_child_type_2),*) -> Self {
#elem_name {
#( #attr_name_2: None, )*
#( #req_child_name_4, )*
#construct_children
}
}
}
impl Node for #elem_name {}
impl Element for #elem_name {}
#(
impl #trait_name for #trait_for {}
)*
impl std::fmt::Display for #elem_name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(f, "<{}", #name);
#(
if let Some(ref value) = self.#attr_name_3 {
write!(f, " {}={:?}", #attr_name_str, value.to_string())?;
}
)*
#print_children
}
}
)
}
}
fn type_spec<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenStream>> {
let valid = ident().map(TokenTree::Ident)
| punct(':').map(TokenTree::Punct)
| punct('<').map(TokenTree::Punct)
| punct('>').map(TokenTree::Punct)
| punct('&').map(TokenTree::Punct)
| punct('\'').map(TokenTree::Punct);
valid.repeat(1..).map(|tokens| {
let mut stream = TokenStream::new();
stream.extend(tokens);
stream
})
}
fn declare_attrs<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Vec<(Ident, TokenStream)>>>
{
group().map(|group: Group| {
let attr = ident() - punct(':') + type_spec();
let input: Vec<TokenTree> = group.stream().into_iter().collect();
let result = attr.repeat(0..).parse(&input);
result.unwrap()
})
}
fn declare_children<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Vec<Ident>>> {
group().map(|group: Group| {
let input: Vec<TokenTree> = group.stream().into_iter().collect();
let children = (ident() - punct(',').opt()).repeat(0..);
let result = children.parse(&input);
result.unwrap()
})
}
fn declare_traits<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Vec<TokenStream>>> {
group().map(|group: Group| {
let input: Vec<TokenTree> = group.stream().into_iter().collect();
let traits = (type_spec() - punct(',').opt()).repeat(0..);
let result = traits.parse(&input);
result.unwrap()
})
}
fn declare<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Declare>> {
(ident() + declare_attrs() + declare_children() + declare_traits().opt() + type_spec().opt())
.map(|((((name, attrs), children), traits), child_type)| {
let mut declare = Declare::new(name);
for (key, value) in attrs {
declare.attrs.insert(key, value);
}
for child in children {
declare.req_children.push(child);
}
declare.opt_children = child_type;
declare.traits = traits.unwrap_or_default();
declare
})
}
fn expand_declare(input: &[TokenTree]) -> pom::Result<TokenStream> {
declare().parse(input).map(|decl| decl.into_token_stream())
}
#[proc_macro] #[proc_macro]
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: TokenStream = input.into(); let input: TokenStream = input.into();
let input: Vec<TokenTree> = input.into_iter().collect(); let input: Vec<TokenTree> = input.into_iter().collect();
let result = macro_expand(&input); let result = expand_html(&input);
match result {
Err(error) => panic!("error: {:?}", error),
Ok(ts) => ts.into(),
}
}
#[proc_macro]
pub fn declare_element(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: TokenStream = input.into();
let input: Vec<TokenTree> = input.into_iter().collect();
let result = expand_declare(&input);
match result { match result {
Err(error) => panic!("error: {:?}", error), Err(error) => panic!("error: {:?}", error),
Ok(ts) => ts.into(), Ok(ts) => ts.into(),

View File

@ -6,3 +6,4 @@ edition = "2018"
[dependencies] [dependencies]
typed-html-macros = { path = "../macros" } typed-html-macros = { path = "../macros" }
http = "0.1.13"

View File

@ -1,10 +1,10 @@
#![feature(proc_macro_hygiene)] #![feature(proc_macro_hygiene)]
use typed_html::Node; use typed_html::elements::TextNode;
use typed_html_macros::html; use typed_html_macros::html;
fn main() { fn main() {
let the_big_question = Node::text("How does she eat?"); let the_big_question = TextNode::new("How does she eat?");
let splain_class = "well-actually"; let splain_class = "well-actually";
let doc = html!( let doc = html!(
<html> <html>
@ -17,7 +17,7 @@ fn main() {
<p class="mind-blown">{the_big_question}</p> <p class="mind-blown">{the_big_question}</p>
{ {
(1..4).map(|i| { (1..4).map(|i| {
html!(<p>{ Node::text(format!("Generated paragraph {}", i)) }</p>) html!(<p>{ TextNode::new(format!("Generated paragraph {}", i)) }</p>)
}) })
} }
</body> </body>

View File

@ -0,0 +1,52 @@
#![allow(non_camel_case_types)]
#![allow(dead_code)]
use http::Uri;
use std::fmt::Display;
use typed_html_macros::declare_element;
pub type CssId = String;
pub type CssClass = String;
pub trait Node: Display {}
pub trait Element: Node {}
pub trait MetadataContent: Node {}
pub trait FlowContent: Node {}
pub trait PhrasingContent: Node {}
impl IntoIterator for TextNode {
type Item = TextNode;
type IntoIter = std::vec::IntoIter<TextNode>;
fn into_iter(self) -> Self::IntoIter {
vec![self].into_iter()
}
}
pub struct TextNode(String);
impl TextNode {
pub fn new<S: Into<String>>(s: S) -> Self {
TextNode(s.into())
}
}
impl Display for TextNode {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
impl Node for TextNode {}
impl FlowContent for TextNode {}
impl PhrasingContent for TextNode {}
declare_element!(html {
xmlns: Uri,
} [head, body]);
declare_element!(head {} [title] MetadataContent);
declare_element!(title {} [] [MetadataContent] TextNode);
declare_element!(body {} [] FlowContent);
declare_element!(p {} [] [FlowContent] PhrasingContent);
declare_element!(h1 {} [] [FlowContent] PhrasingContent);
declare_element!(em {} [] [FlowContent, PhrasingContent] PhrasingContent);

View File

@ -1,3 +1,5 @@
pub mod node; pub mod node;
pub use crate::node::{Element, Node}; pub use crate::node::{Element, Node};
pub mod elements;