toon-parser 2.2.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -62
- package/dist/core.cjs +630 -0
- package/dist/core.d.ts +73 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +625 -0
- package/dist/core.js.map +1 -0
- package/dist/csv.cjs +16 -24
- package/dist/csv.d.ts +2 -1
- package/dist/csv.d.ts.map +1 -1
- package/dist/csv.js +8 -21
- package/dist/csv.js.map +1 -1
- package/dist/html.cjs +9 -7
- package/dist/html.d.ts +4 -2
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +7 -5
- package/dist/html.js.map +1 -1
- package/dist/index.cjs +5 -816
- package/dist/index.d.ts +2 -51
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -875
- package/dist/index.js.map +1 -1
- package/dist/inferType.cjs +39 -0
- package/dist/inferType.d.ts +10 -0
- package/dist/inferType.d.ts.map +1 -0
- package/dist/inferType.js +21 -0
- package/dist/inferType.js.map +1 -0
- package/dist/internal/constants.cjs +52 -0
- package/dist/internal/constants.d.ts +9 -0
- package/dist/internal/constants.d.ts.map +1 -0
- package/dist/internal/constants.js +15 -0
- package/dist/internal/constants.js.map +1 -0
- package/dist/internal/errors.cjs +34 -0
- package/dist/internal/errors.d.ts +5 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +8 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/primitives.cjs +304 -0
- package/dist/internal/primitives.d.ts +23 -0
- package/dist/internal/primitives.d.ts.map +1 -0
- package/dist/internal/primitives.js +289 -0
- package/dist/internal/primitives.js.map +1 -0
- package/dist/internal/security.cjs +118 -0
- package/dist/internal/security.d.ts +31 -0
- package/dist/internal/security.d.ts.map +1 -0
- package/dist/internal/security.js +87 -0
- package/dist/internal/security.js.map +1 -0
- package/dist/internal/types.cjs +16 -0
- package/dist/internal/types.d.ts +37 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/log.cjs +43 -17
- package/dist/log.d.ts +4 -3
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +47 -19
- package/dist/log.js.map +1 -1
- package/dist/url.cjs +22 -21
- package/dist/url.d.ts +1 -1
- package/dist/url.d.ts.map +1 -1
- package/dist/url.js +23 -29
- package/dist/url.js.map +1 -1
- package/dist/xml.cjs +7 -5
- package/dist/xml.d.ts +1 -1
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +5 -3
- package/dist/xml.js.map +1 -1
- package/package.json +40 -10
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"use strict";
|
|
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var core_exports = {};
|
|
20
|
+
__export(core_exports, {
|
|
21
|
+
ToonError: () => import_errors2.ToonError,
|
|
22
|
+
enforceInputLength: () => import_security2.enforceInputLength,
|
|
23
|
+
jsonToToon: () => jsonToToon,
|
|
24
|
+
toonToJson: () => toonToJson
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(core_exports);
|
|
27
|
+
var import_constants = require("./internal/constants.cjs");
|
|
28
|
+
var import_errors = require("./internal/errors.cjs");
|
|
29
|
+
var import_security = require("./internal/security.cjs");
|
|
30
|
+
var import_primitives = require("./internal/primitives.cjs");
|
|
31
|
+
var import_errors2 = require("./internal/errors.cjs");
|
|
32
|
+
var import_security2 = require("./internal/security.cjs");
|
|
33
|
+
function jsonToToon(value, options = {}) {
|
|
34
|
+
const indentSize = options.indent ?? 2;
|
|
35
|
+
if (!Number.isInteger(indentSize) || indentSize <= 0) {
|
|
36
|
+
throw new import_errors.ToonError("Indent must be a positive integer.");
|
|
37
|
+
}
|
|
38
|
+
const delimiter = options.delimiter ?? import_constants.DEFAULT_DELIMITER;
|
|
39
|
+
const limits = (0, import_security.applyLimits)(options);
|
|
40
|
+
const state = { nodes: 0 };
|
|
41
|
+
const lines = [];
|
|
42
|
+
const indentUnit = " ".repeat(indentSize);
|
|
43
|
+
const keyFolding = options.keyFolding ?? "off";
|
|
44
|
+
const flattenDepth = options.flattenDepth ?? Infinity;
|
|
45
|
+
const tryFoldChain = (firstKey, firstValue, siblingKeys) => {
|
|
46
|
+
if (keyFolding !== "safe") return null;
|
|
47
|
+
if (!import_constants.IDENTIFIER_SEGMENT_RE.test(firstKey)) return null;
|
|
48
|
+
if (limits.disallowedKeys.includes(firstKey)) return null;
|
|
49
|
+
const segments = [firstKey];
|
|
50
|
+
const valuesAtEachStep = [firstValue];
|
|
51
|
+
let current = firstValue;
|
|
52
|
+
while ((0, import_security.isPlainObject)(current) && Object.keys(current).length === 1) {
|
|
53
|
+
const onlyKey = Object.keys(current)[0];
|
|
54
|
+
if (!import_constants.IDENTIFIER_SEGMENT_RE.test(onlyKey)) break;
|
|
55
|
+
if (limits.disallowedKeys.includes(onlyKey)) break;
|
|
56
|
+
const nextValue = current[onlyKey];
|
|
57
|
+
segments.push(onlyKey);
|
|
58
|
+
valuesAtEachStep.push(nextValue);
|
|
59
|
+
current = nextValue;
|
|
60
|
+
}
|
|
61
|
+
const endpointIsEmptyObject = (0, import_security.isPlainObject)(current) && Object.keys(current).length === 0;
|
|
62
|
+
const endpointFoldable = (0, import_security.isPrimitive)(current) || Array.isArray(current) || current instanceof Date || endpointIsEmptyObject;
|
|
63
|
+
if (!endpointFoldable) return null;
|
|
64
|
+
if (segments.length < 2) return null;
|
|
65
|
+
const cap = Math.max(2, Math.min(segments.length, flattenDepth));
|
|
66
|
+
const usedSegments = segments.slice(0, cap);
|
|
67
|
+
const leaf = valuesAtEachStep[cap - 1];
|
|
68
|
+
const path = usedSegments.join(".");
|
|
69
|
+
if (siblingKeys.has(path)) return null;
|
|
70
|
+
return { path, leaf };
|
|
71
|
+
};
|
|
72
|
+
const encodeValue = (input, depth, key, activeDelimiter) => {
|
|
73
|
+
(0, import_security.enforceLimits)(depth, limits, state);
|
|
74
|
+
if ((0, import_security.isPrimitive)(input)) {
|
|
75
|
+
const line = (0, import_primitives.primitiveLine)(key, input, indentUnit.repeat(depth), activeDelimiter, limits);
|
|
76
|
+
lines.push(line);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(input)) {
|
|
80
|
+
encodeArray(key, input, depth, activeDelimiter);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if ((0, import_security.isPlainObject)(input)) {
|
|
84
|
+
encodeObject(key, input, depth, activeDelimiter);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (input instanceof Date) {
|
|
88
|
+
const line = (0, import_primitives.primitiveLine)(key, input.toISOString(), indentUnit.repeat(depth), activeDelimiter, limits);
|
|
89
|
+
lines.push(line);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw new import_errors.ToonError(`Unsupported value type: ${typeof input}`);
|
|
93
|
+
};
|
|
94
|
+
const encodeObject = (key, obj, depth, activeDelimiter) => {
|
|
95
|
+
(0, import_security.enforceDepth)(depth, limits);
|
|
96
|
+
const entries = Object.entries(obj);
|
|
97
|
+
const sortedEntries = options.sortKeys ? [...entries].sort(([a], [b]) => a.localeCompare(b)) : entries;
|
|
98
|
+
const prefix = indentUnit.repeat(depth);
|
|
99
|
+
if (key !== null) {
|
|
100
|
+
(0, import_security.validateKeySafety)(key, limits);
|
|
101
|
+
if (sortedEntries.length === 0) {
|
|
102
|
+
lines.push(`${prefix}${(0, import_primitives.encodeKey)(key, activeDelimiter)}:`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
lines.push(`${prefix}${(0, import_primitives.encodeKey)(key, activeDelimiter)}:`);
|
|
106
|
+
} else if (depth > 0 && sortedEntries.length === 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const siblingKeys = new Set(sortedEntries.map(([k]) => k));
|
|
110
|
+
for (const [childKey, childValue] of sortedEntries) {
|
|
111
|
+
const nextDepth = key === null ? depth : depth + 1;
|
|
112
|
+
(0, import_security.enforceDepth)(nextDepth, limits);
|
|
113
|
+
const folded = tryFoldChain(childKey, childValue, siblingKeys);
|
|
114
|
+
if (folded) {
|
|
115
|
+
encodeValue(folded.leaf, nextDepth, folded.path, activeDelimiter);
|
|
116
|
+
} else {
|
|
117
|
+
encodeValue(childValue, nextDepth, childKey, activeDelimiter);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const encodeArray = (key, arr, depth, activeDelimiter) => {
|
|
122
|
+
(0, import_security.enforceDepth)(depth, limits);
|
|
123
|
+
if (key !== null) {
|
|
124
|
+
(0, import_security.validateKeySafety)(key, limits);
|
|
125
|
+
}
|
|
126
|
+
if (arr.length > limits.maxArrayLength) {
|
|
127
|
+
throw new import_errors.ToonError(`Array length ${arr.length} exceeds limit ${limits.maxArrayLength}.`);
|
|
128
|
+
}
|
|
129
|
+
const prefix = indentUnit.repeat(depth);
|
|
130
|
+
const headerKey = key === null ? "" : (0, import_primitives.encodeKey)(key, activeDelimiter);
|
|
131
|
+
if (arr.every(import_security.isPrimitive)) {
|
|
132
|
+
const encoded = arr.map((v) => (0, import_primitives.encodePrimitive)(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
|
|
133
|
+
const spacing = arr.length > 0 ? " " : "";
|
|
134
|
+
lines.push(`${prefix}${headerKey}[${arr.length}]:${spacing}${encoded}`);
|
|
135
|
+
(0, import_security.bumpNodes)(state, limits, arr.length);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const tabular = (0, import_security.detectTabular)(arr);
|
|
139
|
+
if (tabular) {
|
|
140
|
+
const { fields, rows } = tabular;
|
|
141
|
+
const encodedFields = fields.map((f) => (0, import_primitives.encodeKey)(f, activeDelimiter)).join(activeDelimiter);
|
|
142
|
+
lines.push(`${prefix}${headerKey}[${arr.length}]{${encodedFields}}:`);
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
const rowValues = fields.map((f) => (0, import_primitives.encodePrimitive)(row[f], activeDelimiter, activeDelimiter)).join(activeDelimiter);
|
|
145
|
+
lines.push(`${indentUnit.repeat(depth + 1)}${rowValues}`);
|
|
146
|
+
}
|
|
147
|
+
(0, import_security.bumpNodes)(state, limits, arr.length * fields.length);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
lines.push(`${prefix}${headerKey}[${arr.length}]:`);
|
|
151
|
+
const itemIndent = depth + 1;
|
|
152
|
+
const itemPrefix = indentUnit.repeat(itemIndent);
|
|
153
|
+
for (const item of arr) {
|
|
154
|
+
(0, import_security.enforceDepth)(itemIndent, limits);
|
|
155
|
+
if ((0, import_security.isPrimitive)(item)) {
|
|
156
|
+
lines.push(`${itemPrefix}- ${(0, import_primitives.encodePrimitive)(item, activeDelimiter, activeDelimiter)}`);
|
|
157
|
+
(0, import_security.bumpNodes)(state, limits, 1);
|
|
158
|
+
} else if (Array.isArray(item)) {
|
|
159
|
+
(0, import_security.bumpNodes)(state, limits, 1);
|
|
160
|
+
const inline = item.every(import_security.isPrimitive);
|
|
161
|
+
if (inline) {
|
|
162
|
+
const encoded = item.map((v) => (0, import_primitives.encodePrimitive)(v, activeDelimiter, activeDelimiter)).join(activeDelimiter);
|
|
163
|
+
const spacing = item.length > 0 ? " " : "";
|
|
164
|
+
lines.push(`${itemPrefix}- [${item.length}]:${spacing}${encoded}`);
|
|
165
|
+
(0, import_security.bumpNodes)(state, limits, item.length);
|
|
166
|
+
} else {
|
|
167
|
+
lines.push(`${itemPrefix}-`);
|
|
168
|
+
encodeArray(null, item, itemIndent + 1, activeDelimiter);
|
|
169
|
+
}
|
|
170
|
+
} else if ((0, import_security.isPlainObject)(item)) {
|
|
171
|
+
(0, import_security.bumpNodes)(state, limits, 1);
|
|
172
|
+
const objEntries = Object.entries(item);
|
|
173
|
+
if (objEntries.length === 0) {
|
|
174
|
+
lines.push(`${itemPrefix}-`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
lines.push(`${itemPrefix}-`);
|
|
178
|
+
for (const [childKey, childValue] of objEntries) {
|
|
179
|
+
encodeValue(childValue, itemIndent + 1, childKey, activeDelimiter);
|
|
180
|
+
}
|
|
181
|
+
} else if (item instanceof Date) {
|
|
182
|
+
lines.push(`${itemPrefix}- ${(0, import_primitives.encodePrimitive)(item.toISOString(), activeDelimiter, activeDelimiter)}`);
|
|
183
|
+
(0, import_security.bumpNodes)(state, limits, 1);
|
|
184
|
+
} else {
|
|
185
|
+
throw new import_errors.ToonError(`Unsupported array item type: ${typeof item}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
encodeValue(value, 0, null, delimiter);
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
function toonToJson(text, options = {}) {
|
|
193
|
+
const limits = (0, import_security.applyLimits)(options);
|
|
194
|
+
if (text.length > limits.maxInputLength) {
|
|
195
|
+
throw new import_errors.ToonError(`Input length ${text.length} exceeds limit ${limits.maxInputLength}.`);
|
|
196
|
+
}
|
|
197
|
+
const strict = options.strict ?? true;
|
|
198
|
+
const expandPaths = options.expandPaths ?? "off";
|
|
199
|
+
const delimiterFallback = import_constants.DEFAULT_DELIMITER;
|
|
200
|
+
const trySplitDottedKey = (key, lineNo) => {
|
|
201
|
+
if (expandPaths !== "safe") return null;
|
|
202
|
+
if (!key.includes(".")) return null;
|
|
203
|
+
const segments = key.split(".");
|
|
204
|
+
for (const seg of segments) {
|
|
205
|
+
if (!import_constants.IDENTIFIER_SEGMENT_RE.test(seg)) return null;
|
|
206
|
+
if (limits.disallowedKeys.includes(seg)) {
|
|
207
|
+
throw new import_errors.ToonError(
|
|
208
|
+
`Disallowed key segment "${seg}" in expanded path "${key}".`,
|
|
209
|
+
lineNo
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return segments;
|
|
214
|
+
};
|
|
215
|
+
const assignExpandedPath = (target, segments, value, lineNo) => {
|
|
216
|
+
let cursor = target;
|
|
217
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
218
|
+
const seg = segments[i];
|
|
219
|
+
const existing2 = cursor[seg];
|
|
220
|
+
if (existing2 === void 0) {
|
|
221
|
+
const next2 = (0, import_security.createSafeObject)();
|
|
222
|
+
cursor[seg] = next2;
|
|
223
|
+
cursor = next2;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if ((0, import_security.isPlainObject)(existing2)) {
|
|
227
|
+
cursor = existing2;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (strict) {
|
|
231
|
+
throw new import_errors.ToonError(
|
|
232
|
+
`Expansion conflict at path "${segments.slice(0, i + 1).join(".")}" (object vs primitive).`,
|
|
233
|
+
lineNo
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const next = (0, import_security.createSafeObject)();
|
|
237
|
+
cursor[seg] = next;
|
|
238
|
+
cursor = next;
|
|
239
|
+
}
|
|
240
|
+
const leafKey = segments[segments.length - 1];
|
|
241
|
+
const existing = cursor[leafKey];
|
|
242
|
+
if (existing === void 0) {
|
|
243
|
+
cursor[leafKey] = value;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if ((0, import_security.isPlainObject)(existing) && (0, import_security.isPlainObject)(value)) {
|
|
247
|
+
Object.assign(existing, value);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (strict) {
|
|
251
|
+
throw new import_errors.ToonError(`Expansion conflict at path "${segments.join(".")}".`, lineNo);
|
|
252
|
+
}
|
|
253
|
+
cursor[leafKey] = value;
|
|
254
|
+
};
|
|
255
|
+
const lines = text.split(/\r?\n/);
|
|
256
|
+
const contexts = [];
|
|
257
|
+
let rootContainer = null;
|
|
258
|
+
let indentStep = null;
|
|
259
|
+
let root = null;
|
|
260
|
+
const state = { nodes: 0 };
|
|
261
|
+
const finalizeContainer = (container, lineNo) => {
|
|
262
|
+
if (container.type === "tabular") {
|
|
263
|
+
if (strict && container.value.length !== container.expectedLength) {
|
|
264
|
+
throw new import_errors.ToonError(
|
|
265
|
+
`Tabular array length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`,
|
|
266
|
+
lineNo
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} else if (container.type === "list") {
|
|
270
|
+
if (strict && container.expectedLength !== null && container.value.length !== container.expectedLength) {
|
|
271
|
+
throw new import_errors.ToonError(`List length mismatch: expected ${container.expectedLength}, got ${container.value.length}.`, lineNo);
|
|
272
|
+
}
|
|
273
|
+
} else if (container.type === "placeholder") {
|
|
274
|
+
if (!container.filled) {
|
|
275
|
+
container.assign((0, import_security.createSafeObject)());
|
|
276
|
+
container.filled = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const attachValue = (value, parent, key, lineNo) => {
|
|
281
|
+
const ensureRootObject = () => {
|
|
282
|
+
if (rootContainer) {
|
|
283
|
+
return rootContainer;
|
|
284
|
+
}
|
|
285
|
+
const obj = { type: "object", value: (0, import_security.createSafeObject)(), indent: 0 };
|
|
286
|
+
rootContainer = obj;
|
|
287
|
+
root = obj.value;
|
|
288
|
+
contexts.push(obj);
|
|
289
|
+
return obj;
|
|
290
|
+
};
|
|
291
|
+
if (!parent) {
|
|
292
|
+
if (key !== null) {
|
|
293
|
+
const target = ensureRootObject();
|
|
294
|
+
const segments = trySplitDottedKey(key, lineNo);
|
|
295
|
+
if (segments) {
|
|
296
|
+
assignExpandedPath(target.value, segments, value, lineNo);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (expandPaths === "safe" && Object.prototype.hasOwnProperty.call(target.value, key)) {
|
|
300
|
+
assignExpandedPath(target.value, [key], value, lineNo);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
target.value[key] = value;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (root !== null) {
|
|
307
|
+
throw new import_errors.ToonError("Multiple root values detected.", lineNo);
|
|
308
|
+
}
|
|
309
|
+
root = value;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (parent.type === "object") {
|
|
313
|
+
if (key === null) {
|
|
314
|
+
throw new import_errors.ToonError("Missing key for object assignment.");
|
|
315
|
+
}
|
|
316
|
+
const segments = trySplitDottedKey(key, lineNo);
|
|
317
|
+
if (segments) {
|
|
318
|
+
assignExpandedPath(parent.value, segments, value, lineNo);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (expandPaths === "safe" && Object.prototype.hasOwnProperty.call(parent.value, key)) {
|
|
322
|
+
assignExpandedPath(parent.value, [key], value, lineNo);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
parent.value[key] = value;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (parent.type === "list") {
|
|
329
|
+
parent.value.push(value);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (parent.type === "placeholder") {
|
|
333
|
+
if (parent.filled) {
|
|
334
|
+
throw new import_errors.ToonError("List item already filled.", lineNo);
|
|
335
|
+
}
|
|
336
|
+
parent.assign(value);
|
|
337
|
+
parent.filled = true;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
throw new import_errors.ToonError("Invalid parent container.");
|
|
341
|
+
};
|
|
342
|
+
const parseArrayHeader = (token, lineNo) => {
|
|
343
|
+
const match = token.match(/^\[(\d+)([,\|\t])?\](\{(.+)\})?$/);
|
|
344
|
+
if (!match) {
|
|
345
|
+
throw new import_errors.ToonError(`Invalid array header "${token}".`, lineNo);
|
|
346
|
+
}
|
|
347
|
+
const length = parseInt(match[1], 10);
|
|
348
|
+
if (!Number.isFinite(length)) {
|
|
349
|
+
throw new import_errors.ToonError("Invalid array length.", lineNo);
|
|
350
|
+
}
|
|
351
|
+
const delimiter = match[2] ?? delimiterFallback;
|
|
352
|
+
const fieldsRaw = match[4];
|
|
353
|
+
if (fieldsRaw === void 0) {
|
|
354
|
+
return { length, delimiter };
|
|
355
|
+
}
|
|
356
|
+
const fields = (0, import_primitives.splitDelimited)(fieldsRaw, delimiter, lineNo).map((f) => (0, import_primitives.decodeKey)(f, lineNo));
|
|
357
|
+
if (fields.length === 0 && strict) {
|
|
358
|
+
throw new import_errors.ToonError("Tabular arrays require at least one field.", lineNo);
|
|
359
|
+
}
|
|
360
|
+
return { length, delimiter, fields };
|
|
361
|
+
};
|
|
362
|
+
const processKeyValueLine = (indentLevel, keyToken, valueToken, lineNo, parent) => {
|
|
363
|
+
const { rawKey, header } = (0, import_primitives.splitKeyHeader)(keyToken);
|
|
364
|
+
const key = rawKey === "" ? null : (0, import_primitives.decodeKey)(rawKey, lineNo);
|
|
365
|
+
if (key !== null) {
|
|
366
|
+
(0, import_security.validateKeySafety)(key, limits, lineNo);
|
|
367
|
+
}
|
|
368
|
+
if (header) {
|
|
369
|
+
const { length, delimiter, fields } = parseArrayHeader(header, lineNo);
|
|
370
|
+
if (length > limits.maxArrayLength) {
|
|
371
|
+
throw new import_errors.ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
|
|
372
|
+
}
|
|
373
|
+
if (fields) {
|
|
374
|
+
if (valueToken !== "") {
|
|
375
|
+
throw new import_errors.ToonError("Tabular array header must not have inline values.", lineNo);
|
|
376
|
+
}
|
|
377
|
+
const arr = [];
|
|
378
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
379
|
+
attachValue(arr, parent, key, lineNo);
|
|
380
|
+
contexts.push({
|
|
381
|
+
type: "tabular",
|
|
382
|
+
value: arr,
|
|
383
|
+
indent: indentLevel + 1,
|
|
384
|
+
expectedLength: length,
|
|
385
|
+
delimiter,
|
|
386
|
+
fields
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (valueToken === "") {
|
|
391
|
+
const arr = [];
|
|
392
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
393
|
+
attachValue(arr, parent, key, lineNo);
|
|
394
|
+
contexts.push({
|
|
395
|
+
type: "list",
|
|
396
|
+
value: arr,
|
|
397
|
+
indent: indentLevel + 1,
|
|
398
|
+
expectedLength: length,
|
|
399
|
+
delimiter
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const values = (0, import_primitives.splitDelimited)(valueToken, delimiter, lineNo).map((t) => (0, import_primitives.parsePrimitiveToken)(t, delimiter, lineNo, strict));
|
|
404
|
+
if (strict && values.length !== length) {
|
|
405
|
+
throw new import_errors.ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
|
|
406
|
+
}
|
|
407
|
+
attachValue(values, parent, key, lineNo);
|
|
408
|
+
(0, import_security.bumpNodes)(state, limits, values.length, lineNo);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (valueToken === "") {
|
|
412
|
+
const obj = (0, import_security.createSafeObject)();
|
|
413
|
+
attachValue(obj, parent, key, lineNo);
|
|
414
|
+
contexts.push({ type: "object", value: obj, indent: indentLevel + 1 });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const value = (0, import_primitives.parsePrimitiveToken)(valueToken, delimiterFallback, lineNo, strict);
|
|
418
|
+
attachValue(value, parent, key, lineNo);
|
|
419
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
420
|
+
};
|
|
421
|
+
lines.forEach((rawLine, index) => {
|
|
422
|
+
const lineNo = index + 1;
|
|
423
|
+
const trimmedEnd = rawLine.replace(/[ \t]+$/, "");
|
|
424
|
+
if (trimmedEnd.trim() === "") {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const indentSpaces = (0, import_primitives.countLeadingSpaces)(trimmedEnd, lineNo);
|
|
428
|
+
if (indentStep === null) {
|
|
429
|
+
indentStep = indentSpaces === 0 ? 2 : indentSpaces;
|
|
430
|
+
}
|
|
431
|
+
if (indentSpaces % indentStep !== 0) {
|
|
432
|
+
throw new import_errors.ToonError(`Inconsistent indentation: expected multiples of ${indentStep} spaces.`, lineNo);
|
|
433
|
+
}
|
|
434
|
+
const indentLevel = indentSpaces / indentStep;
|
|
435
|
+
if (indentLevel > limits.maxDepth) {
|
|
436
|
+
throw new import_errors.ToonError(`Maximum depth ${limits.maxDepth} exceeded.`, lineNo);
|
|
437
|
+
}
|
|
438
|
+
const line = trimmedEnd.slice(indentSpaces);
|
|
439
|
+
while (true) {
|
|
440
|
+
const top = contexts[contexts.length - 1];
|
|
441
|
+
if (!top || indentLevel >= top.indent) {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
contexts.pop();
|
|
445
|
+
finalizeContainer(top, lineNo);
|
|
446
|
+
}
|
|
447
|
+
let handled = false;
|
|
448
|
+
let consumed = false;
|
|
449
|
+
while (!handled) {
|
|
450
|
+
const top = contexts[contexts.length - 1];
|
|
451
|
+
if (top && top.type === "tabular" && indentLevel === top.indent) {
|
|
452
|
+
const classification = (0, import_primitives.classifyTabularLine)(line, top.delimiter);
|
|
453
|
+
if (classification === "row") {
|
|
454
|
+
const cells = (0, import_primitives.splitDelimited)(line, top.delimiter, lineNo);
|
|
455
|
+
if (strict && cells.length !== top.fields.length) {
|
|
456
|
+
throw new import_errors.ToonError(
|
|
457
|
+
`Tabular row width mismatch: expected ${top.fields.length}, got ${cells.length}.`,
|
|
458
|
+
lineNo
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const obj = (0, import_security.createSafeObject)();
|
|
462
|
+
top.fields.forEach((field, idx) => {
|
|
463
|
+
(0, import_security.validateKeySafety)(field, limits, lineNo);
|
|
464
|
+
const token = cells[idx] ?? "";
|
|
465
|
+
obj[field] = (0, import_primitives.parsePrimitiveToken)(token, top.delimiter, lineNo, strict);
|
|
466
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
467
|
+
});
|
|
468
|
+
top.value.push(obj);
|
|
469
|
+
if (top.value.length > limits.maxArrayLength) {
|
|
470
|
+
throw new import_errors.ToonError(
|
|
471
|
+
`Tabular array length exceeds limit ${limits.maxArrayLength}.`,
|
|
472
|
+
lineNo
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
handled = true;
|
|
476
|
+
consumed = true;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
finalizeContainer(top, lineNo);
|
|
480
|
+
contexts.pop();
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
handled = true;
|
|
484
|
+
}
|
|
485
|
+
if (consumed) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const parent = contexts[contexts.length - 1];
|
|
489
|
+
if (parent && parent.type === "list") {
|
|
490
|
+
if (indentLevel !== parent.indent) {
|
|
491
|
+
throw new import_errors.ToonError("List items must align under their header.", lineNo);
|
|
492
|
+
}
|
|
493
|
+
parseListItem(line, parent, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (parent && parent.type === "placeholder") {
|
|
497
|
+
if (indentLevel !== parent.indent) {
|
|
498
|
+
throw new import_errors.ToonError('List item body must indent one level below "-".', lineNo);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (indentLevel !== (parent ? parent.indent : 0)) {
|
|
502
|
+
throw new import_errors.ToonError("Unexpected indentation level.", lineNo);
|
|
503
|
+
}
|
|
504
|
+
const colonIndex = (0, import_primitives.findUnquotedColon)(line);
|
|
505
|
+
if (colonIndex === -1) {
|
|
506
|
+
if (root === null && !parent) {
|
|
507
|
+
const value = (0, import_primitives.parsePrimitiveToken)(line.trim(), delimiterFallback, lineNo, strict);
|
|
508
|
+
root = value;
|
|
509
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
throw new import_errors.ToonError("Expected key-value pair.", lineNo);
|
|
513
|
+
}
|
|
514
|
+
const keyToken = line.slice(0, colonIndex).trim();
|
|
515
|
+
const valueToken = line.slice(colonIndex + 1).trim();
|
|
516
|
+
if (parent && parent.type === "placeholder") {
|
|
517
|
+
if (!keyToken.startsWith("[")) {
|
|
518
|
+
if (!parent.current) {
|
|
519
|
+
const obj = (0, import_security.createSafeObject)();
|
|
520
|
+
parent.assign(obj);
|
|
521
|
+
parent.current = { value: obj, indent: indentLevel + 1 };
|
|
522
|
+
parent.filled = true;
|
|
523
|
+
}
|
|
524
|
+
const objContext = { type: "object", value: parent.current.value, indent: indentLevel + 1 };
|
|
525
|
+
processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, objContext);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
processKeyValueLine(indentLevel, keyToken, valueToken, lineNo, parent);
|
|
530
|
+
});
|
|
531
|
+
while (contexts.length > 0) {
|
|
532
|
+
const container = contexts.pop();
|
|
533
|
+
finalizeContainer(container, lines.length);
|
|
534
|
+
}
|
|
535
|
+
if (root === null) {
|
|
536
|
+
return (0, import_security.createSafeObject)();
|
|
537
|
+
}
|
|
538
|
+
return root;
|
|
539
|
+
}
|
|
540
|
+
function parseListItem(line, list, indentLevel, lineNo, processKeyValueLine, contexts, state, limits, strict) {
|
|
541
|
+
const trimmed = line.trim();
|
|
542
|
+
if (!trimmed.startsWith("-")) {
|
|
543
|
+
throw new import_errors.ToonError('List items must start with "-".', lineNo);
|
|
544
|
+
}
|
|
545
|
+
const content = trimmed.slice(1).trim();
|
|
546
|
+
if (content === "") {
|
|
547
|
+
const placeholder = {
|
|
548
|
+
type: "placeholder",
|
|
549
|
+
indent: indentLevel + 1,
|
|
550
|
+
filled: false,
|
|
551
|
+
current: void 0,
|
|
552
|
+
assign: function(value2) {
|
|
553
|
+
if (this.current && typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && Object.keys(value2).length === 0) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
list.value.push(value2);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
contexts.push(placeholder);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (content.startsWith("[")) {
|
|
563
|
+
const colonIndex2 = (0, import_primitives.findUnquotedColon)(content);
|
|
564
|
+
const headerToken = colonIndex2 === -1 ? content : content.slice(0, colonIndex2).trim();
|
|
565
|
+
const valueToken = colonIndex2 === -1 ? "" : content.slice(colonIndex2 + 1).trim();
|
|
566
|
+
const { length, delimiter, fields } = (0, import_primitives.parseArrayHeaderFromList)(headerToken, lineNo);
|
|
567
|
+
if (length > limits.maxArrayLength) {
|
|
568
|
+
throw new import_errors.ToonError(`Array length ${length} exceeds limit ${limits.maxArrayLength}.`, lineNo);
|
|
569
|
+
}
|
|
570
|
+
if (fields) {
|
|
571
|
+
if (valueToken !== "") {
|
|
572
|
+
throw new import_errors.ToonError("Tabular header in list item cannot have inline values.", lineNo);
|
|
573
|
+
}
|
|
574
|
+
const arr = [];
|
|
575
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
576
|
+
list.value.push(arr);
|
|
577
|
+
contexts.push({
|
|
578
|
+
type: "tabular",
|
|
579
|
+
value: arr,
|
|
580
|
+
indent: indentLevel + 1,
|
|
581
|
+
expectedLength: length,
|
|
582
|
+
delimiter,
|
|
583
|
+
fields
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (valueToken === "") {
|
|
588
|
+
const arr = [];
|
|
589
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
590
|
+
list.value.push(arr);
|
|
591
|
+
contexts.push({
|
|
592
|
+
type: "list",
|
|
593
|
+
value: arr,
|
|
594
|
+
indent: indentLevel + 1,
|
|
595
|
+
expectedLength: length,
|
|
596
|
+
delimiter
|
|
597
|
+
});
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const values = (0, import_primitives.splitDelimited)(valueToken, delimiter, lineNo).map((t) => (0, import_primitives.parsePrimitiveToken)(t, delimiter, lineNo, strict));
|
|
601
|
+
if (strict && values.length !== length) {
|
|
602
|
+
throw new import_errors.ToonError(`Inline array length mismatch: expected ${length}, got ${values.length}.`, lineNo);
|
|
603
|
+
}
|
|
604
|
+
(0, import_security.bumpNodes)(state, limits, values.length, lineNo);
|
|
605
|
+
list.value.push(values);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const colonIndex = (0, import_primitives.findUnquotedColon)(content);
|
|
609
|
+
if (colonIndex !== -1) {
|
|
610
|
+
const keyToken = content.slice(0, colonIndex).trim();
|
|
611
|
+
const valueToken = content.slice(colonIndex + 1).trim();
|
|
612
|
+
const obj = (0, import_security.createSafeObject)();
|
|
613
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
614
|
+
list.value.push(obj);
|
|
615
|
+
const objContext = { type: "object", value: obj, indent: indentLevel + 1 };
|
|
616
|
+
contexts.push(objContext);
|
|
617
|
+
processKeyValueLine(indentLevel + 1, keyToken, valueToken, lineNo, objContext);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const value = (0, import_primitives.parsePrimitiveToken)(content, list.delimiter, lineNo, strict);
|
|
621
|
+
(0, import_security.bumpNodes)(state, limits, 1, lineNo);
|
|
622
|
+
list.value.push(value);
|
|
623
|
+
}
|
|
624
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
625
|
+
0 && (module.exports = {
|
|
626
|
+
ToonError,
|
|
627
|
+
enforceInputLength,
|
|
628
|
+
jsonToToon,
|
|
629
|
+
toonToJson
|
|
630
|
+
});
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type { JsonPrimitive } from './internal/types.js';
|
|
2
|
+
export { ToonError } from './internal/errors.js';
|
|
3
|
+
export { enforceInputLength } from './internal/security.js';
|
|
4
|
+
export interface SecurityOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Maximum nesting depth (objects + arrays). Defaults to 64.
|
|
7
|
+
*/
|
|
8
|
+
maxDepth?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum allowed array length. Defaults to 50_000.
|
|
11
|
+
*/
|
|
12
|
+
maxArrayLength?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum total nodes (object fields + array items) processed.
|
|
15
|
+
* Defaults to 250_000 to limit resource exhaustion.
|
|
16
|
+
*/
|
|
17
|
+
maxTotalNodes?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Keys that are rejected to avoid prototype pollution.
|
|
20
|
+
* Defaults to ["__proto__", "constructor", "prototype"].
|
|
21
|
+
*/
|
|
22
|
+
disallowedKeys?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Maximum allowed length of a raw input string passed to a `*ToToon` /
|
|
25
|
+
* `*ToJson` decoder. Defaults to 5_000_000 (5 MB).
|
|
26
|
+
* Set to `Infinity` to disable.
|
|
27
|
+
*/
|
|
28
|
+
maxInputLength?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface JsonToToonOptions extends SecurityOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Number of spaces per indentation level. Defaults to 2.
|
|
33
|
+
*/
|
|
34
|
+
indent?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Delimiter to use for inline arrays and tabular rows.
|
|
37
|
+
* Defaults to comma.
|
|
38
|
+
*/
|
|
39
|
+
delimiter?: ',' | '|' | '\t';
|
|
40
|
+
/**
|
|
41
|
+
* When true, object keys are sorted alphabetically to keep output deterministic.
|
|
42
|
+
* Defaults to false (preserve encounter order).
|
|
43
|
+
*/
|
|
44
|
+
sortKeys?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* TOON v3 §13.4 key folding. When `'safe'`, single-key object chains are folded
|
|
47
|
+
* into dotted paths (e.g. `{a:{b:{c:1}}}` -> `a.b.c: 1`) provided every segment
|
|
48
|
+
* matches the IdentifierSegment grammar. Defaults to `'off'`.
|
|
49
|
+
*/
|
|
50
|
+
keyFolding?: 'off' | 'safe';
|
|
51
|
+
/**
|
|
52
|
+
* Maximum number of segments included in a folded path (counting from the
|
|
53
|
+
* outermost key). Has no practical effect below 2. Defaults to Infinity when
|
|
54
|
+
* `keyFolding` is `'safe'`.
|
|
55
|
+
*/
|
|
56
|
+
flattenDepth?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ToonToJsonOptions extends SecurityOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Enforce declared array lengths, field counts, and indentation consistency.
|
|
61
|
+
* Defaults to true.
|
|
62
|
+
*/
|
|
63
|
+
strict?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* TOON v3 §13.4 path expansion. When `'safe'`, dotted keys are expanded into
|
|
66
|
+
* nested objects (e.g. `a.b.c: 1` -> `{a:{b:{c:1}}}`) provided every segment
|
|
67
|
+
* matches the IdentifierSegment grammar. Defaults to `'off'`.
|
|
68
|
+
*/
|
|
69
|
+
expandPaths?: 'off' | 'safe';
|
|
70
|
+
}
|
|
71
|
+
export declare function jsonToToon(value: unknown, options?: JsonToToonOptions): string;
|
|
72
|
+
export declare function toonToJson(text: string, options?: ToonToJsonOptions): unknown;
|
|
73
|
+
//# sourceMappingURL=core.d.ts.map
|