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

163 lines
4.8 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';
import { getIndexedData } from '../../data-loader/file-index';
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 } {
// 分割内容
2026-03-13 15:52:51 +08:00
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
2026-02-28 11:58:55 +08:00
// 至少需要三个部分空字符串、front matter、剩余内容
2026-03-13 15:52:51 +08:00
if (parts.length !== 3 || parts[0] !== '') {
2026-02-28 11:58:55 +08:00
return { remainingContent: content };
}
try {
// 解析 YAML front matter
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
2026-02-28 11:58:55 +08:00
// 剩余内容是第三部分及之后的所有内容
const remainingContent = parts.slice(2).join('---\n').trimStart();
2026-02-28 11:58:55 +08:00
return { frontmatter, remainingContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { remainingContent: content };
}
}
2026-03-21 17:48:30 +08:00
/**
* CSV
* @param str
* @returns CSV true
*/
export function isCSV(str: string): boolean {
const trimmed = str.trim();
// 检查是否以 YAML front matter 开头
if (trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n')) {
return true;
}
// 检查是否包含 CSV 特征:多行且有分隔符
const lines = trimmed.split(/\r?\n/).filter(line => line.trim() !== '');
if (lines.length < 2) {
return false;
}
// 检测常见 CSV 分隔符
const separators = [',', '\t', ';', '|'];
const firstLine = lines[0];
for (const sep of separators) {
if (firstLine.includes(sep)) {
// 检查其他行是否也有相同的分隔符
const hasSeparatorInOtherLines = lines.slice(1).some(line => line.includes(sep));
if (hasSeparatorInOtherLines) {
return true;
}
}
}
return false;
}
/**
* CSV
* @template T Record<string, string>
* @param csvString CSV
* @param sourcePath
* @returns CSV
*/
export function parseCSVString<T = Record<string, string>>(csvString: string, sourcePath: string = 'inline'): CSV<T> {
// 解析 front matter
const { frontmatter, remainingContent } = parseFrontMatter(csvString);
const records = parse(remainingContent, {
columns: true,
comment: '#',
trim: true,
skipEmptyLines: true
});
const result = records as Record<string, string>[];
// 添加 front matter 到结果中
const csvResult = result as CSV<T>;
if (frontmatter) {
csvResult.frontmatter = frontmatter;
for(const each of result){
Object.assign(each, frontmatter);
}
}
csvResult.sourcePath = sourcePath;
return csvResult;
}
2026-02-27 12:24:51 +08:00
/**
* CSV
2026-02-27 12:38:09 +08:00
* @template T Record<string, string>
2026-03-21 17:48:30 +08:00
* @param pathOrContent inline CSV
* @returns CSV
2026-02-27 12:24:51 +08:00
*/
2026-03-21 17:48:30 +08:00
export async function loadCSV<T = Record<string, string>>(pathOrContent: string): Promise<CSV<T>> {
// 检测是否是 inline CSV 数据
if (isCSV(pathOrContent)) {
return parseCSVString<T>(pathOrContent, 'inline');
}
// 从索引获取文件内容
const content = await getIndexedData(pathOrContent);
2026-02-27 12:24:51 +08:00
2026-02-28 11:58:55 +08:00
// 解析 front matter
const { frontmatter, remainingContent } = parseFrontMatter(content);
2026-02-28 11:58:55 +08:00
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-21 17:48:30 +08:00
csvResult.sourcePath = pathOrContent;
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)}`) || '';
}