/** * 强调语法兼容插件 * * v1 用 indexOf 顺序配对解析 ** / *,不校验 flanking 规则: * - 「** 文本 **」这类星号内侧带空格的写法在 v1 中正常加粗/斜体 * - 标准 markdown 会因空格拒绝配对而原样输出星号,对存量简历是肉眼可见的回退 * * 本插件按 v1 的顺序配对语义扫描 ** / * 标记对, * 把配对内容的首尾空白移到定界符外侧,使标准解析器能正确配对: * ** 2021.12 - 至今 ** → ` **2021.12 - 至今** ` * 普通的 **加粗** / *斜体*(内容无首尾空白)完全不受影响。 */ interface Run { index: number length: number // 2 = **, 1 = * } /** 找出所有标记 run 的位置(** 优先于 *) */ function findRuns(content: string, star: "**" | "*"): Run[] { const runs: Run[] = [] if (star === "*") { // 单星:排除 ** 的成员 for (let i = 0; i < content.length; i++) { if (content[i] === "*" && content[i - 1] !== "*" && content[i + 1] !== "*") { runs.push({ index: i, length: 1 }) } } } else { let idx = content.indexOf("**") while (idx !== -1) { runs.push({ index: idx, length: 2 }) idx = content.indexOf("**", idx + 2) } } return runs } /** v1 式顺序配对:run1-run2, run3-run4...,返回需要重写的区间 */ function collectReplacements(content: string, star: "**" | "*"): Array<{ start: number; end: number; lead: string; body: string; trail: string }> { const runs = findRuns(content, star) const result: Array<{ start: number; end: number; lead: string; body: string; trail: string }> = [] const mark = star as string for (let i = 0; i + 1 < runs.length; i += 2) { const open = runs[i] const close = runs[i + 1] const body = content.slice(open.index + open.length, close.index) if (!body.trim()) continue const lead = body.slice(0, body.length - body.trimStart().length) const trail = body.slice(body.trimEnd().length) if (!lead && !trail) continue // 内容中间还有未配对的标记时不安全,跳过(保底为标准行为) if (body.slice(1, -1).includes(mark)) continue result.push({ start: open.index, end: close.index + close.length, lead, body: body.trim(), trail, }) } return result } function rewrite(content: string, star: "**" | "*"): string { const replacements = collectReplacements(content, star) if (!replacements.length) return content let result = content for (let i = replacements.length - 1; i >= 0; i--) { const { start, end, lead, body, trail } = replacements[i] result = result.slice(0, start) + lead + star + body + star + trail + result.slice(end) } return result } function emphasisCompatRule(state: any) { for (const token of state.tokens) { if (token.type !== "inline") continue const content = token.content if (!content || !content.includes("*")) continue let replaced = rewrite(content, "**") replaced = rewrite(replaced, "*") if (replaced !== content) token.content = replaced } } export function emphasisCompatPlugin(md: any) { // 锚定 block:无论 paragraphPerLine 是否开启都存在 md.core.ruler.after("block", "resume_emphasis_compat", emphasisCompatRule) }