markdown_it/parser/
renderer.rs

1use std::collections::HashMap;
2use std::fmt::Debug;
3
4use crate::common::utils::escape_html;
5use crate::parser::extset::RenderExtSet;
6use 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.
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 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.
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 { 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
85impl<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
103impl<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}