Cleanup, reorg, data attributes.
This commit is contained in:
parent
4f8b4e2b20
commit
dee331c5eb
|
@ -0,0 +1,18 @@
|
||||||
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub fn required_children(element: &str) -> &[&str] {
|
||||||
|
match element {
|
||||||
|
"html" => &["head", "body"],
|
||||||
|
"head" => &["title"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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
|
||||||
|
}
|
|
@ -0,0 +1,261 @@
|
||||||
|
use pom::combinator::*;
|
||||||
|
use pom::Parser;
|
||||||
|
use proc_macro2::{Group, Ident, TokenStream, TokenTree};
|
||||||
|
use quote::quote;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::config::global_attrs;
|
||||||
|
use crate::parser::*;
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
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 elem_name(&self) -> Ident {
|
||||||
|
Ident::new(
|
||||||
|
&format!("Element_{}", self.name.to_string()),
|
||||||
|
self.name.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr_names(&self) -> impl Iterator<Item = Ident> + '_ {
|
||||||
|
self.attrs
|
||||||
|
.keys()
|
||||||
|
.map(|k| Ident::new(&format!("attr_{}", k.to_string()), k.span()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr_names_str(&self) -> impl Iterator<Item = String> + '_ {
|
||||||
|
self.attrs.keys().map(|k| k.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn req_child_names(&self) -> impl Iterator<Item = Ident> + '_ {
|
||||||
|
self.req_children
|
||||||
|
.iter()
|
||||||
|
.map(|c| Ident::new(&format!("child_{}", c.to_string()), c.span()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn req_child_names_str(&self) -> impl Iterator<Item = String> + '_ {
|
||||||
|
self.req_children.iter().map(|i| i.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn req_child_types(&self) -> impl Iterator<Item = Ident> + '_ {
|
||||||
|
self.req_children
|
||||||
|
.iter()
|
||||||
|
.map(|c| Ident::new(&format!("Element_{}", c.to_string()), c.span()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token_stream(self) -> TokenStream {
|
||||||
|
let mut stream = TokenStream::new();
|
||||||
|
stream.extend(self.struct_());
|
||||||
|
stream.extend(self.impl_());
|
||||||
|
stream.extend(self.impl_node());
|
||||||
|
stream.extend(self.impl_element());
|
||||||
|
stream.extend(self.impl_marker_traits());
|
||||||
|
stream.extend(self.impl_display());
|
||||||
|
stream
|
||||||
|
}
|
||||||
|
|
||||||
|
fn struct_(&self) -> TokenStream {
|
||||||
|
let elem_name = self.elem_name();
|
||||||
|
let attr_name = self.attr_names();
|
||||||
|
let attr_type = self.attrs.values();
|
||||||
|
let req_child_name = self.req_child_names();
|
||||||
|
let req_child_type = self.req_child_types();
|
||||||
|
|
||||||
|
let children = match &self.opt_children {
|
||||||
|
Some(child_constraint) => quote!(pub children: Vec<Box<#child_constraint>>),
|
||||||
|
None => TokenStream::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
pub struct #elem_name {
|
||||||
|
#( pub #attr_name: Option<#attr_type>, )*
|
||||||
|
pub data_attributes: std::collections::BTreeMap<String, String>,
|
||||||
|
#( pub #req_child_name: #req_child_type, )*
|
||||||
|
#children
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_(&self) -> TokenStream {
|
||||||
|
let elem_name = self.elem_name();
|
||||||
|
let req_child_name = self.req_child_names();
|
||||||
|
let req_child_type = self.req_child_types();
|
||||||
|
let req_child_name_again = self.req_child_names();
|
||||||
|
let attr_name = self.attr_names();
|
||||||
|
|
||||||
|
let construct_children = match self.opt_children {
|
||||||
|
Some(_) => quote!(children: Vec::new()),
|
||||||
|
None => TokenStream::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
impl #elem_name {
|
||||||
|
pub fn new(#(#req_child_name: #req_child_type),*) -> Self {
|
||||||
|
#elem_name {
|
||||||
|
#( #attr_name: None, )*
|
||||||
|
data_attributes: std::collections::BTreeMap::new(),
|
||||||
|
#( #req_child_name_again, )*
|
||||||
|
#construct_children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_node(&self) -> TokenStream {
|
||||||
|
let elem_name = self.elem_name();
|
||||||
|
quote!(
|
||||||
|
impl Node for #elem_name {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_element(&self) -> TokenStream {
|
||||||
|
let elem_name = self.elem_name();
|
||||||
|
let attr_name_str = self.attr_names_str();
|
||||||
|
let req_child_str_name = self.req_child_names_str();
|
||||||
|
quote!(
|
||||||
|
impl Element for #elem_name {
|
||||||
|
fn attributes() -> &'static [&'static str] {
|
||||||
|
&[ #(#attr_name_str),* ]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_children() -> &'static [&'static str] {
|
||||||
|
&[ #(#req_child_str_name),* ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_marker_traits(&self) -> TokenStream {
|
||||||
|
let trait_for = std::iter::repeat(self.elem_name());
|
||||||
|
let trait_name = self.traits.iter();
|
||||||
|
quote!(
|
||||||
|
#(
|
||||||
|
impl #trait_name for #trait_for {}
|
||||||
|
)*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impl_display(&self) -> TokenStream {
|
||||||
|
let elem_name = self.elem_name();
|
||||||
|
let name = self.name.to_string();
|
||||||
|
let attr_name = self.attr_names();
|
||||||
|
let attr_name_str = self.attr_names_str();
|
||||||
|
let req_child_name: Vec<_> = self.req_child_names().collect();
|
||||||
|
|
||||||
|
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.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.fmt(f)?;
|
||||||
|
)*
|
||||||
|
#print_opt_children
|
||||||
|
write!(f, "</{}>", #name)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
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 {
|
||||||
|
write!(f, " {}={:?}", #attr_name_str, value.to_string())?;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
for (key, value) in &self.data_attributes {
|
||||||
|
write!(f, " data-{}={:?}", key, value)?;
|
||||||
|
}
|
||||||
|
#print_children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_declare(input: &[TokenTree]) -> pom::Result<TokenStream> {
|
||||||
|
declare().parse(input).map(|decl| decl.into_token_stream())
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
use pom::combinator::*;
|
||||||
|
use pom::Parser;
|
||||||
|
use proc_macro2::{Group, Ident, Literal, TokenStream, TokenTree};
|
||||||
|
use quote::quote;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::config::required_children;
|
||||||
|
use crate::parser::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Node {
|
||||||
|
Element(Element),
|
||||||
|
Text(Literal),
|
||||||
|
Block(Group),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
fn into_token_stream(self) -> TokenStream {
|
||||||
|
match self {
|
||||||
|
Node::Element(el) => el.into_token_stream(),
|
||||||
|
Node::Text(text) => quote!(typed_html::elements::TextNode::new(#text.to_string())),
|
||||||
|
Node::Block(_) => panic!("cannot have a block in this position"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_child_stream(self) -> TokenStream {
|
||||||
|
match self {
|
||||||
|
Node::Element(el) => {
|
||||||
|
let el = el.into_token_stream();
|
||||||
|
quote!(
|
||||||
|
element.children.push(Box::new(#el));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tx @ Node::Text(_) => {
|
||||||
|
let tx = tx.into_token_stream();
|
||||||
|
quote!(
|
||||||
|
element.children.push(Box::new(#tx));
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Node::Block(group) => quote!({
|
||||||
|
let iter = #group.into_iter();
|
||||||
|
for child in iter {
|
||||||
|
element.children.push(Box::new(child));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Element {
|
||||||
|
name: Ident,
|
||||||
|
attributes: BTreeMap<Ident, TokenTree>,
|
||||||
|
children: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_data_attrs(attrs: &mut BTreeMap<Ident, TokenTree>) -> BTreeMap<String, TokenTree> {
|
||||||
|
let mut data = BTreeMap::new();
|
||||||
|
let keys: Vec<Ident> = attrs.keys().cloned().collect();
|
||||||
|
for key in keys {
|
||||||
|
let key_name = key.to_string();
|
||||||
|
let prefix = "data_";
|
||||||
|
if key_name.starts_with(prefix) {
|
||||||
|
let value = attrs.remove(&key).unwrap();
|
||||||
|
data.insert(key_name[prefix.len()..].to_string(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
fn new(name: Ident) -> Self {
|
||||||
|
Element {
|
||||||
|
name,
|
||||||
|
attributes: BTreeMap::new(),
|
||||||
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_token_stream(mut self) -> TokenStream {
|
||||||
|
let name = self.name;
|
||||||
|
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 data_attrs = extract_data_attrs(&mut self.attributes);
|
||||||
|
let data_keys = data_attrs.keys().cloned();
|
||||||
|
let data_values = data_attrs.values().cloned();
|
||||||
|
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 opt_children = self
|
||||||
|
.children
|
||||||
|
.split_off(req_names.len())
|
||||||
|
.into_iter()
|
||||||
|
.map(Node::into_child_stream);
|
||||||
|
let req_children = self.children.into_iter().map(Node::into_token_stream);
|
||||||
|
quote!(
|
||||||
|
{
|
||||||
|
let mut element = typed_html::elements::#typename::new(
|
||||||
|
#({ #req_children }),*
|
||||||
|
);
|
||||||
|
#(
|
||||||
|
element.#keys = Some(#values.into());
|
||||||
|
)*
|
||||||
|
#(
|
||||||
|
element.data_attributes.insert(#data_keys.into(), #data_values.into());
|
||||||
|
)*
|
||||||
|
#(
|
||||||
|
#opt_children
|
||||||
|
)*
|
||||||
|
element
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_start<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
||||||
|
(punct('<') * html_ident()).map(Element::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr_value<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenTree>> {
|
||||||
|
literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = (Ident, TokenTree)>> {
|
||||||
|
html_ident() + (punct('=') * attr_value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_with_attrs<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
||||||
|
(element_start() + attr().repeat(0..)).map(|(mut el, attrs)| {
|
||||||
|
for (name, value) in attrs {
|
||||||
|
el.attributes.insert(name, value);
|
||||||
|
}
|
||||||
|
el
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_single<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
||||||
|
element_with_attrs() - punct('/') - punct('>')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_open<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
||||||
|
element_with_attrs() - punct('>')
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
punct('<') * punct('/') * ident_match(name) * punct('>').discard()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_with_children<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
||||||
|
(element_open() + comb(node).repeat(0..)).map(|(mut el, children)| {
|
||||||
|
el.children.extend(children.into_iter());
|
||||||
|
el
|
||||||
|
}) >> |el: Element| element_close(&el.name.to_string()).expect("closing tag") * unit(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(input: &[TokenTree], start: usize) -> pom::Result<(Node, usize)> {
|
||||||
|
(element_single().map(Node::Element)
|
||||||
|
| element_with_children().map(Node::Element)
|
||||||
|
| literal().map(Node::Text)
|
||||||
|
| group().map(Node::Block))
|
||||||
|
.0
|
||||||
|
.parse(input, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_html(input: &[TokenTree]) -> pom::Result<TokenStream> {
|
||||||
|
comb(node).parse(input).map(|el| el.into_token_stream())
|
||||||
|
}
|
|
@ -1,451 +1,21 @@
|
||||||
#![recursion_limit = "32768"]
|
#![feature(proc_macro_span)]
|
||||||
|
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
|
||||||
use pom::combinator::*;
|
use proc_macro2::{TokenStream, TokenTree};
|
||||||
use pom::{Error, Parser};
|
|
||||||
use proc_macro2::{Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
|
|
||||||
use quote::quote;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
fn required_children(element: &str) -> &[&str] {
|
mod config;
|
||||||
match element {
|
mod declare;
|
||||||
"html" => &["head", "body"],
|
mod html;
|
||||||
"head" => &["title"],
|
mod parser;
|
||||||
_ => &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
|
||||||
enum Node {
|
|
||||||
Element(Element),
|
|
||||||
Text(Literal),
|
|
||||||
Block(Group),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
fn into_token_stream(self) -> TokenStream {
|
|
||||||
match self {
|
|
||||||
Node::Element(el) => el.into_token_stream(),
|
|
||||||
Node::Text(text) => quote!(typed_html::elements::TextNode::new(#text.to_string())),
|
|
||||||
Node::Block(_) => panic!("cannot have a block in this position"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_child_stream(self) -> TokenStream {
|
|
||||||
match self {
|
|
||||||
Node::Element(el) => {
|
|
||||||
let el = el.into_token_stream();
|
|
||||||
quote!(
|
|
||||||
element.children.push(Box::new(#el));
|
|
||||||
)
|
|
||||||
}
|
|
||||||
tx @ Node::Text(_) => {
|
|
||||||
let tx = tx.into_token_stream();
|
|
||||||
quote!(
|
|
||||||
element.children.push(Box::new(#tx));
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Node::Block(group) => quote!({
|
|
||||||
let iter = #group.into_iter();
|
|
||||||
for child in iter {
|
|
||||||
element.children.push(Box::new(child));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Element {
|
|
||||||
name: Ident,
|
|
||||||
attributes: HashMap<Ident, TokenTree>,
|
|
||||||
children: Vec<Node>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element {
|
|
||||||
fn new(name: Ident) -> Self {
|
|
||||||
Element {
|
|
||||||
name,
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
children: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_token_stream(mut self) -> TokenStream {
|
|
||||||
let name = self.name;
|
|
||||||
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 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::elements::#typename::new(
|
|
||||||
#({ #req_children }),*
|
|
||||||
);
|
|
||||||
#(
|
|
||||||
element.#keys = Some(#values.into());
|
|
||||||
)*
|
|
||||||
#(
|
|
||||||
#opt_children
|
|
||||||
)*
|
|
||||||
element
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unit<'a, I: 'a, A: Clone>(value: A) -> Combinator<impl Parser<'a, I, Output = A>> {
|
|
||||||
comb(move |_, start| Ok((value.clone(), start)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn punct<'a>(punct: char) -> Combinator<impl Parser<'a, TokenTree, Output = Punct>> {
|
|
||||||
comb(move |input: &[TokenTree], start| match input.get(start) {
|
|
||||||
Some(TokenTree::Punct(p)) if p.as_char() == punct => Ok((p.clone(), start + 1)),
|
|
||||||
_ => Err(Error::Mismatch {
|
|
||||||
message: format!("expected {:?}", punct),
|
|
||||||
position: start,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ident<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Ident>> {
|
|
||||||
comb(|input: &[TokenTree], start| match input.get(start) {
|
|
||||||
Some(TokenTree::Ident(i)) => Ok((i.clone(), start + 1)),
|
|
||||||
_ => Err(Error::Mismatch {
|
|
||||||
message: "expected identifier".to_string(),
|
|
||||||
position: start,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ident_match<'a>(name: String) -> Combinator<impl Parser<'a, TokenTree, Output = ()>> {
|
|
||||||
comb(move |input: &[TokenTree], start| match input.get(start) {
|
|
||||||
Some(TokenTree::Ident(i)) => {
|
|
||||||
if *i == name {
|
|
||||||
Ok(((), start + 1))
|
|
||||||
} else {
|
|
||||||
Err(Error::Mismatch {
|
|
||||||
message: format!("expected '</{}>', found '</{}>'", name, i.to_string()),
|
|
||||||
position: start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(Error::Mismatch {
|
|
||||||
message: "expected identifier".to_string(),
|
|
||||||
position: start,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn literal<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Literal>> {
|
|
||||||
comb(|input: &[TokenTree], start| match input.get(start) {
|
|
||||||
Some(TokenTree::Literal(l)) => Ok((l.clone(), start + 1)),
|
|
||||||
_ => Err(Error::Mismatch {
|
|
||||||
message: "expected literal".to_string(),
|
|
||||||
position: start,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn group<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Group>> {
|
|
||||||
comb(|input: &[TokenTree], start| match input.get(start) {
|
|
||||||
Some(TokenTree::Group(g)) => Ok((g.clone(), start + 1)),
|
|
||||||
_ => Err(Error::Mismatch {
|
|
||||||
message: "expected group".to_string(),
|
|
||||||
position: start,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_start<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
|
||||||
(punct('<') * ident()).map(Element::new)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attr_value<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenTree>> {
|
|
||||||
literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attr<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = (Ident, TokenTree)>> {
|
|
||||||
ident() + (punct('=') * attr_value())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_with_attrs<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
|
||||||
(element_start() + attr().repeat(0..)).map(|(mut el, attrs)| {
|
|
||||||
for (name, value) in attrs {
|
|
||||||
el.attributes.insert(name, value);
|
|
||||||
}
|
|
||||||
el
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_single<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
|
||||||
element_with_attrs() - punct('/') - punct('>')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_open<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
|
||||||
element_with_attrs() - punct('>')
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
punct('<') * punct('/') * ident_match(name) * punct('>').discard()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn element_with_children<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Element>> {
|
|
||||||
(element_open() + comb(node).repeat(0..)).map(|(mut el, children)| {
|
|
||||||
el.children.extend(children.into_iter());
|
|
||||||
el
|
|
||||||
}) >> |el: Element| element_close(&el.name.to_string()).expect("closing tag") * unit(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn node(input: &[TokenTree], start: usize) -> pom::Result<(Node, usize)> {
|
|
||||||
(element_single().map(Node::Element)
|
|
||||||
| element_with_children().map(Node::Element)
|
|
||||||
| literal().map(Node::Text)
|
|
||||||
| group().map(Node::Block))
|
|
||||||
.0
|
|
||||||
.parse(input, start)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_html(input: &[TokenTree]) -> pom::Result<TokenStream> {
|
|
||||||
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 = expand_html(&input);
|
let result = html::expand_html(&input);
|
||||||
match result {
|
match result {
|
||||||
Err(error) => panic!("error: {:?}", error),
|
Err(error) => panic!(parser::parse_error(&input, &error)),
|
||||||
Ok(ts) => ts.into(),
|
Ok(ts) => ts.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -454,9 +24,9 @@ pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
pub fn declare_element(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn declare_element(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 = expand_declare(&input);
|
let result = declare::expand_declare(&input);
|
||||||
match result {
|
match result {
|
||||||
Err(error) => panic!("error: {:?}", error),
|
Err(error) => panic!(parser::parse_error(&input, &error)),
|
||||||
Ok(ts) => ts.into(),
|
Ok(ts) => ts.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
use pom::combinator::*;
|
||||||
|
use pom::{Error, Parser};
|
||||||
|
use proc_macro2::{Group, Ident, Literal, Punct, TokenStream, TokenTree};
|
||||||
|
|
||||||
|
pub fn unit<'a, I: 'a, A: Clone>(value: A) -> Combinator<impl Parser<'a, I, Output = A>> {
|
||||||
|
comb(move |_, start| Ok((value.clone(), start)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn punct<'a>(punct: char) -> Combinator<impl Parser<'a, TokenTree, Output = Punct>> {
|
||||||
|
comb(move |input: &[TokenTree], start| match input.get(start) {
|
||||||
|
Some(TokenTree::Punct(p)) if p.as_char() == punct => Ok((p.clone(), start + 1)),
|
||||||
|
_ => Err(Error::Mismatch {
|
||||||
|
message: format!("expected {:?}", punct),
|
||||||
|
position: start,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ident<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Ident>> {
|
||||||
|
comb(|input: &[TokenTree], start| match input.get(start) {
|
||||||
|
Some(TokenTree::Ident(i)) => Ok((i.clone(), start + 1)),
|
||||||
|
_ => Err(Error::Mismatch {
|
||||||
|
message: "expected identifier".to_string(),
|
||||||
|
position: start,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ident_match<'a>(name: String) -> Combinator<impl Parser<'a, TokenTree, Output = ()>> {
|
||||||
|
comb(move |input: &[TokenTree], start| match input.get(start) {
|
||||||
|
Some(TokenTree::Ident(i)) => {
|
||||||
|
if *i == name {
|
||||||
|
Ok(((), start + 1))
|
||||||
|
} else {
|
||||||
|
Err(Error::Mismatch {
|
||||||
|
message: format!("expected '</{}>', found '</{}>'", name, i.to_string()),
|
||||||
|
position: start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(Error::Mismatch {
|
||||||
|
message: "expected identifier".to_string(),
|
||||||
|
position: start,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn literal<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Literal>> {
|
||||||
|
comb(|input: &[TokenTree], start| match input.get(start) {
|
||||||
|
Some(TokenTree::Literal(l)) => Ok((l.clone(), start + 1)),
|
||||||
|
_ => Err(Error::Mismatch {
|
||||||
|
message: "expected literal".to_string(),
|
||||||
|
position: start,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Group>> {
|
||||||
|
comb(|input: &[TokenTree], start| match input.get(start) {
|
||||||
|
Some(TokenTree::Group(g)) => Ok((g.clone(), start + 1)),
|
||||||
|
_ => Err(Error::Mismatch {
|
||||||
|
message: "expected group".to_string(),
|
||||||
|
position: start,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_stream<'a, I: IntoIterator<Item = &'a TokenTree>>(tokens: I) -> TokenStream {
|
||||||
|
let mut stream = TokenStream::new();
|
||||||
|
stream.extend(tokens.into_iter().cloned());
|
||||||
|
stream
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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..).collect().map(to_stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a sequence of idents and dashes, and merge them into a single ident
|
||||||
|
/// with the dashes replaced by underscores.
|
||||||
|
pub fn html_ident<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = Ident>> {
|
||||||
|
let start = ident();
|
||||||
|
let next = punct('-') * ident();
|
||||||
|
(start * next.repeat(0..)).collect().map(|stream| {
|
||||||
|
let (span, name) = stream
|
||||||
|
.into_iter()
|
||||||
|
.fold((None, String::new()), |(span, name), token| {
|
||||||
|
(
|
||||||
|
match span {
|
||||||
|
None => Some(token.span()),
|
||||||
|
// FIXME: Some(span) => Some(span.join(token.span())),
|
||||||
|
span => span,
|
||||||
|
},
|
||||||
|
match token {
|
||||||
|
TokenTree::Ident(ident) => name + &ident.to_string(),
|
||||||
|
TokenTree::Punct(_) => name + "_",
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
Ident::new(&name, span.unwrap())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_location(input: &[TokenTree], position: usize) -> String {
|
||||||
|
format!("{:?}", input[position].span())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_error(input: &[TokenTree], error: &pom::Error) -> String {
|
||||||
|
match error {
|
||||||
|
pom::Error::Incomplete => "Incomplete token stream".to_string(),
|
||||||
|
pom::Error::Mismatch { message, position } => {
|
||||||
|
format!("{}: {}", error_location(input, *position), message)
|
||||||
|
}
|
||||||
|
pom::Error::Conversion { message, position } => {
|
||||||
|
format!("{}: {}", error_location(input, *position), message)
|
||||||
|
}
|
||||||
|
pom::Error::Expect {
|
||||||
|
message,
|
||||||
|
position,
|
||||||
|
inner,
|
||||||
|
} => format!(
|
||||||
|
"{}: {}\n{}",
|
||||||
|
error_location(input, *position),
|
||||||
|
message,
|
||||||
|
parse_error(input, &inner)
|
||||||
|
),
|
||||||
|
pom::Error::Custom {
|
||||||
|
message,
|
||||||
|
position,
|
||||||
|
inner,
|
||||||
|
} => {
|
||||||
|
let mut out = format!("{}: {}", error_location(input, *position), message);
|
||||||
|
if let Some(error) = inner {
|
||||||
|
out += &format!("\n{}", parse_error(input, error));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,12 +12,12 @@ fn main() {
|
||||||
<title>"Hello Kitty!"</title>
|
<title>"Hello Kitty!"</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>"Hello Kitty!"</h1>
|
<h1 data-lol="foo">"Hello Kitty!"</h1>
|
||||||
<p class=splain_class>"She is not a cat. She is a human girl."</p>
|
<p class=splain_class>"She is not a cat. She is a human girl."</p>
|
||||||
<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>{ TextNode::new(format!("Generated paragraph {}", i)) }</p>)
|
html!(<p>{ TextNode::new(format!("{}. Ceci n'est pas une chatte.", i)) }</p>)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -9,7 +9,12 @@ pub type CssId = String;
|
||||||
pub type CssClass = String;
|
pub type CssClass = String;
|
||||||
|
|
||||||
pub trait Node: Display {}
|
pub trait Node: Display {}
|
||||||
pub trait Element: Node {}
|
|
||||||
|
pub trait Element: Node {
|
||||||
|
fn attributes() -> &'static [&'static str];
|
||||||
|
fn required_children() -> &'static [&'static str];
|
||||||
|
}
|
||||||
|
|
||||||
pub trait MetadataContent: Node {}
|
pub trait MetadataContent: Node {}
|
||||||
pub trait FlowContent: Node {}
|
pub trait FlowContent: Node {}
|
||||||
pub trait PhrasingContent: Node {}
|
pub trait PhrasingContent: Node {}
|
||||||
|
|
|
@ -1,5 +1 @@
|
||||||
pub mod node;
|
|
||||||
|
|
||||||
pub use crate::node::{Element, Node};
|
|
||||||
|
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::{Debug, Display, Error, Formatter};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
|
||||||
pub enum Node {
|
|
||||||
Element(Element),
|
|
||||||
Text(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
pub fn text<S: Into<String>>(t: S) -> Self {
|
|
||||||
Node::Text(t.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Node {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
|
||||||
match self {
|
|
||||||
Node::Element(el) => (el as &Display).fmt(f),
|
|
||||||
Node::Text(tx) => (tx as &Display).fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for Node {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
|
||||||
(self as &Display).fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for Node {
|
|
||||||
type Item = Node;
|
|
||||||
type IntoIter = std::vec::IntoIter<Node>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
vec![self].into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
|
||||||
pub struct Element {
|
|
||||||
name: String,
|
|
||||||
attributes: HashMap<String, String>,
|
|
||||||
children: Vec<Node>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element {
|
|
||||||
pub fn new<S: Into<String>>(name: S) -> Self {
|
|
||||||
Element {
|
|
||||||
name: name.into(),
|
|
||||||
attributes: HashMap::new(),
|
|
||||||
children: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_attr<S1: Into<String>, S2: Into<String>>(&mut self, attr: S1, value: S2) {
|
|
||||||
self.attributes.insert(attr.into(), value.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append_child(&mut self, child: Node) {
|
|
||||||
self.children.push(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Element {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
|
||||||
write!(f, "<{}", self.name)?;
|
|
||||||
for (attr, value) in &self.attributes {
|
|
||||||
write!(f, " {}={:?}", attr, value)?;
|
|
||||||
}
|
|
||||||
if self.children.is_empty() {
|
|
||||||
write!(f, "/>")
|
|
||||||
} else {
|
|
||||||
write!(f, ">")?;
|
|
||||||
for child in &self.children {
|
|
||||||
(child as &Display).fmt(f)?;
|
|
||||||
}
|
|
||||||
write!(f, "</{}>", self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for Element {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
|
||||||
(self as &Display).fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn construct() {
|
|
||||||
let el1 = Element::new("html");
|
|
||||||
let el2 = Element::new("html".to_string());
|
|
||||||
assert_eq!(el1, el2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_string() {
|
|
||||||
let mut doc = Element::new("html");
|
|
||||||
doc.set_attr("version", "1.0");
|
|
||||||
let mut head = Element::new("head");
|
|
||||||
let mut style = Element::new("style");
|
|
||||||
style.set_attr("src", "lol.css");
|
|
||||||
let mut title = Element::new("title");
|
|
||||||
title.append_child(Node::Text("Hello kitty!".to_string()));
|
|
||||||
head.append_child(Node::Element(title));
|
|
||||||
head.append_child(Node::Element(style));
|
|
||||||
doc.append_child(Node::Element(head));
|
|
||||||
assert_eq!(
|
|
||||||
"<html version=\"1.0\"><head><title>Hello kitty!</title><style src=\"lol.css\"/></head></html>",
|
|
||||||
&doc.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue