toon-parser 2.2.1 → 3.0.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.
Files changed (67) hide show
  1. package/README.md +148 -62
  2. package/dist/core.cjs +630 -0
  3. package/dist/core.d.ts +73 -0
  4. package/dist/core.d.ts.map +1 -0
  5. package/dist/core.js +625 -0
  6. package/dist/core.js.map +1 -0
  7. package/dist/csv.cjs +16 -24
  8. package/dist/csv.d.ts +2 -1
  9. package/dist/csv.d.ts.map +1 -1
  10. package/dist/csv.js +8 -21
  11. package/dist/csv.js.map +1 -1
  12. package/dist/html.cjs +9 -7
  13. package/dist/html.d.ts +4 -2
  14. package/dist/html.d.ts.map +1 -1
  15. package/dist/html.js +7 -5
  16. package/dist/html.js.map +1 -1
  17. package/dist/index.cjs +5 -816
  18. package/dist/index.d.ts +2 -51
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -875
  21. package/dist/index.js.map +1 -1
  22. package/dist/inferType.cjs +39 -0
  23. package/dist/inferType.d.ts +10 -0
  24. package/dist/inferType.d.ts.map +1 -0
  25. package/dist/inferType.js +21 -0
  26. package/dist/inferType.js.map +1 -0
  27. package/dist/internal/constants.cjs +52 -0
  28. package/dist/internal/constants.d.ts +9 -0
  29. package/dist/internal/constants.d.ts.map +1 -0
  30. package/dist/internal/constants.js +15 -0
  31. package/dist/internal/constants.js.map +1 -0
  32. package/dist/internal/errors.cjs +34 -0
  33. package/dist/internal/errors.d.ts +5 -0
  34. package/dist/internal/errors.d.ts.map +1 -0
  35. package/dist/internal/errors.js +8 -0
  36. package/dist/internal/errors.js.map +1 -0
  37. package/dist/internal/primitives.cjs +304 -0
  38. package/dist/internal/primitives.d.ts +23 -0
  39. package/dist/internal/primitives.d.ts.map +1 -0
  40. package/dist/internal/primitives.js +289 -0
  41. package/dist/internal/primitives.js.map +1 -0
  42. package/dist/internal/security.cjs +118 -0
  43. package/dist/internal/security.d.ts +31 -0
  44. package/dist/internal/security.d.ts.map +1 -0
  45. package/dist/internal/security.js +87 -0
  46. package/dist/internal/security.js.map +1 -0
  47. package/dist/internal/types.cjs +16 -0
  48. package/dist/internal/types.d.ts +37 -0
  49. package/dist/internal/types.d.ts.map +1 -0
  50. package/dist/internal/types.js +2 -0
  51. package/dist/internal/types.js.map +1 -0
  52. package/dist/log.cjs +43 -17
  53. package/dist/log.d.ts +4 -3
  54. package/dist/log.d.ts.map +1 -1
  55. package/dist/log.js +47 -19
  56. package/dist/log.js.map +1 -1
  57. package/dist/url.cjs +22 -21
  58. package/dist/url.d.ts +1 -1
  59. package/dist/url.d.ts.map +1 -1
  60. package/dist/url.js +23 -29
  61. package/dist/url.js.map +1 -1
  62. package/dist/xml.cjs +7 -5
  63. package/dist/xml.d.ts +1 -1
  64. package/dist/xml.d.ts.map +1 -1
  65. package/dist/xml.js +5 -3
  66. package/dist/xml.js.map +1 -1
  67. package/package.json +40 -10
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAoCA,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC5B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CAC9B;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CA0MlF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CA2YjF"}
package/dist/core.js ADDED
@@ -0,0 +1,625 @@
1
+ import { DEFAULT_DELIMITER, IDENTIFIER_SEGMENT_RE } from './internal/constants.js';
2
+ import { ToonError } from './internal/errors.js';
3
+ import { applyLimits, bumpNodes, createSafeObject, detectTabular, enforceDepth, enforceLimits, isPlainObject, isPrimitive, validateKeySafety } from './internal/security.js';
4
+ import { classifyTabularLine, countLeadingSpaces, decodeKey, encodeKey, encodePrimitive, findUnquotedColon, parseArrayHeaderFromList, parsePrimitiveToken, primitiveLine, splitDelimited, splitKeyHeader } from './internal/primitives.js';
5
+ export { ToonError } from './internal/errors.js';
6
+ export { enforceInputLength } from './internal/security.js';
7
+ export function jsonToToon(value, options = {}) {
8
+ const indentSize = options.indent ?? 2;
9
+ if (!Number.isInteger(indentSize) || indentSize <= 0) {
10
+ throw new ToonError('Indent must be a positive integer.');
11
+ }
12
+ const delimiter = options.delimiter ?? DEFAULT_DELIMITER;
13
+ const limits = applyLimits(options);
14
+ const state = { nodes: 0 };
15
+ const lines = [];
16
+ const indentUnit = ' '.repeat(indentSize);
17
+ const keyFolding = options.keyFolding ?? 'off';
18
+ const flattenDepth = options.flattenDepth ?? Infinity;
19
+ // TOON v3 §13.4: attempt to fold a single-key chain rooted at (firstKey -> firstValue).
20
+ // Walks the maximal natural chain; verifies the natural endpoint qualifies (primitive,
21
+ // array, Date, or empty plain object); then truncates to flattenDepth if needed.
22
+ const tryFoldChain = (firstKey, firstValue, siblingKeys) => {
23
+ if (keyFolding !== 'safe')
24
+ return null;
25
+ if (!IDENTIFIER_SEGMENT_RE.test(firstKey))
26
+ return null;
27
+ if (limits.disallowedKeys.includes(firstKey))
28
+ return null;
29
+ const segments = [firstKey];
30
+ const valuesAtEachStep = [firstValue];
31
+ let current = firstValue;
32
+ while (isPlainObject(current) &&
33
+ Object.keys(current).length === 1) {
34
+ const onlyKey = Object.keys(current)[0];
35
+ if (!IDENTIFIER_SEGMENT_RE.test(onlyKey))
36
+ break;
37
+ if (limits.disallowedKeys.includes(onlyKey))
38
+ break;
39
+ const nextValue = current[onlyKey];
40
+ segments.push(onlyKey);
41
+ valuesAtEachStep.push(nextValue);
42
+ current = nextValue;
43
+ }
44
+ const endpointIsEmptyObject = isPlainObject(current) && Object.keys(current).length === 0;
45
+ const endpointFoldable = isPrimitive(current) || Array.isArray(current) || current instanceof Date || endpointIsEmptyObject;
46
+ if (!endpointFoldable)
47
+ return null;
48
+ if (segments.length < 2)
49
+ return null;
50
+ const cap = Math.max(2, Math.min(segments.length, flattenDepth));
51
+ const usedSegments = segments.slice(0, cap);
52
+ const leaf = valuesAtEachStep[cap - 1];
53
+ const path = usedSegments.join('.');
54
+ if (siblingKeys.has(path))
55
+ return null;
56
+ return { path, leaf };
57
+ };
58
+ const encodeValue = (input, depth, key, activeDelimiter) => {
59
+ enforceLimits(depth, limits, state);
60
+ if (isPrimitive(input)) {
61
+ const line = primitiveLine(key, input, indentUnit.repeat(depth), activeDelimiter, limits);
62
+ lines.push(line);
63
+ return;
64
+ }
65
+ if (Array.isArray(input)) {
66
+ encodeArray(key, input, depth, activeDelimiter);
67
+ return;
68
+ }
69
+ if (isPlainObject(input)) {
70
+ encodeObject(key, input, depth, activeDelimiter);
71
+ return;
72
+ }
73
+ if (input instanceof Date) {
74
+ const line = primitiveLine(key, input.toISOString(), indentUnit.repeat(depth), activeDelimiter, limits);
75
+ lines.push(line);
76
+ return;
77
+ }
78
+ throw new ToonError(`Unsupported value type: ${typeof input}`);
79
+ };
80
+ const encodeObject = (key, obj, depth, activeDelimiter) => {
81
+ // Caller already bumped via encodeValue; just enforce depth.
82
+ enforceDepth(depth, limits);
83
+ const entries = Object.entries(obj);
84
+ const sortedEntries = options.sortKeys ? [...entries].sort(([a], [b]) => a.localeCompare(b)) : entries;
85
+ const prefix = indentUnit.repeat(depth);
86
+ if (key !== null) {
87
+ validateKeySafety(key, limits);
88
+ if (sortedEntries.length === 0) {
89
+ lines.push(`${prefix}${encodeKey(key, activeDelimiter)}:`);
90
+ return;
91
+ }
92
+ lines.push(`${prefix}${encodeKey(key, activeDelimiter)}:`);
93
+ }
94
+ else if (depth > 0 && sortedEntries.length === 0) {
95
+ return;
96
+ }
97
+ const siblingKeys = new Set(sortedEntries.map(([k]) => k));
98
+ for (const [childKey, childValue] of sortedEntries) {
99
+ const nextDepth = key === null ? depth : depth + 1;
100
+ // No explicit bump here — encodeValue does it for the child.
101
+ enforceDepth(nextDepth, limits);
102
+ const folded = tryFoldChain(childKey, childValue, siblingKeys);
103
+ if (folded) {
104
+ encodeValue(folded.leaf, nextDepth, folded.path, activeDelimiter);
105
+ }
106
+ else {
107
+ encodeValue(childValue, nextDepth, childKey, activeDelimiter);
108
+ }
109
+ }
110
+ };
111
+ const encodeArray = (key, arr, depth, activeDelimiter) => {
112
+ // Caller already bumped via encodeValue (or for the rare nested non-inline
113
+ // array case, via the loop's enforceLimits below).
114
+ enforceDepth(depth, limits);
115
+ if (key !== null) {
116
+ validateKeySafety(key, limits);
117
+ }
118
+ if (arr.length > limits.maxArrayLength) {
119
+ throw new ToonError(`Array length ${arr.length} exceeds limit ${limits.maxArrayLength}.`);
120
+ }
121
+ const prefix = indentUnit.repeat(depth);
122
+ const headerKey = key === null ? '' : encodeKey(key, activeDelimiter);
123
+ if (arr.every(isPrimitive)) {
124
+ const encoded = arr.map(v => encodePrimitive(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
125
+ const spacing = arr.length > 0 ? ' ' : '';
126
+ lines.push(`${prefix}${headerKey}[${arr.length}]:${spacing}${encoded}`);
127
+ bumpNodes(state, limits, arr.length);
128
+ return;
129
+ }
130
+ const tabular = detectTabular(arr);
131
+ if (tabular) {
132
+ const { fields, rows } = tabular;
133
+ const encodedFields = fields.map(f => encodeKey(f, activeDelimiter)).join(activeDelimiter);
134
+ lines.push(`${prefix}${headerKey}[${arr.length}]{${encodedFields}}:`);
135
+ for (const row of rows) {
136
+ const rowValues = fields
137
+ .map(f => encodePrimitive(row[f], activeDelimiter, activeDelimiter))
138
+ .join(activeDelimiter);
139
+ lines.push(`${indentUnit.repeat(depth + 1)}${rowValues}`);
140
+ }
141
+ bumpNodes(state, limits, arr.length * fields.length);
142
+ return;
143
+ }
144
+ lines.push(`${prefix}${headerKey}[${arr.length}]:`);
145
+ const itemIndent = depth + 1;
146
+ const itemPrefix = indentUnit.repeat(itemIndent);
147
+ for (const item of arr) {
148
+ enforceDepth(itemIndent, limits);
149
+ if (isPrimitive(item)) {
150
+ lines.push(`${itemPrefix}- ${encodePrimitive(item, activeDelimiter, activeDelimiter)}`);
151
+ bumpNodes(state, limits, 1);
152
+ }
153
+ else if (Array.isArray(item)) {
154
+ // Count the inner array container itself.
155
+ bumpNodes(state, limits, 1);
156
+ const inline = item.every(isPrimitive);
157
+ if (inline) {
158
+ const encoded = item.map(v => encodePrimitive(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
159
+ const spacing = item.length > 0 ? ' ' : '';
160
+ lines.push(`${itemPrefix}- [${item.length}]:${spacing}${encoded}`);
161
+ bumpNodes(state, limits, item.length);
162
+ }
163
+ else {
164
+ lines.push(`${itemPrefix}-`);
165
+ encodeArray(null, item, itemIndent + 1, activeDelimiter);
166
+ }
167
+ }
168
+ else if (isPlainObject(item)) {
169
+ // Count the inner object container itself.
170
+ bumpNodes(state, limits, 1);
171
+ const objEntries = Object.entries(item);
172
+ if (objEntries.length === 0) {
173
+ lines.push(`${itemPrefix}-`);
174
+ continue;
175
+ }
176
+ lines.push(`${itemPrefix}-`);
177
+ for (const [childKey, childValue] of objEntries) {
178
+ encodeValue(childValue, itemIndent + 1, childKey, activeDelimiter);
179
+ }
180
+ }
181
+ else if (item instanceof Date) {
182
+ lines.push(`${itemPrefix}- ${encodePrimitive(item.toISOString(), activeDelimiter, activeDelimiter)}`);
183
+ bumpNodes(state, limits, 1);
184
+ }
185
+ else {
186
+ throw new ToonError(`Unsupported array item type: ${typeof item}`);
187
+ }
188
+ }
189
+ };
190
+ encodeValue(value, 0, null, delimiter);
191
+ return lines.join('\n');
192
+ }
193
+ export function toonToJson(text, options = {}) {
194
+ const limits = applyLimits(options);
195
+ if (text.length > limits.maxInputLength) {
196
+ throw new ToonError(`Input length ${text.length} exceeds limit ${limits.maxInputLength}.`);
197
+ }
198
+ const strict = options.strict ?? true;
199
+ const expandPaths = options.expandPaths ?? 'off';
200
+ const delimiterFallback = DEFAULT_DELIMITER;
201
+ // TOON v3 §13.4: split a dotted key into IdentifierSegment-only segments.
202
+ // Returns null if the key contains no dots OR any segment fails the IdentifierSegment check.
203
+ // Throws when a segment is in `disallowedKeys` to prevent prototype-pollution bypass.
204
+ const trySplitDottedKey = (key, lineNo) => {
205
+ if (expandPaths !== 'safe')
206
+ return null;
207
+ if (!key.includes('.'))
208
+ return null;
209
+ const segments = key.split('.');
210
+ for (const seg of segments) {
211
+ if (!IDENTIFIER_SEGMENT_RE.test(seg))
212
+ return null;
213
+ if (limits.disallowedKeys.includes(seg)) {
214
+ throw new ToonError(`Disallowed key segment "${seg}" in expanded path "${key}".`, lineNo);
215
+ }
216
+ }
217
+ return segments;
218
+ };
219
+ // TOON v3 §13.4 deep-merge semantics for path expansion. In strict mode any
220
+ // type conflict throws; in non-strict mode the new value wins (LWW).
221
+ const assignExpandedPath = (target, segments, value, lineNo) => {
222
+ let cursor = target;
223
+ for (let i = 0; i < segments.length - 1; i++) {
224
+ const seg = segments[i];
225
+ const existing = cursor[seg];
226
+ if (existing === undefined) {
227
+ const next = createSafeObject();
228
+ cursor[seg] = next;
229
+ cursor = next;
230
+ continue;
231
+ }
232
+ if (isPlainObject(existing)) {
233
+ cursor = existing;
234
+ continue;
235
+ }
236
+ if (strict) {
237
+ throw new ToonError(`Expansion conflict at path "${segments.slice(0, i + 1).join('.')}" (object vs primitive).`, lineNo);
238
+ }
239
+ const next = createSafeObject();
240
+ cursor[seg] = next;
241
+ cursor = next;
242
+ }
243
+ const leafKey = segments[segments.length - 1];
244
+ const existing = cursor[leafKey];
245
+ if (existing === undefined) {
246
+ cursor[leafKey] = value;
247
+ return;
248
+ }
249
+ if (isPlainObject(existing) && isPlainObject(value)) {
250
+ Object.assign(existing, value);
251
+ return;
252
+ }
253
+ if (strict) {
254
+ throw new ToonError(`Expansion conflict at path "${segments.join('.')}".`, lineNo);
255
+ }
256
+ cursor[leafKey] = value;
257
+ };
258
+ const lines = text.split(/\r?\n/);
259
+ const contexts = [];
260
+ let rootContainer = null;
261
+ let indentStep = null;
262
+ let root = null;
263
+ const state = { nodes: 0 };
264
+ const finalizeContainer = (container, lineNo) => {
265
+ if (container.type === 'tabular') {
266
+ if (strict && container.value.length !== container.expectedLength) {
267
+ throw new ToonError(`Tabular array length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`, lineNo);
268
+ }
269
+ }
270
+ else if (container.type === 'list') {
271
+ if (strict && container.expectedLength !== null && container.value.length !== container.expectedLength) {
272
+ throw new ToonError(`List length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`, lineNo);
273
+ }
274
+ }
275
+ else if (container.type === 'placeholder') {
276
+ // Empty `-` items decode as empty objects (mirrors the encoder).
277
+ if (!container.filled) {
278
+ container.assign(createSafeObject());
279
+ container.filled = true;
280
+ }
281
+ }
282
+ };
283
+ const attachValue = (value, parent, key, lineNo) => {
284
+ const ensureRootObject = () => {
285
+ if (rootContainer) {
286
+ return rootContainer;
287
+ }
288
+ const obj = { type: 'object', value: createSafeObject(), indent: 0 };
289
+ rootContainer = obj;
290
+ root = obj.value;
291
+ contexts.push(obj);
292
+ return obj;
293
+ };
294
+ if (!parent) {
295
+ if (key !== null) {
296
+ const target = ensureRootObject();
297
+ const segments = trySplitDottedKey(key, lineNo);
298
+ if (segments) {
299
+ assignExpandedPath(target.value, segments, value, lineNo);
300
+ return;
301
+ }
302
+ if (expandPaths === 'safe' && Object.prototype.hasOwnProperty.call(target.value, key)) {
303
+ assignExpandedPath(target.value, [key], value, lineNo);
304
+ return;
305
+ }
306
+ target.value[key] = value;
307
+ return;
308
+ }
309
+ if (root !== null) {
310
+ throw new ToonError('Multiple root values detected.', lineNo);
311
+ }
312
+ root = value;
313
+ return;
314
+ }
315
+ if (parent.type === 'object') {
316
+ if (key === null) {
317
+ throw new ToonError('Missing key for object assignment.');
318
+ }
319
+ const segments = trySplitDottedKey(key, lineNo);
320
+ if (segments) {
321
+ assignExpandedPath(parent.value, segments, value, lineNo);
322
+ return;
323
+ }
324
+ if (expandPaths === 'safe' && Object.prototype.hasOwnProperty.call(parent.value, key)) {
325
+ assignExpandedPath(parent.value, [key], value, lineNo);
326
+ return;
327
+ }
328
+ parent.value[key] = value;
329
+ return;
330
+ }
331
+ if (parent.type === 'list') {
332
+ parent.value.push(value);
333
+ return;
334
+ }
335
+ if (parent.type === 'placeholder') {
336
+ if (parent.filled) {
337
+ throw new ToonError('List item already filled.', lineNo);
338
+ }
339
+ parent.assign(value);
340
+ parent.filled = true;
341
+ return;
342
+ }
343
+ throw new ToonError('Invalid parent container.');
344
+ };
345
+ const parseArrayHeader = (token, lineNo) => {
346
+ const match = token.match(/^\[(\d+)([,\|\t])?\](\{(.+)\})?$/);
347
+ if (!match) {
348
+ throw new ToonError(`Invalid array header "${token}".`, lineNo);
349
+ }
350
+ const length = parseInt(match[1], 10);
351
+ if (!Number.isFinite(length)) {
352
+ throw new ToonError('Invalid array length.', lineNo);
353
+ }
354
+ const delimiter = match[2] ?? delimiterFallback;
355
+ const fieldsRaw = match[4];
356
+ if (fieldsRaw === undefined) {
357
+ return { length, delimiter };
358
+ }
359
+ const fields = splitDelimited(fieldsRaw, delimiter, lineNo).map(f => decodeKey(f, lineNo));
360
+ if (fields.length === 0 && strict) {
361
+ throw new ToonError('Tabular arrays require at least one field.', lineNo);
362
+ }
363
+ return { length, delimiter, fields };
364
+ };
365
+ const processKeyValueLine = (indentLevel, keyToken, valueToken, lineNo, parent) => {
366
+ const { rawKey, header } = splitKeyHeader(keyToken);
367
+ const key = rawKey === '' ? null : decodeKey(rawKey, lineNo);
368
+ if (key !== null) {
369
+ validateKeySafety(key, limits, lineNo);
370
+ }
371
+ if (header) {
372
+ const { length, delimiter, fields } = parseArrayHeader(header, lineNo);
373
+ if (length > limits.maxArrayLength) {
374
+ throw new ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
375
+ }
376
+ if (fields) {
377
+ if (valueToken !== '') {
378
+ throw new ToonError('Tabular array header must not have inline values.', lineNo);
379
+ }
380
+ const arr = [];
381
+ bumpNodes(state, limits, 1, lineNo);
382
+ attachValue(arr, parent, key, lineNo);
383
+ contexts.push({
384
+ type: 'tabular',
385
+ value: arr,
386
+ indent: indentLevel + 1,
387
+ expectedLength: length,
388
+ delimiter,
389
+ fields
390
+ });
391
+ return;
392
+ }
393
+ if (valueToken === '') {
394
+ const arr = [];
395
+ bumpNodes(state, limits, 1, lineNo);
396
+ attachValue(arr, parent, key, lineNo);
397
+ contexts.push({
398
+ type: 'list',
399
+ value: arr,
400
+ indent: indentLevel + 1,
401
+ expectedLength: length,
402
+ delimiter
403
+ });
404
+ return;
405
+ }
406
+ const values = splitDelimited(valueToken, delimiter, lineNo).map(t => parsePrimitiveToken(t, delimiter, lineNo, strict));
407
+ if (strict && values.length !== length) {
408
+ throw new ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
409
+ }
410
+ attachValue(values, parent, key, lineNo);
411
+ bumpNodes(state, limits, values.length, lineNo);
412
+ return;
413
+ }
414
+ if (valueToken === '') {
415
+ const obj = createSafeObject();
416
+ attachValue(obj, parent, key, lineNo);
417
+ contexts.push({ type: 'object', value: obj, indent: indentLevel + 1 });
418
+ return;
419
+ }
420
+ const value = parsePrimitiveToken(valueToken, delimiterFallback, lineNo, strict);
421
+ attachValue(value, parent, key, lineNo);
422
+ bumpNodes(state, limits, 1, lineNo);
423
+ };
424
+ lines.forEach((rawLine, index) => {
425
+ const lineNo = index + 1;
426
+ const trimmedEnd = rawLine.replace(/[ \t]+$/, '');
427
+ if (trimmedEnd.trim() === '') {
428
+ return;
429
+ }
430
+ const indentSpaces = countLeadingSpaces(trimmedEnd, lineNo);
431
+ if (indentStep === null) {
432
+ indentStep = indentSpaces === 0 ? 2 : indentSpaces;
433
+ }
434
+ if (indentSpaces % indentStep !== 0) {
435
+ throw new ToonError(`Inconsistent indentation: expected multiples of ${indentStep} spaces.`, lineNo);
436
+ }
437
+ const indentLevel = indentSpaces / indentStep;
438
+ if (indentLevel > limits.maxDepth) {
439
+ throw new ToonError(`Maximum depth ${limits.maxDepth} exceeded.`, lineNo);
440
+ }
441
+ const line = trimmedEnd.slice(indentSpaces);
442
+ while (true) {
443
+ const top = contexts[contexts.length - 1];
444
+ if (!top || indentLevel >= top.indent) {
445
+ break;
446
+ }
447
+ contexts.pop();
448
+ finalizeContainer(top, lineNo);
449
+ }
450
+ let handled = false;
451
+ let consumed = false;
452
+ while (!handled) {
453
+ const top = contexts[contexts.length - 1];
454
+ if (top && top.type === 'tabular' && indentLevel === top.indent) {
455
+ const classification = classifyTabularLine(line, top.delimiter);
456
+ if (classification === 'row') {
457
+ const cells = splitDelimited(line, top.delimiter, lineNo);
458
+ if (strict && cells.length !== top.fields.length) {
459
+ throw new ToonError(`Tabular row width mismatch: expected ${top.fields.length}, got ${cells.length}.`, lineNo);
460
+ }
461
+ const obj = createSafeObject();
462
+ top.fields.forEach((field, idx) => {
463
+ validateKeySafety(field, limits, lineNo);
464
+ const token = cells[idx] ?? '';
465
+ obj[field] = parsePrimitiveToken(token, top.delimiter, lineNo, strict);
466
+ bumpNodes(state, limits, 1, lineNo);
467
+ });
468
+ top.value.push(obj);
469
+ if (top.value.length > limits.maxArrayLength) {
470
+ throw new ToonError(`Tabular array length exceeds limit ${limits.maxArrayLength}.`, lineNo);
471
+ }
472
+ handled = true;
473
+ consumed = true;
474
+ break;
475
+ }
476
+ finalizeContainer(top, lineNo);
477
+ contexts.pop();
478
+ continue;
479
+ }
480
+ handled = true;
481
+ }
482
+ if (consumed) {
483
+ return;
484
+ }
485
+ const parent = contexts[contexts.length - 1];
486
+ if (parent && parent.type === 'list') {
487
+ if (indentLevel !== parent.indent) {
488
+ throw new ToonError('List items must align under their header.', lineNo);
489
+ }
490
+ parseListItem(line, parent, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict);
491
+ return;
492
+ }
493
+ if (parent && parent.type === 'placeholder') {
494
+ if (indentLevel !== parent.indent) {
495
+ throw new ToonError('List item body must indent one level below "-".', lineNo);
496
+ }
497
+ }
498
+ if (indentLevel !== (parent ? parent.indent : 0)) {
499
+ throw new ToonError('Unexpected indentation level.', lineNo);
500
+ }
501
+ const colonIndex = findUnquotedColon(line);
502
+ if (colonIndex === -1) {
503
+ if (root === null && !parent) {
504
+ const value = parsePrimitiveToken(line.trim(), delimiterFallback, lineNo, strict);
505
+ root = value;
506
+ bumpNodes(state, limits, 1, lineNo);
507
+ return;
508
+ }
509
+ throw new ToonError('Expected key-value pair.', lineNo);
510
+ }
511
+ const keyToken = line.slice(0, colonIndex).trim();
512
+ const valueToken = line.slice(colonIndex + 1).trim();
513
+ if (parent && parent.type === 'placeholder') {
514
+ // Placeholder body: route into the placeholder's tracked object so
515
+ // subsequent body lines build the same record.
516
+ if (!keyToken.startsWith('[')) {
517
+ if (!parent.current) {
518
+ const obj = createSafeObject();
519
+ parent.assign(obj);
520
+ parent.current = { value: obj, indent: indentLevel + 1 };
521
+ parent.filled = true;
522
+ }
523
+ const objContext = { type: 'object', value: parent.current.value, indent: indentLevel + 1 };
524
+ processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, objContext);
525
+ return;
526
+ }
527
+ }
528
+ processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, parent);
529
+ });
530
+ while (contexts.length > 0) {
531
+ const container = contexts.pop();
532
+ finalizeContainer(container, lines.length);
533
+ }
534
+ if (root === null) {
535
+ return createSafeObject();
536
+ }
537
+ return root;
538
+ }
539
+ function parseListItem(line, list, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict) {
540
+ const trimmed = line.trim();
541
+ if (!trimmed.startsWith('-')) {
542
+ throw new ToonError('List items must start with "-".', lineNo);
543
+ }
544
+ const content = trimmed.slice(1).trim();
545
+ if (content === '') {
546
+ const placeholder = {
547
+ type: 'placeholder',
548
+ indent: indentLevel + 1,
549
+ filled: false,
550
+ current: undefined,
551
+ assign: function (value) {
552
+ // Skip the empty-object follow-up that finalizeContainer pushes when a
553
+ // placeholder body has already produced its real object.
554
+ if (this.current && typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length === 0) {
555
+ return;
556
+ }
557
+ list.value.push(value);
558
+ }
559
+ };
560
+ contexts.push(placeholder);
561
+ return;
562
+ }
563
+ if (content.startsWith('[')) {
564
+ const colonIndex = findUnquotedColon(content);
565
+ const headerToken = colonIndex === -1 ? content : content.slice(0, colonIndex).trim();
566
+ const valueToken = colonIndex === -1 ? '' : content.slice(colonIndex + 1).trim();
567
+ const { length, delimiter, fields } = parseArrayHeaderFromList(headerToken, lineNo);
568
+ if (length > limits.maxArrayLength) {
569
+ throw new ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
570
+ }
571
+ if (fields) {
572
+ if (valueToken !== '') {
573
+ throw new ToonError('Tabular header in list item cannot have inline values.', lineNo);
574
+ }
575
+ const arr = [];
576
+ bumpNodes(state, limits, 1, lineNo);
577
+ list.value.push(arr);
578
+ contexts.push({
579
+ type: 'tabular',
580
+ value: arr,
581
+ indent: indentLevel + 1,
582
+ expectedLength: length,
583
+ delimiter,
584
+ fields
585
+ });
586
+ return;
587
+ }
588
+ if (valueToken === '') {
589
+ const arr = [];
590
+ bumpNodes(state, limits, 1, lineNo);
591
+ list.value.push(arr);
592
+ contexts.push({
593
+ type: 'list',
594
+ value: arr,
595
+ indent: indentLevel + 1,
596
+ expectedLength: length,
597
+ delimiter
598
+ });
599
+ return;
600
+ }
601
+ const values = splitDelimited(valueToken, delimiter, lineNo).map(t => parsePrimitiveToken(t, delimiter, lineNo, strict));
602
+ if (strict && values.length !== length) {
603
+ throw new ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
604
+ }
605
+ bumpNodes(state, limits, values.length, lineNo);
606
+ list.value.push(values);
607
+ return;
608
+ }
609
+ const colonIndex = findUnquotedColon(content);
610
+ if (colonIndex !== -1) {
611
+ const keyToken = content.slice(0, colonIndex).trim();
612
+ const valueToken = content.slice(colonIndex + 1).trim();
613
+ const obj = createSafeObject();
614
+ bumpNodes(state, limits, 1, lineNo);
615
+ list.value.push(obj);
616
+ const objContext = { type: 'object', value: obj, indent: indentLevel + 1 };
617
+ contexts.push(objContext);
618
+ processKeyValueLine(indentLevel + 1, keyToken, valueToken, lineNo, objContext);
619
+ return;
620
+ }
621
+ const value = parsePrimitiveToken(content, list.delimiter, lineNo, strict);
622
+ bumpNodes(state, limits, 1, lineNo);
623
+ list.value.push(value);
624
+ }
625
+ //# sourceMappingURL=core.js.map