toon-parser 1.1.1 → 2.0.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.
package/dist/index.cjs CHANGED
@@ -1,874 +1,835 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ToonError = void 0;
4
- exports.jsonToToon = jsonToToon;
5
- exports.toonToJson = toonToJson;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ ToonError: () => ToonError,
23
+ jsonToToon: () => jsonToToon,
24
+ toonToJson: () => toonToJson
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ __reExport(index_exports, require("./xml.cjs"), module.exports);
6
28
  const DEFAULT_LIMITS = Object.freeze({
7
- maxDepth: 64,
8
- maxArrayLength: 50000,
9
- maxTotalNodes: 250000,
10
- disallowedKeys: ['__proto__', 'constructor', 'prototype']
29
+ maxDepth: 64,
30
+ maxArrayLength: 5e4,
31
+ maxTotalNodes: 25e4,
32
+ disallowedKeys: ["__proto__", "constructor", "prototype"]
11
33
  });
12
- const DEFAULT_DELIMITER = ',';
34
+ const DEFAULT_DELIMITER = ",";
13
35
  const NUMERIC_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
14
36
  const NUMERIC_LIKE_RE = /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i;
15
37
  const LEADING_ZERO_RE = /^0\d+$/;
16
38
  const SAFE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_.]*$/;
17
39
  class ToonError extends Error {
18
- constructor(message, line) {
19
- super(line ? `Line ${line}: ${message}` : message);
20
- this.name = 'ToonError';
21
- this.line = line;
22
- }
40
+ constructor(message, line) {
41
+ super(line ? `Line ${line}: ${message}` : message);
42
+ this.name = "ToonError";
43
+ this.line = line;
44
+ }
23
45
  }
24
- exports.ToonError = ToonError;
25
46
  function jsonToToon(value, options = {}) {
26
- const indentSize = options.indent ?? 2;
27
- if (!Number.isInteger(indentSize) || indentSize <= 0) {
28
- throw new ToonError('Indent must be a positive integer.');
29
- }
30
- const delimiter = options.delimiter ?? DEFAULT_DELIMITER;
31
- const limits = applyLimits(options);
32
- const state = { nodes: 0 };
33
- const lines = [];
34
- const indentUnit = ' '.repeat(indentSize);
35
- const encodeValue = (input, depth, key, activeDelimiter) => {
36
- enforceLimits(depth, limits, state);
37
- if (isPrimitive(input)) {
38
- const line = primitiveLine(key, input, indentUnit.repeat(depth), activeDelimiter, limits);
39
- lines.push(line);
40
- return;
41
- }
42
- if (Array.isArray(input)) {
43
- encodeArray(key, input, depth, activeDelimiter);
44
- return;
45
- }
46
- if (isPlainObject(input)) {
47
- encodeObject(key, input, depth, activeDelimiter);
48
- return;
49
- }
50
- throw new ToonError(`Unsupported value type: ${typeof input}`);
51
- };
52
- const encodeObject = (key, obj, depth, activeDelimiter) => {
53
- enforceLimits(depth, limits, state);
54
- const entries = Object.entries(obj);
55
- const sortedEntries = options.sortKeys ? [...entries].sort(([a], [b]) => a.localeCompare(b)) : entries;
56
- const prefix = indentUnit.repeat(depth);
57
- if (key !== null) {
58
- validateKeySafety(key, limits);
59
- if (sortedEntries.length === 0) {
60
- lines.push(`${prefix}${encodeKey(key, activeDelimiter)}:`);
61
- return;
62
- }
63
- lines.push(`${prefix}${encodeKey(key, activeDelimiter)}:`);
64
- }
65
- else if (depth > 0 && sortedEntries.length === 0) {
66
- // empty anonymous object
67
- return;
68
- }
69
- for (const [childKey, childValue] of sortedEntries) {
70
- const nextDepth = key === null ? depth : depth + 1;
71
- enforceLimits(nextDepth, limits, state);
72
- encodeValue(childValue, nextDepth, childKey, activeDelimiter);
73
- }
74
- };
75
- const encodeArray = (key, arr, depth, activeDelimiter) => {
76
- enforceLimits(depth, limits, state);
77
- if (key !== null) {
78
- validateKeySafety(key, limits);
79
- }
80
- if (arr.length > limits.maxArrayLength) {
81
- throw new ToonError(`Array length ${arr.length} exceeds limit ${limits.maxArrayLength}.`);
82
- }
83
- const prefix = indentUnit.repeat(depth);
84
- const headerKey = key === null ? '' : encodeKey(key, activeDelimiter);
85
- if (arr.every(isPrimitive)) {
86
- const encoded = arr.map(v => encodePrimitive(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
87
- const spacing = arr.length > 0 ? ' ' : '';
88
- lines.push(`${prefix}${headerKey}[${arr.length}]:${spacing}${encoded}`);
89
- bumpNodes(state, limits, arr.length);
90
- return;
91
- }
92
- const tabular = detectTabular(arr);
93
- if (tabular) {
94
- const { fields, rows } = tabular;
95
- const encodedFields = fields.map(f => encodeKey(f, activeDelimiter)).join(activeDelimiter);
96
- lines.push(`${prefix}${headerKey}[${arr.length}]{${encodedFields}}:`);
97
- for (const row of rows) {
98
- const rowValues = fields
99
- .map(f => encodePrimitive(row[f], activeDelimiter, activeDelimiter))
100
- .join(activeDelimiter);
101
- lines.push(`${indentUnit.repeat(depth + 1)}${rowValues}`);
102
- }
103
- bumpNodes(state, limits, arr.length * fields.length);
104
- return;
105
- }
106
- // expanded list
107
- lines.push(`${prefix}${headerKey}[${arr.length}]:`);
108
- const itemIndent = depth + 1;
109
- const itemPrefix = indentUnit.repeat(itemIndent);
110
- for (const item of arr) {
111
- enforceLimits(itemIndent, limits, state);
112
- if (isPrimitive(item)) {
113
- lines.push(`${itemPrefix}- ${encodePrimitive(item, activeDelimiter, activeDelimiter)}`);
114
- bumpNodes(state, limits, 1);
115
- }
116
- else if (Array.isArray(item)) {
117
- const inline = item.every(isPrimitive);
118
- if (inline) {
119
- const encoded = item.map(v => encodePrimitive(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
120
- const spacing = item.length > 0 ? ' ' : '';
121
- lines.push(`${itemPrefix}- [${item.length}]:${spacing}${encoded}`);
122
- bumpNodes(state, limits, item.length);
123
- }
124
- else {
125
- lines.push(`${itemPrefix}-`);
126
- encodeArray(null, item, itemIndent + 1, activeDelimiter);
127
- }
128
- }
129
- else if (isPlainObject(item)) {
130
- const objEntries = Object.entries(item);
131
- if (objEntries.length === 0) {
132
- lines.push(`${itemPrefix}-`);
133
- continue;
134
- }
135
- lines.push(`${itemPrefix}-`);
136
- for (const [childKey, childValue] of objEntries) {
137
- encodeValue(childValue, itemIndent + 1, childKey, activeDelimiter);
138
- }
139
- }
140
- else {
141
- throw new ToonError(`Unsupported array item type: ${typeof item}`);
142
- }
143
- }
144
- };
145
- encodeValue(value, 0, null, delimiter);
146
- return lines.join('\n');
47
+ const indentSize = options.indent ?? 2;
48
+ if (!Number.isInteger(indentSize) || indentSize <= 0) {
49
+ throw new ToonError("Indent must be a positive integer.");
50
+ }
51
+ const delimiter = options.delimiter ?? DEFAULT_DELIMITER;
52
+ const limits = applyLimits(options);
53
+ const state = { nodes: 0 };
54
+ const lines = [];
55
+ const indentUnit = " ".repeat(indentSize);
56
+ const encodeValue = (input, depth, key, activeDelimiter) => {
57
+ enforceLimits(depth, limits, state);
58
+ if (isPrimitive(input)) {
59
+ const line = primitiveLine(key, input, indentUnit.repeat(depth), activeDelimiter, limits);
60
+ lines.push(line);
61
+ return;
62
+ }
63
+ if (Array.isArray(input)) {
64
+ encodeArray(key, input, depth, activeDelimiter);
65
+ return;
66
+ }
67
+ if (isPlainObject(input)) {
68
+ encodeObject(key, input, depth, activeDelimiter);
69
+ return;
70
+ }
71
+ if (input instanceof Date) {
72
+ const line = primitiveLine(key, input.toISOString(), indentUnit.repeat(depth), activeDelimiter, limits);
73
+ lines.push(line);
74
+ return;
75
+ }
76
+ throw new ToonError(`Unsupported value type: ${typeof input}`);
77
+ };
78
+ const encodeObject = (key, obj, depth, activeDelimiter) => {
79
+ enforceLimits(depth, limits, state);
80
+ const entries = Object.entries(obj);
81
+ const sortedEntries = options.sortKeys ? [...entries].sort(([a], [b]) => a.localeCompare(b)) : entries;
82
+ const prefix = indentUnit.repeat(depth);
83
+ if (key !== null) {
84
+ validateKeySafety(key, limits);
85
+ if (sortedEntries.length === 0) {
86
+ lines.push(`${prefix}${encodeKey(key, activeDelimiter)}:`);
87
+ return;
88
+ }
89
+ lines.push(`${prefix}${encodeKey(key, activeDelimiter)}:`);
90
+ } else if (depth > 0 && sortedEntries.length === 0) {
91
+ return;
92
+ }
93
+ for (const [childKey, childValue] of sortedEntries) {
94
+ const nextDepth = key === null ? depth : depth + 1;
95
+ enforceLimits(nextDepth, limits, state);
96
+ encodeValue(childValue, nextDepth, childKey, activeDelimiter);
97
+ }
98
+ };
99
+ const encodeArray = (key, arr, depth, activeDelimiter) => {
100
+ enforceLimits(depth, limits, state);
101
+ if (key !== null) {
102
+ validateKeySafety(key, limits);
103
+ }
104
+ if (arr.length > limits.maxArrayLength) {
105
+ throw new ToonError(`Array length ${arr.length} exceeds limit ${limits.maxArrayLength}.`);
106
+ }
107
+ const prefix = indentUnit.repeat(depth);
108
+ const headerKey = key === null ? "" : encodeKey(key, activeDelimiter);
109
+ if (arr.every(isPrimitive)) {
110
+ const encoded = arr.map((v) => encodePrimitive(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
111
+ const spacing = arr.length > 0 ? " " : "";
112
+ lines.push(`${prefix}${headerKey}[${arr.length}]:${spacing}${encoded}`);
113
+ bumpNodes(state, limits, arr.length);
114
+ return;
115
+ }
116
+ const tabular = detectTabular(arr);
117
+ if (tabular) {
118
+ const { fields, rows } = tabular;
119
+ const encodedFields = fields.map((f) => encodeKey(f, activeDelimiter)).join(activeDelimiter);
120
+ lines.push(`${prefix}${headerKey}[${arr.length}]{${encodedFields}}:`);
121
+ for (const row of rows) {
122
+ const rowValues = fields.map((f) => encodePrimitive(row[f], activeDelimiter, activeDelimiter)).join(activeDelimiter);
123
+ lines.push(`${indentUnit.repeat(depth + 1)}${rowValues}`);
124
+ }
125
+ bumpNodes(state, limits, arr.length * fields.length);
126
+ return;
127
+ }
128
+ lines.push(`${prefix}${headerKey}[${arr.length}]:`);
129
+ const itemIndent = depth + 1;
130
+ const itemPrefix = indentUnit.repeat(itemIndent);
131
+ for (const item of arr) {
132
+ enforceLimits(itemIndent, limits, state);
133
+ if (isPrimitive(item)) {
134
+ lines.push(`${itemPrefix}- ${encodePrimitive(item, activeDelimiter, activeDelimiter)}`);
135
+ bumpNodes(state, limits, 1);
136
+ } else if (Array.isArray(item)) {
137
+ const inline = item.every(isPrimitive);
138
+ if (inline) {
139
+ const encoded = item.map((v) => encodePrimitive(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
140
+ const spacing = item.length > 0 ? " " : "";
141
+ lines.push(`${itemPrefix}- [${item.length}]:${spacing}${encoded}`);
142
+ bumpNodes(state, limits, item.length);
143
+ } else {
144
+ lines.push(`${itemPrefix}-`);
145
+ encodeArray(null, item, itemIndent + 1, activeDelimiter);
146
+ }
147
+ } else if (isPlainObject(item)) {
148
+ const objEntries = Object.entries(item);
149
+ if (objEntries.length === 0) {
150
+ lines.push(`${itemPrefix}-`);
151
+ continue;
152
+ }
153
+ lines.push(`${itemPrefix}-`);
154
+ for (const [childKey, childValue] of objEntries) {
155
+ encodeValue(childValue, itemIndent + 1, childKey, activeDelimiter);
156
+ }
157
+ } else if (item instanceof Date) {
158
+ lines.push(`${itemPrefix}- ${encodePrimitive(item.toISOString(), activeDelimiter, activeDelimiter)}`);
159
+ bumpNodes(state, limits, 1);
160
+ } else {
161
+ throw new ToonError(`Unsupported array item type: ${typeof item}`);
162
+ }
163
+ }
164
+ };
165
+ encodeValue(value, 0, null, delimiter);
166
+ return lines.join("\n");
147
167
  }
148
168
  function toonToJson(text, options = {}) {
149
- const limits = applyLimits(options);
150
- const strict = options.strict ?? true;
151
- const delimiterFallback = DEFAULT_DELIMITER;
152
- const lines = text.split(/\r?\n/);
153
- const contexts = [];
154
- let rootContainer = null;
155
- let indentStep = null;
156
- let root = null;
157
- const state = { nodes: 0 };
158
- const finalizeContainer = (container, lineNo) => {
159
- if (container.type === 'tabular') {
160
- if (strict && container.value.length !== container.expectedLength) {
161
- throw new ToonError(`Tabular array length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`, lineNo);
162
- }
163
- }
164
- else if (container.type === 'list') {
165
- if (strict && container.expectedLength !== null && container.value.length !== container.expectedLength) {
166
- throw new ToonError(`List length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`, lineNo);
167
- }
168
- }
169
- else if (container.type === 'placeholder') {
170
- // If a placeholder was not filled, treat it as an empty object value.
171
- // This matches how `jsonToToon` emits empty objects as a `-` list item with no body.
172
- if (!container.filled) {
173
- container.assign(createSafeObject());
174
- container.filled = true;
175
- }
176
- }
177
- };
178
- const attachValue = (value, parent, key, lineNo) => {
179
- const ensureRootObject = () => {
180
- if (rootContainer) {
181
- return rootContainer;
182
- }
183
- const obj = { type: 'object', value: createSafeObject(), indent: 0 };
184
- rootContainer = obj;
185
- root = obj.value;
186
- contexts.push(obj);
187
- return obj;
188
- };
189
- if (!parent) {
190
- if (key !== null) {
191
- const target = ensureRootObject();
192
- target.value[key] = value;
193
- return;
194
- }
195
- if (root !== null) {
196
- throw new ToonError('Multiple root values detected.', lineNo);
197
- }
198
- root = value;
199
- return;
200
- }
201
- if (parent.type === 'object') {
202
- if (key === null) {
203
- throw new ToonError('Missing key for object assignment.');
204
- }
205
- parent.value[key] = value;
206
- return;
207
- }
208
- if (parent.type === 'list') {
209
- parent.value.push(value);
210
- return;
211
- }
212
- if (parent.type === 'placeholder') {
213
- if (parent.filled) {
214
- throw new ToonError('List item already filled.', lineNo);
215
- }
216
- parent.assign(value);
217
- parent.filled = true;
218
- return;
219
- }
220
- throw new ToonError('Invalid parent container.');
221
- };
222
- const parseArrayHeader = (token, lineNo) => {
223
- const match = token.match(/^\[(\d+)([,\|\t])?\](\{(.+)\})?$/);
224
- if (!match) {
225
- throw new ToonError(`Invalid array header "${token}".`, lineNo);
226
- }
227
- const length = parseInt(match[1], 10);
228
- if (!Number.isFinite(length)) {
229
- throw new ToonError('Invalid array length.', lineNo);
230
- }
231
- const delimiter = match[2] ?? delimiterFallback;
232
- const fieldsRaw = match[4];
233
- if (fieldsRaw === undefined) {
234
- return { length, delimiter };
235
- }
236
- const fields = splitDelimited(fieldsRaw, delimiter, lineNo).map(f => decodeKey(f, delimiter, lineNo));
237
- if (fields.length === 0 && strict) {
238
- throw new ToonError('Tabular arrays require at least one field.', lineNo);
239
- }
240
- return { length, delimiter, fields };
241
- };
242
- const processKeyValueLine = (indentLevel, keyToken, valueToken, lineNo, parent) => {
243
- const { rawKey, header } = splitKeyHeader(keyToken);
244
- const key = rawKey === '' ? null : decodeKey(rawKey, delimiterFallback, lineNo);
245
- if (key !== null) {
246
- validateKeySafety(key, limits, lineNo);
247
- }
248
- if (header) {
249
- const { length, delimiter, fields } = parseArrayHeader(header, lineNo);
250
- if (length > limits.maxArrayLength) {
251
- throw new ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
252
- }
253
- if (fields) {
254
- if (valueToken !== '') {
255
- throw new ToonError('Tabular array header must not have inline values.', lineNo);
256
- }
257
- const arr = [];
258
- bumpNodes(state, limits, 1, lineNo);
259
- attachValue(arr, parent, key, lineNo);
260
- contexts.push({
261
- type: 'tabular',
262
- value: arr,
263
- indent: indentLevel + 1,
264
- expectedLength: length,
265
- delimiter,
266
- fields
267
- });
268
- return;
269
- }
270
- if (valueToken === '') {
271
- const arr = [];
272
- bumpNodes(state, limits, 1, lineNo);
273
- attachValue(arr, parent, key, lineNo);
274
- contexts.push({
275
- type: 'list',
276
- value: arr,
277
- indent: indentLevel + 1,
278
- expectedLength: length,
279
- delimiter
280
- });
281
- return;
282
- }
283
- const values = splitDelimited(valueToken, delimiter, lineNo).map(t => parsePrimitiveToken(t, delimiter, lineNo, strict));
284
- if (strict && values.length !== length) {
285
- throw new ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
286
- }
287
- attachValue(values, parent, key, lineNo);
288
- bumpNodes(state, limits, values.length, lineNo);
289
- return;
290
- }
291
- if (valueToken === '') {
292
- const obj = createSafeObject();
293
- attachValue(obj, parent, key, lineNo);
294
- contexts.push({ type: 'object', value: obj, indent: indentLevel + 1 });
295
- return;
296
- }
297
- const value = parsePrimitiveToken(valueToken, delimiterFallback, lineNo, strict);
298
- attachValue(value, parent, key, lineNo);
299
- bumpNodes(state, limits, 1, lineNo);
169
+ const limits = applyLimits(options);
170
+ const strict = options.strict ?? true;
171
+ const delimiterFallback = DEFAULT_DELIMITER;
172
+ const lines = text.split(/\r?\n/);
173
+ const contexts = [];
174
+ let rootContainer = null;
175
+ let indentStep = null;
176
+ let root = null;
177
+ const state = { nodes: 0 };
178
+ const finalizeContainer = (container, lineNo) => {
179
+ if (container.type === "tabular") {
180
+ if (strict && container.value.length !== container.expectedLength) {
181
+ throw new ToonError(
182
+ `Tabular array length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`,
183
+ lineNo
184
+ );
185
+ }
186
+ } else if (container.type === "list") {
187
+ if (strict && container.expectedLength !== null && container.value.length !== container.expectedLength) {
188
+ throw new ToonError(`List length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`, lineNo);
189
+ }
190
+ } else if (container.type === "placeholder") {
191
+ if (!container.filled) {
192
+ container.assign(createSafeObject());
193
+ container.filled = true;
194
+ }
195
+ }
196
+ };
197
+ const attachValue = (value, parent, key, lineNo) => {
198
+ const ensureRootObject = () => {
199
+ if (rootContainer) {
200
+ return rootContainer;
201
+ }
202
+ const obj = { type: "object", value: createSafeObject(), indent: 0 };
203
+ rootContainer = obj;
204
+ root = obj.value;
205
+ contexts.push(obj);
206
+ return obj;
300
207
  };
301
- lines.forEach((rawLine, index) => {
302
- const lineNo = index + 1;
303
- const trimmedEnd = rawLine.replace(/[ \t]+$/, '');
304
- if (trimmedEnd.trim() === '') {
305
- return; // skip blank lines
306
- }
307
- const indentSpaces = countLeadingSpaces(trimmedEnd, lineNo);
308
- if (indentStep === null) {
309
- indentStep = indentSpaces === 0 ? 2 : indentSpaces;
310
- }
311
- if (indentSpaces % indentStep !== 0) {
312
- throw new ToonError(`Inconsistent indentation: expected multiples of ${indentStep} spaces.`, lineNo);
313
- }
314
- const indentLevel = indentSpaces / indentStep;
315
- if (indentLevel > limits.maxDepth) {
316
- throw new ToonError(`Maximum depth ${limits.maxDepth} exceeded.`, lineNo);
317
- }
318
- const line = trimmedEnd.slice(indentSpaces);
319
- while (true) {
320
- const top = contexts[contexts.length - 1];
321
- if (!top || indentLevel >= top.indent) {
322
- break;
323
- }
324
- contexts.pop();
325
- finalizeContainer(top, lineNo);
326
- }
327
- // tabular row disambiguation
328
- let handled = false;
329
- let consumed = false;
330
- while (!handled) {
331
- const top = contexts[contexts.length - 1];
332
- if (top && top.type === 'tabular' && indentLevel === top.indent) {
333
- const classification = classifyTabularLine(line, top.delimiter);
334
- if (classification === 'row') {
335
- const cells = splitDelimited(line, top.delimiter, lineNo);
336
- if (strict && cells.length !== top.fields.length) {
337
- throw new ToonError(`Tabular row width mismatch: expected ${top.fields.length}, got ${cells.length}.`, lineNo);
338
- }
339
- const obj = createSafeObject();
340
- top.fields.forEach((field, idx) => {
341
- validateKeySafety(field, limits, lineNo);
342
- const token = cells[idx] ?? '';
343
- obj[field] = parsePrimitiveToken(token, top.delimiter, lineNo, strict);
344
- bumpNodes(state, limits, 1, lineNo);
345
- });
346
- top.value.push(obj);
347
- if (top.value.length > limits.maxArrayLength) {
348
- throw new ToonError(`Tabular array length exceeds limit ${limits.maxArrayLength}.`, lineNo);
349
- }
350
- handled = true;
351
- consumed = true;
352
- break;
353
- }
354
- finalizeContainer(top, lineNo);
355
- contexts.pop();
356
- continue;
357
- }
358
- handled = true;
359
- }
360
- if (consumed) {
361
- return;
362
- }
363
- const parent = contexts[contexts.length - 1];
364
- if (parent && parent.type === 'list') {
365
- if (indentLevel !== parent.indent) {
366
- throw new ToonError('List items must align under their header.', lineNo);
367
- }
368
- parseListItem(line, parent, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict);
369
- return;
370
- }
371
- if (parent && parent.type === 'placeholder') {
372
- if (indentLevel !== parent.indent) {
373
- throw new ToonError('List item body must indent one level below "-".', lineNo);
374
- }
375
- // allow body to be parsed as if parent were absent; attachValue will route through placeholder.
376
- }
377
- if (indentLevel !== (parent ? parent.indent : 0)) {
378
- throw new ToonError('Unexpected indentation level.', lineNo);
379
- }
380
- const colonIndex = findUnquotedColon(line);
381
- if (colonIndex === -1) {
382
- // possible root primitive
383
- if (root === null && !parent) {
384
- const value = parsePrimitiveToken(line.trim(), delimiterFallback, lineNo, strict);
385
- root = value;
386
- bumpNodes(state, limits, 1, lineNo);
387
- return;
388
- }
389
- throw new ToonError('Expected key-value pair.', lineNo);
390
- }
391
- const keyToken = line.slice(0, colonIndex).trim();
392
- const valueToken = line.slice(colonIndex + 1).trim();
393
- // If we're in a placeholder parent (a `-` with no inline content), and the body
394
- // line contains a key:value, we should attach to the current object context.
395
- // If this is the first body line, create and track the object on placeholder.current.
396
- // Subsequent body lines will reuse the same object.
397
- if (parent && parent.type === 'placeholder') {
398
- // Don't automatically create an object for array headers (e.g., `[2]{...}:`)
399
- // — handle those via the normal `processKeyValueLine` flow so headers attach
400
- // to the placeholder correctly as arrays/tabular types.
401
- if (!keyToken.startsWith('[')) {
402
- // Reuse existing current object or create new one on first body line
403
- if (!parent.current) {
404
- const obj = createSafeObject();
405
- // Call assign BEFORE setting current, so the skip logic can distinguish
406
- // first assignment from finalizeContainer's empty object push
407
- parent.assign(obj);
408
- parent.current = { value: obj, indent: indentLevel + 1 };
409
- parent.filled = true;
410
- }
411
- const objContext = { type: 'object', value: parent.current.value, indent: indentLevel + 1 };
412
- processKeyValueLine(indentLevel + 1, keyToken, valueToken, lineNo, objContext);
413
- return;
414
- }
415
- }
416
- processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, parent);
417
- });
418
- while (contexts.length > 0) {
419
- const container = contexts.pop();
420
- finalizeContainer(container, lines.length);
208
+ if (!parent) {
209
+ if (key !== null) {
210
+ const target = ensureRootObject();
211
+ target.value[key] = value;
212
+ return;
213
+ }
214
+ if (root !== null) {
215
+ throw new ToonError("Multiple root values detected.", lineNo);
216
+ }
217
+ root = value;
218
+ return;
219
+ }
220
+ if (parent.type === "object") {
221
+ if (key === null) {
222
+ throw new ToonError("Missing key for object assignment.");
223
+ }
224
+ parent.value[key] = value;
225
+ return;
226
+ }
227
+ if (parent.type === "list") {
228
+ parent.value.push(value);
229
+ return;
230
+ }
231
+ if (parent.type === "placeholder") {
232
+ if (parent.filled) {
233
+ throw new ToonError("List item already filled.", lineNo);
234
+ }
235
+ parent.assign(value);
236
+ parent.filled = true;
237
+ return;
238
+ }
239
+ throw new ToonError("Invalid parent container.");
240
+ };
241
+ const parseArrayHeader = (token, lineNo) => {
242
+ const match = token.match(/^\[(\d+)([,\|\t])?\](\{(.+)\})?$/);
243
+ if (!match) {
244
+ throw new ToonError(`Invalid array header "${token}".`, lineNo);
421
245
  }
422
- if (root === null) {
423
- return createSafeObject();
246
+ const length = parseInt(match[1], 10);
247
+ if (!Number.isFinite(length)) {
248
+ throw new ToonError("Invalid array length.", lineNo);
424
249
  }
425
- return root;
426
- }
427
- function classifyTabularLine(line, delimiter) {
428
- let inQuote = false;
429
- let escape = false;
430
- let firstColon = -1;
431
- let firstDelim = -1;
432
- for (let i = 0; i < line.length; i++) {
433
- const ch = line[i];
434
- if (escape) {
435
- escape = false;
436
- continue;
437
- }
438
- if (ch === '\\') {
439
- escape = true;
440
- continue;
441
- }
442
- if (ch === '"') {
443
- inQuote = !inQuote;
444
- continue;
445
- }
446
- if (inQuote)
447
- continue;
448
- if (ch === ':' && firstColon === -1)
449
- firstColon = i;
450
- if (ch === delimiter && firstDelim === -1)
451
- firstDelim = i;
452
- }
453
- if (firstColon === -1)
454
- return 'row';
455
- if (firstDelim === -1)
456
- return 'field';
457
- return firstDelim < firstColon ? 'row' : 'field';
458
- }
459
- function parseListItem(line, list, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict) {
460
- const trimmed = line.trim();
461
- if (!trimmed.startsWith('-')) {
462
- throw new ToonError('List items must start with "-".', lineNo);
463
- }
464
- const content = trimmed.slice(1).trim();
465
- if (content === '') {
466
- const placeholder = {
467
- type: 'placeholder',
468
- indent: indentLevel + 1,
469
- filled: false,
470
- current: undefined,
471
- assign: function (value) {
472
- // Push value to list; the current field tracks multi-line object context
473
- // Skip only if: current already exists (meaning we've already assigned) AND
474
- // the value being assigned is an empty object (from finalizeContainer).
475
- if (this.current && typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length === 0) {
476
- // Empty object from finalizeContainer; skip pushing again
477
- return;
478
- }
479
- list.value.push(value);
480
- }
481
- };
482
- contexts.push(placeholder);
483
- return;
250
+ const delimiter = match[2] ?? delimiterFallback;
251
+ const fieldsRaw = match[4];
252
+ if (fieldsRaw === void 0) {
253
+ return { length, delimiter };
484
254
  }
485
- // Array list item (header on the same line)
486
- if (content.startsWith('[')) {
487
- const colonIndex = findUnquotedColon(content);
488
- const headerToken = colonIndex === -1 ? content : content.slice(0, colonIndex).trim();
489
- const valueToken = colonIndex === -1 ? '' : content.slice(colonIndex + 1).trim();
490
- const { length, delimiter, fields } = parseArrayHeaderFromList(headerToken, lineNo);
491
- if (length > limits.maxArrayLength) {
492
- throw new ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
493
- }
494
- if (fields) {
495
- if (valueToken !== '') {
496
- throw new ToonError('Tabular header in list item cannot have inline values.', lineNo);
497
- }
498
- const arr = [];
499
- bumpNodes(state, limits, 1, lineNo);
500
- list.value.push(arr);
501
- contexts.push({
502
- type: 'tabular',
503
- value: arr,
504
- indent: indentLevel + 1,
505
- expectedLength: length,
506
- delimiter,
507
- fields
508
- });
509
- return;
510
- }
511
- if (valueToken === '') {
512
- const arr = [];
513
- bumpNodes(state, limits, 1, lineNo);
514
- list.value.push(arr);
515
- contexts.push({
516
- type: 'list',
517
- value: arr,
518
- indent: indentLevel + 1,
519
- expectedLength: length,
520
- delimiter
521
- });
522
- return;
523
- }
524
- const values = splitDelimited(valueToken, delimiter, lineNo).map(t => parsePrimitiveToken(t, delimiter, lineNo, strict));
525
- if (strict && values.length !== length) {
526
- throw new ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
527
- }
528
- bumpNodes(state, limits, values.length, lineNo);
529
- list.value.push(values);
530
- return;
255
+ const fields = splitDelimited(fieldsRaw, delimiter, lineNo).map((f) => decodeKey(f, lineNo));
256
+ if (fields.length === 0 && strict) {
257
+ throw new ToonError("Tabular arrays require at least one field.", lineNo);
531
258
  }
532
- // Inline object field or array header
533
- const colonIndex = findUnquotedColon(content);
534
- if (colonIndex !== -1) {
535
- const keyToken = content.slice(0, colonIndex).trim();
536
- const valueToken = content.slice(colonIndex + 1).trim();
537
- const obj = createSafeObject();
259
+ return { length, delimiter, fields };
260
+ };
261
+ const processKeyValueLine = (indentLevel, keyToken, valueToken, lineNo, parent) => {
262
+ const { rawKey, header } = splitKeyHeader(keyToken);
263
+ const key = rawKey === "" ? null : decodeKey(rawKey, lineNo);
264
+ if (key !== null) {
265
+ validateKeySafety(key, limits, lineNo);
266
+ }
267
+ if (header) {
268
+ const { length, delimiter, fields } = parseArrayHeader(header, lineNo);
269
+ if (length > limits.maxArrayLength) {
270
+ throw new ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
271
+ }
272
+ if (fields) {
273
+ if (valueToken !== "") {
274
+ throw new ToonError("Tabular array header must not have inline values.", lineNo);
275
+ }
276
+ const arr = [];
538
277
  bumpNodes(state, limits, 1, lineNo);
539
- list.value.push(obj);
540
- const objContext = { type: 'object', value: obj, indent: indentLevel + 1 };
541
- contexts.push(objContext);
542
- processKeyValueLine(indentLevel + 1, keyToken, valueToken, lineNo, objContext);
278
+ attachValue(arr, parent, key, lineNo);
279
+ contexts.push({
280
+ type: "tabular",
281
+ value: arr,
282
+ indent: indentLevel + 1,
283
+ expectedLength: length,
284
+ delimiter,
285
+ fields
286
+ });
543
287
  return;
544
- }
545
- // Primitive value
546
- const value = parsePrimitiveToken(content, list.delimiter, lineNo, strict);
288
+ }
289
+ if (valueToken === "") {
290
+ const arr = [];
291
+ bumpNodes(state, limits, 1, lineNo);
292
+ attachValue(arr, parent, key, lineNo);
293
+ contexts.push({
294
+ type: "list",
295
+ value: arr,
296
+ indent: indentLevel + 1,
297
+ expectedLength: length,
298
+ delimiter
299
+ });
300
+ return;
301
+ }
302
+ const values = splitDelimited(valueToken, delimiter, lineNo).map((t) => parsePrimitiveToken(t, delimiter, lineNo, strict));
303
+ if (strict && values.length !== length) {
304
+ throw new ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
305
+ }
306
+ attachValue(values, parent, key, lineNo);
307
+ bumpNodes(state, limits, values.length, lineNo);
308
+ return;
309
+ }
310
+ if (valueToken === "") {
311
+ const obj = createSafeObject();
312
+ attachValue(obj, parent, key, lineNo);
313
+ contexts.push({ type: "object", value: obj, indent: indentLevel + 1 });
314
+ return;
315
+ }
316
+ const value = parsePrimitiveToken(valueToken, delimiterFallback, lineNo, strict);
317
+ attachValue(value, parent, key, lineNo);
547
318
  bumpNodes(state, limits, 1, lineNo);
548
- list.value.push(value);
319
+ };
320
+ lines.forEach((rawLine, index) => {
321
+ const lineNo = index + 1;
322
+ const trimmedEnd = rawLine.replace(/[ \t]+$/, "");
323
+ if (trimmedEnd.trim() === "") {
324
+ return;
325
+ }
326
+ const indentSpaces = countLeadingSpaces(trimmedEnd, lineNo);
327
+ if (indentStep === null) {
328
+ indentStep = indentSpaces === 0 ? 2 : indentSpaces;
329
+ }
330
+ if (indentSpaces % indentStep !== 0) {
331
+ throw new ToonError(`Inconsistent indentation: expected multiples of ${indentStep} spaces.`, lineNo);
332
+ }
333
+ const indentLevel = indentSpaces / indentStep;
334
+ if (indentLevel > limits.maxDepth) {
335
+ throw new ToonError(`Maximum depth ${limits.maxDepth} exceeded.`, lineNo);
336
+ }
337
+ const line = trimmedEnd.slice(indentSpaces);
338
+ while (true) {
339
+ const top = contexts[contexts.length - 1];
340
+ if (!top || indentLevel >= top.indent) {
341
+ break;
342
+ }
343
+ contexts.pop();
344
+ finalizeContainer(top, lineNo);
345
+ }
346
+ let handled = false;
347
+ let consumed = false;
348
+ while (!handled) {
349
+ const top = contexts[contexts.length - 1];
350
+ if (top && top.type === "tabular" && indentLevel === top.indent) {
351
+ const classification = classifyTabularLine(line, top.delimiter);
352
+ if (classification === "row") {
353
+ const cells = splitDelimited(line, top.delimiter, lineNo);
354
+ if (strict && cells.length !== top.fields.length) {
355
+ throw new ToonError(
356
+ `Tabular row width mismatch: expected ${top.fields.length}, got ${cells.length}.`,
357
+ lineNo
358
+ );
359
+ }
360
+ const obj = createSafeObject();
361
+ top.fields.forEach((field, idx) => {
362
+ validateKeySafety(field, limits, lineNo);
363
+ const token = cells[idx] ?? "";
364
+ obj[field] = parsePrimitiveToken(token, top.delimiter, lineNo, strict);
365
+ bumpNodes(state, limits, 1, lineNo);
366
+ });
367
+ top.value.push(obj);
368
+ if (top.value.length > limits.maxArrayLength) {
369
+ throw new ToonError(
370
+ `Tabular array length exceeds limit ${limits.maxArrayLength}.`,
371
+ lineNo
372
+ );
373
+ }
374
+ handled = true;
375
+ consumed = true;
376
+ break;
377
+ }
378
+ finalizeContainer(top, lineNo);
379
+ contexts.pop();
380
+ continue;
381
+ }
382
+ handled = true;
383
+ }
384
+ if (consumed) {
385
+ return;
386
+ }
387
+ const parent = contexts[contexts.length - 1];
388
+ if (parent && parent.type === "list") {
389
+ if (indentLevel !== parent.indent) {
390
+ throw new ToonError("List items must align under their header.", lineNo);
391
+ }
392
+ parseListItem(line, parent, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict);
393
+ return;
394
+ }
395
+ if (parent && parent.type === "placeholder") {
396
+ if (indentLevel !== parent.indent) {
397
+ throw new ToonError('List item body must indent one level below "-".', lineNo);
398
+ }
399
+ }
400
+ if (indentLevel !== (parent ? parent.indent : 0)) {
401
+ throw new ToonError("Unexpected indentation level.", lineNo);
402
+ }
403
+ const colonIndex = findUnquotedColon(line);
404
+ if (colonIndex === -1) {
405
+ if (root === null && !parent) {
406
+ const value = parsePrimitiveToken(line.trim(), delimiterFallback, lineNo, strict);
407
+ root = value;
408
+ bumpNodes(state, limits, 1, lineNo);
409
+ return;
410
+ }
411
+ throw new ToonError("Expected key-value pair.", lineNo);
412
+ }
413
+ const keyToken = line.slice(0, colonIndex).trim();
414
+ const valueToken = line.slice(colonIndex + 1).trim();
415
+ if (parent && parent.type === "placeholder") {
416
+ if (!keyToken.startsWith("[")) {
417
+ if (!parent.current) {
418
+ const obj = createSafeObject();
419
+ parent.assign(obj);
420
+ parent.current = { value: obj, indent: indentLevel + 1 };
421
+ parent.filled = true;
422
+ }
423
+ const objContext = { type: "object", value: parent.current.value, indent: indentLevel + 1 };
424
+ processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, objContext);
425
+ return;
426
+ }
427
+ }
428
+ processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, parent);
429
+ });
430
+ while (contexts.length > 0) {
431
+ const container = contexts.pop();
432
+ finalizeContainer(container, lines.length);
433
+ }
434
+ if (root === null) {
435
+ return createSafeObject();
436
+ }
437
+ return root;
438
+ }
439
+ function classifyTabularLine(line, delimiter) {
440
+ let inQuote = false;
441
+ let escape = false;
442
+ let firstColon = -1;
443
+ let firstDelim = -1;
444
+ for (let i = 0; i < line.length; i++) {
445
+ const ch = line[i];
446
+ if (escape) {
447
+ escape = false;
448
+ continue;
449
+ }
450
+ if (ch === "\\") {
451
+ escape = true;
452
+ continue;
453
+ }
454
+ if (ch === '"') {
455
+ inQuote = !inQuote;
456
+ continue;
457
+ }
458
+ if (inQuote) continue;
459
+ if (ch === ":" && firstColon === -1) firstColon = i;
460
+ if (ch === delimiter && firstDelim === -1) firstDelim = i;
461
+ }
462
+ if (firstColon === -1) return "row";
463
+ if (firstDelim === -1) return "field";
464
+ return firstDelim < firstColon ? "row" : "field";
465
+ }
466
+ function parseListItem(line, list, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict) {
467
+ const trimmed = line.trim();
468
+ if (!trimmed.startsWith("-")) {
469
+ throw new ToonError('List items must start with "-".', lineNo);
470
+ }
471
+ const content = trimmed.slice(1).trim();
472
+ if (content === "") {
473
+ const placeholder = {
474
+ type: "placeholder",
475
+ indent: indentLevel + 1,
476
+ filled: false,
477
+ current: void 0,
478
+ assign: function(value2) {
479
+ if (this.current && typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && Object.keys(value2).length === 0) {
480
+ return;
481
+ }
482
+ list.value.push(value2);
483
+ }
484
+ };
485
+ contexts.push(placeholder);
486
+ return;
487
+ }
488
+ if (content.startsWith("[")) {
489
+ const colonIndex2 = findUnquotedColon(content);
490
+ const headerToken = colonIndex2 === -1 ? content : content.slice(0, colonIndex2).trim();
491
+ const valueToken = colonIndex2 === -1 ? "" : content.slice(colonIndex2 + 1).trim();
492
+ const { length, delimiter, fields } = parseArrayHeaderFromList(headerToken, lineNo);
493
+ if (length > limits.maxArrayLength) {
494
+ throw new ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
495
+ }
496
+ if (fields) {
497
+ if (valueToken !== "") {
498
+ throw new ToonError("Tabular header in list item cannot have inline values.", lineNo);
499
+ }
500
+ const arr = [];
501
+ bumpNodes(state, limits, 1, lineNo);
502
+ list.value.push(arr);
503
+ contexts.push({
504
+ type: "tabular",
505
+ value: arr,
506
+ indent: indentLevel + 1,
507
+ expectedLength: length,
508
+ delimiter,
509
+ fields
510
+ });
511
+ return;
512
+ }
513
+ if (valueToken === "") {
514
+ const arr = [];
515
+ bumpNodes(state, limits, 1, lineNo);
516
+ list.value.push(arr);
517
+ contexts.push({
518
+ type: "list",
519
+ value: arr,
520
+ indent: indentLevel + 1,
521
+ expectedLength: length,
522
+ delimiter
523
+ });
524
+ return;
525
+ }
526
+ const values = splitDelimited(valueToken, delimiter, lineNo).map((t) => parsePrimitiveToken(t, delimiter, lineNo, strict));
527
+ if (strict && values.length !== length) {
528
+ throw new ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
529
+ }
530
+ bumpNodes(state, limits, values.length, lineNo);
531
+ list.value.push(values);
532
+ return;
533
+ }
534
+ const colonIndex = findUnquotedColon(content);
535
+ if (colonIndex !== -1) {
536
+ const keyToken = content.slice(0, colonIndex).trim();
537
+ const valueToken = content.slice(colonIndex + 1).trim();
538
+ const obj = createSafeObject();
539
+ bumpNodes(state, limits, 1, lineNo);
540
+ list.value.push(obj);
541
+ const objContext = { type: "object", value: obj, indent: indentLevel + 1 };
542
+ contexts.push(objContext);
543
+ processKeyValueLine(indentLevel + 1, keyToken, valueToken, lineNo, objContext);
544
+ return;
545
+ }
546
+ const value = parsePrimitiveToken(content, list.delimiter, lineNo, strict);
547
+ bumpNodes(state, limits, 1, lineNo);
548
+ list.value.push(value);
549
549
  }
550
550
  function splitKeyHeader(token) {
551
- let inQuote = false;
552
- let escape = false;
553
- for (let i = 0; i < token.length; i++) {
554
- const ch = token[i];
555
- if (escape) {
556
- escape = false;
557
- continue;
558
- }
559
- if (ch === '\\') {
560
- escape = true;
561
- continue;
562
- }
563
- if (ch === '"') {
564
- inQuote = !inQuote;
565
- continue;
566
- }
567
- if (!inQuote && ch === '[') {
568
- return {
569
- rawKey: token.slice(0, i).trim(),
570
- header: token.slice(i).trim()
571
- };
572
- }
551
+ let inQuote = false;
552
+ let escape = false;
553
+ for (let i = 0; i < token.length; i++) {
554
+ const ch = token[i];
555
+ if (escape) {
556
+ escape = false;
557
+ continue;
558
+ }
559
+ if (ch === "\\") {
560
+ escape = true;
561
+ continue;
562
+ }
563
+ if (ch === '"') {
564
+ inQuote = !inQuote;
565
+ continue;
573
566
  }
574
- return { rawKey: token.trim() };
567
+ if (!inQuote && ch === "[") {
568
+ return {
569
+ rawKey: token.slice(0, i).trim(),
570
+ header: token.slice(i).trim()
571
+ };
572
+ }
573
+ }
574
+ return { rawKey: token.trim() };
575
575
  }
576
576
  function parseArrayHeaderFromList(token, lineNo) {
577
- const match = token.match(/^\[(\d+)([,\|\t])?\](\{(.+)\})?$/);
578
- if (!match) {
579
- throw new ToonError(`Invalid array header "${token}".`, lineNo);
580
- }
581
- const length = parseInt(match[1], 10);
582
- const delimiter = match[2] ?? DEFAULT_DELIMITER;
583
- const fieldsRaw = match[4];
584
- const fields = fieldsRaw ? splitDelimited(fieldsRaw, delimiter, lineNo).map(f => decodeKey(f, delimiter, lineNo)) : undefined;
585
- return { length, delimiter, fields };
577
+ const match = token.match(/^\[(\d+)([,\|\t])?\](\{(.+)\})?$/);
578
+ if (!match) {
579
+ throw new ToonError(`Invalid array header "${token}".`, lineNo);
580
+ }
581
+ const length = parseInt(match[1], 10);
582
+ const delimiter = match[2] ?? DEFAULT_DELIMITER;
583
+ const fieldsRaw = match[4];
584
+ const fields = fieldsRaw ? splitDelimited(fieldsRaw, delimiter, lineNo).map((f) => decodeKey(f, lineNo)) : void 0;
585
+ return { length, delimiter, fields };
586
586
  }
587
587
  function findUnquotedColon(text) {
588
- let inQuote = false;
589
- let escape = false;
590
- for (let i = 0; i < text.length; i++) {
591
- const ch = text[i];
592
- if (escape) {
593
- escape = false;
594
- continue;
595
- }
596
- if (ch === '\\') {
597
- escape = true;
598
- continue;
599
- }
600
- if (ch === '"') {
601
- inQuote = !inQuote;
602
- continue;
603
- }
604
- if (!inQuote && ch === ':') {
605
- return i;
606
- }
588
+ let inQuote = false;
589
+ let escape = false;
590
+ for (let i = 0; i < text.length; i++) {
591
+ const ch = text[i];
592
+ if (escape) {
593
+ escape = false;
594
+ continue;
595
+ }
596
+ if (ch === "\\") {
597
+ escape = true;
598
+ continue;
599
+ }
600
+ if (ch === '"') {
601
+ inQuote = !inQuote;
602
+ continue;
607
603
  }
608
- return -1;
604
+ if (!inQuote && ch === ":") {
605
+ return i;
606
+ }
607
+ }
608
+ return -1;
609
609
  }
610
610
  function countLeadingSpaces(text, lineNo) {
611
- let count = 0;
612
- for (const ch of text) {
613
- if (ch === ' ') {
614
- count++;
615
- }
616
- else if (ch === '\t') {
617
- throw new ToonError('Tabs are not allowed for indentation.', lineNo);
618
- }
619
- else {
620
- break;
621
- }
622
- }
623
- return count;
611
+ let count = 0;
612
+ for (const ch of text) {
613
+ if (ch === " ") {
614
+ count++;
615
+ } else if (ch === " ") {
616
+ throw new ToonError("Tabs are not allowed for indentation.", lineNo);
617
+ } else {
618
+ break;
619
+ }
620
+ }
621
+ return count;
624
622
  }
625
623
  function parsePrimitiveToken(token, delimiter, lineNo, strict) {
626
- if (token === '') {
627
- return '';
628
- }
629
- const trimmed = token.trim();
630
- if (trimmed !== token && strict) {
631
- throw new ToonError('Unquoted values may not contain leading or trailing whitespace.', lineNo);
632
- }
633
- if (trimmed.startsWith('"')) {
634
- return decodeQuotedString(trimmed, lineNo);
635
- }
636
- if (trimmed === 'true')
637
- return true;
638
- if (trimmed === 'false')
639
- return false;
640
- if (trimmed === 'null')
641
- return null;
642
- // If token is a plain integer with leading zeros (e.g., 007) this is disallowed
643
- // and should be quoted - reject early.
644
- if (/^-?\d+$/.test(trimmed) && /^-?0\d+$/.test(trimmed)) {
645
- throw new ToonError('Numbers with leading zeros must be quoted.', lineNo);
646
- }
647
- if (NUMERIC_RE.test(trimmed)) {
648
- if (/^-?0\d+/.test(trimmed)) {
649
- throw new ToonError('Numbers with leading zeros must be quoted.', lineNo);
650
- }
651
- const num = Number(trimmed);
652
- if (!Number.isFinite(num)) {
653
- throw new ToonError('Invalid numeric value.', lineNo);
654
- }
655
- return num;
656
- }
657
- if (strict && trimmed.includes(delimiter)) {
658
- throw new ToonError('Unquoted value contains the active delimiter.', lineNo);
659
- }
660
- return trimmed;
624
+ if (token === "") {
625
+ return "";
626
+ }
627
+ const trimmed = token.trim();
628
+ if (trimmed !== token && strict) {
629
+ throw new ToonError("Unquoted values may not contain leading or trailing whitespace.", lineNo);
630
+ }
631
+ if (trimmed.startsWith('"')) {
632
+ return decodeQuotedString(trimmed, lineNo);
633
+ }
634
+ if (trimmed === "true") return true;
635
+ if (trimmed === "false") return false;
636
+ if (trimmed === "null") return null;
637
+ if (/^-?\d+$/.test(trimmed) && /^-?0\d+$/.test(trimmed)) {
638
+ throw new ToonError("Numbers with leading zeros must be quoted.", lineNo);
639
+ }
640
+ if (NUMERIC_RE.test(trimmed)) {
641
+ if (/^-?0\d+/.test(trimmed)) {
642
+ throw new ToonError("Numbers with leading zeros must be quoted.", lineNo);
643
+ }
644
+ const num = Number(trimmed);
645
+ if (!Number.isFinite(num)) {
646
+ throw new ToonError("Invalid numeric value.", lineNo);
647
+ }
648
+ return num;
649
+ }
650
+ if (strict && trimmed.includes(delimiter)) {
651
+ throw new ToonError("Unquoted value contains the active delimiter.", lineNo);
652
+ }
653
+ return trimmed;
661
654
  }
662
655
  function decodeQuotedString(token, lineNo) {
663
- if (!token.endsWith('"')) {
664
- throw new ToonError('Unterminated string.', lineNo);
665
- }
666
- let result = '';
667
- let escape = false;
668
- for (let i = 1; i < token.length - 1; i++) {
669
- const ch = token[i];
670
- if (escape) {
671
- if (ch === '"' || ch === '\\') {
672
- result += ch;
673
- }
674
- else if (ch === 'n') {
675
- result += '\n';
676
- }
677
- else if (ch === 'r') {
678
- result += '\r';
679
- }
680
- else if (ch === 't') {
681
- result += '\t';
682
- }
683
- else {
684
- throw new ToonError(`Invalid escape sequence \\${ch}.`, lineNo);
685
- }
686
- escape = false;
687
- continue;
688
- }
689
- if (ch === '\\') {
690
- escape = true;
691
- continue;
692
- }
693
- result += ch;
694
- }
656
+ if (!token.endsWith('"')) {
657
+ throw new ToonError("Unterminated string.", lineNo);
658
+ }
659
+ let result = "";
660
+ let escape = false;
661
+ for (let i = 1; i < token.length - 1; i++) {
662
+ const ch = token[i];
695
663
  if (escape) {
696
- throw new ToonError('Unterminated escape sequence.', lineNo);
697
- }
698
- return result;
664
+ if (ch === '"' || ch === "\\") {
665
+ result += ch;
666
+ } else if (ch === "n") {
667
+ result += "\n";
668
+ } else if (ch === "r") {
669
+ result += "\r";
670
+ } else if (ch === "t") {
671
+ result += " ";
672
+ } else {
673
+ throw new ToonError(`Invalid escape sequence \\${ch}.`, lineNo);
674
+ }
675
+ escape = false;
676
+ continue;
677
+ }
678
+ if (ch === "\\") {
679
+ escape = true;
680
+ continue;
681
+ }
682
+ result += ch;
683
+ }
684
+ if (escape) {
685
+ throw new ToonError("Unterminated escape sequence.", lineNo);
686
+ }
687
+ return result;
699
688
  }
700
689
  function splitDelimited(text, delimiter, lineNo) {
701
- const tokens = [];
702
- let current = '';
703
- let inQuote = false;
704
- let escape = false;
705
- for (let i = 0; i < text.length; i++) {
706
- const ch = text[i];
707
- if (escape) {
708
- current += ch;
709
- escape = false;
710
- continue;
711
- }
712
- if (ch === '\\') {
713
- current += ch;
714
- escape = true;
715
- continue;
716
- }
717
- if (ch === '"') {
718
- current += ch;
719
- inQuote = !inQuote;
720
- continue;
721
- }
722
- if (!inQuote && ch === delimiter) {
723
- tokens.push(current);
724
- current = '';
725
- continue;
726
- }
727
- current += ch;
728
- }
729
- if (inQuote) {
730
- throw new ToonError('Unterminated quoted value.', lineNo);
731
- }
732
- tokens.push(current);
733
- return tokens;
690
+ const tokens = [];
691
+ let current = "";
692
+ let inQuote = false;
693
+ let escape = false;
694
+ for (let i = 0; i < text.length; i++) {
695
+ const ch = text[i];
696
+ if (escape) {
697
+ current += ch;
698
+ escape = false;
699
+ continue;
700
+ }
701
+ if (ch === "\\") {
702
+ current += ch;
703
+ escape = true;
704
+ continue;
705
+ }
706
+ if (ch === '"') {
707
+ current += ch;
708
+ inQuote = !inQuote;
709
+ continue;
710
+ }
711
+ if (!inQuote && ch === delimiter) {
712
+ tokens.push(current);
713
+ current = "";
714
+ continue;
715
+ }
716
+ current += ch;
717
+ }
718
+ if (inQuote) {
719
+ throw new ToonError("Unterminated quoted value.", lineNo);
720
+ }
721
+ tokens.push(current);
722
+ return tokens;
734
723
  }
735
724
  function encodePrimitive(value, activeDelimiter, documentDelimiter) {
736
- if (value === null)
737
- return 'null';
738
- if (typeof value === 'boolean')
739
- return value ? 'true' : 'false';
740
- if (typeof value === 'number') {
741
- if (!Number.isFinite(value)) {
742
- throw new ToonError('Numeric values must be finite.');
743
- }
744
- if (Object.is(value, -0))
745
- return '-0';
746
- return String(value);
747
- }
748
- return encodeString(value, activeDelimiter, documentDelimiter);
725
+ if (value === null) return "null";
726
+ if (typeof value === "boolean") return value ? "true" : "false";
727
+ if (typeof value === "number") {
728
+ if (!Number.isFinite(value)) {
729
+ throw new ToonError("Numeric values must be finite.");
730
+ }
731
+ if (Object.is(value, -0)) return "-0";
732
+ return String(value);
733
+ }
734
+ return encodeString(value, activeDelimiter, documentDelimiter);
749
735
  }
750
736
  function encodeString(value, activeDelimiter, documentDelimiter) {
751
- const needsQuote = value.length === 0 ||
752
- /^\s|\s$/.test(value) ||
753
- value === 'true' ||
754
- value === 'false' ||
755
- value === 'null' ||
756
- NUMERIC_LIKE_RE.test(value) ||
757
- LEADING_ZERO_RE.test(value) ||
758
- value.includes(':') ||
759
- value.includes('"') ||
760
- value.includes('\\') ||
761
- /[\[\]{}]/.test(value) ||
762
- /[\n\r\t]/.test(value) ||
763
- value.includes(activeDelimiter) ||
764
- value.includes(documentDelimiter) ||
765
- value === '-' ||
766
- value.startsWith('-');
767
- if (!needsQuote) {
768
- return value;
769
- }
770
- return `"${escapeString(value)}"`;
737
+ const needsQuote = value.length === 0 || /^\s|\s$/.test(value) || value === "true" || value === "false" || value === "null" || NUMERIC_LIKE_RE.test(value) || LEADING_ZERO_RE.test(value) || value.includes(":") || value.includes('"') || value.includes("\\") || /[\[\]{}]/.test(value) || /[\n\r\t]/.test(value) || value.includes(activeDelimiter) || value.includes(documentDelimiter) || value === "-" || value.startsWith("-");
738
+ if (!needsQuote) {
739
+ return value;
740
+ }
741
+ return `"${escapeString(value)}"`;
771
742
  }
772
743
  function escapeString(value) {
773
- return value
774
- .replace(/\\/g, '\\\\')
775
- .replace(/"/g, '\\"')
776
- .replace(/\n/g, '\\n')
777
- .replace(/\r/g, '\\r')
778
- .replace(/\t/g, '\\t');
744
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
779
745
  }
780
746
  function primitiveLine(key, value, indent, activeDelimiter, limits = DEFAULT_LIMITS) {
781
- const encodedValue = encodePrimitive(value, activeDelimiter, activeDelimiter);
782
- if (key === null) {
783
- return `${indent}${encodedValue}`;
784
- }
785
- validateKeySafety(key, limits);
786
- return `${indent}${encodeKey(key, activeDelimiter)}: ${encodedValue}`;
747
+ const encodedValue = encodePrimitive(value, activeDelimiter, activeDelimiter);
748
+ if (key === null) {
749
+ return `${indent}${encodedValue}`;
750
+ }
751
+ validateKeySafety(key, limits);
752
+ return `${indent}${encodeKey(key, activeDelimiter)}: ${encodedValue}`;
787
753
  }
788
754
  function encodeKey(key, activeDelimiter) {
789
- if (SAFE_KEY_RE.test(key) && !key.includes(activeDelimiter)) {
790
- return key;
791
- }
792
- return `"${escapeString(key)}"`;
755
+ if (SAFE_KEY_RE.test(key) && !key.includes(activeDelimiter)) {
756
+ return key;
757
+ }
758
+ return `"${escapeString(key)}"`;
793
759
  }
794
- function decodeKey(token, delimiter, lineNo) {
795
- const trimmed = token.trim();
796
- if (trimmed.startsWith('"')) {
797
- return decodeQuotedString(trimmed, lineNo);
798
- }
799
- if (!SAFE_KEY_RE.test(trimmed)) {
800
- throw new ToonError('Invalid key token.', lineNo);
801
- }
802
- if (trimmed.includes(delimiter)) {
803
- throw new ToonError('Key contains active delimiter and must be quoted.', lineNo);
804
- }
805
- return trimmed;
760
+ function decodeKey(token, lineNo) {
761
+ const trimmed = token.trim();
762
+ if (trimmed.startsWith('"')) {
763
+ return decodeQuotedString(trimmed, lineNo);
764
+ }
765
+ if (!SAFE_KEY_RE.test(trimmed)) {
766
+ throw new ToonError("Invalid key token.", lineNo);
767
+ }
768
+ return trimmed;
806
769
  }
807
770
  function bumpNodes(state, limits, count, lineNo) {
808
- state.nodes += count;
809
- if (state.nodes > limits.maxTotalNodes) {
810
- throw new ToonError(`Node count ${state.nodes} exceeds limit ${limits.maxTotalNodes}.`, lineNo);
811
- }
771
+ state.nodes += count;
772
+ if (state.nodes > limits.maxTotalNodes) {
773
+ throw new ToonError(`Node count ${state.nodes} exceeds limit ${limits.maxTotalNodes}.`, lineNo);
774
+ }
812
775
  }
813
776
  function enforceLimits(depth, limits, state) {
814
- if (depth > limits.maxDepth) {
815
- throw new ToonError(`Maximum depth ${limits.maxDepth} exceeded.`);
816
- }
817
- bumpNodes(state, limits, 1);
777
+ if (depth > limits.maxDepth) {
778
+ throw new ToonError(`Maximum depth ${limits.maxDepth} exceeded.`);
779
+ }
780
+ bumpNodes(state, limits, 1);
818
781
  }
819
782
  function applyLimits(options) {
820
- return {
821
- maxDepth: options.maxDepth ?? DEFAULT_LIMITS.maxDepth,
822
- maxArrayLength: options.maxArrayLength ?? DEFAULT_LIMITS.maxArrayLength,
823
- maxTotalNodes: options.maxTotalNodes ?? DEFAULT_LIMITS.maxTotalNodes,
824
- disallowedKeys: options.disallowedKeys ?? DEFAULT_LIMITS.disallowedKeys
825
- };
783
+ return {
784
+ maxDepth: options.maxDepth ?? DEFAULT_LIMITS.maxDepth,
785
+ maxArrayLength: options.maxArrayLength ?? DEFAULT_LIMITS.maxArrayLength,
786
+ maxTotalNodes: options.maxTotalNodes ?? DEFAULT_LIMITS.maxTotalNodes,
787
+ disallowedKeys: options.disallowedKeys ?? DEFAULT_LIMITS.disallowedKeys
788
+ };
826
789
  }
827
790
  function isPrimitive(value) {
828
- return (value === null ||
829
- typeof value === 'string' ||
830
- typeof value === 'number' ||
831
- typeof value === 'boolean');
791
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
832
792
  }
833
793
  function isPlainObject(value) {
834
- if (value === null || typeof value !== 'object')
835
- return false;
836
- const proto = Object.getPrototypeOf(value);
837
- return proto === Object.prototype || proto === null;
794
+ if (value === null || typeof value !== "object") return false;
795
+ const proto = Object.getPrototypeOf(value);
796
+ return proto === Object.prototype || proto === null;
838
797
  }
839
798
  function detectTabular(arr) {
840
- if (arr.length === 0)
841
- return null;
842
- if (!arr.every(item => isPlainObject(item))) {
843
- return null;
844
- }
845
- const first = arr[0];
846
- const fields = Object.keys(first);
847
- if (fields.length === 0) {
848
- return null;
849
- }
850
- const rows = [];
851
- for (const item of arr) {
852
- const obj = item;
853
- const objKeys = Object.keys(obj);
854
- if (objKeys.length !== fields.length)
855
- return null;
856
- for (const field of fields) {
857
- if (!Object.prototype.hasOwnProperty.call(obj, field))
858
- return null;
859
- if (!isPrimitive(obj[field]))
860
- return null;
861
- }
862
- rows.push(obj);
863
- }
864
- return { fields, rows };
799
+ if (arr.length === 0) return null;
800
+ if (!arr.every((item) => isPlainObject(item))) {
801
+ return null;
802
+ }
803
+ const first = arr[0];
804
+ const fields = Object.keys(first);
805
+ if (fields.length === 0) {
806
+ return null;
807
+ }
808
+ const rows = [];
809
+ for (const item of arr) {
810
+ const obj = item;
811
+ const objKeys = Object.keys(obj);
812
+ if (objKeys.length !== fields.length) return null;
813
+ for (const field of fields) {
814
+ if (!Object.prototype.hasOwnProperty.call(obj, field)) return null;
815
+ if (!isPrimitive(obj[field])) return null;
816
+ }
817
+ rows.push(obj);
818
+ }
819
+ return { fields, rows };
865
820
  }
866
821
  function validateKeySafety(key, limits = DEFAULT_LIMITS, lineNo) {
867
- if (limits.disallowedKeys.includes(key)) {
868
- throw new ToonError(`Disallowed key "${key}" to prevent prototype pollution.`, lineNo);
869
- }
822
+ if (limits.disallowedKeys.includes(key)) {
823
+ throw new ToonError(`Disallowed key "${key}" to prevent prototype pollution.`, lineNo);
824
+ }
870
825
  }
871
826
  function createSafeObject() {
872
- return Object.create(null);
827
+ return /* @__PURE__ */ Object.create(null);
873
828
  }
874
- //# sourceMappingURL=index.js.map
829
+ // Annotate the CommonJS export names for ESM import in node:
830
+ 0 && (module.exports = {
831
+ ToonError,
832
+ jsonToToon,
833
+ toonToJson,
834
+ ...require("./xml.cjs")
835
+ });