@rr0/cms
Version:
RR0 Content Management System (CMS)
126 lines (125 loc) • 4.85 kB
JavaScript
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;
}
}