/** * markdown-transform-html v2 引擎 * * 基于 markdown-it(100% CommonMark 兼容),在标准语法之上扩展简历布局语法。 * 公开 API 与 v1 完全兼容:markdownToHTML(template, options) * * 相比 v1 的行为变化: * - 标准 markdown 全量支持:*_列表、__粗体__、嵌套引用、表格对齐、转义、自动链接、 * 代码块语言标注、硬换行等(v1 均缺失或错误) * - 安全修复:属性值转义、javascript: 链接拦截、href/src 加引号, * html 默认 false(v1 布局内部完全不过滤的问题已修复) * - 输出属性带引号(v1 为 class=flex-layout 无引号风格),结构 class 不变 * * options: * - xss: true(默认)转义内嵌 HTML;false 时允许内嵌 HTML 透传 * - paragraphPerLine: true(默认)保持 v1 的「一行一个段落」结构,关闭后为标准段落合并 * - highlight / lineNumber: 代码块 class 标注(与 v1 选项一致) * - svgMap: svg:name 图标映射 */ import MarkdownIt from "markdown-it" import markdownItCJKFriendly from "markdown-it-cjk-friendly" import { svgLoaderManager } from "../../utils/svgLoader" import { containersPlugin } from "./plugins/containers" import { iconsPlugin } from "./plugins/icons" import { paragraphPerLinePlugin } from "./plugins/paragraphPerLine" import { emphasisCompatPlugin } from "./plugins/emphasisCompat" import { tightenListsPlugin } from "./plugins/tightenLists" import { rendererCompatPlugin } from "./plugins/rendererCompat" export interface ITransformOptions { lineNumber?: boolean highlight?: boolean xss?: boolean svgMap?: Record paragraphPerLine?: boolean tightenLists?: boolean } const defaultOptions: Required> = { lineNumber: false, highlight: false, xss: true, paragraphPerLine: true, tightenLists: true, } export function createMarkdownRenderer(options: ITransformOptions = {}): MarkdownIt { const md: any = new MarkdownIt({ // xss: false 时允许内嵌 HTML(与 v1 的 xss 语义一致) html: options.xss === false, // 软换行渲染为
:段落级换行由 paragraphPerLine 拆分为独立

, // 列表项/引用内部的换行保持
,与 tiptap 富文本回写行为对齐 breaks: true, linkify: false, }) applyResumeDialectPlugins(md, options) // icon:/svg: 渲染规则仅在预览渲染实例注册(编辑器解析实例中保持文本) md.use(iconsPlugin) md.use(rendererCompatPlugin) return md } /** * 将简历方言插件注入到任意 markdown-it 实例。 * * 供富文本编辑器(tiptap-markdown)解析 markdown 时复用同一套方言: * 布局容器 / 段落逐行 / 列表收紧 / 强调兼容 / CJK 标点强调。 * 刻意不包含 icons 渲染(icon:/svg: 在编辑器内保持为文本,与历史行为一致) * 和 rendererCompat(渲染契约仅对最终 HTML 输出有意义)。 */ export function applyResumeDialectPlugins(md: any, options: ITransformOptions = {}) { // tiptap-markdown 每次解析都会重复调用 parse.setup,防止规则重复注册 if ((md as any).__resumeDialectApplied) return md ;(md as any).__resumeDialectApplied = true md.use(containersPlugin) if (options.tightenLists !== false) md.use(tightenListsPlugin) if (options.paragraphPerLine !== false) md.use(paragraphPerLinePlugin) md.use(emphasisCompatPlugin) md.use(markdownItCJKFriendly) return md } /** * 将 markdown 转换为 HTML * @param template markdown 文本 * @param options 转换选项 */ export function markdownToHTML(template: string, options?: ITransformOptions): string { const op = { ...defaultOptions, ...(options || {}) } if (op.svgMap) { svgLoaderManager.setSvgMap(op.svgMap) } const md = createMarkdownRenderer(op) // 压缩标签间的装饰性换行,恢复 v1 的紧凑输出。 // 消费方 CSS(.markdown-transform-html)带有 white-space: pre-line, // markdown-it 风格的块间换行会被渲染成真实空行,导致块间距整体变大。 // pre 内部换行不受影响(内容换行不紧跟在 > 后);
后的换行一并去掉, // 避免 pre-line 下出现双倍换行。 return md .render(template ?? "") .replace(/(]*>)\n+/g, "$1") .replace(/>\n+(?=<)/g, ">") }