2026-02-26 14:24:48 +08:00
|
|
|
|
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
|
|
|
|
|
|
|
|
|
|
|
|
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
|
|
|
|
|
|
|
2026-02-26 09:31:12 +08:00
|
|
|
|
let dataIndex: Record<string, string> | null = null;
|
2026-02-26 14:30:09 +08:00
|
|
|
|
let indexLoadPromise: Promise<void> | null = null;
|
2026-02-26 09:31:12 +08:00
|
|
|
|
|
2026-02-26 09:24:26 +08:00
|
|
|
|
/**
|
2026-02-26 09:31:12 +08:00
|
|
|
|
* 从索引获取数据
|
2026-02-26 09:24:26 +08:00
|
|
|
|
*/
|
2026-02-26 09:31:12 +08:00
|
|
|
|
function getIndexedData(path: string): string | null {
|
|
|
|
|
|
if (dataIndex && dataIndex[path]) {
|
|
|
|
|
|
return dataIndex[path];
|
2026-02-26 09:24:26 +08:00
|
|
|
|
}
|
2026-02-26 09:31:12 +08:00
|
|
|
|
return null;
|
2026-02-26 09:24:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:24:48 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有索引路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getIndexedPaths(): string[] {
|
|
|
|
|
|
if (!dataIndex) return [];
|
|
|
|
|
|
return Object.keys(dataIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:30:09 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载索引(只加载一次)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function ensureIndexLoaded(): Promise<void> {
|
|
|
|
|
|
if (indexLoadPromise) return indexLoadPromise;
|
|
|
|
|
|
|
|
|
|
|
|
indexLoadPromise = (async () => {
|
|
|
|
|
|
// 尝试 CLI 环境:从 /__CONTENT_INDEX.json 加载
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch("/__CONTENT_INDEX.json");
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const index = await response.json();
|
|
|
|
|
|
dataIndex = { ...dataIndex, ...index };
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// CLI 索引不可用时尝试 dev 环境
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:40:02 +08:00
|
|
|
|
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载
|
|
|
|
|
|
try {
|
|
|
|
|
|
const context = import.meta.webpackContext("../../content", {
|
|
|
|
|
|
recursive: true,
|
|
|
|
|
|
regExp: /\.md$/,
|
|
|
|
|
|
});
|
|
|
|
|
|
const keys = context.keys();
|
|
|
|
|
|
const index: Record<string, string> = {};
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
|
// context 返回的是模块,需要访问其 default 导出(raw-loader 处理后的内容)
|
|
|
|
|
|
const module = context(key) as { default?: string } | string;
|
|
|
|
|
|
const content = typeof module === "string" ? module : module.default ?? "";
|
|
|
|
|
|
const normalizedPath = "/content" + key.slice(1);
|
|
|
|
|
|
index[normalizedPath] = content;
|
2026-02-26 14:30:09 +08:00
|
|
|
|
}
|
2026-02-26 14:40:02 +08:00
|
|
|
|
dataIndex = { ...dataIndex, ...index };
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// webpackContext 不可用时忽略
|
2026-02-26 14:30:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
return indexLoadPromise;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:24:48 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成目录树(文件树 + 标题结构)
|
|
|
|
|
|
*/
|
2026-02-26 14:30:09 +08:00
|
|
|
|
export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> {
|
|
|
|
|
|
await ensureIndexLoaded();
|
|
|
|
|
|
|
2026-02-26 14:24:48 +08:00
|
|
|
|
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 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 09:24:26 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 异步加载数据
|
|
|
|
|
|
* @param path 数据路径
|
|
|
|
|
|
* @returns 数据内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function fetchData(path: string): Promise<string> {
|
2026-02-26 14:30:09 +08:00
|
|
|
|
await ensureIndexLoaded();
|
2026-02-26 09:31:12 +08:00
|
|
|
|
|
2026-02-26 09:24:26 +08:00
|
|
|
|
// 首先尝试从索引获取
|
|
|
|
|
|
const indexedData = getIndexedData(path);
|
|
|
|
|
|
if (indexedData) {
|
|
|
|
|
|
return indexedData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 索引不存在时,使用 fetch 加载
|
2026-02-26 14:07:58 +08:00
|
|
|
|
if (dataIndex !== null) throw new Error(`no data in index: ${path}`);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(path);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Failed to fetch: ${path}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return await response.text();
|
|
|
|
|
|
} catch (error) {
|
2026-02-26 14:07:58 +08:00
|
|
|
|
console.error("fetchData error:", error);
|
2026-02-26 09:24:26 +08:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|