Make it so.

This commit is contained in:
Bodil Stokke 2018-10-26 22:02:21 +01:00
commit 3bc444068c
8 changed files with 384 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

5
Cargo.toml Normal file
View File

@ -0,0 +1,5 @@
[workspace]
members = [
"typed-html",
"macros"
]

13
macros/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "typed-html-macros"
version = "0.1.0"
authors = ["Bodil Stokke <bodil@bodil.org>"]
edition = "2018"
[lib]
proc-macro = true
[dependencies]
quote = "0.6.8"
pom = "2.0.1"
proc-macro2 = "0.4.20"

208
macros/src/lib.rs Normal file
View File

@ -0,0 +1,208 @@
#![recursion_limit = "32768"]
extern crate proc_macro;
use pom::combinator::*;
use pom::{Error, Parser};
use proc_macro2::{Group, Ident, Literal, TokenStream, TokenTree};
use quote::quote;
use std::collections::HashMap;
#[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(_) => panic!("top level must be an element"),
Node::Block(_) => panic!("top level must be an element"),
}
}
fn into_child_stream(self) -> TokenStream {
match self {
Node::Element(el) => {
let el = el.into_token_stream();
quote!(
element.append_child(#el);
)
}
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);
}
}),
}
}
}
#[derive(Clone)]
struct Element {
name: String,
attributes: HashMap<String, TokenTree>,
children: Vec<Node>,
}
impl Element {
fn new(name: String) -> Self {
Element {
name,
attributes: HashMap::new(),
children: Vec::new(),
}
}
fn into_token_stream(self) -> TokenStream {
let name = self.name;
let keys: Vec<_> = self.attributes.keys().cloned().collect();
let values: Vec<TokenTree> = self.attributes.values().cloned().collect();
let children = self.children.into_iter().map(Node::into_child_stream);
quote!(
{
let mut element = typed_html::Element::new(#name);
#(
element.set_attr(#keys, #values.to_string());
)*
#(#children)*
typed_html::Node::Element(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 = ()>> {
comb(move |input: &[TokenTree], start| match input.get(start) {
Some(TokenTree::Punct(p)) if p.as_char() == punct => Ok(((), 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(|i| Element::new(i.to_string()))
}
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 = (String, TokenTree)>> {
ident().map(|i| i.to_string()) + (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: String) -> Combinator<impl Parser<'a, TokenTree, Output = ()>> {
// TODO make this return an error message containing the tag name
punct('<') * punct('/') * ident_match(name) * punct('>')
}
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.clone()).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 macro_expand(input: &[TokenTree]) -> pom::Result<TokenStream> {
comb(node).parse(input).map(|el| el.into_token_stream())
}
#[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 = macro_expand(&input);
match result {
Err(error) => panic!("error: {:?}", error),
Ok(ts) => ts.into(),
}
}

8
typed-html/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "typed-html"
version = "0.1.0"
authors = ["Bodil Stokke <bodil@bodil.org>"]
edition = "2018"
[dependencies]
typed-html-macros = { path = "../macros" }

View File

@ -0,0 +1,27 @@
#![feature(proc_macro_hygiene)]
use typed_html::Node;
use typed_html_macros::html;
fn main() {
let the_big_question = Node::text("How does she eat?");
let splain_class = "well-actually";
let doc = html!(
<html>
<head>
<title>"Hello Kitty!"</title>
</head>
<body>
<h1>"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>{ Node::text(format!("Generated paragraph {}", i)) }</p>)
})
}
</body>
</html>
);
println!("{}", doc.to_string());
}

3
typed-html/src/lib.rs Normal file
View File

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

117
typed-html/src/node.rs Normal file
View File

@ -0,0 +1,117 @@
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()
);
}
}