From 91a31b17c2af03105195d8669a9014630c2d27ac Mon Sep 17 00:00:00 2001 From: SolitudeRA Date: Mon, 12 Jan 2026 06:57:48 +0900 Subject: [PATCH] refactor: optimize code highlighting performance --- src/api/utils/codeHighlight.ts | 241 ++++++++++++++++++ .../posts/detail/CodeHighlighter.tsx | 67 ----- src/components/posts/detail/PostContent.astro | 8 +- 3 files changed, 246 insertions(+), 70 deletions(-) create mode 100644 src/api/utils/codeHighlight.ts delete mode 100644 src/components/posts/detail/CodeHighlighter.tsx diff --git a/src/api/utils/codeHighlight.ts b/src/api/utils/codeHighlight.ts new file mode 100644 index 0000000..c5943c7 --- /dev/null +++ b/src/api/utils/codeHighlight.ts @@ -0,0 +1,241 @@ +import { + codeToHtml, + bundledLanguages, + type BundledLanguage, + type SpecialLanguage, +} from 'shiki'; + +type SupportedLanguage = BundledLanguage | SpecialLanguage; + +/** + * 支持的编程语言列表 + * 只包含常用语言以优化打包体积 + */ +const SUPPORTED_LANGUAGES = new Set([ + // Web 前端 + 'javascript', + 'typescript', + 'jsx', + 'tsx', + 'html', + 'css', + 'scss', + 'less', + 'json', + 'jsonc', + 'yaml', + 'toml', + 'xml', + + // 后端语言 + 'python', + 'java', + 'kotlin', + 'go', + 'rust', + 'c', + 'cpp', + 'csharp', + 'php', + 'ruby', + 'swift', + 'scala', + + // Shell & DevOps + 'bash', + 'shell', + 'powershell', + 'dockerfile', + 'nginx', + + // 数据库 & 查询 + 'sql', + 'graphql', + + // 标记语言 + 'markdown', + 'mdx', + 'latex', + + // 配置文件 + 'ini', + 'properties', + 'dotenv', + + // 其他 + 'diff', + 'regex', + 'text', + 'plaintext', +]); + +/** + * 代码块正则表达式 + * 匹配
...
格式 + */ +const CODE_BLOCK_REGEX = + /]*>\s*]*class="[^"]*language-(\w+)[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi; + +/** + * 解码 HTML 实体 + */ +function decodeHtmlEntities(text: string): string { + const entities: Record = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + ''': "'", + ''': "'", + ' ': ' ', + }; + + return text.replace( + /&(?:lt|gt|amp|quot|#39|#x27|nbsp);/g, + (match) => entities[match] || match, + ); +} + +/** + * 检查语言是否被支持 + */ +function isSupportedLanguage(lang: string): boolean { + const normalizedLang = lang.toLowerCase(); + return ( + SUPPORTED_LANGUAGES.has(normalizedLang) && + normalizedLang in bundledLanguages + ); +} + +/** + * 获取有效的语言标识 + * 如果语言不支持,返回 'plaintext' + */ +function getValidLanguage(lang: string): SupportedLanguage { + const normalizedLang = lang.toLowerCase(); + + // 语言别名映射 + const aliases: Record = { + js: 'javascript', + ts: 'typescript', + py: 'python', + rb: 'ruby', + sh: 'bash', + zsh: 'bash', + yml: 'yaml', + md: 'markdown', + cs: 'csharp', + 'c++': 'cpp', + 'c#': 'csharp', + text: 'plaintext', + }; + + const resolvedLang = aliases[normalizedLang] || normalizedLang; + + if (isSupportedLanguage(resolvedLang)) { + return resolvedLang as SupportedLanguage; + } + + return 'plaintext'; +} + +/** + * 高亮单个代码块 + */ +async function highlightCode( + code: string, + lang: string, +): Promise { + try { + const validLang = getValidLanguage(lang); + const decodedCode = decodeHtmlEntities(code); + + const html = await codeToHtml(decodedCode, { + lang: validLang, + themes: { + light: 'github-light', + dark: 'github-dark', + }, + }); + + return html; + } catch (error) { + console.warn(`[codeHighlight] Failed to highlight ${lang}:`, error); + return null; + } +} + +/** + * 处理 HTML 中的所有代码块,进行语法高亮 + * + * @param html - 原始 HTML 内容 + * @returns 高亮后的 HTML 内容 + * + * @example + * ```typescript + * const html = '
const x = 1;
'; + * const highlighted = await highlightCodeBlocks(html); + * ``` + */ +export async function highlightCodeBlocks(html: string): Promise { + if (!html) return html; + + // 收集所有匹配的代码块 + const matches: Array<{ + fullMatch: string; + lang: string; + code: string; + index: number; + }> = []; + + const regex = new RegExp(CODE_BLOCK_REGEX.source, 'gi'); + let match: RegExpExecArray | null; + + while ((match = regex.exec(html)) !== null) { + const lang = match[1]; + const code = match[2]; + if (lang !== undefined && code !== undefined) { + matches.push({ + fullMatch: match[0], + lang, + code, + index: match.index, + }); + } + } + + if (matches.length === 0) { + return html; + } + + // 并行处理所有代码块 + const highlightedResults = await Promise.all( + matches.map(async ({ lang, code }) => highlightCode(code, lang)), + ); + + // 从后向前替换(避免索引偏移问题) + let result = html; + for (let i = matches.length - 1; i >= 0; i--) { + const matchItem = matches[i]; + const highlighted = highlightedResults[i]; + + if (matchItem && highlighted) { + const { fullMatch, lang } = matchItem; + // 添加标记 class 表示已高亮 + const enhancedHtml = highlighted.replace( + ' { - const highlightCodeBlocks = async () => { - // 获取所有未高亮的代码块 - const codeBlocks = document.querySelectorAll( - '.solitude-article-content pre code:not([data-highlighted])', - ); - - if (codeBlocks.length === 0) return; - - for (const block of codeBlocks) { - // 标记为已处理,避免重复高亮 - block.setAttribute('data-highlighted', 'true'); - - // 从 class 中提取语言,如 "language-typescript" - const langMatch = block.className.match(/language-(\w+)/); - const lang = langMatch?.[1] || 'text'; - const code = block.textContent || ''; - - try { - // 检查语言是否支持 - const supportedLang = - lang in bundledLanguages ? lang : 'text'; - - const html = await codeToHtml(code, { - lang: supportedLang, - themes: { - light: 'github-light', - dark: 'github-dark', - }, - }); - - // 替换原始的 pre 元素 - const parent = block.parentElement; - if (parent && parent.tagName === 'PRE') { - // 创建临时容器解析 HTML - const temp = document.createElement('div'); - temp.innerHTML = html; - const newPre = temp.firstElementChild; - - if (newPre) { - // 保留原始 class - newPre.classList.add('shiki-highlighted'); - parent.replaceWith(newPre); - } - } - } catch (error) { - console.warn( - `Failed to highlight code block with language: ${lang}`, - error, - ); - } - } - }; - - highlightCodeBlocks(); - }, []); - - return null; -} diff --git a/src/components/posts/detail/PostContent.astro b/src/components/posts/detail/PostContent.astro index 36e4491..161a29f 100644 --- a/src/components/posts/detail/PostContent.astro +++ b/src/components/posts/detail/PostContent.astro @@ -1,9 +1,9 @@ --- import PostHeader from './PostHeader.astro'; import TableOfContents from './TableOfContents.tsx'; -import CodeHighlighter from './CodeHighlighter.tsx'; import type { PostTag } from '@api/ghost/types'; import { sanitizeHtml, containsSuspiciousContent } from '@api/utils/sanitize'; +import { highlightCodeBlocks } from '@api/utils/codeHighlight'; interface Props { title: string; @@ -26,6 +26,9 @@ if (import.meta.env.DEV && containsSuspiciousContent(html)) { // 净化 HTML,移除潜在的 XSS 攻击向量 const sanitizedHtml = sanitizeHtml(html); + +// 服务端代码高亮 - 在构建时处理所有代码块 +const highlightedHtml = await highlightCodeBlocks(sanitizedHtml); ---
@@ -49,8 +52,7 @@ const sanitizedHtml = sanitizeHtml(html); />
-
- +