1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! HTML escaping, reproduced from markdown-it rather than approximated.
//!
//! Upstream's HTML renderer escapes with markdown-it's `escapeHtml`
//! (`reference/src/renderers/html.ts:4`). This crate has no markdown-it, and no
//! two escapers agree on the edges: some add `'` as `'`, some add `/`, some
//! escape non-ASCII. Reaching for the nearest available escaper would produce
//! output that looks right and differs on a character the corpus does not
//! cover -- and this is the function standing between authored content and a
//! browser, so an approximation is not acceptable.
//!
//! # What upstream actually does
//!
//! markdown-it 12.3.2, `lib/common/utils.js`, verified against the published
//! source at that tag. Upstream pins 12.3.2 in `package-lock.json` and patches
//! it (`reference/patches/markdown-it+12.3.2.patch`), but the patch touches
//! nine block rules and never `lib/common/utils.js`, so the escaper in force is
//! stock:
//!
//! ```js
//! var HTML_REPLACEMENTS = {
//! '&': '&',
//! '<': '<',
//! '>': '>',
//! '"': '"'
//! };
//!
//! function escapeHtml(str) {
//! if (HTML_ESCAPE_TEST_RE.test(str)) {
//! return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar);
//! }
//! return str;
//! }
//! ```
//!
//! Four characters, each replaced independently in one pass, and nothing else
//! touched. The fast path for a string with none of them is an optimisation,
//! not a behaviour, which is why [`escape_html`] returns a [`Cow`]: same
//! answer, same allocation profile.
//!
//! # Why the single quote is not in the set, and what that asks of a renderer
//!
//! An escaper that omits `'` is only safe if every attribute it feeds is
//! double-quoted. For [`Html`](super::Html) that is not a convention, it is
//! the code: its `open` writes `="` and `"` around every value with no branch
//! that could choose otherwise, so an apostrophe in a value can never end the
//! attribute. Adding `'` would be strictly safer in the abstract and a
//! divergence in fact -- it changes the bytes upstream produces for ordinary
//! prose.
//!
//! The escaper is public, and a [`TagRenderer`](super::TagRenderer) inherits
//! the condition with it. In text and between double quotes these four
//! replacements are enough. Between single quotes they are not: a value of
//! `' onmouseover='...` walks straight through and ends the attribute. A
//! renderer that delimits attributes any other way needs an escaper that
//! matches its delimiter, and this one is deliberately not it.
use Cow;
/// Escape a string exactly as markdown-it's `escapeHtml` does.
///
/// Replaces `&`, `<`, `>` and `"` with their named entities and leaves
/// everything else, including `'`, unchanged. Returns the input borrowed when
/// it contains none of the four.
///
/// Safe for text and for double-quoted attribute values only. `'` is not
/// replaced, so a single-quoted attribute can be ended by its value; see the
/// module documentation.
///
/// # Examples
///
/// ```
/// use accent_proust::render::escape_html;
///
/// assert_eq!(escape_html("a & b"), "a & b");
/// assert_eq!(escape_html(r#""quoted""#), ""quoted"");
/// // Not in upstream's set, so not escaped.
/// assert_eq!(escape_html("it's"), "it's");
/// ```
/// Append the escaped form of `input` to `out`.
///
/// The renderer builds one string for a whole document, so it appends rather
/// than allocating a `Cow` per text node. Public for the same reason: a
/// [`TagRenderer`](super::TagRenderer) writes into the document's one string,
/// and a host keeping upstream's escaping while changing something else should
/// not have to pay an allocation per text node to do it.
///
/// The same condition as [`escape_html`]: text and double-quoted attribute
/// values only, because `'` is not replaced.
///
/// # Examples
///
/// ```
/// use accent_proust::render::escape_html_into;
///
/// let mut out = String::from("<p>");
/// escape_html_into(&mut out, "a < b");
/// assert_eq!(out, "<p>a < b");
/// ```