@tehw0lf/n8n-nodes-toon 1.3.4 → 1.4.1

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,486 @@ 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;
127
+ if (this.options.strict && this.depthOf(first) !== 0) {
128
+ throw new types_1.ToonDecodingError('Root content must start at depth 0', {
129
+ lineNumber: first.lineNumber,
130
+ line: first.content,
131
+ });
132
+ }
112
133
  if (content === '[]') {
113
- return { key: null, length: 0, delimiter: ',', fields: null, rawLine: content, lineNumber: line.lineNumber };
134
+ this.pos = 1;
135
+ this.checkNoTrailingContent();
136
+ return [];
114
137
  }
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 });
138
+ const header = this.tryParseHeader(first);
139
+ if (header !== null && header.key === null && this.depthOf(first) === 0) {
140
+ if (header.keyed) {
141
+ const value = this.parseKeyedTabular(header, 0);
142
+ this.checkNoTrailingContent();
143
+ return value;
119
144
  }
120
- throw new types_1.ToonDecodingError('Invalid array header format', {
121
- lineNumber: line.lineNumber,
122
- line: content,
123
- });
145
+ const value = this.parseArrayBody(header, 0);
146
+ this.checkNoTrailingContent();
147
+ return value;
124
148
  }
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
- }
149
+ if (this.lines.length === 1 && utils.findUnquoted(content, ':') < 0) {
150
+ this.pos = 1;
151
+ return this.parseValueToken(content, first);
131
152
  }
132
- else {
133
- key = null;
153
+ const scalarLines = this.lines.filter((line) => this.depthOf(line) === 0 &&
154
+ utils.findUnquoted(line.content, ':') < 0 &&
155
+ !line.content.startsWith('- '));
156
+ if (scalarLines.length >= 2) {
157
+ 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
158
  }
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,
159
+ return this.parseObject(0);
160
+ }
161
+ checkNoTrailingContent() {
162
+ const next = this.peek();
163
+ if (next === null) {
164
+ return;
165
+ }
166
+ if (this.options.strict) {
167
+ throw new types_1.ToonDecodingError('Trailing content after completed root form', {
168
+ lineNumber: next.lineNumber,
169
+ line: next.content,
139
170
  });
140
171
  }
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;
172
+ this.pos = this.lines.length;
173
+ }
174
+ tryParseHeader(line) {
175
+ const outcome = utils.tryParseHeader(line.content);
176
+ if (outcome.error !== null && this.options.strict) {
177
+ throw new types_1.ToonDecodingError(outcome.error, {
178
+ lineNumber: line.lineNumber,
179
+ line: line.content,
155
180
  });
156
181
  }
157
- return {
158
- key,
159
- length,
160
- delimiter,
161
- fields,
162
- rawLine: content,
163
- lineNumber: line.lineNumber,
164
- };
182
+ return outcome.header;
165
183
  }
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;
184
+ parseObject(depth) {
185
+ const obj = {};
186
+ while (this.pos < this.lines.length) {
187
+ const line = this.lines[this.pos];
188
+ const lineDepth = this.depthOf(line);
189
+ if (lineDepth < depth) {
190
+ break;
182
191
  }
183
- if (char === '"' && !escaped) {
184
- inQuotes = !inQuotes;
185
- current += char;
192
+ if (lineDepth > depth) {
193
+ if (this.options.strict) {
194
+ throw new types_1.ToonDecodingError('Over-indented line: no enclosing scope was opened', { lineNumber: line.lineNumber, line: line.content });
195
+ }
196
+ this.pos++;
186
197
  continue;
187
198
  }
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;
199
+ this.checkBlankInSpan(line, 0);
200
+ const { key, value } = this.parseFieldLine(line, depth);
201
+ this.assignKey(obj, key, line);
202
+ utils.setDecodedKey(obj, key, value);
203
+ }
204
+ return obj;
205
+ }
206
+ assignKey(obj, key, line) {
207
+ if (Object.prototype.hasOwnProperty.call(obj, key) && this.options.strict) {
208
+ throw new types_1.ToonDecodingError(`Duplicate key '${key}' at the same depth`, {
209
+ lineNumber: line.lineNumber,
210
+ line: line.content,
211
+ });
212
+ }
213
+ }
214
+ parseFieldLine(line, depth) {
215
+ const header = this.tryParseHeader(line);
216
+ if (header !== null) {
217
+ if (header.key === null) {
218
+ if (this.options.strict) {
219
+ throw new types_1.ToonDecodingError('Keyless header is not valid in object-field position', { lineNumber: line.lineNumber, line: line.content });
199
220
  }
200
221
  }
201
- current += char;
222
+ else {
223
+ this.pos++;
224
+ const value = header.keyed
225
+ ? this.parseKeyedTabularBody(header, depth + 1)
226
+ : this.parseArrayBodyAfterHeader(header, depth);
227
+ return { key: header.key, value };
228
+ }
202
229
  }
203
- if (current.trim()) {
204
- tokens.push(current.trim());
230
+ const colonIndex = utils.findUnquoted(line.content, ':');
231
+ if (colonIndex < 0) {
232
+ throw new types_1.ToonDecodingError('Missing colon in key context', {
233
+ lineNumber: line.lineNumber,
234
+ line: line.content,
235
+ });
205
236
  }
206
- return tokens;
237
+ const key = this.decodeKeyToken(line.content.slice(0, colonIndex), line);
238
+ const rest = utils.trimSpaces(line.content.slice(colonIndex + 1));
239
+ this.pos++;
240
+ if (rest === '[]') {
241
+ return { key, value: [] };
242
+ }
243
+ if (rest !== '') {
244
+ return { key, value: this.parseValueToken(rest, line) };
245
+ }
246
+ return { key, value: this.parseNestedScope(depth) };
207
247
  }
208
- parseArray(lines, startIndex) {
209
- const headerLine = lines[startIndex];
210
- const header = this.parseArrayHeader(headerLine);
211
- if (header.rawLine === '[]') {
212
- return [];
248
+ parseNestedScope(depth) {
249
+ const next = this.peek();
250
+ if (next === null || this.depthOf(next) <= depth) {
251
+ return {};
213
252
  }
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
- }
253
+ if (this.options.strict && this.depthOf(next) > depth + 1) {
254
+ throw new types_1.ToonDecodingError('Indentation depth jump: nested scope must start one level deeper', { lineNumber: next.lineNumber, line: next.content });
255
+ }
256
+ return this.parseObject(depth + 1);
257
+ }
258
+ parseArrayBodyAfterHeader(header, headerDepth) {
259
+ return this.parseArrayContent(header, headerDepth + 1);
260
+ }
261
+ parseArrayBody(header, headerDepth) {
262
+ this.pos++;
263
+ return this.parseArrayContent(header, headerDepth + 1);
264
+ }
265
+ parseArrayContent(header, contentDepth) {
266
+ var _a;
267
+ if (header.fields !== null && header.inline !== '') {
268
+ throw new types_1.ToonDecodingError('Content after a fields-bearing header colon', {
269
+ lineNumber: (_a = this.lines[this.pos - 1]) === null || _a === void 0 ? void 0 : _a.lineNumber,
270
+ line: header.inline,
271
+ });
272
+ }
273
+ if (header.fields !== null) {
274
+ return this.parseTabularRows(header, contentDepth);
275
+ }
276
+ if (header.inline !== '') {
277
+ const values = utils
278
+ .splitDelimited(header.inline, header.delimiter)
279
+ .map((token) => this.parseValueToken(token, this.lines[this.pos - 1]));
280
+ this.checkCount(values.length, header.length, 'inline array values');
224
281
  return values;
225
282
  }
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;
283
+ if (header.length === 0) {
284
+ const next = this.peek();
285
+ if (next === null || this.depthOf(next) < contentDepth) {
286
+ return [];
245
287
  }
246
- if (line.indent <= baseIndent) {
247
- if (currentObj !== null) {
248
- values.push(currentObj);
249
- currentObj = null;
250
- }
288
+ }
289
+ return this.parseListItems(header, contentDepth);
290
+ }
291
+ parseTabularRows(header, rowDepth) {
292
+ const fields = header.fields;
293
+ const leafCount = utils.countLeafFields(fields);
294
+ const rows = [];
295
+ while (this.pos < this.lines.length) {
296
+ const line = this.lines[this.pos];
297
+ if (this.depthOf(line) !== rowDepth) {
251
298
  break;
252
299
  }
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
- }
300
+ const colonIndex = utils.findUnquoted(line.content, ':');
301
+ const delimIndex = utils.findUnquoted(line.content, header.delimiter);
302
+ if (colonIndex >= 0 && (delimIndex < 0 || colonIndex < delimIndex)) {
303
+ break;
278
304
  }
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++;
305
+ this.checkBlankInSpan(line, rows.length);
306
+ const cells = utils.splitDelimited(line.content, header.delimiter);
307
+ this.checkWidth(cells.length, leafCount, line);
308
+ rows.push(this.materializeRow(cells, fields, line));
309
+ this.pos++;
310
+ }
311
+ this.checkCount(rows.length, header.length, 'tabular rows');
312
+ return rows;
313
+ }
314
+ materializeRow(cells, fields, line) {
315
+ let index = 0;
316
+ const walk = (entries) => {
317
+ const obj = {};
318
+ for (const entry of entries) {
319
+ if (entry.children === null) {
320
+ if (index < cells.length) {
321
+ utils.setDecodedKey(obj, entry.name, this.parseValueToken(cells[index], line));
315
322
  }
323
+ index++;
316
324
  }
317
325
  else {
318
- if (currentObj !== null) {
319
- values.push(currentObj);
320
- currentObj = null;
321
- seenKeys.clear();
322
- }
323
- values.push(this.parseTokenValue(content));
324
- currentIndex++;
326
+ utils.setDecodedKey(obj, entry.name, walk(entry.children));
325
327
  }
326
328
  }
327
- else {
328
- if (currentObj !== null) {
329
- values.push(currentObj);
330
- currentObj = null;
331
- seenKeys.clear();
332
- }
333
- values.push(this.parseTokenValue(content));
334
- currentIndex++;
335
- }
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,
329
+ return obj;
330
+ };
331
+ return walk(fields);
332
+ }
333
+ parseKeyedTabular(header, headerDepth) {
334
+ this.pos++;
335
+ return this.parseKeyedTabularBody(header, headerDepth + 1);
336
+ }
337
+ parseKeyedTabularBody(header, entryDepth) {
338
+ var _a;
339
+ if (header.inline !== '') {
340
+ throw new types_1.ToonDecodingError('Content after a keyed header colon', {
341
+ lineNumber: (_a = this.lines[this.pos - 1]) === null || _a === void 0 ? void 0 : _a.lineNumber,
342
+ line: header.inline,
343
343
  });
344
344
  }
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) {
345
+ const fields = header.fields;
346
+ const leafCount = utils.countLeafFields(fields);
347
+ const obj = {};
348
+ let count = 0;
349
+ while (this.pos < this.lines.length) {
350
+ const line = this.lines[this.pos];
351
+ if (this.depthOf(line) < entryDepth) {
352
+ break;
353
+ }
354
+ if (this.depthOf(line) > entryDepth) {
354
355
  if (this.options.strict) {
355
- throw new types_1.ToonDecodingError('Blank lines not allowed in arrays in strict mode', {
356
+ throw new types_1.ToonDecodingError('Over-indented line in keyed tabular scope', {
356
357
  lineNumber: line.lineNumber,
358
+ line: line.content,
357
359
  });
358
360
  }
359
- currentIndex++;
361
+ this.pos++;
360
362
  continue;
361
363
  }
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;
377
- }
378
- rows.push(row);
379
- currentIndex++;
380
- }
381
- return rows;
382
- }
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++;
364
+ const colonIndex = utils.findUnquoted(line.content, ':');
365
+ if (colonIndex < 0) {
366
+ if (this.options.strict) {
367
+ throw new types_1.ToonDecodingError('Line at entry depth without an unquoted colon', {
368
+ lineNumber: line.lineNumber,
369
+ line: line.content,
370
+ });
371
+ }
372
+ this.pos++;
390
373
  continue;
391
374
  }
392
- if (currentIndex > startIndex && line.indent < expectedIndent) {
375
+ this.checkBlankInSpan(line, count);
376
+ const entryKey = this.decodeKeyToken(line.content.slice(0, colonIndex), line);
377
+ const rest = utils.trimSpaces(line.content.slice(colonIndex + 1));
378
+ const cells = rest === '' ? [] : utils.splitDelimited(rest, header.delimiter);
379
+ this.checkWidth(cells.length, leafCount, line);
380
+ this.assignKey(obj, entryKey, line);
381
+ utils.setDecodedKey(obj, entryKey, this.materializeRow(cells, fields, line));
382
+ count++;
383
+ this.pos++;
384
+ }
385
+ this.checkCount(count, header.length, 'keyed entry rows');
386
+ return obj;
387
+ }
388
+ parseListItems(header, itemDepth) {
389
+ const items = [];
390
+ while (this.pos < this.lines.length) {
391
+ const line = this.lines[this.pos];
392
+ const lineDepth = this.depthOf(line);
393
+ if (lineDepth < itemDepth) {
393
394
  break;
394
395
  }
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 });
396
+ if (lineDepth === itemDepth && !this.isListItemLine(line)) {
397
+ break;
398
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++;
399
+ if (lineDepth > itemDepth) {
400
+ if (this.options.strict) {
401
+ throw new types_1.ToonDecodingError('Over-indented line in list scope', {
402
+ lineNumber: line.lineNumber,
403
+ line: line.content,
404
+ });
411
405
  }
406
+ this.pos++;
412
407
  continue;
413
408
  }
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++;
409
+ this.checkBlankInSpan(line, items.length);
410
+ items.push(this.withinHeaderSpan(() => this.parseListItem(line, itemDepth)));
411
+ }
412
+ this.checkCount(items.length, header.length, 'list items');
413
+ return items;
414
+ }
415
+ isListItemLine(line) {
416
+ return line.content === '-' || line.content.startsWith('- ');
417
+ }
418
+ parseListItem(line, itemDepth) {
419
+ if (line.content === '-') {
420
+ this.pos++;
421
+ return {};
422
+ }
423
+ const rest = line.content.slice(2);
424
+ if (rest === '[]') {
425
+ this.pos++;
426
+ return [];
427
+ }
428
+ const inner = {
429
+ content: rest,
430
+ indent: line.indent + 2,
431
+ lineNumber: line.lineNumber,
432
+ isEmpty: false,
433
+ blankBefore: false,
434
+ };
435
+ const header = this.tryParseHeader(inner);
436
+ if (header !== null) {
437
+ this.pos++;
438
+ if (header.key === null) {
439
+ if (header.fields !== null && this.options.strict) {
440
+ throw new types_1.ToonDecodingError('Keyless fields-bearing header is not valid as a list item', { lineNumber: line.lineNumber, line: line.content });
445
441
  }
442
+ return this.parseArrayContent(header, itemDepth + 1);
443
+ }
444
+ const obj = {};
445
+ utils.setDecodedKey(obj, header.key, header.keyed
446
+ ? this.parseKeyedTabularBody(header, itemDepth + 2)
447
+ : this.parseArrayContent(header, itemDepth + 2));
448
+ this.parseListItemRestFields(obj, itemDepth + 1);
449
+ return obj;
450
+ }
451
+ const colonIndex = utils.findUnquoted(rest, ':');
452
+ if (colonIndex >= 0) {
453
+ const key = this.decodeKeyToken(rest.slice(0, colonIndex), line);
454
+ const valuePart = utils.trimSpaces(rest.slice(colonIndex + 1));
455
+ this.pos++;
456
+ const obj = {};
457
+ if (valuePart === '[]') {
458
+ utils.setDecodedKey(obj, key, []);
459
+ }
460
+ else if (valuePart !== '') {
461
+ utils.setDecodedKey(obj, key, this.parseValueToken(valuePart, line));
446
462
  }
447
463
  else {
448
- if (valuePart === '[]') {
449
- obj[key] = [];
450
- }
451
- else {
452
- obj[key] = this.parseTokenValue(valuePart);
453
- }
454
- currentIndex++;
464
+ utils.setDecodedKey(obj, key, this.parseNestedScope(itemDepth + 1));
455
465
  }
466
+ this.parseListItemRestFields(obj, itemDepth + 1);
467
+ return obj;
456
468
  }
457
- return obj;
469
+ this.pos++;
470
+ return this.parseValueToken(rest, line);
458
471
  }
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;
479
- }
480
- if (char === '"') {
481
- inQuotes = !inQuotes;
482
- continue;
483
- }
484
- if (char === ':' && !inQuotes) {
485
- return i;
472
+ parseListItemRestFields(obj, fieldDepth) {
473
+ while (this.pos < this.lines.length) {
474
+ const line = this.lines[this.pos];
475
+ if (this.depthOf(line) !== fieldDepth || this.isListItemLine(line)) {
476
+ break;
486
477
  }
478
+ this.checkBlankInSpan(line, 1);
479
+ const { key, value } = this.parseFieldLine(line, fieldDepth);
480
+ this.assignKey(obj, key, line);
481
+ utils.setDecodedKey(obj, key, value);
487
482
  }
488
- return -1;
489
483
  }
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;
497
- }
498
- if (line.indent <= expectedIndent) {
499
- return index;
484
+ checkCount(actual, declared, what) {
485
+ if (this.options.strict && actual !== declared) {
486
+ throw new types_1.ToonDecodingError(`Count mismatch for ${what}: expected ${declared}, got ${actual}`);
487
+ }
488
+ }
489
+ checkWidth(actual, expected, line) {
490
+ if (this.options.strict && actual !== expected) {
491
+ throw new types_1.ToonDecodingError(`Row width mismatch: expected ${expected} cells, got ${actual}`, { lineNumber: line.lineNumber, line: line.content });
492
+ }
493
+ }
494
+ decodeKeyToken(token, line) {
495
+ const trimmed = utils.trimSpaces(token);
496
+ if (trimmed.startsWith('"')) {
497
+ const decoded = utils.tryUnescapeQuotedToken(trimmed, 'key');
498
+ if (decoded.error !== null) {
499
+ throw new types_1.ToonDecodingError(decoded.error, {
500
+ lineNumber: line.lineNumber,
501
+ line: line.content,
502
+ });
500
503
  }
501
- index++;
504
+ return decoded.value;
502
505
  }
503
- return lines.length;
506
+ return trimmed;
504
507
  }
505
- parseTokenValue(token) {
506
- token = token.trim();
507
- if (token.startsWith('"') && token.endsWith('"')) {
508
- return utils.unescapeString(token.slice(1, -1));
508
+ parseValueToken(token, line) {
509
+ const trimmed = utils.trimSpaces(token);
510
+ if (trimmed.startsWith('"')) {
511
+ const decoded = utils.tryUnescapeQuotedToken(trimmed, 'string');
512
+ if (decoded.error !== null) {
513
+ throw new types_1.ToonDecodingError(decoded.error, {
514
+ lineNumber: line === null || line === void 0 ? void 0 : line.lineNumber,
515
+ line: line === null || line === void 0 ? void 0 : line.content,
516
+ });
517
+ }
518
+ return decoded.value;
509
519
  }
510
- return utils.parseToken(token);
520
+ return utils.parseToken(trimmed);
511
521
  }
512
522
  expandPaths(obj) {
513
523
  const result = {};