use std::fmt::Write;
use std::{rc::Rc, cell::RefCell};
use crate::parser::*;
fn escape_html(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for c in text.chars() {
match c {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
_ => result.push(c),
}
}
result
}
fn escape_href(href: &str) -> String {
let href = href.trim();
if !href.starts_with("http://") && !href.starts_with("https://") {
format!("https://{}", href)
} else {
href.to_string()
}
}
#[derive(Debug, Clone)]
struct Link(Rc<RefCell<Option<LinkData>>>);
#[derive(Debug)]
struct LinkData {
href: String,
replacer: String,
}
impl Link {
fn new(id: u32) -> Self {
let data = LinkData {
href: String::new(),
replacer: format!("§§HREF{id}§§"),
};
Self(Rc::new(RefCell::new(Some(data))))
}
fn append(&self, text: &str) {
let mut reference = self.0.borrow_mut();
let data = reference.as_mut().expect("link already taken");
data.href.push_str(text)
}
fn replacer(&self) -> String {
let reference = self.0.borrow();
let data = reference.as_ref().expect("link already taken");
data.replacer.clone()
}
fn take(&self) -> LinkData {
self.0.take().expect("link already taken")
}
}
impl PartialEq<Link> for Link {
fn eq(&self, other: &Link) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Debug, Clone, PartialEq)]
enum Element {
Strong,
Em,
Ins,
Del,
Span { color: Color },
A { link: Link },
}
#[derive(Default, Debug)]
struct Renderer {
html: String,
elements: Vec<Element>,
link_counter: u32,
link_list: Vec<Link>,
is_editor: bool,
}
macro_rules! write_html {
($self:ident, $($arg:tt)*) => {
write!(&mut $self.html, $($arg)*).unwrap()
}
}
macro_rules! write_meta {
($self:ident, $($arg:tt)*) => {
if $self.is_editor {
write_html!($self, "<span class=\"sillycode-meta\">");
write_html!($self, $($arg)*);
write_html!($self, "</span>");
}
};
}
impl Renderer {
fn new() -> Self {
Self::default()
}
fn open(&mut self, element: &Element) {
match element {
Element::Strong => write_html!(self, "<strong>"),
Element::Em => write_html!(self, "<em>"),
Element::Ins => write_html!(self, "<ins>"),
Element::Del => write_html!(self, "<del>"),
Element::Span { color } => {
write_html!(self, "<span style=\"color: {color}\">");
}
Element::A { link } => {
write_html!(self, "<a href=\"{}\">", link.replacer());
}
}
}
fn close(&mut self, element: &Element) {
match element {
Element::Strong => write_html!(self, "</strong>"),
Element::Em => write_html!(self, "</em>"),
Element::Ins => write_html!(self, "</ins>"),
Element::Del => write_html!(self, "</del>"),
Element::Span { color: _ } => write_html!(self, "</span>"),
Element::A { link: _ } => write_html!(self, "</a>"),
}
}
fn open_all(&mut self, elements: &[Element]) {
for element in elements.iter() {
self.open(element);
}
}
fn close_all(&mut self, elements: &[Element]) {
for element in elements.iter().rev() {
self.close(element);
}
}
fn push(&mut self, element: Element) {
self.open(&element);
self.elements.push(element);
}
fn contains(&self, element: &Element) -> bool {
self.elements.contains(element)
}
fn remove(&mut self, predicate: impl Fn(&Element) -> bool) -> bool {
for i in (0..self.elements.len()).rev() {
if predicate(&self.elements[i]) {
let removed: Element = self.elements.remove(i);
let preserved: Vec<Element> = self.elements[i..self.elements.len()].into();
self.close_all(&preserved);
self.close(&removed);
self.open_all(&preserved);
return true;
}
}
false
}
fn apply(&mut self, element: Element, enable: bool) {
if enable {
if !self.contains(&element) {
self.push(element);
}
} else {
self.remove(|e| e == &element);
}
}
fn push_link(&mut self) {
let link = Link::new(self.link_counter);
self.link_counter += 1;
self.link_list.push(link.clone());
self.push(Element::A { link });
}
fn append_link(&mut self, text: &str) {
for element in self.elements.iter() {
if let Element::A { link } = element {
link.append(text);
}
}
}
fn on_text(&mut self, text: &str) {
let text = escape_html(text);
write_html!(self, "{text}");
self.append_link(text.as_str());
}
fn on_escape(&mut self) {
write_meta!(self, "\\");
}
fn on_newline(&mut self) {
let elements = self.elements.clone();
self.close_all(&elements);
write_html!(self, "</div><div>");
self.open_all(&elements);
}
fn on_style(&mut self, style: StyleKind, enable: bool) {
if style == StyleKind::Link {
if enable {
write_meta!(self, "[url]");
self.push_link();
} else {
self.remove(|e| matches!(e, Element::A { .. }));
write_meta!(self, "[/url]");
}
} else {
if enable {
write_meta!(self, "[{}]", style.to_tag());
}
match style {
StyleKind::Bold => self.apply(Element::Strong, enable),
StyleKind::Italic => self.apply(Element::Em, enable),
StyleKind::Underline => self.apply(Element::Ins, enable),
StyleKind::Strikethrough => self.apply(Element::Del, enable),
_ => unreachable!(),
}
if !enable {
write_meta!(self, "[/{}]", style.to_tag());
}
}
}
fn on_color(&mut self, color: Color, enable: bool) {
if enable {
write_meta!(self, "[color={color}]");
self.push(Element::Span { color });
} else {
self.remove(|e| matches!(e, Element::Span { .. }));
write_meta!(self, "[/color]");
}
}
fn on_emote(&mut self, emote: EmoteKind) {
let tag = emote.to_tag();
let name = emote.to_name();
let path = format!("/static/emoticons/{}.png", name);
if self.is_editor {
write_html!(self, "<span class=\"sillycode-emote\" style=\"background-image: url({path})\">[{tag}]</span>");
} else {
write_html!(self, "<img class=\"sillycode-emote\" src=\"{path}\" alt=\"{name}\">");
}
}
fn render(mut self, parts: impl IntoIterator<Item = Part>) -> String {
write_html!(self, "<div>");
for part in parts {
match part {
Part::Text(text) => self.on_text(&text),
Part::Escape => self.on_escape(),
Part::Newline => self.on_newline(),
Part::Style(style, enable) => self.on_style(style, enable),
Part::Color(color, enable) => self.on_color(color, enable),
Part::Emote(emote) => self.on_emote(emote),
}
}
self.close_all(&self.elements.clone());
write_html!(self, "</div>");
for link in self.link_list.iter().map(|link| link.take()) {
self.html = self.html.replace(&link.replacer, &escape_href(&link.href));
}
self.html = self.html
.replace("<div> ", "<div> ")
.replace(" </div>", " <br></div>")
.replace("<div></div>", "<div><br></div>");
self.html
}
}
pub fn render(parts: impl IntoIterator<Item = Part>, is_editor: bool) -> String {
let mut renderer = Renderer::new();
renderer.is_editor = is_editor;
renderer.render(parts)
}