use super::attributes::AddedAttributes;
use super::context::HtmlContext;
use super::render::ItemRender;
use std::collections::HashSet;
macro_rules! tag_method {
($tag:tt) => {
pub fn $tag(self) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
self.tag(stringify!($tag))
}
};
}
const SOLO_HTML_TAGS: [&str; 14] = [
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
];
#[derive(Debug)]
pub struct HtmlBuilder<'c, 'i, 'h, 'e, 't>
where
'e: 't,
{
ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>,
}
impl<'c, 'i, 'h, 'e, 't> HtmlBuilder<'c, 'i, 'h, 'e, 't>
where
'e: 't,
{
#[inline]
pub fn new(ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>) -> Self {
HtmlBuilder { ctx }
}
#[inline]
pub fn tag(self, tag: &'t str) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
debug_assert!(is_alphanumeric(tag));
let HtmlBuilder { ctx } = self;
HtmlBuilderTag::new(ctx, tag)
}
#[inline]
pub fn element(self, tag: &'t str) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
debug_assert!(tag.starts_with("wj-"));
self.tag(tag)
}
#[inline]
pub fn table_cell(self, header: bool) -> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
if header {
self.tag("th")
} else {
self.tag("td")
}
}
pub fn sprite(self, id: &'t str) {
let viewbox = match id {
"wj-karma" => "0 0 64 114",
_ => "0 0 24 24",
};
let class = format!("wj-sprite sprite-{id}");
let href = format!("/files--static/media/ui.svg#{id}");
self.tag("svg")
.attr(attr!(
"class" => &class,
"viewBox" => viewbox,
))
.inner(|ctx| {
ctx.html().tag("use").attr(attr!("href" => &href));
});
}
tag_method!(a);
tag_method!(br);
tag_method!(code);
tag_method!(dd);
tag_method!(details);
tag_method!(div);
tag_method!(dl);
tag_method!(dt);
tag_method!(hr);
tag_method!(iframe);
tag_method!(img);
tag_method!(input);
tag_method!(li);
tag_method!(ol);
tag_method!(pre);
tag_method!(rp);
tag_method!(script);
tag_method!(style);
tag_method!(span);
tag_method!(sub);
tag_method!(sup);
tag_method!(summary);
tag_method!(table);
tag_method!(tbody);
tag_method!(tr);
tag_method!(ul);
#[inline]
pub fn text(&mut self, text: &str) {
self.ctx.push_escaped(text);
}
}
#[derive(Debug)]
pub struct HtmlBuilderTag<'c, 'i, 'h, 'e, 't>
where
'e: 't,
{
ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>,
tag: &'t str,
in_tag: bool,
in_contents: bool,
}
impl<'c, 'i, 'h, 'e, 't> HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
pub fn new(ctx: &'c mut HtmlContext<'i, 'h, 'e, 't>, tag: &'t str) -> Self {
ctx.push_raw('<');
ctx.push_raw_str(tag);
HtmlBuilderTag {
ctx,
tag,
in_tag: true,
in_contents: false,
}
}
fn attr_key(&mut self, key: &str, has_value: bool) {
debug_assert!(is_alphanumeric(key));
debug_assert!(self.in_tag);
self.ctx.push_raw(' ');
self.ctx.push_escaped(key);
if has_value {
self.ctx.push_raw('=');
}
}
fn attr_value(&mut self, value_parts: &[&str]) {
self.ctx.push_raw('"');
for part in value_parts {
self.ctx.push_escaped(part);
}
self.ctx.push_raw('"');
}
pub fn attr_single(&mut self, key: &str, value_parts: &[&str]) -> &mut Self {
let has_value = !value_parts.iter().all(|s| s.is_empty());
self.attr_key(key, has_value);
if has_value {
self.attr_value(value_parts);
}
self
}
pub fn attr(&mut self, attributes: AddedAttributes) -> &mut Self {
fn filter_entries<'a>(
attributes: &AddedAttributes<'a>,
) -> impl Iterator<Item = (&'a str, &'a [&'a str])> {
attributes.entries.iter().filter_map(
|(item, accept)| {
if *accept {
Some(*item)
} else {
None
}
},
)
}
let mut merged = HashSet::new();
let mut merged_value = Vec::new();
if let Some(attribute_map) = attributes.map {
let attribute_map = attribute_map.get();
for (key, value_parts) in filter_entries(&attributes) {
if let Some(map_value) = attribute_map.get(&cow!(key)) {
merged_value.clear();
merged_value.extend(value_parts);
merged_value.push(" ");
merged_value.push(map_value);
self.attr_single(key, &merged_value);
merged.insert(key);
}
}
}
for (key, value_parts) in filter_entries(&attributes) {
if !merged.contains(key) {
self.attr_single(key, value_parts);
}
}
if let Some(attribute_map) = attributes.map {
for (key, value) in attribute_map.get() {
if !merged.contains(key.as_ref()) {
self.attr_single(key, &[value]);
}
}
}
self
}
fn content_start(&mut self) {
if self.in_tag {
self.ctx.push_raw('>');
self.in_tag = false;
}
assert!(!self.in_contents, "Already in tag contents");
self.in_contents = true;
}
#[inline]
pub fn contents<R: ItemRender>(&mut self, item: R) -> &mut Self {
self.content_start();
item.render(self.ctx);
self
}
pub fn inner<F>(&mut self, mut f: F) -> &mut Self
where
F: FnMut(&mut HtmlContext),
{
self.content_start();
f(self.ctx);
self
}
}
impl<'c, 'i, 'h, 'e, 't> Drop for HtmlBuilderTag<'c, 'i, 'h, 'e, 't> {
fn drop(&mut self) {
if self.in_tag && !self.in_contents {
self.ctx.push_raw('>');
}
if should_close_tag(self.tag) {
self.ctx.push_raw_str("</");
self.ctx.push_raw_str(self.tag);
self.ctx.push_raw('>');
}
}
}
fn is_alphanumeric(value: &str) -> bool {
value
.chars()
.all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '-')
}
#[inline]
fn should_close_tag(tag: &str) -> bool {
!SOLO_HTML_TAGS.contains(&tag)
}