Files
Agent-System/web/src/engine/markdownToBlock.ts
T

272 lines
7.6 KiB
TypeScript

import { DocumentAST, DocumentBlock } from '../types/block';
import { recomputeNumbering, cleanHeadingContent } from './autoNumber';
const REG_PATTERN = /(?:GB(?:\/T)?\s*\d+(?:\.\d+)?(?:-\d{4})?|YY(?:\/T)?\s*\d+(?:\.\d+)?(?:-\d{4})?|ISO\s*\d+|IEC\s*\d+|MDR|FDA|NMPA)/g;
function generateId(): string {
return 'blk_' + Math.random().toString(36).substring(2, 10);
}
/**
* markdownToBlockAST: 将 Markdown 文本解析为 Block AST 树
*/
export function markdownToBlockAST(
md: string,
meta?: Partial<DocumentAST>
): DocumentAST {
const lines = md.split(/\r?\n/);
const blocks: DocumentBlock[] = [];
let i = 0;
while (i < lines.length) {
const rawLine = lines[i];
const trimmed = rawLine.trim();
// 空行跳过
if (trimmed === '') {
i++;
continue;
}
// 1. 分隔线 (Divider)
if (/^(\*{3,}|-{3,}|_{3,})$/.test(trimmed)) {
blocks.push({
id: generateId(),
type: 'divider',
content: '',
});
i++;
continue;
}
// 2. 标题 (Heading # ~ ######)
const headingMatch = rawLine.match(/^(#{1,6})\s+(.*)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const rawTitle = headingMatch[2].trim();
const cleanedTitle = cleanHeadingContent(rawTitle);
blocks.push({
id: generateId(),
type: 'heading',
level,
content: cleanedTitle || rawTitle,
});
i++;
continue;
}
// 3. 代码围栏 (Code Block: ```mermaid / ```math / ```)
if (trimmed.startsWith('```')) {
const lang = trimmed.slice(3).trim().toLowerCase();
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i].trim().startsWith('```')) {
codeLines.push(lines[i]);
i++;
}
if (i < lines.length && lines[i].trim().startsWith('```')) {
i++; // 跳过结束 fence
}
const codeContent = codeLines.join('\n');
if (lang === 'mermaid') {
blocks.push({
id: generateId(),
type: 'mermaid',
content: codeContent,
properties: {
caption: '系统拓扑与流转时序图',
},
});
} else if (lang === 'math' || lang === 'latex') {
blocks.push({
id: generateId(),
type: 'math',
content: codeContent,
});
} else {
// 普通段落代码块
blocks.push({
id: generateId(),
type: 'paragraph',
content: '```' + lang + '\n' + codeContent + '\n```',
});
}
continue;
}
// 4. 数学公式 ($$ ... $$)
if (trimmed.startsWith('$$')) {
const mathLines: string[] = [];
if (trimmed.length > 2 && trimmed.endsWith('$$')) {
// 单行 $$公式$$
mathLines.push(trimmed.slice(2, -2).trim());
i++;
} else {
i++;
while (i < lines.length && !lines[i].trim().endsWith('$$')) {
mathLines.push(lines[i]);
i++;
}
if (i < lines.length && lines[i].trim().endsWith('$$')) {
const last = lines[i].trim();
if (last !== '$$') {
mathLines.push(last.replace(/\$\$$/, '').trim());
}
i++;
}
}
blocks.push({
id: generateId(),
type: 'math',
content: mathLines.join('\n'),
});
continue;
}
// 5. 引用/合规警示块 (> ...)
if (trimmed.startsWith('>')) {
const calloutLines: string[] = [];
while (i < lines.length && lines[i].trim().startsWith('>')) {
calloutLines.push(lines[i].trim().replace(/^>\s?/, ''));
i++;
}
const calloutText = calloutLines.join(' ');
const regs = Array.from(calloutText.matchAll(REG_PATTERN), (m) => m[0]);
blocks.push({
id: generateId(),
type: 'callout',
content: calloutText,
properties: {
calloutType: regs.length > 0 ? 'regulatory' : 'info',
regulations: regs,
},
});
continue;
}
// 6. GFM 表格 (| col1 | col2 |)
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
const tableRows: string[][] = [];
while (i < lines.length && lines[i].trim().startsWith('|') && lines[i].trim().endsWith('|')) {
const curRow = lines[i].trim();
// 忽略表头分割线行 |---|---|
if (!/^\|(?:\s*:?-+:?\s*\|)+$/.test(curRow)) {
const cells = curRow
.slice(1, -1)
.split('|')
.map((c) => c.trim());
tableRows.push(cells);
}
i++;
}
blocks.push({
id: generateId(),
type: 'table',
content: '',
properties: {
tableData: tableRows,
caption: '技术参数与法规符合性对照表',
},
});
continue;
}
// 7. 纯独立列表块 (List Block: 全部非空行均为列表语法)
if (/^\s*(\d+\.|[-*+])\s+/.test(trimmed)) {
const listLines: string[] = [];
const isOrdered = /^\s*\d+\.\s+/.test(trimmed);
let startNumber = 1;
if (isOrdered) {
const m = trimmed.match(/^\s*(\d+)\./);
if (m) startNumber = parseInt(m[1], 10) || 1;
}
while (
i < lines.length &&
lines[i].trim() !== '' &&
/^\s*(\d+\.|[-*+])\s+/.test(lines[i].trim())
) {
listLines.push(lines[i]);
i++;
}
const listItems = listLines
.map((l) => l.replace(/^\s*(\d+\.|[-*+])\s+/, '').trim())
.filter(Boolean);
blocks.push({
id: generateId(),
type: 'list',
content: listLines.join('\n'),
properties: {
isOrdered,
startNumber,
listItems,
},
});
continue;
}
// 8. 常规段落 (Paragraph)
const paraLines: string[] = [];
while (
i < lines.length &&
lines[i].trim() !== '' &&
!lines[i].trim().startsWith('#') &&
!lines[i].trim().startsWith('```') &&
!lines[i].trim().startsWith('$$') &&
!lines[i].trim().startsWith('>') &&
!(lines[i].trim().startsWith('|') && lines[i].trim().endsWith('|')) &&
!/^\s*(\d+\.|[-*+])\s+/.test(lines[i].trim()) &&
!/^(\*{3,}|-{3,}|_{3,})$/.test(lines[i].trim())
) {
paraLines.push(lines[i]);
i++;
}
const paraContent = paraLines.join('\n');
const regs = Array.from(paraContent.matchAll(REG_PATTERN), (m) => m[0]);
blocks.push({
id: generateId(),
type: 'paragraph',
content: paraContent,
properties: {
regulations: regs.length > 0 ? regs : undefined,
},
});
}
const rootBlock: DocumentBlock = {
id: 'root-doc',
type: 'document',
content: '',
children: blocks,
};
// 执行自动重编号
recomputeNumbering(rootBlock);
return {
docId: meta?.docId || 'doc_' + Math.random().toString(36).substring(2, 8),
title: meta?.title || (blocks[0]?.type === 'heading' ? blocks[0].content : '未命名受控文档'),
taskKey: meta?.taskKey || 'feasibility_reg',
version: meta?.version || 'V1.0',
category: meta?.category || 'workflow',
creatorId: meta?.creatorId,
creatorName: meta?.creatorName,
sharedAt: meta?.sharedAt,
lifecycleState: meta?.lifecycleState || 'drafting',
isClaimed: meta?.isClaimed ?? false,
author: meta?.author || '张工',
reviewer: meta?.reviewer || '李主管',
approver: meta?.approver || '王总监',
summary: meta?.summary || '由智能体小队自动生成的受控交付物',
updatedAt: meta?.updatedAt || new Date().toISOString().replace('T', ' ').substring(0, 16),
rootBlock,
};
}