UNPKG

@rr0/cms

Version:

RR0 Content Management System (CMS)

126 lines (125 loc) 4.85 kB
import { Level2Date as EdtfDate } from "@rr0/time"; export class CsvMapper { constructor(sep = ",", escapeStr = "\"", prefix = "", maxLevel = 1) { this.sep = sep; this.escapeStr = escapeStr; this.prefix = prefix; this.maxLevel = maxLevel; this.fields = new Set(); this.fieldMapper = (context, key, value, sourceTime, level = 0) => { let addField = true; let val; if (value instanceof Date) { val = value.toISOString(); } else if (typeof value === "string") { val = this.escape(value); } else if (value instanceof URL || value instanceof EdtfDate) { val = value.toString(); } else if (Array.isArray(value)) { val = this.escape(value.map((item, i) => this.fieldMapper(context, String(i), item, sourceTime, level + 1)).join(this.sep), true); } else if (typeof value === "object") { if (level <= this.maxLevel) { const subMapper = new CsvMapper(this.sep, this.escapeStr, this.prefix + key + ".", level); const subValues = subMapper.map(context, value, sourceTime, level + 1); let addSubFields = !isFinite(key); if (addSubFields) { subMapper.fields.forEach(subField => this.fields.add(subField)); } val = subValues; } addField = false; } else { val = value; } if (addField) { this.fields.add(this.prefix + key); } return val; }; } /** * Map a case to a CSV row. * * @param context * @param sourceCase * @param sourceTime * @param level */ map(context, sourceCase, sourceTime, level = 0) { const sourceCaseEntries = Object.entries(sourceCase); const entries = Array.from(sourceCaseEntries).sort((entry1, entry2) => entry1[0].localeCompare(entry2[0])); return entries.map(entry => this.fieldMapper(context, entry[0], entry[1], sourceTime, level)).join(this.sep); } /** * Reduce a set of cases to a CSV string. * * @param context * @param sourceCases * @param sourceTime */ mapAll(context, sourceCases, sourceTime) { const values = sourceCases.map(c => this.map(context, c, sourceTime)); return Array.from(this.fields).join(this.sep) + "\n" + values.join("\n"); } escape(value, force) { if (this.escapeStr && (force || value.indexOf(this.sep) >= 0)) { value = value.replaceAll(this.escapeStr, this.escapeStr + this.escapeStr); return this.escapeStr + value + this.escapeStr; } else { return value; } } /** * Converts CSV contents to a list of cases. * * @param data * @param headers The headers info to fill, to keep CSV columns order. */ parse(data, headers = []) { let eol = data.indexOf("\n"); const header = data.substring(0, eol); data = data.substring(eol + 1).replaceAll(`""`, "''"); this.fields.clear(); const columns = header.split(this.sep); headers.push(...columns); columns.forEach(column => this.fields.add(column)); const records = []; let regex = new RegExp(`(?:${this.escapeStr}(.*?)${this.escapeStr}(?:${this.sep}|\n))|(?:(.*?)(?:${this.sep}|\n))`, "gs"); let values = []; let m; const fields = Array.from(this.fields); while ((m = regex.exec(data)) !== null) { if (m.index === regex.lastIndex) { // This is necessary to avoid infinite loops with zero-width matches regex.lastIndex++; if (regex.lastIndex > data.length) { break; } } m.forEach((match, group) => { let empty = match === this.sep && group; if (match !== undefined && group) { const val = empty ? "" : match; values.push(val); const c = {}; if (values.length === fields.length) { for (let i = 0; i < fields.length; i++) { const field = fields[i]; c[field] = values[i]; } records.push(c); values = []; // data = data.substring(regex.lastIndex) //regex.lastIndex = 0 } } }); } return records; } }