127 lines
5.2 KiB
TypeScript
127 lines
5.2 KiB
TypeScript
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;
|
||
}
|