feat: ai's attempt

This commit is contained in:
2026-02-26 14:24:48 +08:00
parent b370e6c302
commit ba7b264a97
6 changed files with 405 additions and 49 deletions
+30
View File
@@ -1,3 +1,7 @@
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
let dataIndex: Record<string, string> | null = null;
let isDevIndexLoaded = false;
let isCliIndexLoaded = false;
@@ -12,6 +16,32 @@ function getIndexedData(path: string): string | null {
return null;
}
/**
* 获取所有索引路径
*/
export function getIndexedPaths(): string[] {
if (!dataIndex) return [];
return Object.keys(dataIndex);
}
/**
* 生成目录树(文件树 + 标题结构)
*/
export function generateToc(): { fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> } {
const paths = getIndexedPaths();
const fileTree = buildFileTree(paths);
const pathHeadings: Record<string, TocNode[]> = {};
for (const path of paths) {
const content = getIndexedData(path);
if (content) {
pathHeadings[path] = extractHeadings(content);
}
}
return { fileTree, pathHeadings };
}
/**
* 在 dev 环境加载 glob 索引
*/
+157
View File
@@ -0,0 +1,157 @@
/**
* 目录树节点
*/
export interface TocNode {
title: string;
path?: string;
level: number;
children?: TocNode[];
}
/**
* 文件树节点
*/
export interface FileNode {
name: string;
path: string;
children?: FileNode[];
}
/**
* 从 markdown 内容提取标题结构
*/
export function extractHeadings(content: string): TocNode[] {
const headings: TocNode[] = [];
const lines = content.split("\n");
const stack: { node: TocNode; level: number }[] = [];
for (const line of lines) {
const match = line.match(/^(#{1,6})\s+(.+)$/);
if (!match) continue;
const level = match[1].length;
const title = match[2].trim();
const node: TocNode = { title, level };
// 找到合适的父节点
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
stack.pop();
}
if (stack.length === 0) {
headings.push(node);
} else {
const parent = stack[stack.length - 1].node;
if (!parent.children) parent.children = [];
parent.children.push(node);
}
stack.push({ node, level });
}
return headings;
}
/**
* 从路径列表生成文件树
*/
export function buildFileTree(paths: string[]): FileNode[] {
const root: FileNode[] = [];
const map = new Map<string, FileNode>();
for (const path of paths) {
const parts = path.split("/").filter(Boolean);
let currentPath = "";
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isFile = i === parts.length - 1 && part.includes(".");
currentPath += "/" + part;
if (map.has(currentPath)) continue;
const node: FileNode = {
name: part.replace(/\.md$/, ""),
path: currentPath,
};
if (!isFile) {
node.children = [];
}
map.set(currentPath, node);
// 找到父节点并添加
const parentPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
const parent = parentPath ? map.get(parentPath) : null;
if (parent?.children) {
parent.children.push(node);
} else {
root.push(node);
}
}
}
// 对节点排序:文件夹在前,文件在后,按名称字母排序
const sortNodes = (nodes: FileNode[]) => {
nodes.sort((a, b) => {
const aIsDir = !!a.children;
const bIsDir = !!b.children;
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return a.name.localeCompare(b.name);
});
for (const node of nodes) {
if (node.children) sortNodes(node.children);
}
};
sortNodes(root);
return root;
}
/**
* 转义正则表达式特殊字符
*/
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* 从 markdown 内容中提取指定标题下的内容
*/
export function extractSection(content: string, sectionTitle: string): string {
// 匹配标题(支持 1-6 级标题)
const sectionRegex = new RegExp(
`^(#{1,6})\\s*${escapeRegExp(sectionTitle)}\\s*$`,
"im"
);
const match = content.match(sectionRegex);
if (!match) {
// 没有找到指定标题,返回空字符串
return "";
}
const headerLevel = match[1].length;
const startIndex = match.index!;
// 查找下一个同级或更高级别的标题
const nextHeaderRegex = new RegExp(
`^#{1,${headerLevel}}\\s+.+$`,
"gm"
);
// 从标题后开始搜索
nextHeaderRegex.lastIndex = startIndex + match[0].length;
const nextMatch = nextHeaderRegex.exec(content);
if (nextMatch) {
// 返回从当前标题到下一个同级/更高级标题之间的内容
return content.slice(startIndex, nextMatch.index).trim();
}
// 返回从当前标题到文件末尾的内容
return content.slice(startIndex).trim();
}