sillycode/
renderer.rs

1use std::fmt::Write;
2use std::{rc::Rc, cell::RefCell};
3
4use crate::parser::*;
5
6/// escapes text so it can be safely used in HTML
7fn escape_html(text: &str) -> String {
8  let mut result = String::with_capacity(text.len());
9  for c in text.chars() {
10    match c {
11      '&' => result.push_str("&"),
12      '<' => result.push_str("&lt;"),
13      '>' => result.push_str("&gt;"),
14      '"' => result.push_str("&quot;"),
15      '\'' => result.push_str("&#39;"),
16      _ => result.push(c),
17    }
18  }
19  result
20}
21
22/// escapes a URL by adding the http(s) protocol if it's not there
23fn escape_href(href: &str) -> String {
24  // trim the input
25  let href = href.trim();
26
27  // add the https protocol if it's not there (allows http too!)
28  if !href.starts_with("http://") && !href.starts_with("https://") {
29    format!("https://{}", href)
30  } else {
31    href.to_string()
32  }
33}
34
35/// represents a reference to the `href` field of a link in the outputted HTML
36#[derive(Debug, Clone)]
37struct Link(Rc<RefCell<Option<LinkData>>>);
38
39/// represents the shared data of a [Link]
40#[derive(Debug)]
41struct LinkData {
42  href: String,
43  replacer: String,
44}
45
46impl Link {
47
48  /// creates a new link with the given id
49  fn new(id: u32) -> Self {
50    let data = LinkData {
51      href: String::new(),
52      replacer: format!("§§HREF{id}§§"),
53    };
54
55    Self(Rc::new(RefCell::new(Some(data))))
56  }
57
58  /// appends text to the link's `href` field
59  fn append(&self, text: &str) {
60    let mut reference = self.0.borrow_mut();
61    let data = reference.as_mut().expect("link already taken");
62    data.href.push_str(text)
63  }
64
65  /// returns the replacer string of the link
66  fn replacer(&self) -> String {
67    let reference = self.0.borrow();
68    let data = reference.as_ref().expect("link already taken");
69    data.replacer.clone()
70  }
71
72  /// returns the link's data, removing it from the link
73  fn take(&self) -> LinkData {
74    self.0.take().expect("link already taken")
75  }
76
77}
78
79impl PartialEq<Link> for Link {
80
81  /// checks if two links are the same via pointer equality
82  fn eq(&self, other: &Link) -> bool {
83    Rc::ptr_eq(&self.0, &other.0)
84  }
85
86}
87
88/// represents a HTML element in the element stack
89#[derive(Debug, Clone, PartialEq)]
90enum Element {
91    Strong,
92    Em,
93    Ins,
94    Del,
95    Span { color: Color },
96    A { link: Link },
97}
98
99/// renderer for sillycode markup
100#[derive(Default, Debug)]
101struct Renderer {
102  /// html output
103  html: String,
104
105  /// element stack
106  elements: Vec<Element>,
107
108  /// counter for link ids
109  link_counter: u32,
110  /// list of all links
111  link_list: Vec<Link>,
112
113  /// whether the output is for an editor or not
114  is_editor: bool,
115}
116
117/// writes HTML to the renderer's buffer
118macro_rules! write_html {
119  ($self:ident, $($arg:tt)*) => {
120    write!(&mut $self.html, $($arg)*).unwrap()
121  }
122}
123
124/// writes "meta" text, usually tags like "[url]" or "[b]",
125/// wrapped in a span, to the renderer's buffer, if isEditor is true
126macro_rules! write_meta {
127  ($self:ident, $($arg:tt)*) => {
128    if $self.is_editor {
129      write_html!($self, "<span class=\"sillycode-meta\">");
130      write_html!($self, $($arg)*);
131      write_html!($self, "</span>");
132    }
133  };
134}
135
136impl Renderer {
137
138  /// creates a new renderer
139  fn new() -> Self {
140    Self::default()
141  }
142
143  /// opens an element
144  fn open(&mut self, element: &Element) {
145    match element {
146      Element::Strong => write_html!(self, "<strong>"),
147      Element::Em => write_html!(self, "<em>"),
148      Element::Ins => write_html!(self, "<ins>"),
149      Element::Del => write_html!(self, "<del>"),
150      Element::Span { color } => {
151        write_html!(self, "<span style=\"color: {color}\">");
152      }
153      Element::A { link } => {
154        write_html!(self, "<a href=\"{}\">", link.replacer());
155      }
156    }
157  }
158
159  /// closes an element
160  fn close(&mut self, element: &Element) {
161    match element {
162      Element::Strong => write_html!(self, "</strong>"),
163      Element::Em => write_html!(self, "</em>"),
164      Element::Ins => write_html!(self, "</ins>"),
165      Element::Del => write_html!(self, "</del>"),
166      Element::Span { color: _ } => write_html!(self, "</span>"),
167      Element::A { link: _ } => write_html!(self, "</a>"),
168    }
169  }
170
171  /// opens all elements in the element stack
172  fn open_all(&mut self, elements: &[Element]) {
173    for element in elements.iter() {
174      self.open(element);
175    }
176  }
177
178  /// closes all elements in the element stack in reverse order
179  fn close_all(&mut self, elements: &[Element]) {
180    for element in elements.iter().rev() {
181      self.close(element);
182    }
183  }
184
185  /// pushes an element onto the element stack
186  fn push(&mut self, element: Element) {
187    self.open(&element);
188    self.elements.push(element);
189  }
190
191  /// checks if the element stack contains an element
192  fn contains(&self, element: &Element) -> bool {
193    self.elements.contains(element)
194  }
195
196  /// removes an element from the element stack
197  fn remove(&mut self, predicate: impl Fn(&Element) -> bool) -> bool {
198    for i in (0..self.elements.len()).rev() {
199      if predicate(&self.elements[i]) {
200        // remove the element from the stack
201        let removed: Element = self.elements.remove(i);
202        // select all preserved elements
203        let preserved: Vec<Element> = self.elements[i..self.elements.len()].into();
204
205        // close all preserved elements, in reverse order
206        self.close_all(&preserved);
207
208        // close the removed element
209        self.close(&removed);
210
211        // re-open all preserved elements
212        self.open_all(&preserved);
213
214        return true;
215      }
216    }
217
218    false
219  }
220
221  /// applies a specific style element,
222  /// possibly pushing/popping from the element stack
223  fn apply(&mut self, element: Element, enable: bool) {
224    if enable {
225      if !self.contains(&element) {
226        self.push(element);
227      }
228    } else {
229      self.remove(|e| e == &element);
230    }
231  }
232
233  /// creates a new link and adds it to the link list,
234  /// then pushes it to the element stack
235  fn push_link(&mut self) {
236    let link = Link::new(self.link_counter);
237    self.link_counter += 1;
238    self.link_list.push(link.clone());
239    self.push(Element::A { link });
240  }
241
242  /// appends to all links in the element stack
243  fn append_link(&mut self, text: &str) {
244    for element in self.elements.iter() {
245      if let Element::A { link } = element {
246        link.append(text);
247      }
248    }
249  }
250
251  /// handles text parts
252  fn on_text(&mut self, text: &str) {
253    // escape the text for HTML
254    let text = escape_html(text);
255
256    // append the text to the HTML output
257    write_html!(self, "{text}");
258
259    // update the link hrefs
260    self.append_link(text.as_str());
261  }
262
263  /// handles escape parts
264  fn on_escape(&mut self) {
265    // emit the backslash if isEditor is true
266    // we don't need to do anything else! it's handled by the parser
267    write_meta!(self, "\\");
268  }
269
270  /// handles newline parts
271  fn on_newline(&mut self) {
272    // clone the elements to avoid borrowing issues, sorry rust
273    let elements = self.elements.clone();
274
275    // close all elements used for styling to get back to the root of the tree
276    self.close_all(&elements);
277
278    // close and open a new div to start a new line
279    write_html!(self, "</div><div>");
280
281    // re-open all elements
282    self.open_all(&elements);
283  }
284
285  /// handles style parts
286  fn on_style(&mut self, style: StyleKind, enable: bool) {
287    // links are a special case
288    if style == StyleKind::Link {
289      if enable {
290        write_meta!(self, "[url]");
291        self.push_link();
292      } else {
293        self.remove(|e| matches!(e, Element::A { .. }));
294        write_meta!(self, "[/url]");
295      }
296    // all other styles are handled by apply
297    } else {
298      if enable {
299        write_meta!(self, "[{}]", style.to_tag());
300      }
301
302      match style {
303          StyleKind::Bold => self.apply(Element::Strong, enable),
304          StyleKind::Italic => self.apply(Element::Em, enable),
305          StyleKind::Underline => self.apply(Element::Ins, enable),
306          StyleKind::Strikethrough => self.apply(Element::Del, enable),
307          _ => unreachable!(),
308      }
309
310      if !enable {
311        write_meta!(self, "[/{}]", style.to_tag());
312      }
313    }
314  }
315
316  /// handles color parts
317  fn on_color(&mut self, color: Color, enable: bool) {
318    if enable {
319      write_meta!(self, "[color={color}]");
320      self.push(Element::Span { color });
321    } else {
322      self.remove(|e| matches!(e, Element::Span { .. }));
323      write_meta!(self, "[/color]");
324    }
325  }
326
327  /// handles emote parts
328  fn on_emote(&mut self, emote: EmoteKind) {
329    let tag = emote.to_tag();
330    let name = emote.to_name();
331    let path = format!("/static/emoticons/{}.png", name);
332    if self.is_editor {
333      write_html!(self, "<span class=\"sillycode-emote\" style=\"background-image: url({path})\">[{tag}]</span>");
334    } else {
335      write_html!(self, "<img class=\"sillycode-emote\" src=\"{path}\" alt=\"{name}\">");
336    }
337  }
338
339  /// renders a bunch of parts as HTML
340  fn render(mut self, parts: impl IntoIterator<Item = Part>) -> String {
341    // start the output
342    write_html!(self, "<div>");
343
344    // render the parts
345    for part in parts {
346      match part {
347        Part::Text(text) => self.on_text(&text),
348        Part::Escape => self.on_escape(),
349        Part::Newline => self.on_newline(),
350        Part::Style(style, enable) => self.on_style(style, enable),
351        Part::Color(color, enable) => self.on_color(color, enable),
352        Part::Emote(emote) => self.on_emote(emote),
353      }
354    }
355
356    // close all elements
357    self.close_all(&self.elements.clone());
358
359    // close the output
360    write_html!(self, "</div>");
361
362    // replace all link references with the actual hrefs
363    for link in self.link_list.iter().map(|link| link.take()) {
364      self.html = self.html.replace(&link.replacer, &escape_href(&link.href));
365    }
366
367    // postprocess the html to add <br> tags where needed
368    self.html = self.html
369      .replace("<div> ", "<div>&nbsp;")
370      .replace(" </div>", " <br></div>")
371      .replace("<div></div>", "<div><br></div>");
372
373    // we are done :3
374    self.html
375  }
376
377}
378
379/// Renders parsed sillycode parts as HTML.
380///
381/// Set `is_editor` to `true` to include visible markup tags for editing purposes.
382pub fn render(parts: impl IntoIterator<Item = Part>, is_editor: bool) -> String {
383  let mut renderer = Renderer::new();
384  renderer.is_editor = is_editor;
385  renderer.render(parts)
386}