- Phase 0: 实现服务端回复导出策略矩阵引擎 (replyexport.EvaluatePolicy)、数据库迁移 (2026-09-11-message-export-policy.sql)、SSE done 事件挂载与能力探测 API - Phase 1: 构建前端 ReplyDocumentIR 语法树解析与渲染管道,支持 Markdown / DOCX 文件下载及带格式富文本剪贴板复制 - Phase 2: 实现私密自由文档 (freeform_documents) 领域服务、PostgreSQL 乐观锁版本管理、ACL 隐私隔离、一键共享与右侧栏编辑联动 - 专项修复: 1. 修复后台 API 路由注册并加入双向拉取同步,确保私密保存实时进入我的私密列表 2. 实现轻量回复专属 DOCX 生成与静默下载逻辑 (downloadDocxReply) 3. 将 LaTeX 公式转为标准 MathML 矢量标记,支持粘贴到 Microsoft Word / WPS 自动识别为原生公式
86 lines
2.4 KiB
TypeScript
86 lines
2.4 KiB
TypeScript
import { ReplyDocumentIR, ReplyNode, ReplyInline } from '../../types/replyExport';
|
|
|
|
/**
|
|
* 将 Reply Document IR 序列化为规范化 GFM Markdown
|
|
*/
|
|
export function serializeMarkdown(ir: ReplyDocumentIR): string {
|
|
return ir.nodes.map(renderNodeToMarkdown).join('\n\n');
|
|
}
|
|
|
|
function renderNodeToMarkdown(node: ReplyNode): string {
|
|
switch (node.type) {
|
|
case 'heading': {
|
|
const hashes = '#'.repeat(node.level);
|
|
return `${hashes} ${node.children.map(renderInlineToMarkdown).join('')}`;
|
|
}
|
|
case 'paragraph': {
|
|
return node.children.map(renderInlineToMarkdown).join('');
|
|
}
|
|
case 'list': {
|
|
return node.items
|
|
.map((item, idx) => {
|
|
const prefix = node.ordered ? `${idx + 1}. ` : '- ';
|
|
return `${prefix}${item.map(renderInlineToMarkdown).join('')}`;
|
|
})
|
|
.join('\n');
|
|
}
|
|
case 'table': {
|
|
const headerLine = `| ${node.header.map((c) => c.map(renderInlineToMarkdown).join('')).join(' | ')} |`;
|
|
const sepLine = `| ${node.header.map(() => '---').join(' | ')} |`;
|
|
const rowLines = node.rows.map(
|
|
(row) => `| ${row.map((c) => c.map(renderInlineToMarkdown).join('')).join(' | ')} |`
|
|
);
|
|
return [headerLine, sepLine, ...rowLines].join('\n');
|
|
}
|
|
case 'blockquote': {
|
|
const inner = node.children.map(renderNodeToMarkdown).join('\n\n');
|
|
return inner
|
|
.split('\n')
|
|
.map((l) => `> ${l}`)
|
|
.join('\n');
|
|
}
|
|
case 'code': {
|
|
const lang = node.language || '';
|
|
return `\`\`\`${lang}\n${node.value}\n\`\`\``;
|
|
}
|
|
case 'math': {
|
|
if (node.display) {
|
|
return `$$\n${node.latex}\n$$`;
|
|
}
|
|
return `$${node.latex}$`;
|
|
}
|
|
case 'mermaid': {
|
|
return `\`\`\`mermaid\n${node.source}\n\`\`\``;
|
|
}
|
|
case 'divider': {
|
|
return '---';
|
|
}
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function renderInlineToMarkdown(inline: ReplyInline): string {
|
|
switch (inline.type) {
|
|
case 'text': {
|
|
let val = inline.value;
|
|
if (inline.bold) val = `**${val}**`;
|
|
if (inline.italic) val = `*${val}*`;
|
|
if (inline.strike) val = `~~${val}~~`;
|
|
return val;
|
|
}
|
|
case 'code':
|
|
return `\`${inline.value}\``;
|
|
case 'math':
|
|
return `$${inline.latex}$`;
|
|
case 'link': {
|
|
const text = inline.children.map(renderInlineToMarkdown).join('');
|
|
return `[${text}](${inline.href})`;
|
|
}
|
|
case 'hardBreak':
|
|
return '\n';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|