import { Marked, type MarkedExtension, type Tokens } from 'marked'; import {createDirectives, presetDirectiveConfigs} from 'marked-directive'; import yaml from 'js-yaml'; import markedAlert from "marked-alert"; import markedMermaid from "./mermaid"; import {gfmHeadingId} from "marked-gfm-heading-id"; let globalIconPrefix: string | undefined = undefined; function overrideIconPrefix(path?: string){ globalIconPrefix = path; return { [Symbol.dispose](){ globalIconPrefix = undefined; } } } /** * 将表格数据转换为 CSV 格式字符串 * @param headers 表头数组 * @param rows 表格数据行 * @returns CSV 格式字符串 */ function tableToCSV(headers: string[], rows: string[][]): string { const escapeCell = (cell: string) => { // 如果单元格包含逗号、换行或引号,需要转义 if (cell.includes(',') || cell.includes('\n') || cell.includes('"')) { return `"${cell.replace(/"/g, '""')}"`; } return cell; }; const headerLine = headers.map(escapeCell).join(','); const dataLines = rows.map(row => row.map(escapeCell).join(',')); return [headerLine, ...dataLines].join('\n'); } // 使用 marked-directive 来支持指令语法 const marked = new Marked() .use(gfmHeadingId()) .use(markedAlert()) .use(markedMermaid()) .use(createDirectives([ ...presetDirectiveConfigs, { marker: '::::', level: 'container' }, { marker: ':::::', level: 'container' }, { level: 'inline', marker: ':', // :[blah] becomes renderer(token) { if (!token.meta.name) { const style = globalIconPrefix ? `style="--icon-src: url('${globalIconPrefix}/${token.text}.png')"` : ''; return ``; } return false; } }, ]), { // 自定义代码块渲染器,支持 yaml/tag 格式 extensions: [{ name: 'code-block-yaml-tag', level: 'block', start(src: string) { // 检测 ```yaml/tag 开头的代码块 return src.match(/^```yaml\/tag\s*\n/m)?.index; }, tokenizer(src: string) { const rule = /^```yaml\/tag\s*\n([\s\S]*?)\n```/; const match = rule.exec(src); if (match) { const yamlContent = match[1]?.trim() || ''; const props = yaml.load(yamlContent) as Record || {}; // 提取 tag 名称,默认为 tag-unknown const tagName = (props.tag as string) || 'tag-unknown'; // 移除 tag 属性,剩下的作为 HTML 属性 const { tag, ...rest } = props; // 提取 innerText 内容(如果有 body 字段) let content = ''; if ('body' in rest) { content = String(rest.body || ''); delete (rest as Record).body; } // 构建属性字符串 const propsStr = Object.entries(rest) .map(([key, value]) => { const strValue = String(value); // 如果值包含空格或特殊字符,添加引号 if (strValue.includes(' ') || strValue.includes('"')) { return `${key}="${strValue.replace(/"/g, '"')}"`; } return `${key}="${strValue}"`; }) .join(' '); return { type: 'code-block-yaml-tag', raw: match[0], tagName, props: propsStr, content }; } }, renderer(token: any) { // 渲染为自定义 HTML 标签 const propsAttr = token.props ? ` ${token.props}` : ''; return `<${token.tagName}${propsAttr}>${token.content || ''}\n`; } }] }); // 覆盖默认的 table renderer 以支持自动转换 marked.use({ renderer: { table(token: Tokens.Table) { // 检查表头是否包含 md-table-label const header = token.header; const labelIndex = header.findIndex(cell => cell.text === 'md-table-label'); if (labelIndex !== -1) { // 将 md-table-label 列转换为 label const headers = header.map(cell => cell.text === 'md-table-label' ? 'label' : cell.text); // 转换所有行 const rows = token.rows.map(row => row.map(cell => cell.text) ); // 生成 CSV 数据 const csvData = tableToCSV(headers, rows); // 渲染为 md-table 组件,内联 CSV 数据 return `${csvData}\n`; } // 默认表格渲染 - 使用 marked 默认行为 return false; } } } as MarkedExtension); export function parseMarkdown(content: string, iconPrefix?: string): string { using prefix = overrideIconPrefix(iconPrefix); return marked.parse(content.trimStart()) as string; } export { marked };