Such work.

This commit is contained in:
Bodil Stokke 2018-10-28 00:48:34 +01:00
parent 858b16cf34
commit 2806bde178
12 changed files with 686 additions and 29 deletions

View File

@ -15,8 +15,22 @@ pub fn global_attrs(span: Span) -> StringyMap<Ident, TokenStream> {
{
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
}

View File

@ -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<Item = (TokenTree, TokenStream, TokenTree)> + '_ {
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<String, String>,
));
@ -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<impl Parser<'a, TokenTree, Output = Vec<(Ident, TokenStream)>>>
{
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<TokenTree> = 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()
})
}

View File

@ -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<Ident, TokenTree>) -> StringyMap<St
data
}
fn process_value(value: &TokenTree) -> 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<impl Parser<'a, TokenTree, Output = Element
}
fn attr_value<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenTree>> {
literal().map(TokenTree::Literal) | ident().map(TokenTree::Ident)
literal().map(TokenTree::Literal) | dotted_ident() | group().map(TokenTree::Group)
}
fn attr<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = (Ident, TokenTree)>> {

View File

@ -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<impl Parser<'a, I, Output = A>> {
comb(move |_, start| Ok((value.clone(), start)))
@ -81,6 +81,20 @@ pub fn type_spec<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenSt
valid.repeat(1..).collect().map(to_stream)
}
pub fn dotted_ident<'a>() -> Combinator<impl Parser<'a, TokenTree, Output = TokenTree>> {
(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<impl Parser<'a, TokenTree, Output = Ident>> {

View File

@ -5,4 +5,9 @@ authors = ["Bodil Stokke <bodil@bodil.org>"]
[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"

View File

@ -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!(
<html>
<head>
<title>"Hello Kitty!"</title>
<link rel=LinkType::StyleSheet href="lol.css"/>
</head>
<body>
<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>
<h1 data-lol="omg" data-foo=wibble.foo>"Hello Kitty!"</h1>
<p class=splain_class>
"She is not a "<em>"cat"</em>". She is a "<em>"human girl"</em>"."
</p>
<p class=["foo", "bar"]>{the_big_question}</p>
{
(1..4).map(|i| {
html!(<p>{ text!("{}. Ceci n'est pas une chatte.", i) }</p>)

View File

@ -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);

View File

@ -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;

View File

@ -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<S: Into<String>>(id: S) -> Result<Self, &'static str> {
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<S: Into<String>>(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<String> for Class {
type Error = &'static str;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::try_new(s)
}
}
impl<'a> TryFrom<&'a str> for Class {
type Error = &'static str;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
Self::try_new(s)
}
}
impl From<Id> 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
}
}

View File

@ -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<Class>);
impl ClassList {
pub fn new() -> Self {
ClassList(BTreeSet::new())
}
}
impl Default for ClassList {
fn default() -> Self {
Self::new()
}
}
impl FromIterator<Class> for ClassList {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = Class>,
{
ClassList(iter.into_iter().collect())
}
}
impl<'a> FromIterator<&'a Class> for ClassList {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = &'a Class>,
{
ClassList(iter.into_iter().cloned().collect())
}
}
impl FromIterator<String> for ClassList {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = String>,
{
ClassList(iter.into_iter().map(Class::new).collect())
}
}
impl<'a> FromIterator<&'a str> for ClassList {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = &'a str>,
{
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<Class>;
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);

View File

@ -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<S: Into<String>>(id: S) -> Result<Self, &'static str> {
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<S: Into<String>>(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<String> for Id {
type Error = &'static str;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::try_new(s)
}
}
impl<'a> TryFrom<&'a str> for Id {
type Error = &'static str;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
Self::try_new(s)
}
}
impl From<Class> 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
}
}

View File

@ -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,
}
}