import type { LanguageFn } from 'highlight.js' import bash from 'highlight.js/lib/languages/bash' import c from 'highlight.js/lib/languages/c' import cpp from 'highlight.js/lib/languages/cpp' import csharp from 'highlight.js/lib/languages/csharp' import css from 'highlight.js/lib/languages/css' import diff from 'highlight.js/lib/languages/diff' import go from 'highlight.js/lib/languages/go' import graphql from 'highlight.js/lib/languages/graphql' import ini from 'highlight.js/lib/languages/ini' import java from 'highlight.js/lib/languages/java' import javascript from 'highlight.js/lib/languages/javascript' import json from 'highlight.js/lib/languages/json' import kotlin from 'highlight.js/lib/languages/kotlin' import less from 'highlight.js/lib/languages/less' import lua from 'highlight.js/lib/languages/lua' import makefile from 'highlight.js/lib/languages/makefile' import markdown from 'highlight.js/lib/languages/markdown' import objectivec from 'highlight.js/lib/languages/objectivec' import perl from 'highlight.js/lib/languages/perl' import php from 'highlight.js/lib/languages/php' import phpTemplate from 'highlight.js/lib/languages/php-template' import plaintext from 'highlight.js/lib/languages/plaintext' import python from 'highlight.js/lib/languages/python' import pythonRepl from 'highlight.js/lib/languages/python-repl' import r from 'highlight.js/lib/languages/r' import ruby from 'highlight.js/lib/languages/ruby' import rust from 'highlight.js/lib/languages/rust' import scss from 'highlight.js/lib/languages/scss' import shell from 'highlight.js/lib/languages/shell' import sql from 'highlight.js/lib/languages/sql' import swift from 'highlight.js/lib/languages/swift' import typescript from 'highlight.js/lib/languages/typescript' import vbnet from 'highlight.js/lib/languages/vbnet' import wasm from 'highlight.js/lib/languages/wasm' import xml from 'highlight.js/lib/languages/xml' import yaml from 'highlight.js/lib/languages/yaml' export const COMMON_LANGUAGES: Record = { bash, c, cpp, csharp, css, diff, go, graphql, ini, java, javascript, json, kotlin, less, lua, makefile, markdown, objectivec, perl, php, 'php-template': phpTemplate, plaintext, python, 'python-repl': pythonRepl, r, ruby, rust, scss, shell, sql, swift, typescript, vbnet, wasm, xml, yaml, } // highlight.js CDN 配置 const HLJS_VERSION = `11.11.1` const HLJS_CDN_BASE = `https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlightjs/${HLJS_VERSION}` // 缓存正在加载的语言 const loadingLanguages = new Map>() /** * 生成语言包的 CDN URL */ function grammarUrlFor(language: string): string { return `${HLJS_CDN_BASE}/es/languages/${language}.min.js` } /** * 动态加载并注册语言 * @param language 语言名称 * @param hljs highlight.js 实例 */ export async function loadAndRegisterLanguage(language: string, hljs: any): Promise { // 如果已经注册,直接返回 if (hljs.getLanguage(language)) { return } // 如果正在加载,等待加载完成 if (loadingLanguages.has(language)) { await loadingLanguages.get(language) return } // 开始加载 const loadPromise = (async () => { try { const module = await import(/* @vite-ignore */ grammarUrlFor(language)) hljs.registerLanguage(language, module.default) } catch (error) { console.warn(`Failed to load language: ${language}`, error) throw error } finally { loadingLanguages.delete(language) } })() loadingLanguages.set(language, loadPromise) await loadPromise } /** * 格式化高亮后的代码,处理空格和制表符 */ function formatHighlightedCode(html: string, preserveNewlines = false): string { let formatted = html // 将 span 之间的空格移到 span 内部 formatted = formatted.replace(/(]*>[^<]*<\/span>)(\s+)(]*>[^<]*<\/span>)/g, (_: string, span1: string, spaces: string, span2: string) => span1 + span2.replace(/^(]*>)/, `$1${spaces}`)) formatted = formatted.replace(/(\s+)(]*>)/g, (_: string, spaces: string, span: string) => span.replace(/^(]*>)/, `$1${spaces}`)) // 替换制表符为4个空格 formatted = formatted.replace(/\t/g, ` `) if (preserveNewlines) { // 替换换行符为
,并将空格转换为   formatted = formatted.replace(/\r\n/g, `
`).replace(/\n/g, `
`).replace(/(>[^<]+)|(^[^<]+)/g, (str: string) => str.replace(/\s/g, ` `)) } else { // 只将空格转换为   formatted = formatted.replace(/(>[^<]+)|(^[^<]+)/g, (str: string) => str.replace(/\s/g, ` `)) } return formatted } /** * 高亮代码并格式化(支持行号) * @param text 原始代码文本 * @param language 语言名称 * @param hljs highlight.js 实例 * @param showLineNumber 是否显示行号 * @returns 格式化后的 HTML */ export function highlightAndFormatCode(text: string, language: string, hljs: any, showLineNumber: boolean): string { let highlighted = `` if (showLineNumber) { const rawLines = text.replace(/\r\n/g, `\n`).split(`\n`) const highlightedLines = rawLines.map((lineRaw) => { const lineHtml = hljs.highlight(lineRaw, { language }).value const formatted = formatHighlightedCode(lineHtml, false) return formatted === `` ? ` ` : formatted }) const lineNumbersHtml = highlightedLines.map((_, idx) => `
${idx + 1}
`).join(``) const codeInnerHtml = highlightedLines.join(`
`) const codeLinesHtml = `
${codeInnerHtml}
` const lineNumberColumnStyles = `text-align:right;padding:8px 0;border-right:1px solid rgba(0,0,0,0.04);user-select:none;background:var(--code-bg,transparent);` highlighted = `
${lineNumbersHtml}
${codeLinesHtml}
` } else { const rawHighlighted = hljs.highlight(text, { language }).value highlighted = formatHighlightedCode(rawHighlighted, true) } return highlighted } export function highlightCodeBlock(codeBlock: Element, language: string, hljs: any): void { const rawCode = codeBlock.getAttribute(`data-raw-code`) const showLineNumber = codeBlock.getAttribute(`data-show-line-number`) === `true` if (!rawCode) return const text = rawCode.replace(/"/g, `"`) const highlighted = highlightAndFormatCode(text, language, hljs, showLineNumber) codeBlock.innerHTML = highlighted codeBlock.removeAttribute(`data-language-pending`) codeBlock.removeAttribute(`data-raw-code`) codeBlock.removeAttribute(`data-show-line-number`) } /** * 高亮 DOM 中待处理的代码块 * 查找带有 data-language-pending 属性的代码块,动态加载语言后重新高亮 * @param hljs highlight.js 实例 * @param container 容器元素(可选,默认为 document) */ export function highlightPendingBlocks(hljs: any, container: Document | Element = document): void { const pendingBlocks = container.querySelectorAll(`code[data-language-pending]`) pendingBlocks.forEach((codeBlock) => { const language = codeBlock.getAttribute(`data-language-pending`) if (!language) return if (hljs.getLanguage(language)) { // 语言已加载,直接高亮 highlightCodeBlock(codeBlock, language, hljs) } else { // 动态加载语言后重新高亮 loadAndRegisterLanguage(language, hljs).then(() => { highlightCodeBlock(codeBlock, language, hljs) }).catch(() => { // 加载失败,移除标记 codeBlock.removeAttribute(`data-language-pending`) codeBlock.removeAttribute(`data-raw-code`) codeBlock.removeAttribute(`data-show-line-number`) }) } }) }