use std::collections::HashMap;
use static_assertions::assert_obj_safe;
use crate::{rules::ParserRuleObjBox, BBParser, Token, TokenKind};
pub trait HtmlTagWriter<CustomTy = ()>
where
CustomTy: Clone + 'static,
{
fn match_tag(&self, tag: &str) -> bool;
fn open_tag(
&self,
tk_writer: &dyn HtmlTokenWriter<CustomTy>,
token: &Token<'_, CustomTy>,
out: &mut String,
);
fn close_tag<'a>(
&self,
tk_writer: &dyn HtmlTokenWriter<CustomTy>,
open_token: &Token<'a, CustomTy>,
close_token: &Token<'a, CustomTy>,
out: &mut String,
);
fn standalone_tag(
&self,
tk_writer: &dyn HtmlTokenWriter<CustomTy>,
token: &Token<'_, CustomTy>,
out: &mut String,
);
fn try_special<'a>(
&self,
_token: &'_ Token<'a, CustomTy>,
) -> Option<ParserRuleObjBox<'a, CustomTy>> {
None
}
}
assert_obj_safe!(HtmlTagWriter);
pub trait HtmlTokenWriter<CustomTy = ()>
where
CustomTy: Clone,
{
fn write_token(&self, token: &Token<'_, CustomTy>, out: &mut String);
}
#[derive(Copy, Clone, Debug, Default)]
pub struct SimpleHtmlWriter;
impl HtmlTokenWriter<()> for SimpleHtmlWriter {
fn write_token(&self, token: &Token<'_, ()>, out: &mut String) {
out.push_str(&html_escape::encode_safe(token.span));
}
}
pub struct HtmlSerializer<Writer = SimpleHtmlWriter, CustomTy = ()>
where
CustomTy: Clone,
Writer: HtmlTokenWriter<CustomTy>,
{
writer: Writer,
tag_impls: Vec<Box<dyn HtmlTagWriter<CustomTy>>>,
tag_cache: HashMap<String, usize>,
}
impl<Writer> HtmlSerializer<Writer>
where
Writer: HtmlTokenWriter<()> + Default,
{
pub fn empty() -> Self {
Self::with_tags(vec![])
}
pub fn with_tags(tags: Vec<Box<dyn HtmlTagWriter<()>>>) -> Self {
Self::custom_with_tags(tags)
}
}
impl<Writer, CustomTy> HtmlSerializer<Writer, CustomTy>
where
CustomTy: Clone,
Writer: HtmlTokenWriter<CustomTy> + Default,
{
pub fn custom_with_tags(tags: Vec<Box<dyn HtmlTagWriter<CustomTy>>>) -> Self {
Self::custom(tags, Writer::default())
}
}
impl<Writer, CustomTy> HtmlSerializer<Writer, CustomTy>
where
CustomTy: Clone,
Writer: HtmlTokenWriter<CustomTy>,
{
pub fn custom(tag_impls: Vec<Box<dyn HtmlTagWriter<CustomTy>>>, writer: Writer) -> Self {
Self {
tag_impls,
writer,
tag_cache: Default::default(),
}
}
}
impl<Writer, CustomTy> HtmlSerializer<Writer, CustomTy>
where
CustomTy: Clone + 'static,
Writer: HtmlTokenWriter<CustomTy>,
{
pub fn serialize(&mut self, mut parser: BBParser<'_, CustomTy>) -> String {
let mut out = String::with_capacity(parser.remaining().len());
'outer: while let Some(tk) = parser.next() {
match tk.kind {
TokenKind::OpenBBTag(_)
| TokenKind::CloseBBTag(_, Some(_))
| TokenKind::StandaloneBBTag(_) => {
let Some(writer) = self.get_writer_for_tag(tk.tag_name().unwrap()) else {
self.writer.write_token(&tk, &mut out);
continue 'outer;
};
match tk.kind {
TokenKind::OpenBBTag(_) => writer.open_tag(&self.writer, &tk, &mut out),
TokenKind::CloseBBTag(_, Some(other)) => writer.close_tag(
&self.writer,
&parser.closed_tags()[other],
&tk,
&mut out,
),
TokenKind::StandaloneBBTag(_) => {
writer.standalone_tag(&self.writer, &tk, &mut out)
}
_ => unreachable!(),
}
if let Some(r) = writer.try_special(&tk) {
parser.push_rule_obj(r);
}
}
_ => self.writer.write_token(&tk, &mut out),
}
}
'outer: for tk in parser.open_tags() {
let Some(writer) = self.get_writer_for_tag(tk.tag_name().unwrap()) else {
self.writer.write_token(&tk, &mut out);
continue 'outer;
};
let Token { kind: TokenKind::OpenBBTag(tag_data, ..), .. } = tk else { unreachable!() };
let fake_close = Token {
span: tk.span,
start: tk.start,
kind: TokenKind::CloseBBTag(tag_data.clone(), None)
};
writer.close_tag(&self.writer, tk, &fake_close, &mut out);
}
out
}
}
impl<Writer, CustomTy> HtmlSerializer<Writer, CustomTy>
where
CustomTy: Clone + 'static,
Writer: HtmlTokenWriter<CustomTy>,
{
pub fn register_tags(&mut self, tags: &mut Vec<Box<dyn HtmlTagWriter<CustomTy>>>) {
self.tag_impls.append(tags);
}
pub fn register_tag(&mut self, tag: Box<dyn HtmlTagWriter<CustomTy>>) {
self.tag_impls.push(tag);
}
pub fn get_writer_for_tag(&self, tag_name: &str) -> Option<&dyn HtmlTagWriter<CustomTy>> {
if let Some(imp) = self.tag_cache.get(tag_name) {
return Some(self.tag_impls[*imp].as_ref());
}
let idx: Option<usize> = 'idx: {
for (idx, imp) in self.tag_impls.iter().enumerate() {
if imp.match_tag(tag_name) {
break 'idx Some(idx);
}
}
None
};
idx.map(|idx| self.tag_impls[idx].as_ref())
}
}
pub mod builtins;
#[cfg(test)]
mod tests;