Skip to content

Commit 1be4b76

Browse files
committed
feat: extended character escaping in normalizer
1 parent 4c96cd5 commit 1be4b76

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

src/web/__tests__/htmlNormalizer.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,8 @@ describe('htmlNormalizer', () => {
354354
],
355355

356356
[
357-
'<div><br>here\'s more!</div><div><br></div><img src="https://example.com/image.png" alt="image.png" width="336" height="297">',
358-
'<br><p>here\'s more!</p><br><p><img src="https://example.com/image.png" alt="image.png" width="336" height="297" /></p>',
357+
'<div><br>here&#39;s more!</div><div><br></div><img src="https://example.com/image.png" alt="image.png" width="336" height="297">',
358+
'<br><p>here&#39;s more!</p><br><p><img src="https://example.com/image.png" alt="image.png" width="336" height="297" /></p>',
359359
],
360360
[
361361
'<div>what do you think of this craziness</div><span><blockquote><div><div><ul><li><b>another one </b>hello<div><br></div><div>hi</div></li></ul></div></div></blockquote></span>',
@@ -428,4 +428,23 @@ describe('htmlNormalizer', () => {
428428
);
429429
});
430430
});
431+
432+
describe('character escaping', () => {
433+
// Each special character in text content is re-emitted as its entity.
434+
test.each([
435+
['<p>a & b</p>', '<p>a &amp; b</p>'],
436+
['<p>a < b</p>', '<p>a &lt; b</p>'],
437+
['<p>a > b</p>', '<p>a &gt; b</p>'],
438+
['<p>"quoted"</p>', '<p>&quot;quoted&quot;</p>'],
439+
["<p>it's</p>", '<p>it&#39;s</p>'],
440+
['<p>&amp;&lt;&gt;"\'</p>', '<p>&amp;&lt;&gt;&quot;&#39;</p>'],
441+
['<p>&<>"\'</p>', '<p>&amp;&lt;&gt;&quot;&#39;</p>'],
442+
[
443+
'<a href="https://example.com?a=1&b=\'2\'">x</a>',
444+
'<a href="https://example.com?a=1&amp;b=&#39;2&#39;">x</a>',
445+
],
446+
])('%s → %s', (input, expected) => {
447+
expect(normalizeHtml(input)).toBe(expected);
448+
});
449+
});
431450
});

src/web/normalization/htmlNormalizer.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ function emitStylesClose(s: CssStyles): string {
234234
function emitOneAttr(el: Element, attr: string): string {
235235
const val = el.getAttribute(attr);
236236
if (val == null || val === '') return '';
237-
return ` ${attr}="${val}"`;
237+
const escaped = escapeText(val);
238+
return ` ${attr}="${escaped}"`;
238239
}
239240

240241
function emitAttributes(el: Element, name: string): string {
@@ -272,7 +273,22 @@ function isGoogleDocsWrapper(el: Element, tag: string): boolean {
272273
}
273274

274275
function escapeText(s: string): string {
275-
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
276+
return s.replace(/[&<>"']/g, (match) => {
277+
switch (match) {
278+
case '&':
279+
return '&amp;';
280+
case '<':
281+
return '&lt;';
282+
case '>':
283+
return '&gt;';
284+
case '"':
285+
return '&quot;';
286+
case "'":
287+
return '&#39;';
288+
default:
289+
return match;
290+
}
291+
});
276292
}
277293

278294
// --- Blockquote content flattening ---

0 commit comments

Comments
 (0)