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/LICENSE +21 -21
- package/README.md +177 -151
- package/dist/index.cjs +770 -809
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/index.js.map +1 -1
- package/dist/xml.cjs +47 -0
- package/dist/xml.d.ts +24 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +28 -0
- package/dist/xml.js.map +1 -0
- package/package.json +57 -54
package/dist/index.cjs
CHANGED
|
@@ -1,874 +1,835 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
423
|
-
|
|
246
|
+
const length = parseInt(match[1], 10);
|
|
247
|
+
if (!Number.isFinite(length)) {
|
|
248
|
+
throw new ToonError("Invalid array length.", lineNo);
|
|
424
249
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
604
|
+
if (!inQuote && ch === ":") {
|
|
605
|
+
return i;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return -1;
|
|
609
609
|
}
|
|
610
610
|
function countLeadingSpaces(text, lineNo) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
755
|
+
if (SAFE_KEY_RE.test(key) && !key.includes(activeDelimiter)) {
|
|
756
|
+
return key;
|
|
757
|
+
}
|
|
758
|
+
return `"${escapeString(key)}"`;
|
|
793
759
|
}
|
|
794
|
-
function decodeKey(token,
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
868
|
-
|
|
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
|
-
|
|
827
|
+
return /* @__PURE__ */ Object.create(null);
|
|
873
828
|
}
|
|
874
|
-
|
|
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
|
+
});
|