
本文详解如何在 Svelte 应用中基于 Shiki 实现 Markdown 代码块的双重自定义功能:既支持 {1-3,5} 格式的行号高亮,又兼容 (src/lib/utils/index.js) 形式的顶部标题显示,且两者可共存、互不干扰。
本文详解如何在 svelte 应用中基于 shiki 实现 markdown 代码块的双重自定义功能:既支持 `{1-3,5}` 格式的行号高亮,又兼容 `(src/lib/utils/index.js)` 形式的顶部标题显示,且两者可共存、互不干扰。
在使用 Shiki 进行代码高亮时,常需扩展其元信息(meta)解析能力以支持更丰富的语义表达。原始实现中,meta 字段仅能被用于行高亮(如 {1-3,5})或标题(如 (src/lib/utils/index.js))二者之一,原因在于对 meta 的解析逻辑是排他性的:一旦匹配到括号标题就清空 meta,导致后续无法提取花括号内的行号规则。
解决该问题的核心思路是 非破坏性元信息解析:即从 meta 中提取标题后,仅移除对应部分,保留其余内容(如行高亮指令),再交由 Shiki 原生处理逻辑消费。以下是关键优化点:
✅ 正确解析混合 meta 的正则策略
使用 meta.match(/\(([^)]+)\)/) 精准捕获最外层括号内的标题文本,并通过 meta.replace(metaMatch[0], '') 安全剥离该片段,避免误删嵌套内容(如 ({1-2}) (file.js) 中的花括号不受影响)。
✅ 清晰分离关注点
- 标题提取 → 独立处理,不影响后续逻辑
- 行高亮解析 → 仍复用原有 /{([\d,-]+)}/ 正则,但作用于清洗后的 meta
- HTML 组装 → 标题 始终前置插入,与 Shiki 输出解耦
✅ 完整修复版 highlighter 函数(含注释)
import { parse } from 'node-html-parser'; import { getHighlighter } from 'shiki'; const THEME = 'github-dark'; function escapeHtml(code) { return code.replace(/[{}]/g, (c) => ({ '{': '{', '}': '}' })[c] || c); } function rangeParser(rangeString) { const result = []; for (const part of rangeString.split(',')) { const trimmed = part.trim(); if (!trimmed) continue; if (!trimmed.includes('-')) { result.push(parseInt(trimmed, 10)); } else { const [start, end] = trimmed.split('-').map(Number); for (let i = start; i <= end; i++) result.push(i); } } return result; } function makeFocussable(html) { const root = parse(html); const pre = root.querySelector('pre'); if (pre) pre.setAttribute('tabIndex', '0'); return root.toString(); } async function highlighter(code, lang, meta) { const shikiHighlighter = await getHighlighter({ theme: THEME }); let html; let title = null; // ? 提取并移除标题(如 `(src/lib/utils/index.js)`),保留其余 meta if (meta) { const titleMatch = meta.match(/\(([^)]+)\)/); if (titleMatch) { title = titleMatch[1].trim(); meta = meta.replace(titleMatch[0], '').trim(); } } // ? 根据剩余 meta 决定是否启用行高亮 if (!meta) { html = shikiHighlighter.codeToHtml(code, { lang }); } else { const highlightMatch = /{([\d,-]+)}/.exec(meta); if (highlightMatch) { const highlightLines = rangeParser(highlightMatch[1]); html = shikiHighlighter.codeToHtml(code, { lang, lineOptions: highlightLines.map(line => ({ line, classes: ['highlight-line'] })) }); } else { html = shikiHighlighter.codeToHtml(code, { lang }); } } // ⚙️ 增强可访问性 html = makeFocussable(html); // ?️ 插入标题(若存在) if (title) { html = `<div class="code-block-title">${title}</div>${html}`; } return escapeHtml(html); } export default highlighter;⚠️ 注意事项与最佳实践
- Meta 顺序无关:js {1-3} (utils.js) 和 js (utils.js) {1-3} 均可正确解析(因正则全局匹配且剥离独立);
- 空格容错:{1, 2-4} 和 ( file.js ) 中的空格会被 .trim() 自动处理;
- CSS 样式需配套:确保为 .highlight-line 添加背景色(如 background-color: #2a2d3e;),并为 .code-block-title 设置字体、边框等视觉样式;
- 安全转义增强:escapeHtml 已补充对未匹配字符的兜底返回,防止 undefined 注入;
- 错误防御:在 rangeParser 中加入 !trimmed 判断,避免空字符串导致 NaN 入栈。
通过以上重构,你可在 Markdown 中自由组合使用:
```js {1-3,5} (src/lib/utils/index.js) export function debounce(fn, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }最终渲染效果:顶部显示 src/lib/utils/index.js 标题,第 1–3 行与第 5 行带高亮背景,且整个代码块支持键盘聚焦 —— 功能完备、健壮可靠。










