diff --git a/macros/src/config.rs b/macros/src/config.rs index 1700862..93bcbb7 100644 --- a/macros/src/config.rs +++ b/macros/src/config.rs @@ -15,8 +15,22 @@ pub fn global_attrs(span: Span) -> StringyMap { { 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"); + + insert("id", "crate::types::Id"); + insert("class", "crate::types::ClassList"); + + insert("accesskey", "String"); + insert("autocapitalize", "String"); + insert("contenteditable", "bool"); + insert("contextmenu", "crate::types::Id"); + insert("dir", "String"); + insert("draggable", "bool"); + insert("hidden", "bool"); + insert("is", "String"); + insert("lang", "crate::types::LanguageTag"); + insert("style", "String"); + insert("tabindex", "isize"); + insert("title", "String"); } attrs } diff --git a/macros/src/declare.rs b/macros/src/declare.rs index 5f899e4..6058b39 100644 --- a/macros/src/declare.rs +++ b/macros/src/declare.rs @@ -35,6 +35,14 @@ impl Declare { .into() } + fn attr_type_name(&self) -> TokenTree { + Ident::new( + &format!("ElementAttrs_{}", self.name.to_string()), + self.name.span(), + ) + .into() + } + fn attrs(&self) -> impl Iterator + '_ { self.attrs.iter().map(|(key, value)| { let attr_name: TokenTree = @@ -58,6 +66,7 @@ impl Declare { fn into_token_stream(self) -> TokenStream { let mut stream = TokenStream::new(); + stream.extend(self.attr_struct()); stream.extend(self.struct_()); stream.extend(self.impl_()); stream.extend(self.impl_node()); @@ -67,16 +76,28 @@ impl Declare { stream } - fn struct_(&self) -> TokenStream { - let elem_name = self.elem_name(); - + fn attr_struct(&self) -> TokenStream { let mut body = TokenStream::new(); - for (attr_name, attr_type, _) in self.attrs() { body.extend(quote!( pub $attr_name: Option<$attr_type>, )); } + let attr_type_name = self.attr_type_name(); + quote!( + pub struct $attr_type_name { + $body + } + ) + } + + fn struct_(&self) -> TokenStream { + let elem_name = self.elem_name(); + let attr_type_name = self.attr_type_name(); + + let mut body = TokenStream::new(); + body.extend(quote!( + pub attrs: $attr_type_name, pub data_attributes: std::collections::BTreeMap, )); @@ -98,16 +119,22 @@ impl Declare { fn impl_(&self) -> TokenStream { let elem_name = self.elem_name(); + let attr_type_name = self.attr_type_name(); let mut args = TokenStream::new(); for (child_name, child_type, _) in self.req_children() { args.extend(quote!( $child_name: Box<$child_type>, )); } - let mut body = TokenStream::new(); + let mut attrs = TokenStream::new(); for (attr_name, _, _) in self.attrs() { - body.extend(quote!( $attr_name: None, )); + attrs.extend(quote!( $attr_name: None, )); } + + let mut body = TokenStream::new(); + body.extend(quote!( + attrs: $attr_type_name { $attrs }, + )); body.extend(quote!(data_attributes: std::collections::BTreeMap::new(),)); for (child_name, _, _) in self.req_children() { body.extend(quote!( $child_name, )); @@ -135,6 +162,7 @@ impl Declare { } fn impl_element(&self) -> TokenStream { + let name: TokenTree = Literal::string(&self.name.to_string()).into(); let elem_name = self.elem_name(); let attrs: TokenStream = self.attrs().map(|(_, _, name)| quote!( $name, )).collect(); @@ -143,15 +171,37 @@ impl Declare { .map(|(_, _, name)| quote!( $name, )) .collect(); + let mut push_attrs = TokenStream::new(); + for (attr_name, _, attr_str) in self.attrs() { + push_attrs.extend(quote!( + if let Some(ref value) = self.attrs.$attr_name { + out.push(($attr_str.to_string(), value.to_string())); + } + )); + } + quote!( impl ::elements::Element for $elem_name { - fn attributes() -> &'static [&'static str] { + fn name() -> &'static str { + $name + } + + fn attribute_names() -> &'static [&'static str] { &[ $attrs ] } fn required_children() -> &'static [&'static str] { &[ $reqs ] } + + fn attributes(&self) -> Vec<(String, String)> { + let mut out = Vec::new(); + $push_attrs + for (key, value) in &self.data_attributes { + out.push((format!("data-{}", key), value.to_string())); + } + out + } } ) } @@ -211,7 +261,7 @@ impl Declare { let mut print_attrs = TokenStream::new(); for (attr_name, _, attr_str) in self.attrs() { print_attrs.extend(quote!( - if let Some(ref value) = self.$attr_name { + if let Some(ref value) = self.attrs.$attr_name { write!(f, " {}={:?}", $attr_str, value.to_string())?; } )); @@ -237,13 +287,14 @@ impl Declare { fn declare_attrs<'a>() -> Combinator>> { group().map(|group: Group| -> Vec<(Ident, TokenStream)> { - let attr = ident() - punct(':') + type_spec(); + let attr = ident() - punct(':') + type_spec() - punct(',').opt(); let parser = attr.repeat(0..); let input: Vec = group.stream().into_iter().collect(); // FIXME the borrow checker won't let me use plain &input, it seems like a bug. // It works in Rust 2018, so please get rid of this unsafe block when it stabilises. - let result = parser.parse(unsafe { &*(input.as_slice() as *const _) }); - result.unwrap() + parser + .parse(unsafe { &*(input.as_slice() as *const _) }) + .unwrap() }) } diff --git a/macros/src/html.rs b/macros/src/html.rs index 94ef83d..3ec409d 100644 --- a/macros/src/html.rs +++ b/macros/src/html.rs @@ -1,6 +1,6 @@ use pom::combinator::*; use pom::Parser; -use proc_macro::{quote, Group, Ident, Literal, TokenStream, TokenTree}; +use proc_macro::{quote, Delimiter, Group, Ident, Literal, TokenStream, TokenTree}; use config::required_children; use map::StringyMap; @@ -72,6 +72,25 @@ fn extract_data_attrs(attrs: &mut StringyMap) -> StringyMap TokenStream { + match value { + TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => { + let content = g.stream(); + quote!( [ $content ] ) + } + TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => { + let content = g.stream(); + quote!( ( $content ) ) + } + v => v.clone().into(), + } +} + +fn is_string_literal(literal: &Literal) -> bool { + // This is the worst API + literal.to_string().starts_with('"') +} + impl Element { fn new(name: Ident) -> Self { Element { @@ -97,8 +116,9 @@ impl Element { let data_attrs = extract_data_attrs(&mut self.attributes); let attrs = self.attributes.iter().map(|(key, value)| { ( + key.to_string(), TokenTree::Ident(Ident::new(&format!("attr_{}", key), key.span())), - value.clone(), + value, ) }); let opt_children = self @@ -109,10 +129,35 @@ impl Element { let req_children = self.children.into_iter().map(Node::into_token_stream); let mut body = TokenStream::new(); - for (key, value) in attrs { - body.extend(quote!( - element.$key = Some($value.into()); - )); + for (attr_str, key, value) in attrs { + match value { + TokenTree::Literal(l) if is_string_literal(l) => { + let value = value.clone(); + let tag_name: TokenTree = Literal::string(&name_str).into(); + let attr_str: TokenTree = Literal::string(&attr_str).into(); + let span = value.span(); + let pos = format!( + "{}:{}:{}", + span.source_file().path().to_str().unwrap_or("unknown"), + span.start().line, + span.start().column + ); + let pos_str: TokenTree = Literal::string(&pos).into(); + body.extend(quote!( + element.attrs.$key = Some($value.parse().unwrap_or_else(|err| { + eprintln!("ERROR: {}: <{} {}={:?}> attribute value was not accepted: {:?}", + $pos_str, $tag_name, $attr_str, $value, err); + panic!(); + })); + )); + } + value => { + let value = process_value(value); + body.extend(quote!( + element.attrs.$key = Some(std::convert::TryInto::try_into($value).unwrap()); + )); + } + } } for (key, value) in data_attrs .iter() @@ -144,7 +189,7 @@ fn element_start<'a>() -> Combinator() -> Combinator> { - literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident) + literal().map(TokenTree::Literal) | dotted_ident() | group().map(TokenTree::Group) } fn attr<'a>() -> Combinator> { diff --git a/macros/src/parser.rs b/macros/src/parser.rs index 9efb295..92b8107 100644 --- a/macros/src/parser.rs +++ b/macros/src/parser.rs @@ -1,6 +1,6 @@ use pom::combinator::*; use pom::{Error, Parser}; -use proc_macro::{Group, Ident, Literal, Punct, TokenStream, TokenTree}; +use proc_macro::{Delimiter, Group, Ident, Literal, Punct, TokenStream, TokenTree}; pub fn unit<'a, I: 'a, A: Clone>(value: A) -> Combinator> { comb(move |_, start| Ok((value.clone(), start))) @@ -81,6 +81,20 @@ pub fn type_spec<'a>() -> Combinator() -> Combinator> { + (ident() + + ((punct('.') + ident()).discard() | (punct(':').repeat(2) + ident()).discard()) + .repeat(0..)) + .collect() + .map(|tokens| { + if tokens.len() == 1 { + tokens[0].clone() + } else { + Group::new(Delimiter::Brace, to_stream(tokens)).into() + } + }) +} + /// 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> { diff --git a/typed-html/Cargo.toml b/typed-html/Cargo.toml index bb0bdca..40f6dd5 100644 --- a/typed-html/Cargo.toml +++ b/typed-html/Cargo.toml @@ -5,4 +5,9 @@ authors = ["Bodil Stokke "] [dependencies] typed-html-macros = { path = "../macros" } +strum = "0.11.0" +strum_macros = "0.11.0" +mime = "0.3.12" +language-tags = "0.2.2" +enumset = "0.3.12" http = "0.1.13" diff --git a/typed-html/src/bin/main.rs b/typed-html/src/bin/main.rs index 5fbb5bf..de7cbb6 100644 --- a/typed-html/src/bin/main.rs +++ b/typed-html/src/bin/main.rs @@ -1,23 +1,33 @@ +#![feature(try_from)] #![feature(proc_macro_hygiene)] #[macro_use] extern crate typed_html; extern crate typed_html_macros; +use typed_html::types::*; use typed_html_macros::html; +struct Foo { + foo: &'static str, +} + fn main() { let the_big_question = text!("How does she eat?"); let splain_class = "well-actually"; + let wibble = Foo { foo: "welp" }; let doc = html!( "Hello Kitty!" + -

"Hello Kitty!"

-

"She is not a cat. She is a human girl."

-

{the_big_question}

+

"Hello Kitty!"

+

+ "She is not a ""cat"". She is a ""human girl""." +

+

{the_big_question}

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

{ text!("{}. Ceci n'est pas une chatte.", i) }

) diff --git a/typed-html/src/elements.rs b/typed-html/src/elements.rs index 5044509..7292991 100644 --- a/typed-html/src/elements.rs +++ b/typed-html/src/elements.rs @@ -1,18 +1,18 @@ #![allow(non_camel_case_types)] #![allow(dead_code)] -use http::Uri; use std::fmt::Display; use typed_html_macros::declare_element; -pub type CssId = String; -pub type CssClass = String; +use super::types::*; pub trait Node: Display {} pub trait Element: Node { - fn attributes() -> &'static [&'static str]; + fn name() -> &'static str; + fn attribute_names() -> &'static [&'static str]; fn required_children() -> &'static [&'static str]; + fn attributes(&self) -> Vec<(String, String)>; } pub trait MetadataContent: Node {} @@ -69,8 +69,61 @@ declare_element!(html { xmlns: Uri, } [head, body]); declare_element!(head {} [title] MetadataContent); -declare_element!(title {} [] [MetadataContent] TextNode); declare_element!(body {} [] FlowContent); + +// Metadata content +declare_element!(base { + href: Uri, + target: String, +} [] [MetadataContent]); +declare_element!(link { + as: Mime, + crossorigin: CrossOrigin, + href: Uri, + hreflang: LanguageTag, + media: String, // FIXME media query + rel: LinkType, + sizes: String, // FIXME + title: String, // FIXME + type: Mime, +} [] [MetadataContent]); +declare_element!(meta { + charset: String, // FIXME IANA standard names + content: String, + http_equiv: String, // FIXME string enum + name: String, // FIXME string enum +} [] [MetadataContent]); +declare_element!(style { + type: Mime, + media: String, // FIXME media query + nonce: String, // bigint? + title: String, // FIXME +} [] [MetadataContent] TextNode); +declare_element!(title {} [] [MetadataContent] TextNode); + +// Flow content +declare_element!(div {} [] [FlowContent] FlowContent); declare_element!(p {} [] [FlowContent] PhrasingContent); declare_element!(h1 {} [] [FlowContent] PhrasingContent); +declare_element!(h2 {} [] [FlowContent] PhrasingContent); +declare_element!(h3 {} [] [FlowContent] PhrasingContent); +declare_element!(h4 {} [] [FlowContent] PhrasingContent); +declare_element!(h5 {} [] [FlowContent] PhrasingContent); +declare_element!(h6 {} [] [FlowContent] PhrasingContent); declare_element!(em {} [] [FlowContent, PhrasingContent] PhrasingContent); + +// Don't @ me +declare_element!(blink {} [] [FlowContent, PhrasingContent] PhrasingContent); +declare_element!(marquee { + behavior: String, // FIXME enum + bgcolor: String, // FIXME colour + direction: String, // FIXME direction enum + height: String, // FIXME size + hspace: String, // FIXME size + loop: isize, + scrollamount: usize, + scrolldelay: usize, + truespeed: bool, + vspace: String, // FIXME size + width: String, // FIXME size +} [] [FlowContent, PhrasingContent] PhrasingContent); diff --git a/typed-html/src/lib.rs b/typed-html/src/lib.rs index 5d38bc0..8c64ee5 100644 --- a/typed-html/src/lib.rs +++ b/typed-html/src/lib.rs @@ -1,4 +1,15 @@ +#![feature(try_from)] + +#[macro_use] +extern crate enumset; +#[macro_use] +extern crate strum_macros; + extern crate http; +extern crate language_tags; +extern crate mime; +extern crate strum; extern crate typed_html_macros; pub mod elements; +pub mod types; diff --git a/typed-html/src/types/class.rs b/typed-html/src/types/class.rs new file mode 100644 index 0000000..5de4c98 --- /dev/null +++ b/typed-html/src/types/class.rs @@ -0,0 +1,81 @@ +use std::convert::TryFrom; +use std::fmt::{Display, Error, Formatter}; +use std::ops::Deref; + +use super::Id; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Class(String); + +impl Class { + // Construct a new class name from a string. + // + // Returns `None` if the provided string is invalid. + pub fn try_new>(id: S) -> Result { + let id = id.into(); + { + let mut chars = id.chars(); + match chars.next() { + None => return Err("class name cannot be empty"), + Some(c) if !c.is_alphabetic() => { + return Err("class name must start with an alphabetic character") + } + _ => (), + } + for c in chars { + if !c.is_alphanumeric() && c != '_' && c != '-' && c != '.' { + return Err( + "class name can only contain alphanumerics, dash, dot and underscore", + ); + } + } + } + Ok(Class(id)) + } + + // Construct a new class name from a string. + // + // Panics if the provided string is invalid. + pub fn new>(id: S) -> Self { + let id = id.into(); + Self::try_new(id.clone()).unwrap_or_else(|err| { + panic!( + "typed_html::types::Class: {:?} is not a valid class name: {}", + id, err + ) + }) + } +} + +impl TryFrom for Class { + type Error = &'static str; + fn try_from(s: String) -> Result { + Self::try_new(s) + } +} + +impl<'a> TryFrom<&'a str> for Class { + type Error = &'static str; + fn try_from(s: &'a str) -> Result { + Self::try_new(s) + } +} + +impl From for Class { + fn from(id: Id) -> Self { + Class(id.to_string()) + } +} + +impl Display for Class { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + Display::fmt(&self.0, f) + } +} + +impl Deref for Class { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/typed-html/src/types/classlist.rs b/typed-html/src/types/classlist.rs new file mode 100644 index 0000000..b944187 --- /dev/null +++ b/typed-html/src/types/classlist.rs @@ -0,0 +1,221 @@ +use std::collections::BTreeSet; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::iter::FromIterator; +use std::ops::{Deref, DerefMut}; + +use super::Class; + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct ClassList(BTreeSet); + +impl ClassList { + pub fn new() -> Self { + ClassList(BTreeSet::new()) + } +} + +impl Default for ClassList { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator for ClassList { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + ClassList(iter.into_iter().collect()) + } +} + +impl<'a> FromIterator<&'a Class> for ClassList { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + ClassList(iter.into_iter().cloned().collect()) + } +} + +impl FromIterator for ClassList { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + ClassList(iter.into_iter().map(Class::new).collect()) + } +} + +impl<'a> FromIterator<&'a str> for ClassList { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + ClassList(iter.into_iter().map(Class::new).collect()) + } +} + +impl<'a> From<&'a str> for ClassList { + fn from(s: &'a str) -> Self { + Self::from_iter(s.split_whitespace().map(Class::new)) + } +} + +impl Deref for ClassList { + type Target = BTreeSet; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ClassList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for ClassList { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + let mut it = self.0.iter().peekable(); + while let Some(class) = it.next() { + Display::fmt(class, f)?; + if it.peek().is_some() { + Display::fmt(" ", f)?; + } + } + Ok(()) + } +} + +impl Debug for ClassList { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + f.debug_list().entries(self.0.iter()).finish() + } +} + +impl From<(&str, &str)> for ClassList { + fn from(s: (&str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list + } +} + +impl From<(&str, &str, &str)> for ClassList { + fn from(s: (&str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list.insert(Class::new(s.2)); + list + } +} + +impl From<(&str, &str, &str, &str)> for ClassList { + fn from(s: (&str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list.insert(Class::new(s.2)); + list.insert(Class::new(s.3)); + list + } +} + +impl From<(&str, &str, &str, &str, &str)> for ClassList { + fn from(s: (&str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list.insert(Class::new(s.2)); + list.insert(Class::new(s.3)); + list.insert(Class::new(s.4)); + list + } +} + +impl From<(&str, &str, &str, &str, &str, &str)> for ClassList { + fn from(s: (&str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list.insert(Class::new(s.2)); + list.insert(Class::new(s.3)); + list.insert(Class::new(s.4)); + list.insert(Class::new(s.5)); + list + } +} + +impl From<(&str, &str, &str, &str, &str, &str, &str)> for ClassList { + fn from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list.insert(Class::new(s.2)); + list.insert(Class::new(s.3)); + list.insert(Class::new(s.4)); + list.insert(Class::new(s.5)); + list.insert(Class::new(s.6)); + list + } +} + +impl From<(&str, &str, &str, &str, &str, &str, &str, &str)> for ClassList { + fn from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(Class::new(s.0)); + list.insert(Class::new(s.1)); + list.insert(Class::new(s.2)); + list.insert(Class::new(s.3)); + list.insert(Class::new(s.4)); + list.insert(Class::new(s.5)); + list.insert(Class::new(s.6)); + list.insert(Class::new(s.7)); + list + } +} + +macro_rules! classlist_from_array { + ($num:tt) => { + impl From<[&str; $num]> for ClassList { + fn from(s: [&str; $num]) -> Self { + Self::from_iter(s.into_iter().map(|s| Class::new(*s))) + } + } + }; +} +classlist_from_array!(1); +classlist_from_array!(2); +classlist_from_array!(3); +classlist_from_array!(4); +classlist_from_array!(5); +classlist_from_array!(6); +classlist_from_array!(7); +classlist_from_array!(8); +classlist_from_array!(9); +classlist_from_array!(10); +classlist_from_array!(11); +classlist_from_array!(12); +classlist_from_array!(13); +classlist_from_array!(14); +classlist_from_array!(15); +classlist_from_array!(16); +classlist_from_array!(17); +classlist_from_array!(18); +classlist_from_array!(19); +classlist_from_array!(20); +classlist_from_array!(21); +classlist_from_array!(22); +classlist_from_array!(23); +classlist_from_array!(24); +classlist_from_array!(25); +classlist_from_array!(26); +classlist_from_array!(27); +classlist_from_array!(28); +classlist_from_array!(29); +classlist_from_array!(30); +classlist_from_array!(31); +classlist_from_array!(32); diff --git a/typed-html/src/types/id.rs b/typed-html/src/types/id.rs new file mode 100644 index 0000000..e3e5e86 --- /dev/null +++ b/typed-html/src/types/id.rs @@ -0,0 +1,82 @@ +use std::convert::TryFrom; +use std::fmt::{Display, Error, Formatter}; +use std::ops::Deref; + +use super::Class; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Id(String); + +impl Id { + // Construct a new ID from a string. + // + // Returns `None` if the provided string is invalid. + pub fn try_new>(id: S) -> Result { + let id = id.into(); + { + let mut chars = id.chars(); + match chars.next() { + None => return Err("ID cannot be empty"), + Some(c) if !c.is_alphabetic() => { + return Err("ID must start with an alphabetic character") + } + _ => (), + } + for c in chars { + if !c.is_alphanumeric() && c != '_' && c != '-' && c != '.' { + return Err("ID can only contain alphanumerics, dash, dot and underscore"); + } + } + } + Ok(Id(id)) + } + + // Construct a new ID from a string. + // + // Panics if the provided string is invalid. + pub fn new>(id: S) -> Self { + let id = id.into(); + Self::try_new(id.clone()).unwrap_or_else(|err| { + panic!("typed_html::types::Id: {:?} is not a valid ID: {}", id, err) + }) + } +} + +impl TryFrom for Id { + type Error = &'static str; + fn try_from(s: String) -> Result { + Self::try_new(s) + } +} + +impl<'a> TryFrom<&'a str> for Id { + type Error = &'static str; + fn try_from(s: &'a str) -> Result { + Self::try_new(s) + } +} + +impl From for Id { + fn from(c: Class) -> Self { + Id(c.to_string()) + } +} + +impl<'a> From<&'a Class> for Id { + fn from(c: &'a Class) -> Self { + Id(c.to_string()) + } +} + +impl Display for Id { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + Display::fmt(&self.0, f) + } +} + +impl Deref for Id { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/typed-html/src/types/mod.rs b/typed-html/src/types/mod.rs new file mode 100644 index 0000000..4c64ff7 --- /dev/null +++ b/typed-html/src/types/mod.rs @@ -0,0 +1,70 @@ +mod class; +pub use self::class::Class; + +mod id; +pub use self::id::Id; + +mod classlist; +pub use self::classlist::ClassList; + +pub use http::Uri; +pub use language_tags::LanguageTag; +pub use mime::Mime; + +#[derive(EnumString, Display)] +pub enum CrossOrigin { + #[strum(to_string = "anonymous")] + Anonymous, + #[strum(to_string = "use-credentials")] + UseCredentials, +} + +enum_set_type! { + #[derive(EnumString, Display)] + pub enum LinkType { + #[strum(to_string = "alternate")] + Alternate, + #[strum(to_string = "author")] + Author, + #[strum(to_string = "bookmark")] + Bookmark, + #[strum(to_string = "canonical")] + Canonical, + #[strum(to_string = "external")] + External, + #[strum(to_string = "help")] + Help, + #[strum(to_string = "icon")] + Icon, + #[strum(to_string = "license")] + License, + #[strum(to_string = "manifest")] + Manifest, + #[strum(to_string = "modulepreload")] + ModulePreload, + #[strum(to_string = "next")] + Next, + #[strum(to_string = "nofollow")] + NoFollow, + #[strum(to_string = "noopener")] + NoOpener, + #[strum(to_string = "noreferrer")] + NoReferrer, + #[strum(to_string = "pingback")] + PingBack, + #[strum(to_string = "prefetch")] + Prefetch, + #[strum(to_string = "preload")] + Preload, + #[strum(to_string = "prev")] + Prev, + #[strum(to_string = "search")] + Search, + #[strum(to_string = "shortlink")] + ShortLink, + #[strum(to_string = "stylesheet")] + StyleSheet, + #[strum(to_string = "tag")] + Tag, + } +}