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
|
import sanitizeHtml from 'sanitize-html';
import type { MarkdownRenderer } from 'vitepress';
import { createMarkdownRenderer } from 'vitepress';
import vitepressConfig from '../../../docs/.vitepress/config';
import { FILE_PATH_API_DOCS } from './paths';
let markdown: MarkdownRenderer;
export async function initMarkdownRenderer(): Promise<void> {
markdown = await createMarkdownRenderer(
FILE_PATH_API_DOCS,
vitepressConfig.markdown,
'/'
);
}
const htmlSanitizeOptions: sanitizeHtml.IOptions = {
allowedTags: [
'a',
'button',
'code',
'div',
'li',
'p',
'pre',
'span',
'strong',
'ul',
],
allowedAttributes: {
a: ['href', 'target', 'rel'],
button: ['class', 'title'],
div: ['class'],
pre: ['class', 'v-pre', 'tabindex'],
span: ['class', 'style'],
},
selfClosing: [],
};
function comparableSanitizedHtml(html: string): string {
return html
.replaceAll(/&#x[0-9A-F]{2};/g, (x) =>
String.fromCodePoint(Number.parseInt(x.slice(3, -1), 16))
)
.replaceAll('>', '>')
.replaceAll('<', '<')
.replaceAll('&', '&')
.replaceAll('=""', '')
.replaceAll(' ', '');
}
/**
* Converts a Typescript code block to an HTML string and sanitizes it.
*
* @param code The code to convert.
*
* @returns The converted HTML string.
*/
export function codeToHtml(code: string): string {
const delimiter = '```';
return mdToHtml(`${delimiter}ts\n${code}\n${delimiter}`);
}
/**
* Converts Markdown to an HTML string and sanitizes it.
*
* @param md The markdown to convert.
* @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`.
*
* @returns The converted HTML string.
*/
export function mdToHtml(md: string, inline?: boolean): string;
/**
* Converts Markdown to an HTML string and sanitizes it.
*
* @param md The markdown to convert.
* @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`.
*
* @returns The converted HTML string.
*/
export function mdToHtml(
md: string | undefined,
inline?: boolean
): string | undefined;
export function mdToHtml(
md: string | undefined,
inline: boolean = false
): string | undefined {
if (md == null) {
return undefined;
}
const rawHtml = inline ? markdown.renderInline(md) : markdown.render(md);
const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions);
// Revert some escaped characters for comparison.
if (comparableSanitizedHtml(rawHtml) === comparableSanitizedHtml(safeHtml)) {
return adjustUrls(safeHtml);
}
console.debug('Rejected unsafe md:\n', md);
console.error('Rejected unsafe html:\n', rawHtml);
console.error('Clean unsafe html:\n', comparableSanitizedHtml(rawHtml));
console.error('Clean safe html:\n', comparableSanitizedHtml(safeHtml));
console.log('-'.repeat(80));
throw new Error('Found unsafe html');
}
export function adjustUrls(description: string): string {
return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/');
}
|