143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
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)}`) || '';
|
||
}
|