const KEYWORDS = new Set([ 'abstract', 'async', 'await', 'boolean', 'break', 'case', 'catch', 'class', 'const', 'continue', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'from', 'function', 'if', 'import', 'in', 'interface', 'let', 'new', 'null', 'number', 'private', 'protected', 'public', 'return', 'static', 'string', 'super', 'switch', 'this', 'throw', 'true', 'try', 'type', 'typeof', 'undefined', 'var', 'void', 'while', ]) const TOKEN_PATTERN = /(\/\*[\s\S]*?\*\/|\/\/[^\n]*|#[^\n]*||(["'`])(?:\\[\s\S]|(?!\2)[^\\])*\2|<\/?[A-Za-z][^>\n]*?>|\b\d+(?:\.\d+)?\b|\b[A-Za-z_$][\w$]*\b)/g export function highlightArticleHtml(html: string) { if (!html.trim()) { return '' } const doc = new DOMParser().parseFromString(`
${html}
`, 'text/html') const root = doc.body.firstElementChild if (!root) { return html } root.querySelectorAll('pre code').forEach((code) => { const rawCode = code.textContent || '' const language = getLanguage(code) code.innerHTML = highlightCode(rawCode) code.classList.add('code-highlighted') if (language) { code.setAttribute('data-language', language) } }) return root.innerHTML } function highlightCode(code: string) { let html = '' let lastIndex = 0 code.replace(TOKEN_PATTERN, (...args) => { const token = args[0] const offset = args[args.length - 2] as number html += escapeHtml(code.slice(lastIndex, offset)) html += renderToken(token) lastIndex = offset + token.length return token }) html += escapeHtml(code.slice(lastIndex)) return html } function renderToken(token: string) { if (isComment(token)) { return wrapToken(token, 'hl-comment') } if (isString(token)) { return wrapToken(token, 'hl-string') } if (isTag(token)) { return wrapToken(token, 'hl-tag') } if (isNumber(token)) { return wrapToken(token, 'hl-number') } if (KEYWORDS.has(token)) { return wrapToken(token, 'hl-keyword') } return escapeHtml(token) } function wrapToken(token: string, className: string) { return `${escapeHtml(token)}` } function getLanguage(code: Element) { const languageAttr = code.getAttribute('data-language') const languageClass = Array.from(code.classList).find((className) => className.startsWith('language-'), ) return languageAttr || languageClass?.replace(/^language-/, '') || '' } function isComment(token: string) { return ( token.startsWith('//') || token.startsWith('/*') || token.startsWith('#') || token.startsWith('