feat: ai's attempt
This commit is contained in:
@@ -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 索引
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user