/**
* 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 ``
}
}