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;
|
||||
|
||||
use pom::combinator::*;
|
||||
use pom::{Error, Parser};
|
||||
use proc_macro2::{Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
|
||||
use quote::quote;
|
||||
use std::collections::HashMap;
|
||||
use proc_macro2::{TokenStream, TokenTree};
|
||||
|
||||
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)]
|
||||
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())
|
||||
}
|
||||
mod config;
|
||||
mod declare;
|
||||
mod html;
|
||||
mod parser;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let input: TokenStream = input.into();
|
||||
let input: Vec<TokenTree> = input.into_iter().collect();
|
||||
let result = expand_html(&input);
|
||||
let result = html::expand_html(&input);
|
||||
match result {
|
||||
Err(error) => panic!("error: {:?}", error),
|
||||
Err(error) => panic!(parser::parse_error(&input, &error)),
|
||||
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 {
|
||||
let input: TokenStream = input.into();
|
||||
let input: Vec<TokenTree> = input.into_iter().collect();
|
||||
let result = expand_declare(&input);
|
||||
let result = declare::expand_declare(&input);
|
||||
match result {
|
||||
Err(error) => panic!("error: {:?}", error),
|
||||
Err(error) => panic!(parser::parse_error(&input, &error)),
|
||||
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>
|
||||
</head>
|
||||
<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="mind-blown">{the_big_question}</p>
|
||||
{
|
||||
(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>
|
||||
|
|
|
@ -9,7 +9,12 @@ pub type CssId = String;
|
|||
pub type CssClass = String;
|
||||
|
||||
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 FlowContent: Node {}
|
||||
pub trait PhrasingContent: Node {}
|
||||
|
|
|
@ -1,5 +1 @@
|
|||
pub mod node;
|
||||
|
||||
pub use crate::node::{Element, Node};
|
||||
|
||||
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