import { escapeHtml, isString } from "./utilities";
import get from "lodash/get";
/**
* Simple way to retrieve linked data in `value` param from task
* Works only for prefixed values ($image); non-prefixed values left as is
* It's possible to add some text which will be left untouched; that's useful for
* visual Text tags to display some additional info ("Title: $title")
* @param {string} value param
* @param {object} task
*/
export const parseValue = (value, task) => {
const reVar = /\$[\w[\].{}]+/gi;
if (!value) return "";
// value can refer to structures, not only texts, so just replace wouldn't be enough
if (value.match(reVar)?.[0] === value) {
return get(task, value.slice(1)) ?? "";
}
return value.replace(reVar, (v) => get(task, v.slice(1) ?? ""));
};
/**
* Parse CSV
* Accepts only numbers as a data
* Returns hash with names (or indexed hash for headless csv) as a keys
* and arrays of numbers as a values
* @param {string} text
* @returns {{ [string]: number[] }}
*/
export const parseCSV = (text, separator = "auto") => {
// @todo iterate over newlines for better performance
const lines = text.split("\n");
let names;
if (separator !== "auto" && !lines[0].includes(separator)) {
throw new Error([`Cannot find provided separator "${separator}".`, `Row 1: ${lines[0]}`].join("\n"));
}
// detect separator (2nd line is definitely with data)
if (separator === "auto" && lines.length > 1) {
const candidates = lines[1].trim().match(/[,;\s\t]/g);
if (!candidates.length) throw new Error("No separators found");
if (candidates.some((c) => c !== candidates[0])) {
const list = Array.from(new Set(candidates))
.map(escapeHtml)
.map((s) => `"${s}"`)
.join(", ");
throw new Error(
[
`More than one possible separator found: ${list}`,
'You can provide correct one with ',
].join("\n"),
);
}
separator = candidates[0];
if (lines[0].split(separator).length !== lines[1].split(separator).length)
throw new Error(
[
"Different amount of elements in rows.",
`Row 1: ${lines[0]}`,
`Row 2: ${lines[1]}`,
`Guessed separator: ${separator}`,
'You can provide correct one with ',
].join("\n"),
);
}
const re = new RegExp(
[
'"(?:""|[^"])*"', // quoted text with possible quoted quotes inside it ("not a ""value""")
`[^"${separator}]+`, // usual value, no quotes, between separators
`(?=${separator}(?:${separator}|$))`, // empty value in the middle or at the end of string
`^(?=${separator})`, // empty value at the start of the string
].join("|"),
"g",
);
const split = (text) => text.trim().match(re);
// detect header; if it is omitted, use indices as a header names
names = split(lines[0]);
const secondLine = split(lines[1]);
// assume that we have at least one column with numbers
// and name of this column is not number :)
// so we have different types for values in first and second rows
if (!names.every((n, i) => isNaN(n) === isNaN(secondLine[i]))) {
lines.shift();
names = names.map((n) => n.toLowerCase());
} else {
names = names.map((_, i) => String(i));
}
const result = {};
for (const name of names) result[name] = [];
if (names.length !== split(lines[0]).length) {
throw new Error(
[
"Column names count differs from data columns count.",
`Columns: ${names.join(", ")};`,
`Data: ${lines[0]};`,
`Separator: "${separator}".`,
].join("\n"),
);
}
let row;
let i;
for (const line of lines) {
// skip empty lines including the last line
if (!line.trim()) continue;
row = split(line);
for (i = 0; i < row.length; i++) {
const val = +row[i];
result[names[i]].push(isNaN(val) ? row[i] : val);
}
}
return [result, names];
};
/**
* Internal helper to check if string is JSON
* @param {string} value
* @returns {object|false}
*/
export const tryToParseJSON = (value) => {
if (isString(value) && value[0] === "{") {
try {
return JSON.parse(value);
} catch (e) {
// somthing went wrong
}
}
return false;
};
/**
* Parse value type
* Accept value type as a parameter
* Returns type, seperator and options object by analyzing valueType
*/
export const parseTypeAndOption = (valueType) => {
const [, type, sep] = valueType.match(/^(\w+)(.)?/) ?? [];
const options = {};
if (sep) {
const pairs = valueType.split(sep).slice(1);
pairs.forEach((pair) => {
const [k, v] = pair.split("=", 2);
options[k] = v ?? true; // options without values are `true`
});
}
return { type, sep, options };
};