/** * icon: / svg: 行内语法插件 * * 语法与 v1 完全一致: * icon:github → * svg:star → svgMap 中的内联 SVG,找不到时回退为 iconfont 图标 * * 实现方式:inline 解析完成后,对文本 token 做后置切分。 * 不能用普通 inline rule,因为 markdown-it 的 text 规则会把 * "icon" 作为普通文本运行整段消费,行内规则没有触发机会。 * 后置切分天然不影响代码块/行内代码(它们是独立 token 类型)。 * * 兼容细节:v1 的正则 icon:(\w+)(\s|\b) 会吞掉后面一个空白符, * 本实现保持同样行为(icon 与文字之间不留多余空格)。 */ import { svgLoaderManager } from "../../../utils/svgLoader" interface IconHit { name: string prefix: "svg" | "icon" start: number end: number // 含被吞掉的一个空白符 } function findIcons(text: string): IconHit[] { const hits: IconHit[] = [] // 与 v1 正则 icon:(\w+)(\s|\b) 完全对齐:无左边界要求,\s 含全角空格等所有空白符 const re = /(svg|icon):(\w+)(\s|(?=\W)|$)/g let m: RegExpExecArray | null while ((m = re.exec(text))) { const name = m[2] const end = m.index + m[0].length hits.push({ name, prefix: m[1] as "svg" | "icon", start: m.index, end }) re.lastIndex = end } return hits } function splitTextToken(state: any, token: any, parent: any[]) { const text = token.content const hits = findIcons(text) if (!hits.length) return const result: any[] = [] let cursor = 0 for (const hit of hits) { if (hit.start > cursor) { const t = new state.Token("text", "", 0) t.content = text.slice(cursor, hit.start) result.push(t) } const icon = new state.Token(hit.prefix === "svg" ? "svg_icon" : "icon", "", 0) icon.meta = { name: hit.name } result.push(icon) cursor = hit.end } if (cursor < text.length) { const t = new state.Token("text", "", 0) t.content = text.slice(cursor) result.push(t) } // 原位替换 const idx = parent.indexOf(token) if (idx === -1) return parent.splice(idx, 1, ...result) } function walkInlineTokens(state: any, children: any[]) { for (let i = 0; i < children.length; i++) { const child = children[i] if (child.type === "text") { splitTextToken(state, child, children) } else if (child.type !== "code_inline" && child.children) { walkInlineTokens(state, child.children) } } } function iconRule(state: any) { const tokens = state.tokens for (const token of tokens) { if (token.type === "inline" && token.children) { walkInlineTokens(state, token.children) } } } export function iconsPlugin(md: any) { md.core.ruler.after("inline", "resume_icon", iconRule) md.renderer.rules.icon = (tokens: any[], idx: number) => `` md.renderer.rules.svg_icon = (tokens: any[], idx: number) => { const svg = svgLoaderManager.getSvg(tokens[idx].meta.name) if (svg) return svg return `` } }