Propagate summary.duty_policy into deliverables/AST and show 编制人兼任 on the action bar. Co-authored-by: Cursor <cursoragent@cursor.com>
279 lines
7.8 KiB
TypeScript
279 lines
7.8 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 safeMd = typeof md === 'string' ? md : '';
|
|
const lines = safeMd.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 || '',
|
|
authorId: meta?.authorId,
|
|
reviewer: meta?.reviewer || '',
|
|
reviewerId: meta?.reviewerId,
|
|
approver: meta?.approver || '',
|
|
approverId: meta?.approverId,
|
|
dutiesAuthoritative: meta?.dutiesAuthoritative,
|
|
dutyPolicy: meta?.dutyPolicy,
|
|
revisionEtag: meta?.revisionEtag,
|
|
summary: meta?.summary || '由项目助手自动生成的受控交付物',
|
|
updatedAt: meta?.updatedAt || new Date().toISOString().replace('T', ' ').substring(0, 16),
|
|
rootBlock,
|
|
};
|
|
}
|