Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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 <Timeseries sep=",">',
        ].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 <Timeseries sep=",">',
        ].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 };
};