| 1 | use std::collections::HashMap; |
| 2 | use std::fmt::Debug; |
| 3 | |
| 4 | use crate::common::utils::escape_html; |
| 5 | use crate::parser::extset::RenderExtSet; |
| 6 | use crate::Node; |
| 7 | |
| 8 | /// Each node outputs its HTML using this API. |
| 9 | /// |
| 10 | /// Renderer is a struct that walks through AST and collects HTML from each node |
| 11 | /// into internal buffer. |
| 12 | pub trait Renderer { |
| 13 | /// Write opening html tag with attributes, e.g. `<a href="url">`. |
| 14 | fn open(&mut self, tag: &str, attrs: &[(&str, String)]); |
| 15 | /// Write closing html tag, e.g. `</a>`. |
| 16 | fn close(&mut self, tag: &str); |
| 17 | /// Write self-closing html tag with attributes, e.g. `<img src="url"/>`. |
| 18 | fn self_close(&mut self, tag: &str, attrs: &[(&str, String)]); |
| 19 | /// Loop through child nodes and render each one. |
| 20 | fn contents(&mut self, nodes: &[Node]); |
| 21 | /// Write line break (`\n`). Default renderer ignores it if last char in the buffer is `\n` already. |
| 22 | fn cr(&mut self); |
| 23 | /// Write plain text with escaping, `<div>` -> `&lt;div&gt;`. |
| 24 | fn text(&mut self, text: &str); |
| 25 | /// Write plain text without escaping, `<div>` -> `<div>`. |
| 26 | fn text_raw(&mut self, text: &str); |
| 27 | /// Extension set to store custom stuff. |
| 28 | fn ext(&mut self) -> &mut RenderExtSet; |
| 29 | } |
| 30 | |
| 31 | #[derive(Debug, Default)] |
| 32 | /// Default HTML/XHTML renderer. |
| 33 | pub(crate) struct HTMLRenderer<const XHTML: bool> { |
| 34 | result: String, |
| 35 | ext: RenderExtSet, |
| 36 | } |
| 37 | |
| 38 | impl<const XHTML: bool> HTMLRenderer<XHTML> { |
| 39 | pub fn new() -> Self { |
| 40 | Self { |
| 41 | result: String::new(), |
| 42 | ext: RenderExtSet::new(), |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | pub fn render(&mut self, node: &Node) { |
| 47 | node.node_value.render(node, self); |
| 48 | } |
| 49 | |
| 50 | fn make_attr(&mut self, name: &str, value: &str) { |
| 51 | self.result.push(' '); |
| 52 | self.result.push_str(&escape_html(name)); |
| 53 | self.result.push('='); |
| 54 | self.result.push('"'); |
| 55 | self.result.push_str(&escape_html(value)); |
| 56 | self.result.push('"'); |
| 57 | } |
| 58 | |
| 59 | fn make_attrs(&mut self, attrs: &[(&str, String)]) { |
| 60 | let mut attr_hash = HashMap::new(); |
| 61 | let mut attr_order = Vec::with_capacity(attrs.len()); |
| 62 | |
| 63 | for (name, value) in attrs { |
| 64 | let entry = attr_hash.entry(*name).or_insert(Vec::new()); |
| 65 | entry.push(value.as_str()); |
| 66 | attr_order.push(*name); |
| 67 | } |
| 68 | |
| 69 | for name in attr_order { |
| 70 | let Some(value) = attr_hash.remove(name) else { continue; }; |
| 71 | |
| 72 | if name == "class" { |
| 73 | self.make_attr(name, &value.join(" ")); |
| 74 | } else if name == "style" { |
| 75 | self.make_attr(name, &value.join(";")); |
| 76 | } else { |
| 77 | for v in value { |
| 78 | self.make_attr(name, v); |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | impl<const XHTML: bool> From<HTMLRenderer<XHTML>> for String { |
| 86 | fn from(f: HTMLRenderer<XHTML>) -> Self { |
| 87 | #[cold] |
| 88 | fn replace_null(input: String) -> String { |
| 89 | input.replace('\0', "\u{FFFD}") |
| 90 | } |
| 91 | |
| 92 | if f.result.contains('\0') { |
| 93 | // U+0000 must be replaced with U+FFFD as per commonmark spec, |
| 94 | // we do it at the very end in order to avoid messing with byte offsets |
| 95 | // for source maps (since "\0".len() != "\u{FFFD}".len()) |
| 96 | replace_null(f.result) |
| 97 | } else { |
| 98 | f.result |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | impl<const XHTML: bool> Renderer for HTMLRenderer<XHTML> { |
| 104 | fn open(&mut self, tag: &str, attrs: &[(&str, String)]) { |
| 105 | self.result.push('<'); |
| 106 | self.result.push_str(tag); |
| 107 | self.make_attrs(attrs); |
| 108 | self.result.push('>'); |
| 109 | } |
| 110 | |
| 111 | fn close(&mut self, tag: &str) { |
| 112 | self.result.push('<'); |
| 113 | self.result.push('/'); |
| 114 | self.result.push_str(tag); |
| 115 | self.result.push('>'); |
| 116 | } |
| 117 | |
| 118 | fn self_close(&mut self, tag: &str, attrs: &[(&str, String)]) { |
| 119 | self.result.push('<'); |
| 120 | self.result.push_str(tag); |
| 121 | self.make_attrs(attrs); |
| 122 | if XHTML { |
| 123 | self.result.push(' '); |
| 124 | self.result.push('/'); |
| 125 | } |
| 126 | self.result.push('>'); |
| 127 | } |
| 128 | |
| 129 | fn contents(&mut self, nodes: &[Node]) { |
| 130 | for node in nodes.iter() { |
| 131 | self.render(node); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | fn cr(&mut self) { |
| 136 | // only push '\n' if last character isn't it |
| 137 | match self.result.as_bytes().last() { |
| 138 | Some(b'\n') | None => {} |
| 139 | Some(_) => self.result.push('\n') |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | fn text(&mut self, text: &str) { |
| 144 | self.result.push_str(&escape_html(text)); |
| 145 | } |
| 146 | |
| 147 | fn text_raw(&mut self, text: &str) { |
| 148 | self.result.push_str(text); |
| 149 | } |
| 150 | |
| 151 | fn ext(&mut self) -> &mut RenderExtSet { |
| 152 | &mut self.ext |
| 153 | } |
| 154 | } |