-
-
Save Mathspy/4df19d411a1eaa5a7f0a16d1d19bd967 to your computer and use it in GitHub Desktop.
| #![no_std] | |
| extern crate alloc; | |
| use alloc::borrow::Cow; | |
| use core::iter; | |
| enum Tag { | |
| /// A non-HTML tag that renders into nothing for wrapping text | |
| Fragment, | |
| Div, | |
| Strong, | |
| Em, | |
| P, | |
| Span, | |
| } | |
| impl Tag { | |
| fn starting(&self) -> &'static str { | |
| match self { | |
| Tag::Fragment => "", | |
| Tag::Div => "<div", | |
| Tag::Strong => "<strong", | |
| Tag::Em => "<em", | |
| Tag::P => "<p", | |
| Tag::Span => "<span", | |
| } | |
| } | |
| fn ending(&self) -> &'static str { | |
| match self { | |
| Tag::Fragment => "", | |
| Tag::Div => "</div>", | |
| Tag::Strong => "</strong>", | |
| Tag::Em => "</em>", | |
| Tag::P => "</p>", | |
| Tag::Span => "</span>", | |
| } | |
| } | |
| } | |
| pub struct Wrapper<I, T> | |
| where | |
| I: Iterator<Item = T>, | |
| { | |
| before: Option<T>, | |
| wrapped: I, | |
| after: Option<T>, | |
| } | |
| impl<I, T> Iterator for Wrapper<I, T> | |
| where | |
| I: Iterator<Item = T>, | |
| { | |
| type Item = T; | |
| fn next(&mut self) -> Option<Self::Item> { | |
| if self.before.is_some() { | |
| return self.before.take(); | |
| } | |
| if let Some(item) = self.wrapped.next() { | |
| return Some(item); | |
| } | |
| self.after.take() | |
| } | |
| } | |
| pub trait IntoHtml { | |
| const ESCAPED: bool = false; | |
| type HtmlIter: Iterator<Item = Cow<'static, str>>; | |
| fn into_html(self) -> Self::HtmlIter; | |
| } | |
| impl<T> IntoHtml for T | |
| where | |
| T: IntoIterator<Item = Cow<'static, str>>, | |
| { | |
| const ESCAPED: bool = false; | |
| type HtmlIter = T::IntoIter; | |
| fn into_html(self) -> Self::HtmlIter { | |
| self.into_iter() | |
| } | |
| } | |
| pub trait IsEscaped: IntoHtml { | |
| const CHECK: (); | |
| } | |
| impl<T: IntoHtml + ?Sized> IsEscaped for T { | |
| const CHECK: () = [()][(Self::ESCAPED == true) as usize]; | |
| } | |
| pub trait IntoAttributes { | |
| const ESCAPED: bool = false; | |
| type AttributeIter: Iterator<Item = (Cow<'static, str>, Cow<'static, str>)>; | |
| fn into_attributes(self) -> Self::AttributeIter; | |
| } | |
| pub struct ConvertAttributes<I> { | |
| iter: I, | |
| } | |
| impl<I, T> Iterator for ConvertAttributes<I> | |
| where | |
| I: Iterator<Item = (T, T)>, | |
| T: Into<Cow<'static, str>>, | |
| { | |
| type Item = (Cow<'static, str>, Cow<'static, str>); | |
| fn next(&mut self) -> Option<Self::Item> { | |
| if let Some((a, v)) = self.iter.next() { | |
| return Some((a.into(), v.into())); | |
| } else { | |
| return None; | |
| } | |
| } | |
| } | |
| impl<I, T> IntoAttributes for I | |
| where | |
| I: IntoIterator<Item = (T, T)>, | |
| T: Into<Cow<'static, str>>, | |
| { | |
| const ESCAPED: bool = false; | |
| type AttributeIter = ConvertAttributes<I::IntoIter>; | |
| fn into_attributes(self) -> Self::AttributeIter { | |
| ConvertAttributes { | |
| iter: self.into_iter(), | |
| } | |
| } | |
| } | |
| #[inline] | |
| fn escape_text(text: &'static str) -> impl IntoHtml { | |
| text.char_indices() | |
| .map(|(index, c)| match c { | |
| '&' => "&", | |
| '<' => "<", | |
| '>' => ">", | |
| x => text.get(index..index + x.len_utf8()).unwrap(), | |
| }) | |
| .map(Cow::from) | |
| } | |
| pub struct HtmlTag<C, A> { | |
| tag: Tag, | |
| children: C, | |
| attributes: A, | |
| } | |
| impl HtmlTag<iter::Empty<Cow<'static, str>>, iter::Empty<(Cow<'static, str>, Cow<'static, str>)>> { | |
| pub fn text( | |
| text: &'static str, | |
| ) -> HtmlTag<impl IntoHtml, iter::Empty<(Cow<'static, str>, Cow<'static, str>)>> { | |
| HtmlTag { | |
| tag: Tag::Fragment, | |
| children: escape_text(text), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| pub fn escaped_unchecked( | |
| text: &'static str, | |
| ) -> HtmlTag<impl IntoHtml, iter::Empty<(Cow<'static, str>, Cow<'static, str>)>> { | |
| HtmlTag { | |
| tag: Tag::Fragment, | |
| children: iter::once(Cow::from(text)), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| pub fn div() -> Self { | |
| HtmlTag { | |
| tag: Tag::Div, | |
| children: iter::empty(), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| pub fn strong() -> Self { | |
| HtmlTag { | |
| tag: Tag::Strong, | |
| children: iter::empty(), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| pub fn p() -> Self { | |
| HtmlTag { | |
| tag: Tag::P, | |
| children: iter::empty(), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| pub fn em() -> Self { | |
| HtmlTag { | |
| tag: Tag::Em, | |
| children: iter::empty(), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| pub fn span() -> Self { | |
| HtmlTag { | |
| tag: Tag::Span, | |
| children: iter::empty(), | |
| attributes: iter::empty(), | |
| } | |
| } | |
| } | |
| impl<C, A> HtmlTag<C, A> | |
| where | |
| C: IntoHtml, | |
| { | |
| // ) -> HtmlTag<iter::Chain<C::IntoIter, iter::Once<&'static str>>, A> { | |
| pub fn append_text(self, text: &'static str) -> HtmlTag<impl IntoHtml, A> { | |
| HtmlTag { | |
| tag: self.tag, | |
| children: self | |
| .children | |
| .into_html() | |
| .chain(escape_text(text).into_html()), | |
| attributes: self.attributes, | |
| } | |
| } | |
| // ) -> HtmlTag<iter::Chain<C::IntoIter, C2::IntoIter>, A> | |
| pub fn append_child<C2>(self, child: C2) -> HtmlTag<impl IntoHtml, A> | |
| where | |
| C2: IsEscaped, | |
| { | |
| HtmlTag { | |
| tag: self.tag, | |
| children: self.children.into_html().chain(child.into_html()), | |
| attributes: self.attributes, | |
| } | |
| } | |
| } | |
| impl<C, A> HtmlTag<C, A> { | |
| pub fn with_children<C2, C3>(self, children: C2) -> HtmlTag<impl IntoHtml, A> | |
| where | |
| C2: IntoIterator<Item = C3>, | |
| C3: IsEscaped, | |
| { | |
| HtmlTag { | |
| tag: self.tag, | |
| children: children | |
| .into_iter() | |
| .map(|child| child.into_html()) | |
| .flatten(), | |
| attributes: self.attributes, | |
| } | |
| } | |
| // TODO: Needs escaping | |
| pub fn with_attributes<A2>(self, attributes: A2) -> HtmlTag<C, A2> | |
| where | |
| A2: IntoAttributes, | |
| { | |
| HtmlTag { | |
| tag: self.tag, | |
| children: self.children, | |
| attributes, | |
| } | |
| } | |
| } | |
| enum AttributeRenderingSteps { | |
| Start, | |
| RenderedSpace(Cow<'static, str>, Cow<'static, str>), | |
| RenderedName(Cow<'static, str>), | |
| RenderedStartQuote(Cow<'static, str>), | |
| RenderedValue, | |
| } | |
| impl Default for AttributeRenderingSteps { | |
| fn default() -> Self { | |
| AttributeRenderingSteps::Start | |
| } | |
| } | |
| pub struct Attributes<I> { | |
| iter: I, | |
| step: AttributeRenderingSteps, | |
| } | |
| impl<I> Iterator for Attributes<I> | |
| where | |
| I: Iterator<Item = (Cow<'static, str>, Cow<'static, str>)>, | |
| { | |
| type Item = Cow<'static, str>; | |
| fn next(&mut self) -> Option<Self::Item> { | |
| let current_step = core::mem::take(&mut self.step); | |
| match current_step { | |
| AttributeRenderingSteps::Start => { | |
| if let Some((attribute, value)) = self.iter.next() { | |
| self.step = AttributeRenderingSteps::RenderedSpace(attribute, value); | |
| return Some(Cow::from(" ")); | |
| } | |
| return None; | |
| } | |
| AttributeRenderingSteps::RenderedSpace(attribute, value) => { | |
| self.step = AttributeRenderingSteps::RenderedName(value); | |
| return Some(attribute); | |
| } | |
| AttributeRenderingSteps::RenderedName(value) => { | |
| self.step = AttributeRenderingSteps::RenderedStartQuote(value); | |
| return Some(Cow::from("=\"")); | |
| } | |
| AttributeRenderingSteps::RenderedStartQuote(value) => { | |
| self.step = AttributeRenderingSteps::RenderedValue; | |
| return Some(value); | |
| } | |
| AttributeRenderingSteps::RenderedValue => { | |
| self.step = AttributeRenderingSteps::Start; | |
| return Some(Cow::from("\"")); | |
| } | |
| } | |
| } | |
| } | |
| impl<C, A> IntoHtml for HtmlTag<C, A> | |
| where | |
| C: IntoHtml, | |
| A: IntoAttributes, | |
| { | |
| const ESCAPED: bool = true; | |
| type HtmlIter = Wrapper< | |
| core::iter::Chain< | |
| Attributes<A::AttributeIter>, | |
| core::iter::Chain<core::iter::Once<Cow<'static, str>>, C::HtmlIter>, | |
| >, | |
| Cow<'static, str>, | |
| >; | |
| fn into_html(self) -> Self::HtmlIter { | |
| let attributes = Attributes { | |
| iter: self.attributes.into_attributes(), | |
| step: AttributeRenderingSteps::Start, | |
| }; | |
| let children = match self.tag { | |
| Tag::Fragment => iter::once(Cow::from("")).chain(self.children.into_html()), | |
| _ => iter::once(Cow::from(">")).chain(self.children.into_html()), | |
| }; | |
| Wrapper { | |
| before: Some(Cow::from(self.tag.starting())), | |
| wrapped: attributes.chain(children), | |
| after: Some(Cow::from(self.tag.ending())), | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| extern crate std; | |
| use super::{HtmlTag, IntoHtml}; | |
| use std::string::String; | |
| #[test] | |
| fn renders_plain_tags() { | |
| let html = HtmlTag::div(); | |
| assert_eq!( | |
| html.into_html().collect::<String>(), | |
| String::from("<div></div>") | |
| ); | |
| } | |
| #[test] | |
| fn renders_with_string_children() { | |
| let html = HtmlTag::div().append_text("abc"); | |
| assert_eq!( | |
| html.into_html().collect::<String>(), | |
| String::from("<div>abc</div>") | |
| ); | |
| } | |
| #[test] | |
| fn renders_with_nested_tags() { | |
| let html = HtmlTag::div().with_children([HtmlTag::div()]); | |
| assert_eq!( | |
| html.into_html().collect::<String>(), | |
| String::from("<div><div></div></div>") | |
| ); | |
| } | |
| #[test] | |
| fn renders_with_appended_children() { | |
| let html = HtmlTag::strong() | |
| .append_child(HtmlTag::text("STRONG! ")) | |
| .append_child(HtmlTag::em().append_text("and sweet")) | |
| .append_text(" but STRONG!"); | |
| assert_eq!( | |
| html.into_html().collect::<String>(), | |
| String::from("<strong>STRONG! <em>and sweet</em> but STRONG!</strong>") | |
| ); | |
| } | |
| #[test] | |
| fn complex_structure() { | |
| let html = HtmlTag::p().with_children([HtmlTag::strong() | |
| .append_child(HtmlTag::text("You can also nest ")) | |
| .append_child(HtmlTag::em().append_text("italic with")) | |
| .append_child(HtmlTag::text(" bold"))]); | |
| assert_eq!( | |
| html.into_html().collect::<String>(), | |
| String::from("<p><strong>You can also nest <em>italic with</em> bold</strong></p>") | |
| ); | |
| } | |
| #[test] | |
| fn complex_structure_with_attributes() { | |
| let html = HtmlTag::p() | |
| .with_children([HtmlTag::strong() | |
| .append_text("You can also nest ") | |
| .append_child(HtmlTag::em().append_text("italic with")) | |
| .append_text(" bold")]) | |
| .with_attributes([("class", "red"), ("id", "meh")]); | |
| assert_eq!( | |
| html.into_html().collect::<String>(), | |
| String::from( | |
| "<p class=\"red\" id=\"meh\"><strong>You can also nest <em>italic with</em> bold</strong></p>" | |
| ) | |
| ); | |
| } | |
| #[test] | |
| fn it_composes() { | |
| fn composition() -> impl IntoHtml { | |
| HtmlTag::p() | |
| .with_children([HtmlTag::strong() | |
| .append_text("You can also nest ") | |
| .append_child(HtmlTag::em().append_text("italic with")) | |
| .append_text(" bold")]) | |
| .with_attributes([("class", "red"), ("id", "meh")]) | |
| } | |
| fn is_great() -> impl IntoHtml { | |
| HtmlTag::div().with_children((1..3).map(|_| composition())) | |
| } | |
| assert_eq!( | |
| is_great().into_html().collect::<String>(), | |
| String::from( | |
| "<div><p class=\"red\" id=\"meh\"><strong>You can also nest <em>italic with</em> bold</strong></p><p class=\"red\" id=\"meh\"><strong>You can also nest <em>italic with</em> bold</strong></p></div>" | |
| ) | |
| ) | |
| } | |
| #[test] | |
| fn it_does_not_escape_unchecked_escapes() { | |
| let other_things_get_escaped = | |
| HtmlTag::p().append_child(HtmlTag::escaped_unchecked("<escape me!>")); | |
| assert_eq!( | |
| other_things_get_escaped.into_html().collect::<String>(), | |
| String::from("<p><escape me!></p>") | |
| ) | |
| } | |
| // #[test] | |
| // fn it_escapes() { | |
| // let html_tags_do_not_get_escaped = HtmlTag::p().append_child(HtmlTag::div()); | |
| // assert_eq!( | |
| // html_tags_do_not_get_escaped.into_html().collect::<String>(), | |
| // String::from("<p><div></div></p>") | |
| // ); | |
| // let other_things_get_escaped = HtmlTag::p().append_child(std::iter::once("<escape me!>")); | |
| // assert_eq!( | |
| // other_things_get_escaped.into_html().collect::<String>(), | |
| // String::from("<p><escape me!></p>") | |
| // ) | |
| // } | |
| } |
So couple of things
- The first point is fairly straight forward, very little changes required to make it happen
- The third point is incorrect,
with_childrenneeds to implement escaping too. And escaping attributes value is not something you should bother tackling, instead IntoAttributes should be switched toIntoIterator<Item = (Attribute, &'static str)>where Attribute is an enum of all valid HTML attributes - The fourth point kills this project for now. Part of the design and goal fundamentally requires the ability to create an
Iteratorthat owns aString(or some other times a&'static strbut that case is fine) that can yield back references (&str) to thatStringwe own. Sadly this Iterator is not exactly possible with stable Rust today and is one of the motivating goals for GATs.
So with all of that in mind I shall put this little renderer to rest and possibly come back later to it, hopefully with a fresh perspective and more knowledge!
So one way to avoid having to escape except text is to constraint what needs escaping like I have done in v3
The fourth point is still a problem but it's a problem only because we need to do escaping. Ideally if we have a String we should be able to return references from it, but even with LendingIterators this won't fit with our current implementation because the Cows won't be 'static coming from a String.
One way to "dodge" the fourth point at the price of some extra allocation is: in case of &'static str do what we did in v2 escaping. In case of a String, allocate a new String with the characters escaped. This fits the current trait signature but is not ideal.
So yeah I am going to pause work on this and maybe come back to it one day after LendingIterators are stable and see what I can do about it!
After a nap I have decided to actually dog food this library, so now it's going to be used to make my game dev diary