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

143 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { parse } from 'csv-parse/browser/esm/sync';
import yaml from 'js-yaml';
import { getIndexedData } from '../../data-loader/file-index';
/**
* 解析 front matter
* @param content 包含 front matter 的内容
* @returns 解析结果,包含 front matter 和剩余内容
*/
function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainingContent: string } {
// 分割内容
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
// 至少需要三个部分空字符串、front matter、剩余内容
if (parts.length !== 3 || parts[0] !== '') {
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 };
}
}
/**
* 检测字符串是否是 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,
bom: 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;
}
/**
* 加载 CSV 文件
* @template T 返回数据的类型,默认为 Record<string, string>
* @param pathOrContent 文件路径或 inline CSV 字符串
* @returns 解析后的 CSV 数据
*/
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);
return parseCSVString<T>(content, pathOrContent);
}
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;
sourcePath: string;
}
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}}}`;
}
return body?.replace(/\{\{([^}]+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
}