File size: 3,184 Bytes
bc20498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
'use strict';
/* c8 ignore start */
/*! (c) Andrea Giammarchi - ISC */

const {isArray} = Array;
const {parse: jsonParse} = JSON;

/** @typedef {{s: string[], d: Date[]}} Foreign foreign strings and dates */

/**
 * Transform quoted keys into regular keys.
 * @param {string} str the key to eventually normalize
 * @param {Foreign} foreign foreign strings and dates
 * @returns 
 */
const getKey = (str, {s}) => str.replace(/"s(\d+)"/g, (_, $1) => s[$1]);

/**
 * Given a `'string'` or a `"string"` returns a JSON compatible string.
 * @param {string} str a TOML entry to parse
 * @param {Foreign} foreign foreign strings and dates
 * @returns {string}
 */
const getValue = (str, foreign) => jsonParse(
  str.replace(/(\S+?)\s*=/g, '"$1":'),
  (_, value) => typeof value === 'string' ?
    foreign[value[0]][value.slice(1)] :
    value
);

/**
 * Crawl the `json` object via the given array of keys and handle array entries.
 * @param {string[]} keys a path with all keys to reach the entry
 * @param {Foreign} foreign foreign strings and dates
 * @param {object} entry the root entry of the TOML
 * @param {boolean} asArray handle array entries
 * @returns {object} the current entry to handle
 */
const getPath = (keys, foreign, entry, asArray) => {
  for (let i = 0, {length} = keys, last = length - 1; i < length; i++) {
    const key = getKey(keys[i], foreign);
    entry = entry[key] || (entry[key] = (asArray && (i === last) ? [] : {}));
    if (isArray(entry)) {
      if ((i === last) || !entry.length)
        entry.push({});
      entry = entry.at(-1);
    }
  }
  return entry;
};

/**
 * Given a TOML text, removes stirngs and dates for easier parsing +
 * remove multi-line arrays to not need evaluation.
 * @param {string} toml the TOML text to map
 * @param {string[]} strings mapped strings
 * @param {Date[]} dates mapped Dates
 * @returns {[string, Foreign]}
 */
const mapForeign = (toml, strings, dates) => [
  toml
    // map strings in the TOML
    .replace(
      /(["'])(?:(?=(\\?))\2.)*?\1/g,
      value => `"s${strings.push(value.slice(1, -1)) - 1}"`
    )
    // map dates in the TOML
    .replace(
      /\d{2,}([:-]\d{2}){2}([ T:-][\dZ:-]+)?/g,
      value => `"d${dates.push(new Date(value)) - 1}"`
    )
    // avoid multi-line array entries
    .replace(/,\s*[\r\n]+/g, ', ')
    .replace(/\[\s*[\r\n]+/g, '[')
    .replace(/[\r\n]+\s*]/g, ']'),
  {s: strings, d: dates}
];

/**
 * Given a simple subset of a TOML file, returns its JS equivalent.
 * @param {string} toml the TOML text to parse
 * @returns {object} the TOML equivalent as JSON serializable
 */
const parse = toml => {
  const [text, foreign] = mapForeign(toml, [], []);
  const json = {};
  let entry = json;
  for (let line of text.split(/[\r\n]+/)) {
    if ((line = line.trim()) && !line.startsWith('#')) {
      if (/^(\[+)(.*?)\]+/.test(line))
        entry = getPath(RegExp.$2.trim().split('.'), foreign, json, RegExp.$1 !== '[');
      else if (/^(\S+?)\s*=([^#]+)/.test(line)) {
        const {$1: key, $2: value} = RegExp;
        entry[getKey(key, foreign)] = getValue(value.trim(), foreign);
      }
    }
  }
  return json;
};

exports.parse = parse;

/* c8 ignore stop */