ttrpg-tools/src/data-loader/index.ts

116 lines
3.1 KiB
TypeScript
Raw Normal View History

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;
}
}