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('