markdown_that/parser/
renderer.rs

1use std::collections::HashMap;
2use std::fmt::Debug;
3
4use crate::Node;
5use crate::common::utils::escape_html;
6use crate::parser::extset::RenderExtSet;
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 an internal buffer.
12pub 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 a 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`). The default renderer ignores it if the 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.
33pub(crate) struct HTMLRenderer<const XHTML: bool> {
34    result: String,
35    ext: RenderExtSet,
36}
37
38impl<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 {
71                continue;
72            };
73
74            if name == "class" {
75                self.make_attr(name, &value.join(" "));
76            } else if name == "style" {
77                self.make_attr(name, &value.join(";"));
78            } else {
79                for v in value {
80                    self.make_attr(name, v);
81                }
82            }
83        }
84    }
85}
86
87impl<const XHTML: bool> From<HTMLRenderer<XHTML>> for String {
88    fn from(f: HTMLRenderer<XHTML>) -> Self {
89        #[cold]
90        fn replace_null(input: String) -> String {
91            input.replace('\0', "\u{FFFD}")
92        }
93
94        if f.result.contains('\0') {
95            // U+0000 must be replaced with U+FFFD as per commonmark spec,
96            // we do it at the very end to avoid messing with byte offsets
97            // for source maps (since "\0".len() != "\u{FFFD}".len())
98            replace_null(f.result)
99        } else {
100            f.result
101        }
102    }
103}
104
105impl<const XHTML: bool> Renderer for HTMLRenderer<XHTML> {
106    fn open(&mut self, tag: &str, attrs: &[(&str, String)]) {
107        self.result.push('<');
108        self.result.push_str(tag);
109        self.make_attrs(attrs);
110        self.result.push('>');
111    }
112
113    fn close(&mut self, tag: &str) {
114        self.result.push('<');
115        self.result.push('/');
116        self.result.push_str(tag);
117        self.result.push('>');
118    }
119
120    fn self_close(&mut self, tag: &str, attrs: &[(&str, String)]) {
121        self.result.push('<');
122        self.result.push_str(tag);
123        self.make_attrs(attrs);
124        if XHTML {
125            self.result.push(' ');
126            self.result.push('/');
127        }
128        self.result.push('>');
129    }
130
131    fn contents(&mut self, nodes: &[Node]) {
132        for node in nodes.iter() {
133            self.render(node);
134        }
135    }
136
137    fn cr(&mut self) {
138        // only push '\n' if last character isn't it
139        match self.result.as_bytes().last() {
140            Some(b'\n') | None => {}
141            Some(_) => self.result.push('\n'),
142        }
143    }
144
145    fn text(&mut self, text: &str) {
146        self.result.push_str(&escape_html(text));
147    }
148
149    fn text_raw(&mut self, text: &str) {
150        self.result.push_str(text);
151    }
152
153    fn ext(&mut self) -> &mut RenderExtSet {
154        &mut self.ext
155    }
156}