Skip to content
File

Blob: src/client/components/highlight.ts

typescript75 lines
1import hljs from "highlight.js";
2 
3import { escapeHtml } from "@/shared/web/format";
4 
5type HighlightedCode = {
6 html: string;
7 languageClass: string;
8};
9 
10const SPAN_TAG_RE = /<\/?span\b[^>]*>/gi;
11 
12function escapeAttribute(value: string): string {
13 return escapeHtml(value).replace(/'/g, "&#39;");
14}
15 
16export function highlightCode(code: string, language?: string | null): HighlightedCode {
17 const normalizedLanguage = language && hljs.getLanguage(language) ? language : null;
18 
19 return {
20 html: normalizedLanguage
21 ? hljs.highlight(code, { language: normalizedLanguage, ignoreIllegals: true }).value
22 : escapeHtml(code),
23 languageClass: normalizedLanguage
24 ? `language-${escapeAttribute(normalizedLanguage)}`
25 : "language-plaintext",
26 };
27}
28 
29export function splitHighlightedCodeIntoLines(
30 code: string,
31 language?: string | null
32): HighlightedCode & { lines: string[] } {
33 const highlighted = highlightCode(code, language);
34 if (highlighted.html === "") {
35 return {
36 ...highlighted,
37 lines: [],
38 };
39 }
40 
41 const rawLines = highlighted.html.split("\n");
42 
43 if (rawLines.length > 1 && rawLines[rawLines.length - 1] === "") {
44 rawLines.pop();
45 }
46 
47 const openTags: string[] = [];
48 const lines = rawLines.map((line) => {
49 const prefix = openTags.join("");
50 SPAN_TAG_RE.lastIndex = 0;
51 
52 let match: RegExpExecArray | null = null;
53 while ((match = SPAN_TAG_RE.exec(line)) !== null) {
54 if (match[0][1] === "/") {
55 openTags.pop();
56 } else {
57 openTags.push(match[0]);
58 }
59 }
60 
61 const suffix = openTags
62 .slice()
63 .reverse()
64 .map(() => "</span>")
65 .join("");
66 const html = `${prefix}${line}${suffix}`;
67 return html || "&nbsp;";
68 });
69 
70 return {
71 ...highlighted,
72 lines,
73 };
74}