@tehw0lf/n8n-nodes-toon 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,476 +38,513 @@ const types_1 = require("./types");
38
38
  const utils = __importStar(require("./ToonUtils"));
39
39
  class ToonDecoder {
40
40
  constructor(options) {
41
+ this.lines = [];
42
+ this.pos = 0;
43
+ this.inHeaderSpan = false;
41
44
  this.options = options;
42
45
  }
43
46
  decode(toonText) {
44
- const lines = this.parseLines(toonText);
45
- const nonEmptyLines = lines.filter((line) => !line.isEmpty);
46
- if (nonEmptyLines.length === 0) {
47
- return null;
48
- }
49
- const rootForm = this.determineRootForm(nonEmptyLines);
50
- let result;
51
- if (rootForm === 'array') {
52
- result = this.parseArray(nonEmptyLines, 0);
53
- }
54
- else if (rootForm === 'primitive') {
55
- const token = nonEmptyLines[0].content.trim();
56
- result = this.parseTokenValue(token);
57
- }
58
- else {
59
- result = this.parseObject(nonEmptyLines, 0, 0);
47
+ this.lines = this.parseLines(toonText);
48
+ this.pos = 0;
49
+ if (this.lines.length === 0) {
50
+ return {};
60
51
  }
52
+ let result = this.parseRoot();
61
53
  if (this.options.expandPaths === 'safe' && utils.isPlainObject(result)) {
62
54
  result = this.expandPaths(result);
63
55
  }
64
56
  return result;
65
57
  }
66
58
  parseLines(text) {
67
- const rawLines = text.split('\n');
59
+ var _a, _b;
60
+ const withoutBom = text.startsWith('') ? text.slice(1) : text;
61
+ const rawLines = withoutBom.split('\n');
68
62
  const parsed = [];
63
+ let blankBefore = false;
69
64
  for (let i = 0; i < rawLines.length; i++) {
70
- const line = rawLines[i];
71
- if (this.options.strict && /^\t/.test(line)) {
72
- throw new types_1.ToonDecodingError('Tabs not allowed in indentation', {
73
- lineNumber: i + 1,
74
- line,
75
- });
65
+ let line = rawLines[i].replace(/\r$/, '');
66
+ line = line.replace(/ +$/, '');
67
+ const lineNumber = i + 1;
68
+ if (/^ *#/.test(line)) {
69
+ continue;
76
70
  }
77
- const match = line.match(/^( *)/);
78
- const indent = match ? match[1].length : 0;
79
- if (this.options.strict && indent > 0 && indent % this.options.indent !== 0) {
80
- throw new types_1.ToonDecodingError(`Indentation must be multiple of ${this.options.indent}, got ${indent} spaces`, {
81
- lineNumber: i + 1,
82
- line,
83
- });
71
+ if (line.trim() === '') {
72
+ blankBefore = true;
73
+ continue;
74
+ }
75
+ if (/^[ \t]*\t/.test(line)) {
76
+ if (this.options.strict) {
77
+ throw new types_1.ToonDecodingError('Tabs not allowed in indentation', {
78
+ lineNumber,
79
+ line,
80
+ });
81
+ }
82
+ line = line.replace(/^[ \t]+/, (ws) => ws.replace(/\t/g, ''));
83
+ }
84
+ const indent = (_b = (_a = line.match(/^( *)/)) === null || _a === void 0 ? void 0 : _a[1].length) !== null && _b !== void 0 ? _b : 0;
85
+ if (this.options.strict && indent % this.options.indent !== 0) {
86
+ throw new types_1.ToonDecodingError(`Indentation must be multiple of ${this.options.indent}, got ${indent} spaces`, { lineNumber, line });
84
87
  }
85
88
  parsed.push({
86
- content: line,
89
+ content: line.slice(indent),
87
90
  indent,
88
- lineNumber: i + 1,
89
- isEmpty: line.trim() === '',
91
+ lineNumber,
92
+ isEmpty: false,
93
+ blankBefore,
90
94
  });
95
+ blankBefore = false;
91
96
  }
92
97
  return parsed;
93
98
  }
94
- determineRootForm(lines) {
95
- const firstLine = lines[0].content.trim();
96
- if (/^\[(?:0|[1-9]\d*)[\t|]?\]/.test(firstLine) && this.isValidArrayHeader(firstLine)) {
97
- return 'array';
98
- }
99
- if (firstLine === '[]') {
100
- return 'array';
99
+ withinHeaderSpan(parse) {
100
+ const previous = this.inHeaderSpan;
101
+ this.inHeaderSpan = true;
102
+ try {
103
+ return parse();
101
104
  }
102
- if (this.options.strict && this.isInvalidArrayHeader(firstLine)) {
103
- throw new types_1.ToonDecodingError('Non-whitespace content between bracket segment and colon/fields: line MUST NOT be parsed as array header', { lineNumber: lines[0].lineNumber, line: firstLine });
105
+ finally {
106
+ this.inHeaderSpan = previous;
104
107
  }
105
- if (lines.length === 1 && !firstLine.includes(':')) {
106
- return 'primitive';
108
+ }
109
+ checkBlankInSpan(line, seen) {
110
+ const insideSpan = seen > 0 || this.inHeaderSpan;
111
+ if (this.options.strict && insideSpan && line.blankBefore) {
112
+ throw new types_1.ToonDecodingError('Blank line inside a header span', {
113
+ lineNumber: line.lineNumber,
114
+ line: line.content,
115
+ });
107
116
  }
108
- return 'object';
109
117
  }
110
- parseArrayHeader(line) {
111
- const content = line.content.trim();
118
+ depthOf(line) {
119
+ return Math.floor(line.indent / this.options.indent);
120
+ }
121
+ peek() {
122
+ return this.pos < this.lines.length ? this.lines[this.pos] : null;
123
+ }
124
+ parseRoot() {
125
+ const first = this.lines[0];
126
+ const content = first.content;
112
127
  if (content === '[]') {
113
- return { key: null, length: 0, delimiter: ',', fields: null, rawLine: content, lineNumber: line.lineNumber };
128
+ this.pos = 1;
129
+ this.checkNoTrailingContent();
130
+ return [];
114
131
  }
115
- const match = content.match(/^(.*?)\[(0|[1-9]\d*)([\t|])?\]\s*(?:\{([^}]+)\}\s*)?:(.*)$/);
116
- if (!match) {
117
- if (this.options.strict && /\[0\d+[\t|]?\]/.test(content)) {
118
- throw new types_1.ToonDecodingError(`Invalid array length: leading zeros are not allowed in bracket segment`, { lineNumber: line.lineNumber, line: content });
132
+ const header = this.tryParseHeader(first);
133
+ if (header !== null && header.key === null && this.depthOf(first) === 0) {
134
+ if (header.keyed) {
135
+ const value = this.parseKeyedTabular(header, 0);
136
+ this.checkNoTrailingContent();
137
+ return value;
119
138
  }
120
- throw new types_1.ToonDecodingError('Invalid array header format', {
121
- lineNumber: line.lineNumber,
122
- line: content,
123
- });
139
+ const value = this.parseArrayBody(header, 0);
140
+ this.checkNoTrailingContent();
141
+ return value;
124
142
  }
125
- const [, keyPart, lengthStr, delimSym, fieldsPart] = match;
126
- let key = keyPart.trim();
127
- if (key) {
128
- if (key.startsWith('"') && key.endsWith('"')) {
129
- key = utils.unescapeString(key.slice(1, -1));
130
- }
143
+ if (this.lines.length === 1 && utils.findUnquoted(content, ':') < 0) {
144
+ this.pos = 1;
145
+ return this.parseValueToken(content, first);
131
146
  }
132
- else {
133
- key = null;
147
+ const scalarLines = this.lines.filter((line) => this.depthOf(line) === 0 &&
148
+ utils.findUnquoted(line.content, ':') < 0 &&
149
+ !line.content.startsWith('- '));
150
+ if (scalarLines.length >= 2) {
151
+ throw new types_1.ToonDecodingError('Multiple depth-0 lines that are neither headers nor key-value lines', { lineNumber: scalarLines[1].lineNumber, line: scalarLines[1].content });
134
152
  }
135
- const length = parseInt(lengthStr, 10);
136
- if (isNaN(length) || length < 0) {
137
- throw new types_1.ToonDecodingError(`Invalid array length: ${lengthStr}`, {
138
- lineNumber: line.lineNumber,
153
+ return this.parseObject(0);
154
+ }
155
+ checkNoTrailingContent() {
156
+ const next = this.peek();
157
+ if (next === null) {
158
+ return;
159
+ }
160
+ if (this.options.strict) {
161
+ throw new types_1.ToonDecodingError('Trailing content after completed root form', {
162
+ lineNumber: next.lineNumber,
163
+ line: next.content,
139
164
  });
140
165
  }
141
- let delimiter = ',';
142
- if (delimSym === '\t') {
143
- delimiter = '\t';
144
- }
145
- else if (delimSym === '|') {
146
- delimiter = '|';
147
- }
148
- let fields = null;
149
- if (fieldsPart) {
150
- fields = this.parseDelimitedTokens(fieldsPart, delimiter).map((token) => {
151
- if (token.startsWith('"') && token.endsWith('"')) {
152
- return utils.unescapeString(token.slice(1, -1));
153
- }
154
- return token;
166
+ this.pos = this.lines.length;
167
+ }
168
+ tryParseHeader(line) {
169
+ const outcome = utils.tryParseHeader(line.content);
170
+ if (outcome.error !== null && this.options.strict) {
171
+ throw new types_1.ToonDecodingError(outcome.error, {
172
+ lineNumber: line.lineNumber,
173
+ line: line.content,
155
174
  });
156
175
  }
157
- return {
158
- key,
159
- length,
160
- delimiter,
161
- fields,
162
- rawLine: content,
163
- lineNumber: line.lineNumber,
164
- };
176
+ return outcome.header;
165
177
  }
166
- parseDelimitedTokens(text, delimiter) {
167
- const tokens = [];
168
- let current = '';
169
- let inQuotes = false;
170
- let escaped = false;
171
- for (let i = 0; i < text.length; i++) {
172
- const char = text[i];
173
- if (escaped) {
174
- current += char;
175
- escaped = false;
176
- continue;
177
- }
178
- if (char === '\\') {
179
- current += char;
180
- escaped = true;
181
- continue;
178
+ parseObject(depth) {
179
+ const obj = {};
180
+ while (this.pos < this.lines.length) {
181
+ const line = this.lines[this.pos];
182
+ const lineDepth = this.depthOf(line);
183
+ if (lineDepth < depth) {
184
+ break;
182
185
  }
183
- if (char === '"' && !escaped) {
184
- inQuotes = !inQuotes;
185
- current += char;
186
+ if (lineDepth > depth) {
187
+ if (this.options.strict) {
188
+ throw new types_1.ToonDecodingError('Over-indented line: no enclosing scope was opened', { lineNumber: line.lineNumber, line: line.content });
189
+ }
190
+ this.pos++;
186
191
  continue;
187
192
  }
188
- if (!inQuotes) {
189
- if (delimiter === ',' && text.slice(i, i + 2) === ', ') {
190
- tokens.push(current.trim());
191
- current = '';
192
- i++;
193
- continue;
194
- }
195
- else if (char === delimiter) {
196
- tokens.push(current.trim());
197
- current = '';
198
- continue;
193
+ this.checkBlankInSpan(line, 0);
194
+ const { key, value } = this.parseFieldLine(line, depth);
195
+ this.assignKey(obj, key, line);
196
+ utils.setDecodedKey(obj, key, value);
197
+ }
198
+ return obj;
199
+ }
200
+ assignKey(obj, key, line) {
201
+ if (Object.prototype.hasOwnProperty.call(obj, key) && this.options.strict) {
202
+ throw new types_1.ToonDecodingError(`Duplicate key '${key}' at the same depth`, {
203
+ lineNumber: line.lineNumber,
204
+ line: line.content,
205
+ });
206
+ }
207
+ }
208
+ parseFieldLine(line, depth) {
209
+ const header = this.tryParseHeader(line);
210
+ if (header !== null) {
211
+ if (header.key === null) {
212
+ if (this.options.strict) {
213
+ throw new types_1.ToonDecodingError('Keyless header is not valid in object-field position', { lineNumber: line.lineNumber, line: line.content });
199
214
  }
200
215
  }
201
- current += char;
216
+ else {
217
+ this.pos++;
218
+ const value = header.keyed
219
+ ? this.parseKeyedTabularBody(header, depth + 1)
220
+ : this.parseArrayBodyAfterHeader(header, depth);
221
+ return { key: header.key, value };
222
+ }
202
223
  }
203
- if (current.trim()) {
204
- tokens.push(current.trim());
224
+ const colonIndex = utils.findUnquoted(line.content, ':');
225
+ if (colonIndex < 0) {
226
+ throw new types_1.ToonDecodingError('Missing colon in key context', {
227
+ lineNumber: line.lineNumber,
228
+ line: line.content,
229
+ });
230
+ }
231
+ const key = this.decodeKeyToken(line.content.slice(0, colonIndex), line);
232
+ const rest = utils.trimSpaces(line.content.slice(colonIndex + 1));
233
+ this.pos++;
234
+ if (rest === '[]') {
235
+ return { key, value: [] };
205
236
  }
206
- return tokens;
237
+ if (rest !== '') {
238
+ return { key, value: this.parseValueToken(rest, line) };
239
+ }
240
+ return { key, value: this.parseNestedScope(depth) };
207
241
  }
208
- parseArray(lines, startIndex) {
209
- const headerLine = lines[startIndex];
210
- const header = this.parseArrayHeader(headerLine);
211
- if (header.rawLine === '[]') {
212
- return [];
242
+ parseNestedScope(depth) {
243
+ const next = this.peek();
244
+ if (next === null || this.depthOf(next) <= depth) {
245
+ return {};
213
246
  }
214
- const inlineMatch = header.rawLine.match(/:\s*(.+)$/);
215
- if (inlineMatch && inlineMatch[1].trim()) {
216
- const valuesStr = inlineMatch[1].trim();
217
- const tokens = this.parseDelimitedTokens(valuesStr, header.delimiter);
218
- const values = tokens.map((token) => this.parseTokenValue(token));
219
- if (this.options.strict && values.length !== header.length) {
220
- throw new types_1.ToonDecodingError(`Array length mismatch: expected ${header.length}, got ${values.length}`, {
221
- lineNumber: header.lineNumber,
222
- });
223
- }
247
+ if (this.options.strict && this.depthOf(next) > depth + 1) {
248
+ throw new types_1.ToonDecodingError('Indentation depth jump: nested scope must start one level deeper', { lineNumber: next.lineNumber, line: next.content });
249
+ }
250
+ return this.parseObject(depth + 1);
251
+ }
252
+ parseArrayBodyAfterHeader(header, headerDepth) {
253
+ return this.parseArrayContent(header, headerDepth + 1);
254
+ }
255
+ parseArrayBody(header, headerDepth) {
256
+ this.pos++;
257
+ return this.parseArrayContent(header, headerDepth + 1);
258
+ }
259
+ parseArrayContent(header, contentDepth) {
260
+ var _a;
261
+ if (header.fields !== null && header.inline !== '') {
262
+ throw new types_1.ToonDecodingError('Content after a fields-bearing header colon', {
263
+ lineNumber: (_a = this.lines[this.pos - 1]) === null || _a === void 0 ? void 0 : _a.lineNumber,
264
+ line: header.inline,
265
+ });
266
+ }
267
+ if (header.fields !== null) {
268
+ return this.parseTabularRows(header, contentDepth);
269
+ }
270
+ if (header.inline !== '') {
271
+ const values = utils
272
+ .splitDelimited(header.inline, header.delimiter)
273
+ .map((token) => this.parseValueToken(token, this.lines[this.pos - 1]));
274
+ this.checkCount(values.length, header.length, 'inline array values');
224
275
  return values;
225
276
  }
226
- const baseIndent = headerLine.indent;
227
- const valueIndent = baseIndent + this.options.indent;
228
- if (header.fields) {
229
- return this.parseTabularArray(lines, startIndex, header);
230
- }
231
- const values = [];
232
- let currentIndex = startIndex + 1;
233
- let currentObj = null;
234
- const seenKeys = new Set();
235
- while (currentIndex < lines.length) {
236
- const line = lines[currentIndex];
237
- if (line.isEmpty) {
238
- if (this.options.strict) {
239
- throw new types_1.ToonDecodingError('Blank lines not allowed in arrays in strict mode', {
240
- lineNumber: line.lineNumber,
241
- });
242
- }
243
- currentIndex++;
244
- continue;
277
+ if (header.length === 0) {
278
+ const next = this.peek();
279
+ if (next === null || this.depthOf(next) < contentDepth) {
280
+ return [];
245
281
  }
246
- if (line.indent <= baseIndent) {
247
- if (currentObj !== null) {
248
- values.push(currentObj);
249
- currentObj = null;
250
- }
282
+ }
283
+ return this.parseListItems(header, contentDepth);
284
+ }
285
+ parseTabularRows(header, rowDepth) {
286
+ const fields = header.fields;
287
+ const leafCount = utils.countLeafFields(fields);
288
+ const rows = [];
289
+ while (this.pos < this.lines.length) {
290
+ const line = this.lines[this.pos];
291
+ if (this.depthOf(line) !== rowDepth) {
251
292
  break;
252
293
  }
253
- if (line.indent < valueIndent) {
254
- throw new types_1.ToonDecodingError(`Invalid indentation: expected ${valueIndent}, got ${line.indent}`, {
255
- lineNumber: line.lineNumber,
256
- });
257
- }
258
- const content = line.content.trim();
259
- if (this.options.strict && this.isInvalidArrayHeader(content)) {
260
- throw new types_1.ToonDecodingError('Non-whitespace content between bracket segment and colon/fields: line MUST NOT be parsed as array header', { lineNumber: line.lineNumber, line: content });
261
- }
262
- if (/^\[\d+[\t|]?\]/.test(content) && this.isValidArrayHeader(content)) {
263
- if (currentObj !== null) {
264
- values.push(currentObj);
265
- currentObj = null;
266
- seenKeys.clear();
267
- }
268
- const nestedArray = this.parseArray(lines, currentIndex);
269
- values.push(nestedArray);
270
- const arrayHeader = this.parseArrayHeader(lines[currentIndex]);
271
- const inlineMatch = arrayHeader.rawLine.match(/:\s*(.+)$/);
272
- if (inlineMatch && inlineMatch[1].trim()) {
273
- currentIndex++;
274
- }
275
- else {
276
- currentIndex = this.findNextSiblingIndex(lines, currentIndex, valueIndent);
277
- }
294
+ const colonIndex = utils.findUnquoted(line.content, ':');
295
+ const delimIndex = utils.findUnquoted(line.content, header.delimiter);
296
+ if (colonIndex >= 0 && (delimIndex < 0 || colonIndex < delimIndex)) {
297
+ break;
278
298
  }
279
- else if (content.includes(':')) {
280
- const colonIndex = this.findUnquotedColon(content);
281
- if (colonIndex > 0) {
282
- const valuePart = content.slice(colonIndex + 1).trim();
283
- const keyPart = content.slice(0, colonIndex).trim();
284
- let key;
285
- if (keyPart.startsWith('"') && keyPart.endsWith('"')) {
286
- key = utils.unescapeString(keyPart.slice(1, -1));
287
- }
288
- else {
289
- key = keyPart;
290
- }
291
- if (!valuePart) {
292
- if (currentObj !== null) {
293
- values.push(currentObj);
294
- currentObj = null;
295
- seenKeys.clear();
296
- }
297
- const obj = this.parseObject(lines, currentIndex, valueIndent);
298
- values.push(obj);
299
- currentIndex = this.findNextSiblingIndex(lines, currentIndex, valueIndent);
300
- }
301
- else {
302
- if (seenKeys.has(key)) {
303
- if (currentObj !== null) {
304
- values.push(currentObj);
305
- }
306
- currentObj = {};
307
- seenKeys.clear();
308
- }
309
- if (currentObj === null) {
310
- currentObj = {};
311
- }
312
- currentObj[key] = this.parseTokenValue(valuePart);
313
- seenKeys.add(key);
314
- currentIndex++;
299
+ this.checkBlankInSpan(line, rows.length);
300
+ const cells = utils.splitDelimited(line.content, header.delimiter);
301
+ this.checkWidth(cells.length, leafCount, line);
302
+ rows.push(this.materializeRow(cells, fields, line));
303
+ this.pos++;
304
+ }
305
+ this.checkCount(rows.length, header.length, 'tabular rows');
306
+ return rows;
307
+ }
308
+ materializeRow(cells, fields, line) {
309
+ let index = 0;
310
+ const walk = (entries) => {
311
+ const obj = {};
312
+ for (const entry of entries) {
313
+ if (entry.children === null) {
314
+ if (index < cells.length) {
315
+ utils.setDecodedKey(obj, entry.name, this.parseValueToken(cells[index], line));
315
316
  }
317
+ index++;
316
318
  }
317
319
  else {
318
- if (currentObj !== null) {
319
- values.push(currentObj);
320
- currentObj = null;
321
- seenKeys.clear();
322
- }
323
- values.push(this.parseTokenValue(content));
324
- currentIndex++;
325
- }
326
- }
327
- else {
328
- if (currentObj !== null) {
329
- values.push(currentObj);
330
- currentObj = null;
331
- seenKeys.clear();
320
+ utils.setDecodedKey(obj, entry.name, walk(entry.children));
332
321
  }
333
- values.push(this.parseTokenValue(content));
334
- currentIndex++;
335
322
  }
336
- }
337
- if (currentObj !== null) {
338
- values.push(currentObj);
339
- }
340
- if (this.options.strict && values.length !== header.length) {
341
- throw new types_1.ToonDecodingError(`Array length mismatch: expected ${header.length}, got ${values.length}`, {
342
- lineNumber: header.lineNumber,
323
+ return obj;
324
+ };
325
+ return walk(fields);
326
+ }
327
+ parseKeyedTabular(header, headerDepth) {
328
+ this.pos++;
329
+ return this.parseKeyedTabularBody(header, headerDepth + 1);
330
+ }
331
+ parseKeyedTabularBody(header, entryDepth) {
332
+ var _a;
333
+ if (header.inline !== '') {
334
+ throw new types_1.ToonDecodingError('Content after a keyed header colon', {
335
+ lineNumber: (_a = this.lines[this.pos - 1]) === null || _a === void 0 ? void 0 : _a.lineNumber,
336
+ line: header.inline,
343
337
  });
344
338
  }
345
- return values;
346
- }
347
- parseTabularArray(lines, startIndex, header) {
348
- const rows = [];
349
- let currentIndex = startIndex + 1;
350
- const baseIndent = lines[startIndex].indent;
351
- while (currentIndex < lines.length && rows.length < header.length) {
352
- const line = lines[currentIndex];
353
- if (line.isEmpty) {
339
+ const fields = header.fields;
340
+ const leafCount = utils.countLeafFields(fields);
341
+ const obj = {};
342
+ let count = 0;
343
+ while (this.pos < this.lines.length) {
344
+ const line = this.lines[this.pos];
345
+ if (this.depthOf(line) < entryDepth) {
346
+ break;
347
+ }
348
+ if (this.depthOf(line) > entryDepth) {
354
349
  if (this.options.strict) {
355
- throw new types_1.ToonDecodingError('Blank lines not allowed in arrays in strict mode', {
350
+ throw new types_1.ToonDecodingError('Over-indented line in keyed tabular scope', {
356
351
  lineNumber: line.lineNumber,
352
+ line: line.content,
357
353
  });
358
354
  }
359
- currentIndex++;
355
+ this.pos++;
360
356
  continue;
361
357
  }
362
- if (line.indent <= baseIndent) {
363
- break;
364
- }
365
- const content = line.content.trim();
366
- const tokens = this.parseDelimitedTokens(content, header.delimiter);
367
- if (this.options.strict && tokens.length !== header.fields.length) {
368
- throw new types_1.ToonDecodingError(`Row field count mismatch: expected ${header.fields.length}, got ${tokens.length}`, {
369
- lineNumber: line.lineNumber,
370
- });
371
- }
372
- const row = {};
373
- for (let i = 0; i < header.fields.length; i++) {
374
- const field = header.fields[i];
375
- const value = i < tokens.length ? this.parseTokenValue(tokens[i]) : null;
376
- row[field] = value;
358
+ const colonIndex = utils.findUnquoted(line.content, ':');
359
+ if (colonIndex < 0) {
360
+ if (this.options.strict) {
361
+ throw new types_1.ToonDecodingError('Line at entry depth without an unquoted colon', {
362
+ lineNumber: line.lineNumber,
363
+ line: line.content,
364
+ });
365
+ }
366
+ this.pos++;
367
+ continue;
377
368
  }
378
- rows.push(row);
379
- currentIndex++;
380
- }
381
- return rows;
369
+ this.checkBlankInSpan(line, count);
370
+ const entryKey = this.decodeKeyToken(line.content.slice(0, colonIndex), line);
371
+ const rest = utils.trimSpaces(line.content.slice(colonIndex + 1));
372
+ const cells = rest === '' ? [] : utils.splitDelimited(rest, header.delimiter);
373
+ this.checkWidth(cells.length, leafCount, line);
374
+ this.assignKey(obj, entryKey, line);
375
+ utils.setDecodedKey(obj, entryKey, this.materializeRow(cells, fields, line));
376
+ count++;
377
+ this.pos++;
378
+ }
379
+ this.checkCount(count, header.length, 'keyed entry rows');
380
+ return obj;
382
381
  }
383
- parseObject(lines, startIndex, expectedIndent) {
384
- const obj = {};
385
- let currentIndex = startIndex;
386
- while (currentIndex < lines.length) {
387
- const line = lines[currentIndex];
388
- if (line.isEmpty) {
389
- currentIndex++;
390
- continue;
382
+ parseListItems(header, itemDepth) {
383
+ const items = [];
384
+ while (this.pos < this.lines.length) {
385
+ const line = this.lines[this.pos];
386
+ const lineDepth = this.depthOf(line);
387
+ if (lineDepth < itemDepth) {
388
+ break;
391
389
  }
392
- if (currentIndex > startIndex && line.indent < expectedIndent) {
390
+ if (lineDepth === itemDepth && !this.isListItemLine(line)) {
393
391
  break;
394
392
  }
395
- const content = line.content.trim();
396
- if (this.options.strict && this.isInvalidArrayHeader(content)) {
397
- throw new types_1.ToonDecodingError('Non-whitespace content between bracket segment and colon/fields: line MUST NOT be parsed as array header', { lineNumber: line.lineNumber, line: content });
398
- }
399
- if (/\[\d+[\t|]?\]/.test(content) && this.isValidArrayHeader(content)) {
400
- const headerMatch = content.match(/^(.*?)\[/);
401
- if (headerMatch && headerMatch[1].trim()) {
402
- const header = this.parseArrayHeader(line);
403
- const array = this.parseArray(lines, currentIndex);
404
- if (header.key) {
405
- obj[header.key] = array;
406
- }
407
- currentIndex = this.findNextSiblingIndex(lines, currentIndex, expectedIndent);
408
- }
409
- else {
410
- currentIndex++;
393
+ if (lineDepth > itemDepth) {
394
+ if (this.options.strict) {
395
+ throw new types_1.ToonDecodingError('Over-indented line in list scope', {
396
+ lineNumber: line.lineNumber,
397
+ line: line.content,
398
+ });
411
399
  }
400
+ this.pos++;
412
401
  continue;
413
402
  }
414
- const colonIndex = this.findUnquotedColon(content);
415
- if (colonIndex < 0) {
416
- currentIndex++;
417
- continue;
418
- }
419
- const keyPart = content.slice(0, colonIndex).trim();
420
- const valuePart = content.slice(colonIndex + 1).trim();
421
- let key;
422
- if (keyPart.startsWith('"') && keyPart.endsWith('"')) {
423
- key = utils.unescapeString(keyPart.slice(1, -1));
424
- }
425
- else {
426
- key = keyPart;
427
- }
428
- if (!valuePart) {
429
- const nextLine = currentIndex + 1 < lines.length ? lines[currentIndex + 1] : null;
430
- if (nextLine && !nextLine.isEmpty && nextLine.indent > line.indent) {
431
- if (/\[\d+[\t|]?\]/.test(nextLine.content.trim()) && this.isValidArrayHeader(nextLine.content.trim())) {
432
- const array = this.parseArray(lines, currentIndex + 1);
433
- obj[key] = array;
434
- currentIndex = this.findNextSiblingIndex(lines, currentIndex + 1, line.indent);
435
- }
436
- else {
437
- const nestedObj = this.parseObject(lines, currentIndex + 1, line.indent + this.options.indent);
438
- obj[key] = nestedObj;
439
- currentIndex = this.findNextSiblingIndex(lines, currentIndex + 1, line.indent);
440
- }
441
- }
442
- else {
443
- obj[key] = null;
444
- currentIndex++;
403
+ this.checkBlankInSpan(line, items.length);
404
+ items.push(this.withinHeaderSpan(() => this.parseListItem(line, itemDepth)));
405
+ }
406
+ this.checkCount(items.length, header.length, 'list items');
407
+ return items;
408
+ }
409
+ isListItemLine(line) {
410
+ return line.content === '-' || line.content.startsWith('- ');
411
+ }
412
+ parseListItem(line, itemDepth) {
413
+ if (line.content === '-') {
414
+ this.pos++;
415
+ return {};
416
+ }
417
+ const rest = line.content.slice(2);
418
+ if (rest === '[]') {
419
+ this.pos++;
420
+ return [];
421
+ }
422
+ const inner = {
423
+ content: rest,
424
+ indent: line.indent + 2,
425
+ lineNumber: line.lineNumber,
426
+ isEmpty: false,
427
+ blankBefore: false,
428
+ };
429
+ const header = this.tryParseHeader(inner);
430
+ if (header !== null) {
431
+ this.pos++;
432
+ if (header.key === null) {
433
+ if (header.fields !== null && this.options.strict) {
434
+ throw new types_1.ToonDecodingError('Keyless fields-bearing header is not valid as a list item', { lineNumber: line.lineNumber, line: line.content });
445
435
  }
436
+ return this.parseArrayContent(header, itemDepth + 1);
437
+ }
438
+ const obj = {};
439
+ utils.setDecodedKey(obj, header.key, header.keyed
440
+ ? this.parseKeyedTabularBody(header, itemDepth + 2)
441
+ : this.parseArrayContent(header, itemDepth + 2));
442
+ this.parseListItemRestFields(obj, itemDepth + 1);
443
+ return obj;
444
+ }
445
+ const colonIndex = utils.findUnquoted(rest, ':');
446
+ if (colonIndex >= 0) {
447
+ const key = this.decodeKeyToken(rest.slice(0, colonIndex), line);
448
+ const valuePart = utils.trimSpaces(rest.slice(colonIndex + 1));
449
+ this.pos++;
450
+ const obj = {};
451
+ if (valuePart === '[]') {
452
+ utils.setDecodedKey(obj, key, []);
453
+ }
454
+ else if (valuePart !== '') {
455
+ utils.setDecodedKey(obj, key, this.parseValueToken(valuePart, line));
446
456
  }
447
457
  else {
448
- if (valuePart === '[]') {
449
- obj[key] = [];
450
- }
451
- else {
452
- obj[key] = this.parseTokenValue(valuePart);
453
- }
454
- currentIndex++;
458
+ utils.setDecodedKey(obj, key, this.parseNestedScope(itemDepth + 1));
455
459
  }
460
+ this.parseListItemRestFields(obj, itemDepth + 1);
461
+ return obj;
456
462
  }
457
- return obj;
463
+ this.pos++;
464
+ return this.parseValueToken(rest, line);
458
465
  }
459
- isValidArrayHeader(content) {
460
- if (content === '[]')
461
- return true;
462
- return /^(.*?)\[(0|[1-9]\d*)([\t|])?\]\s*(?:\{[^}]+\}\s*)?:/.test(content);
463
- }
464
- isInvalidArrayHeader(content) {
465
- return /\[\d+[\t|]?\]/.test(content) && !this.isValidArrayHeader(content);
466
- }
467
- findUnquotedColon(text) {
468
- let inQuotes = false;
469
- let escaped = false;
470
- for (let i = 0; i < text.length; i++) {
471
- const char = text[i];
472
- if (escaped) {
473
- escaped = false;
474
- continue;
475
- }
476
- if (char === '\\') {
477
- escaped = true;
478
- continue;
466
+ parseListItemRestFields(obj, fieldDepth) {
467
+ while (this.pos < this.lines.length) {
468
+ const line = this.lines[this.pos];
469
+ if (this.depthOf(line) !== fieldDepth || this.isListItemLine(line)) {
470
+ break;
479
471
  }
480
- if (char === '"') {
481
- inQuotes = !inQuotes;
482
- continue;
472
+ this.checkBlankInSpan(line, 1);
473
+ const { key, value } = this.parseFieldLine(line, fieldDepth);
474
+ this.assignKey(obj, key, line);
475
+ utils.setDecodedKey(obj, key, value);
476
+ }
477
+ }
478
+ checkCount(actual, declared, what) {
479
+ if (this.options.strict && actual !== declared) {
480
+ throw new types_1.ToonDecodingError(`Count mismatch for ${what}: expected ${declared}, got ${actual}`);
481
+ }
482
+ }
483
+ checkWidth(actual, expected, line) {
484
+ if (this.options.strict && actual !== expected) {
485
+ throw new types_1.ToonDecodingError(`Row width mismatch: expected ${expected} cells, got ${actual}`, { lineNumber: line.lineNumber, line: line.content });
486
+ }
487
+ }
488
+ decodeKeyToken(token, line) {
489
+ const trimmed = utils.trimSpaces(token);
490
+ if (trimmed.startsWith('"')) {
491
+ if (!trimmed.endsWith('"') || trimmed.length < 2) {
492
+ throw new types_1.ToonDecodingError('Unterminated quoted key', {
493
+ lineNumber: line.lineNumber,
494
+ line: line.content,
495
+ });
483
496
  }
484
- if (char === ':' && !inQuotes) {
485
- return i;
497
+ const unescaped = utils.tryUnescapeString(trimmed.slice(1, -1));
498
+ if (unescaped.error !== null) {
499
+ throw new types_1.ToonDecodingError(unescaped.error, {
500
+ lineNumber: line.lineNumber,
501
+ line: line.content,
502
+ });
486
503
  }
504
+ return unescaped.value;
487
505
  }
488
- return -1;
506
+ return trimmed;
489
507
  }
490
- findNextSiblingIndex(lines, currentIndex, expectedIndent) {
491
- let index = currentIndex + 1;
492
- while (index < lines.length) {
493
- const line = lines[index];
494
- if (line.isEmpty) {
495
- index++;
496
- continue;
508
+ parseValueToken(token, line) {
509
+ const trimmed = utils.trimSpaces(token);
510
+ if (trimmed.startsWith('"')) {
511
+ const closing = this.findClosingQuote(trimmed);
512
+ if (closing < 0) {
513
+ throw new types_1.ToonDecodingError('Unterminated quoted string', {
514
+ lineNumber: line === null || line === void 0 ? void 0 : line.lineNumber,
515
+ line: line === null || line === void 0 ? void 0 : line.content,
516
+ });
497
517
  }
498
- if (line.indent <= expectedIndent) {
499
- return index;
518
+ if (closing !== trimmed.length - 1) {
519
+ throw new types_1.ToonDecodingError('Characters after closing quote', {
520
+ lineNumber: line === null || line === void 0 ? void 0 : line.lineNumber,
521
+ line: line === null || line === void 0 ? void 0 : line.content,
522
+ });
523
+ }
524
+ const unescaped = utils.tryUnescapeString(trimmed.slice(1, -1));
525
+ if (unescaped.error !== null) {
526
+ throw new types_1.ToonDecodingError(unescaped.error, {
527
+ lineNumber: line === null || line === void 0 ? void 0 : line.lineNumber,
528
+ line: line === null || line === void 0 ? void 0 : line.content,
529
+ });
500
530
  }
501
- index++;
531
+ return unescaped.value;
502
532
  }
503
- return lines.length;
533
+ return utils.parseToken(trimmed);
504
534
  }
505
- parseTokenValue(token) {
506
- token = token.trim();
507
- if (token.startsWith('"') && token.endsWith('"')) {
508
- return utils.unescapeString(token.slice(1, -1));
535
+ findClosingQuote(token) {
536
+ let i = 1;
537
+ while (i < token.length) {
538
+ if (token[i] === '\\') {
539
+ i += 2;
540
+ continue;
541
+ }
542
+ if (token[i] === '"') {
543
+ return i;
544
+ }
545
+ i++;
509
546
  }
510
- return utils.parseToken(token);
547
+ return -1;
511
548
  }
512
549
  expandPaths(obj) {
513
550
  const result = {};