ttrpg-tools/src/components/utils/csv-loader.ts

103 lines
2.9 KiB
TypeScript
Raw Normal View History

2026-02-27 12:24:51 +08:00
import { parse } from 'csv-parse/browser/esm/sync';
2026-02-28 11:58:55 +08:00
import yaml from 'js-yaml';
2026-02-27 12:24:51 +08:00
/**
* CSV
*/
2026-02-27 12:38:09 +08:00
const csvCache = new Map<string, Record<string, string>[]>();
2026-02-27 12:24:51 +08:00
2026-02-28 11:58:55 +08:00
/**
* front matter
* @param content front matter
* @returns front matter
*/
function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainingContent: string } {
// 检查是否以 --- 开头
if (!content.trim().startsWith('---')) {
return { remainingContent: content };
}
// 分割内容
const parts = content.split(/(?:^|\n)---\s*\n/);
// 至少需要三个部分空字符串、front matter、剩余内容
if (parts.length < 3) {
return { remainingContent: content };
}
try {
// 解析 YAML front matter
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
// 剩余内容是第三部分及之后的所有内容
const remainingContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, remainingContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { remainingContent: content };
}
}
2026-02-27 12:24:51 +08:00
/**
* CSV
2026-02-27 12:38:09 +08:00
* @template T Record<string, string>
2026-02-27 12:24:51 +08:00
*/
2026-02-28 11:58:55 +08:00
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
2026-02-27 12:24:51 +08:00
if (csvCache.has(path)) {
2026-02-28 11:58:55 +08:00
return csvCache.get(path)! as CSV<T>;
2026-02-27 12:24:51 +08:00
}
const response = await fetch(path);
const content = await response.text();
2026-02-28 11:58:55 +08:00
// 解析 front matter
const { frontmatter, remainingContent } = parseFrontMatter(content);
const records = parse(remainingContent, {
2026-02-27 12:24:51 +08:00
columns: true,
comment: '#',
trim: true,
skipEmptyLines: true
});
2026-02-27 12:38:09 +08:00
const result = records as Record<string, string>[];
2026-02-28 11:58:55 +08:00
// 添加 front matter 到结果中
const csvResult = result as CSV<T>;
if (frontmatter) {
csvResult.frontmatter = frontmatter;
2026-03-13 10:50:08 +08:00
for(const each of result){
2026-03-13 10:55:43 +08:00
Object.assign(each, frontmatter);
2026-03-13 10:50:08 +08:00
}
2026-02-28 11:58:55 +08:00
}
2026-03-13 11:46:18 +08:00
csvResult.sourcePath = path;
2026-02-28 11:58:55 +08:00
2026-02-27 12:24:51 +08:00
csvCache.set(path, result);
2026-02-28 11:58:55 +08:00
return csvResult;
}
type JSONData = JSONArray | JSONObject | string | number | boolean | null;
interface JSONArray extends Array<JSONData> {}
interface JSONObject extends Record<string, JSONData> {}
export type CSV<T> = T[] & {
frontmatter?: JSONObject;
2026-03-13 11:46:18 +08:00
sourcePath: string;
2026-02-27 12:24:51 +08:00
}
2026-02-28 11:58:55 +08:00
export function processVariables<T extends JSONObject> (body: string, currentRow: T, csv: CSV<T>, filtered?: T[], remix?: boolean): string {
const rolled = filtered || csv;
function replaceProp(key: string) {
const row = remix ?
rolled[Math.floor(Math.random() * rolled.length)] :
currentRow;
const frontMatter = csv.frontmatter;
if(key in row) return row[key];
if(frontMatter && key in frontMatter) return frontMatter[key];
return `{{${key}}}`;
2026-02-28 11:58:55 +08:00
}
2026-03-13 10:55:43 +08:00
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
2026-02-28 11:58:55 +08:00
}