-
-
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>") | |
// ) | |
// } | |
} |
Oh epic I made a typo in file name and so it will be like that forever. Welp 🤷♀️
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
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_children
needs 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
Iterator
that owns aString
(or some other times a&'static str
but that case is fine) that can yield back references (&str
) to thatString
we 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 LendingIterator
s this won't fit with our current implementation because the Cow
s 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 LendingIterator
s are stable and see what I can do about it!
Done in v3IntoAttributes
needs to be changed to be similar toIntoHtml
. (No longer be a supertrait and has anESCAPED
constant to determine whether its content has been escaped or needs escaping)You can addDone in v2Tag::Fragment
and removeOption<>
wrapper aroundtag:
in HtmlTag.Fragment
'sstarting()
andending()
should both be""
. And then text can just useTag::Fragment
.To support more thanKinda done in v3, see below&'static str
you could switch toCow<'static, str>
which imposes a bit of extra branching but allows supporting both static string slices and owned strings at the same time