feat: implement granular section visibility control
Introduce the ability to reveal specific sections of a document rather than entire files. This includes: - Updating `revealedPaths` to map normalized file paths to sets of revealed section slugs. - Adding `isPathRevealed` to handle visibility logic for connected clients and GMs. - Enhancing `extractHeadings` to calculate `startLine` and `endLine` for each TOC node to support precise section identification.
This commit is contained in:
+51
-12
@@ -8,6 +8,10 @@ export interface TocNode {
|
||||
id?: string;
|
||||
path?: string;
|
||||
level: number;
|
||||
/** 1-based line index where the heading starts */
|
||||
startLine: number;
|
||||
/** 1-based line index where this heading's section ends (exclusive) */
|
||||
endLine: number;
|
||||
children?: TocNode[];
|
||||
}
|
||||
|
||||
@@ -21,26 +25,58 @@ export interface FileNode {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 markdown 内容提取标题结构
|
||||
* 从 markdown 内容提取标题结构(含行号范围)
|
||||
*/
|
||||
export function extractHeadings(content: string): TocNode[] {
|
||||
const headings: TocNode[] = [];
|
||||
const lines = content.split("\n");
|
||||
const stack: { node: TocNode; level: number }[] = [];
|
||||
const slugger = new Slugger();
|
||||
const totalLines = lines.length;
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.trim().match(/^(#{1,6})\s+(.+)$/);
|
||||
// First pass: collect heading positions
|
||||
interface HeadingPos {
|
||||
lineIndex: number; // 0-based line index
|
||||
level: number;
|
||||
title: string;
|
||||
id: string;
|
||||
}
|
||||
const positions: HeadingPos[] = [];
|
||||
|
||||
for (let i = 0; i < totalLines; i++) {
|
||||
const match = lines[i].trim().match(/^(#{1,6})\s+(.+)$/);
|
||||
if (!match) continue;
|
||||
|
||||
const level = match[1].length;
|
||||
const title = match[2].trim();
|
||||
// 使用 github-slugger 生成 ID,与 marked-gfm-heading-id 保持一致
|
||||
const id = slugger.slug(title.toLowerCase());
|
||||
const node: TocNode = { title, id, level };
|
||||
positions.push({ lineIndex: i, level, title, id });
|
||||
}
|
||||
|
||||
// Compute endLine for each heading: endLine is the 1-based line index of
|
||||
// the next heading of same or higher level (or EOF+1).
|
||||
for (let pi = 0; pi < positions.length; pi++) {
|
||||
const pos = positions[pi];
|
||||
let endLine = totalLines + 1; // default: end of file (1-based, exclusive)
|
||||
for (let pj = pi + 1; pj < positions.length; pj++) {
|
||||
if (positions[pj].level <= pos.level) {
|
||||
endLine = positions[pj].lineIndex + 1; // 1-based
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const node: TocNode = {
|
||||
title: pos.title,
|
||||
id: pos.id,
|
||||
level: pos.level,
|
||||
startLine: pos.lineIndex + 1,
|
||||
endLine,
|
||||
};
|
||||
|
||||
// 找到合适的父节点
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) {
|
||||
// Close out the current parent's endLine at this heading's start
|
||||
stack[stack.length - 1].node.endLine = node.startLine;
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
@@ -52,7 +88,13 @@ export function extractHeadings(content: string): TocNode[] {
|
||||
parent.children.push(node);
|
||||
}
|
||||
|
||||
stack.push({ node, level });
|
||||
stack.push({ node, level: node.level });
|
||||
}
|
||||
|
||||
// Close remaining stack entries at EOF
|
||||
while (stack.length > 0) {
|
||||
stack[stack.length - 1].node.endLine = totalLines + 1;
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
return headings;
|
||||
@@ -136,7 +178,7 @@ export function extractSection(content: string, sectionTitle: string): string {
|
||||
// 匹配标题(支持 1-6 级标题)
|
||||
const sectionRegex = new RegExp(
|
||||
`^(#{1,6})\\s*${escapeRegExp(sectionTitle)}\\s*$`,
|
||||
"im"
|
||||
"im",
|
||||
);
|
||||
|
||||
const match = content.match(sectionRegex);
|
||||
@@ -149,10 +191,7 @@ export function extractSection(content: string, sectionTitle: string): string {
|
||||
const startIndex = match.index!;
|
||||
|
||||
// 查找下一个同级或更高级别的标题
|
||||
const nextHeaderRegex = new RegExp(
|
||||
`^#{1,${headerLevel}}\\s+.+$`,
|
||||
"gm"
|
||||
);
|
||||
const nextHeaderRegex = new RegExp(`^#{1,${headerLevel}}\\s+.+$`, "gm");
|
||||
|
||||
// 从标题后开始搜索
|
||||
nextHeaderRegex.lastIndex = startIndex + match[0].length;
|
||||
|
||||
Reference in New Issue
Block a user