-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrenderer.ts
120 lines (107 loc) · 3.16 KB
/
renderer.ts
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
import { FontStyle } from "./stack_element_metadata.ts";
import { IThemedToken } from "./themed_tokenizer.ts";
import { ElementsOptions, HtmlRendererOptions, LineOption } from "./types.ts";
import { groupBy } from "./utils.ts";
const defaultElements: ElementsOptions = {
pre({ className, style, children }) {
return `<pre class="${className}" style="${style}">${children}</pre>`;
},
code({ children }) {
return `<code>${children}</code>`;
},
line({ className, children }) {
return `<span class="${className}">${children}</span>`;
},
token({ style, children }) {
return `<span style="${style}">${children}</span>`;
},
};
export function renderToHtml(
lines: IThemedToken[][],
options: HtmlRendererOptions = {},
) {
const bg = options.bg || "#fff";
const optionsByLineNumber = groupBy(
options.lineOptions ?? [],
(option) => option.line,
);
const userElements = options.elements || {};
function h(
type: keyof ElementsOptions,
props = {},
children: string[],
): string {
const element = userElements[type] || defaultElements[type];
if (element) {
children = children.filter(Boolean);
// @ts-ignore idk
return element({
...props,
children: type === "code" ? children.join("\n") : children.join(""),
});
}
return "";
}
return h("pre", { className: "shiki", style: `background-color: ${bg}` }, [
options.langId ? `<div class="language-id">${options.langId}</div>` : "",
h(
"code",
{},
lines.map((line, index) => {
const lineNumber = index + 1;
const lineOptions = optionsByLineNumber.get(lineNumber) ?? [];
const lineClasses = getLineClasses(lineOptions).join(" ");
return h(
"line",
{
className: lineClasses,
lines,
line,
index,
},
line.map((token, index) => {
const cssDeclarations = [`color: ${token.color || options.fg}`];
if (token.fontStyle! & FontStyle.Italic) {
cssDeclarations.push("font-style: italic");
}
if (token.fontStyle! & FontStyle.Bold) {
cssDeclarations.push("font-weight: bold");
}
if (token.fontStyle! & FontStyle.Underline) {
cssDeclarations.push("text-decoration: underline");
}
return h(
"token",
{
style: cssDeclarations.join("; "),
tokens: line,
token,
index,
},
[escapeHtml(token.content)],
);
}),
);
}),
),
]);
}
const htmlEscapes: Record<string, string> = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
function escapeHtml(html: string) {
return html.replace(/[&<>"']/g, (chr) => htmlEscapes[chr]);
}
function getLineClasses(lineOptions: LineOption[]): string[] {
const lineClasses = new Set(["line"]);
for (const lineOption of lineOptions) {
for (const lineClass of lineOption.classes ?? []) {
lineClasses.add(lineClass);
}
}
return Array.from(lineClasses);
}