diff --git a/macros/src/lib.rs b/macros/src/lib.rs index bfef96c..e58a081 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -4,10 +4,26 @@ extern crate proc_macro; use pom::combinator::*; 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 std::collections::HashMap; +fn required_children(element: &str) -> &[&str] { + match element { + "html" => &["head", "body"], + "head" => &["title"], + _ => &[], + } +} + +fn global_attrs(span: Span) -> HashMap { + 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)] enum Node { Element(Element), @@ -19,8 +35,8 @@ impl Node { fn into_token_stream(self) -> TokenStream { match self { Node::Element(el) => el.into_token_stream(), - Node::Text(_) => panic!("top level must be an element"), - Node::Block(_) => panic!("top level must be an element"), + Node::Text(text) => quote!(typed_html::elements::TextNode::new(#text.to_string())), + Node::Block(_) => panic!("cannot have a block in this position"), } } @@ -29,16 +45,19 @@ impl Node { Node::Element(el) => { let el = el.into_token_stream(); 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!({ let iter = #group.into_iter(); for child in iter { - element.append_child(child); + element.children.push(Box::new(child)); } }), } @@ -47,13 +66,13 @@ impl Node { #[derive(Clone)] struct Element { - name: String, - attributes: HashMap, + name: Ident, + attributes: HashMap, children: Vec, } impl Element { - fn new(name: String) -> Self { + fn new(name: Ident) -> Self { Element { name, 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 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 = 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!( { - 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(punct: char) -> Combinator> { +fn punct<'a>(punct: char) -> Combinator> { 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 { message: format!("expected {:?}", punct), position: start, @@ -143,15 +197,15 @@ fn group<'a>() -> Combinator> { } fn element_start<'a>() -> Combinator> { - (punct('<') * ident()).map(|i| Element::new(i.to_string())) + (punct('<') * ident()).map(Element::new) } fn attr_value<'a>() -> Combinator> { literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident) } -fn attr<'a>() -> Combinator> { - ident().map(|i| i.to_string()) + (punct('=') * attr_value()) +fn attr<'a>() -> Combinator> { + ident() + (punct('=') * attr_value()) } fn element_with_attrs<'a>() -> Combinator> { @@ -171,16 +225,17 @@ fn element_open<'a>() -> Combinator element_with_attrs() - punct('>') } -fn element_close<'a>(name: String) -> Combinator> { +fn element_close<'a>(name: &str) -> Combinator> { + let name = name.to_lowercase(); // 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> { (element_open() + comb(node).repeat(0..)).map(|(mut el, children)| { el.children.extend(children.into_iter()); 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)> { @@ -192,15 +247,214 @@ fn node(input: &[TokenTree], start: usize) -> pom::Result<(Node, usize)> { .parse(input, start) } -fn macro_expand(input: &[TokenTree]) -> pom::Result { +fn expand_html(input: &[TokenTree]) -> pom::Result { comb(node).parse(input).map(|el| el.into_token_stream()) } +struct Declare { + name: Ident, + attrs: HashMap, + req_children: Vec, + opt_children: Option, + traits: Vec, +} + +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 = 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 = 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 = 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>), + 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> { + 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>> +{ + group().map(|group: Group| { + let attr = ident() - punct(':') + type_spec(); + let input: Vec = group.stream().into_iter().collect(); + let result = attr.repeat(0..).parse(&input); + result.unwrap() + }) +} + +fn declare_children<'a>() -> Combinator>> { + group().map(|group: Group| { + let input: Vec = group.stream().into_iter().collect(); + let children = (ident() - punct(',').opt()).repeat(0..); + let result = children.parse(&input); + result.unwrap() + }) +} + +fn declare_traits<'a>() -> Combinator>> { + group().map(|group: Group| { + let input: Vec = group.stream().into_iter().collect(); + let traits = (type_spec() - punct(',').opt()).repeat(0..); + let result = traits.parse(&input); + result.unwrap() + }) +} + +fn declare<'a>() -> Combinator> { + (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 { + declare().parse(input).map(|decl| decl.into_token_stream()) +} + #[proc_macro] pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input: TokenStream = input.into(); let input: Vec = 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 = input.into_iter().collect(); + let result = expand_declare(&input); match result { Err(error) => panic!("error: {:?}", error), Ok(ts) => ts.into(), diff --git a/typed-html/Cargo.toml b/typed-html/Cargo.toml index d452fef..190e2ff 100644 --- a/typed-html/Cargo.toml +++ b/typed-html/Cargo.toml @@ -6,3 +6,4 @@ edition = "2018" [dependencies] typed-html-macros = { path = "../macros" } +http = "0.1.13" diff --git a/typed-html/src/bin/main.rs b/typed-html/src/bin/main.rs index 56c70da..26f8e81 100644 --- a/typed-html/src/bin/main.rs +++ b/typed-html/src/bin/main.rs @@ -1,10 +1,10 @@ #![feature(proc_macro_hygiene)] -use typed_html::Node; +use typed_html::elements::TextNode; use typed_html_macros::html; 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 doc = html!( @@ -17,7 +17,7 @@ fn main() {

{the_big_question}

{ (1..4).map(|i| { - html!(

{ Node::text(format!("Generated paragraph {}", i)) }

) + html!(

{ TextNode::new(format!("Generated paragraph {}", i)) }

) }) } diff --git a/typed-html/src/elements.rs b/typed-html/src/elements.rs new file mode 100644 index 0000000..68efc21 --- /dev/null +++ b/typed-html/src/elements.rs @@ -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; + + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} + +pub struct TextNode(String); + +impl TextNode { + pub fn new>(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); diff --git a/typed-html/src/lib.rs b/typed-html/src/lib.rs index 2cb0c39..c517512 100644 --- a/typed-html/src/lib.rs +++ b/typed-html/src/lib.rs @@ -1,3 +1,5 @@ pub mod node; pub use crate::node::{Element, Node}; + +pub mod elements;