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

127 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { DocumentBlock } from '../types/block';
/**
* cleanHeadingContent: 清除标题中用户或模型输入的手工章节序号前缀
* 防止与系统自动计算的多级编号产生双重冗余叠加(如出现 "1. 1. 标题" 或 "1.1 1. 标题")
* 支持清洗以下模式:
* - 1. / 1.1 / 1.1.1 / 1、/ 1.1、
* - 一、/ 二、/ 十、
* - (1) / (1) / (一) / (一)
* - 第一章 / 第1章 / 第一节 / 第1节
* 严格保护诸如 "4K超高清"、"5G传输"、"100%国产化" 等专业词汇不被误删。
*/
export function cleanHeadingContent(content: string): string {
if (!content) return '';
return content
.replace(
/^(\d+(?:\.\d+)*[、..\s]+|[一二三四五六七八九十]+[、..\s]+|([一二三四五六七八九十\d]+)\s*|\([一二三四五六七八九十\d]+\)\s*|第[一二三四五六七八九十\d]+[章节部分篇][、..\s]*)/,
''
)
.trim();
}
/**
* recomputeNumbering: 动态单遍重算整个文档块树的章节多级编号、图、表、公式与列表编号
*
* 核心层级规则:
* 1. 标题层级自适应 (Heading Hierarchy):
* - 若文档在开头包含唯一的 H1 一级标题,则该 H1 被识别为【文档总标题/封面标题】,不赋予章节编号(numbering = undefined);
* 正文章节从 H2(二级标题)开始起算为第一级章节(1, 2, 3...),H3 起算为二级子节(1.1, 1.2, 2.1...),H4(1.1.1...)以此类推。
* - 若文档包含多个 H1(或无 H1),则以最小标题层级为基准层级(H1 为 1, 2, 3...,H2 为 1.1, 1.2...)。
* 2. 图/表/公式按章编号 (Chapter-based sub-item counters):
* - 每进入一个新的顶层章节,重置表、图、公式计数器为 1。
* - 表格编号:表 {chapter}-{tableIndex} (如 表 1-1, 表 2-1)
* - 流程图/架构图/插图编号:图 {chapter}-{figureIndex} (如 图 1-1, 图 3-1)
* - 数学公式编号:式 ({chapter}-{formulaIndex}) (如 式 (3-1), 式 (4-1))
* 3. 有序列表编号 (Ordered Lists):
* - 校验并规范化 startNumber,维护其条目序列一致性。
*/
export function recomputeNumbering(root: DocumentBlock): DocumentBlock {
const children = root.children || [];
if (children.length === 0) return root;
// 1. 扫描所有标题块,判定是否存在唯一的顶层文档大标题
const headingBlocks = children.filter((b) => b.type === 'heading');
const h1Blocks = headingBlocks.filter((h) => (h.level || 1) === 1);
const isFirstH1SingleTitle =
headingBlocks.length > 0 &&
(headingBlocks[0].level || 1) === 1 &&
h1Blocks.length === 1;
// 计算基准章节层级 (Base Chapter Level)
// 如果存在唯一 H1 总标题,则章节从 H2 (level 2) 开始;否则以最小 heading level 开始
let baseChapterLevel = 1;
if (isFirstH1SingleTitle) {
baseChapterLevel = 2;
} else if (headingBlocks.length > 0) {
baseChapterLevel = Math.min(...headingBlocks.map((h) => h.level || 1));
}
const chapterStack: number[] = [];
let currentTopChapter = 0;
let tableCounter = 1;
let figureCounter = 1;
let formulaCounter = 1;
function traverse(block: DocumentBlock) {
if (block.type === 'heading') {
const rawLevel = block.level || 1;
// 命中唯一步骤:文档总标题直接免编号
if (isFirstH1SingleTitle && block === headingBlocks[0]) {
block.numbering = undefined;
} else if (block.properties?.noNumbering) {
block.numbering = undefined;
} else {
const depth = Math.max(0, rawLevel - baseChapterLevel);
// 调整层级栈深度
while (chapterStack.length <= depth) {
chapterStack.push(0);
}
chapterStack[depth] += 1;
chapterStack.length = depth + 1; // 截断更深层级,深度更深重置为0
const numStr = chapterStack.join('.');
block.numbering = numStr;
// 若为顶层章节 (depth === 0)
if (depth === 0) {
currentTopChapter = chapterStack[0];
tableCounter = 1;
figureCounter = 1;
formulaCounter = 1;
}
}
} else if (block.type === 'table') {
const chapterPrefix = currentTopChapter > 0 ? `${currentTopChapter}-` : '';
block.numbering = `表 ${chapterPrefix}${tableCounter++}`;
} else if (block.type === 'mermaid' || (block.type as string) === 'image' || (block.type as string) === 'figure') {
const chapterPrefix = currentTopChapter > 0 ? `${currentTopChapter}-` : '';
block.numbering = `图 ${chapterPrefix}${figureCounter++}`;
} else if (block.type === 'math') {
const chapterPrefix = currentTopChapter > 0 ? `${currentTopChapter}-` : '';
block.numbering = `式 (${chapterPrefix}${formulaCounter++})`;
} else if (block.type === 'list') {
if (block.properties?.isOrdered ?? true) {
if (!block.properties) block.properties = {};
if (typeof block.properties.startNumber !== 'number') {
block.properties.startNumber = 1;
}
}
}
if (block.children && block.children.length > 0) {
for (const child of block.children) {
traverse(child);
}
}
}
for (const child of children) {
traverse(child);
}
return root;
}