scon-notation 1.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 -0
- package/README.md +91 -0
- package/package.json +45 -0
- package/src/decoder.js +761 -0
- package/src/encoder.js +506 -0
- package/src/minifier.js +143 -0
- package/src/schema-registry.js +204 -0
- package/src/scon.js +121 -0
- package/src/tree-hash.js +227 -0
- package/src/validator.js +131 -0
package/src/decoder.js
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
// src/decoder.js
|
|
2
|
+
// Port of bX\Scon\Decoder — SCON string → JS object
|
|
3
|
+
|
|
4
|
+
import { SchemaRegistry } from './schema-registry.js';
|
|
5
|
+
import { Minifier } from './minifier.js';
|
|
6
|
+
|
|
7
|
+
const COMMA = ',';
|
|
8
|
+
const TAB = '\t';
|
|
9
|
+
const PIPE = '|';
|
|
10
|
+
const DQUOTE = '"';
|
|
11
|
+
const COLON = ':';
|
|
12
|
+
const OBRACK = '[';
|
|
13
|
+
const CBRACK = ']';
|
|
14
|
+
const OBRACE = '{';
|
|
15
|
+
const CBRACE = '}';
|
|
16
|
+
const LIST_PREFIX = '- ';
|
|
17
|
+
const BACKSLASH = '\\';
|
|
18
|
+
const SEMICOLON = ';';
|
|
19
|
+
|
|
20
|
+
export class Decoder {
|
|
21
|
+
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
// Si indent no se provee, auto-detectar del documento
|
|
24
|
+
this._indentAutoDetect = !('indent' in options);
|
|
25
|
+
this.indent = options.indent ?? 1;
|
|
26
|
+
this.strict = options.strict ?? true;
|
|
27
|
+
this.registry = new SchemaRegistry();
|
|
28
|
+
this.directives = {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getRegistry() { return this.registry; }
|
|
32
|
+
getDirectives() { return this.directives; }
|
|
33
|
+
|
|
34
|
+
// Decode SCON string to JS object/array
|
|
35
|
+
decode(sconString) {
|
|
36
|
+
// Expand if minified
|
|
37
|
+
if (this._isMinified(sconString)) {
|
|
38
|
+
sconString = Minifier.expand(sconString, this.indent);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Auto-detect indent: primera línea que empiece con espacios
|
|
42
|
+
if (this._indentAutoDetect) {
|
|
43
|
+
const m = sconString.match(/\n( +)\S/);
|
|
44
|
+
if (m) this.indent = m[1].length;
|
|
45
|
+
this._indentAutoDetect = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lines = sconString.split('\n');
|
|
49
|
+
const parsedLines = [];
|
|
50
|
+
|
|
51
|
+
// First pass: extract header, directives, and definitions
|
|
52
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
53
|
+
const line = lines[lineNum];
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
|
|
56
|
+
// Skip empty lines and comments
|
|
57
|
+
if (trimmed === '' || trimmed[0] === '#') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Directives (@@)
|
|
62
|
+
if (trimmed.startsWith('@@')) {
|
|
63
|
+
this._parseDirective(trimmed);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Schema definition (s:name ...)
|
|
68
|
+
let match = trimmed.match(/^s:(\S+)\s+/);
|
|
69
|
+
if (match) {
|
|
70
|
+
const name = match[1];
|
|
71
|
+
const defStr = trimmed.slice(match[0].length);
|
|
72
|
+
const def = this._parseInlineValue(defStr);
|
|
73
|
+
this.registry.register('s', name, (typeof def === 'object' && def !== null) ? def : {});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Response group definition (r:name ...)
|
|
78
|
+
match = trimmed.match(/^r:(\S+)\s+/);
|
|
79
|
+
if (match) {
|
|
80
|
+
const name = match[1];
|
|
81
|
+
const defStr = trimmed.slice(match[0].length);
|
|
82
|
+
const def = this._parseResponseGroup(defStr);
|
|
83
|
+
this.registry.register('r', name, def);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Security group definition (sec:name ...)
|
|
88
|
+
match = trimmed.match(/^sec:(\S+)\s+/);
|
|
89
|
+
if (match) {
|
|
90
|
+
const name = match[1];
|
|
91
|
+
const defStr = trimmed.slice(match[0].length);
|
|
92
|
+
const def = this._parseInlineValue(defStr);
|
|
93
|
+
this.registry.register('sec', name, (typeof def === 'object' && def !== null) ? def : {});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// @use import
|
|
98
|
+
if (trimmed.startsWith('@use ')) {
|
|
99
|
+
if (!this.directives.imports) this.directives.imports = [];
|
|
100
|
+
this.directives.imports.push(trimmed);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Body line
|
|
105
|
+
const depth = this._calculateDepth(line);
|
|
106
|
+
parsedLines.push({ depth, content: line.trimStart(), lineNum });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (parsedLines.length === 0) return [];
|
|
110
|
+
|
|
111
|
+
// Second pass: parse body with ref resolution
|
|
112
|
+
const first = parsedLines[0];
|
|
113
|
+
|
|
114
|
+
if (this._isArrayHeader(first.content)) {
|
|
115
|
+
const header = this._parseArrayHeader(first.content);
|
|
116
|
+
if (header.key === null) {
|
|
117
|
+
return this._decodeArrayFromHeader(0, parsedLines);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Explicit empty object marker
|
|
122
|
+
if (parsedLines.length === 1 && first.content === '{}') {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsedLines.length === 1 && !this._isKeyValueLine(first.content)) {
|
|
127
|
+
const val = this._parsePrimitive(first.content);
|
|
128
|
+
return Array.isArray(val) ? val : [val];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this._decodeObject(0, parsedLines, 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Directive parsing ---
|
|
135
|
+
|
|
136
|
+
_parseDirective(line) {
|
|
137
|
+
const directive = line.slice(2);
|
|
138
|
+
if (directive.startsWith('enforce(') && directive.endsWith(')')) {
|
|
139
|
+
this.directives.enforce = directive.slice(8, -1);
|
|
140
|
+
} else {
|
|
141
|
+
this.directives.mode = directive;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Response group parsing ---
|
|
146
|
+
|
|
147
|
+
_parseResponseGroup(input) {
|
|
148
|
+
input = input.trim();
|
|
149
|
+
if (input[0] !== OBRACE) return {};
|
|
150
|
+
|
|
151
|
+
const inner = this._extractBraceContent(input);
|
|
152
|
+
const result = {};
|
|
153
|
+
const parts = this._splitTopLevel(inner, COMMA);
|
|
154
|
+
|
|
155
|
+
for (let part of parts) {
|
|
156
|
+
part = part.trim();
|
|
157
|
+
const m = part.match(/^(\d+):("(?:[^"\\]|\\.)*")\s*(?:@s:(\S+))?\s*(.*)$/);
|
|
158
|
+
if (m) {
|
|
159
|
+
const code = m[1];
|
|
160
|
+
const desc = this._parseStringLiteral(m[2]);
|
|
161
|
+
const entry = { description: desc };
|
|
162
|
+
if (m[3]) entry.schemaRef = m[3];
|
|
163
|
+
if (m[4]) {
|
|
164
|
+
const overridesStr = m[4].trim();
|
|
165
|
+
if (overridesStr !== '' && overridesStr[0] === OBRACE) {
|
|
166
|
+
entry.overrides = this._parseInlineValue(overridesStr);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
result[code] = entry;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Inline value parsing ---
|
|
177
|
+
|
|
178
|
+
_parseInlineValue(input) {
|
|
179
|
+
input = input.trim();
|
|
180
|
+
if (input === '') return '';
|
|
181
|
+
|
|
182
|
+
// Object
|
|
183
|
+
if (input[0] === OBRACE) {
|
|
184
|
+
const inner = this._extractBraceContent(input);
|
|
185
|
+
return this._parseInlineObject(inner);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Array
|
|
189
|
+
if (input[0] === OBRACK) {
|
|
190
|
+
const close = this._findMatchingBracket(input, 0);
|
|
191
|
+
if (close !== -1) {
|
|
192
|
+
const inner = input.slice(1, close);
|
|
193
|
+
const items = this._splitTopLevel(inner, COMMA);
|
|
194
|
+
return items.map(i => this._parseInlineValue(i.trim()));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Reference
|
|
199
|
+
if (input.startsWith('@s:') || input.startsWith('@r:') || input.startsWith('@sec:')) {
|
|
200
|
+
return this._resolveReference(input);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return this._parsePrimitive(input);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_parseInlineObject(inner) {
|
|
207
|
+
const result = {};
|
|
208
|
+
const parts = this._splitTopLevel(inner, COMMA);
|
|
209
|
+
|
|
210
|
+
for (let part of parts) {
|
|
211
|
+
part = part.trim();
|
|
212
|
+
if (part === '') continue;
|
|
213
|
+
|
|
214
|
+
const colonPos = this._findKeyColon(part);
|
|
215
|
+
if (colonPos === -1) continue;
|
|
216
|
+
|
|
217
|
+
let key = part.slice(0, colonPos).trim();
|
|
218
|
+
const val = part.slice(colonPos + 1).trim();
|
|
219
|
+
|
|
220
|
+
key = this._parseStringLiteral(key);
|
|
221
|
+
|
|
222
|
+
if (key.includes('.')) {
|
|
223
|
+
this._setDotPath(result, key, this._parseInlineValue(val));
|
|
224
|
+
} else {
|
|
225
|
+
result[key] = this._parseInlineValue(val);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Reference resolution ---
|
|
233
|
+
|
|
234
|
+
_resolveReference(refStr) {
|
|
235
|
+
// Polymorphic: @s:a | @s:b
|
|
236
|
+
if (refStr.includes(' | ')) {
|
|
237
|
+
const refs = [];
|
|
238
|
+
for (let r of refStr.split(' | ')) {
|
|
239
|
+
r = r.trim();
|
|
240
|
+
const m = r.match(/^@(s|r|sec):(\S+)/);
|
|
241
|
+
if (m) refs.push({ type: m[1], name: m[2] });
|
|
242
|
+
}
|
|
243
|
+
return this.registry.resolvePolymorphic(refs);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const m = refStr.match(/^@(s|r|sec):(\S+)\s*(.*)$/);
|
|
247
|
+
if (m) {
|
|
248
|
+
const type = m[1];
|
|
249
|
+
const name = m[2];
|
|
250
|
+
const rest = (m[3] || '').trim();
|
|
251
|
+
|
|
252
|
+
if (rest !== '' && rest[0] === OBRACE) {
|
|
253
|
+
const overrides = this._parseInlineValue(rest);
|
|
254
|
+
return this.registry.resolveWithOverride(type, name, typeof overrides === 'object' && overrides !== null ? overrides : {});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return this.registry.resolve(type, name);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return refStr;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- Minification detection ---
|
|
264
|
+
|
|
265
|
+
_isMinified(str) {
|
|
266
|
+
return !str.includes('\n') && str.includes(SEMICOLON);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- Body parsing ---
|
|
270
|
+
|
|
271
|
+
_calculateDepth(line) {
|
|
272
|
+
let spaces = 0;
|
|
273
|
+
for (let i = 0; i < line.length; i++) {
|
|
274
|
+
if (line[i] === ' ') spaces++;
|
|
275
|
+
else if (line[i] === '\t') throw new Error('Tabs not allowed for indentation');
|
|
276
|
+
else break;
|
|
277
|
+
}
|
|
278
|
+
if (this.indent > 0 && spaces % this.indent !== 0) {
|
|
279
|
+
throw new Error(`Invalid indentation: ${spaces} spaces (indent=${this.indent})`);
|
|
280
|
+
}
|
|
281
|
+
return this.indent > 0 ? spaces / this.indent : 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_decodeObject(baseDepth, parsedLines, startIndex) {
|
|
285
|
+
const result = {};
|
|
286
|
+
let i = startIndex;
|
|
287
|
+
|
|
288
|
+
while (i < parsedLines.length) {
|
|
289
|
+
const line = parsedLines[i];
|
|
290
|
+
if (line.depth < baseDepth) break;
|
|
291
|
+
if (line.depth > baseDepth) { i++; continue; }
|
|
292
|
+
|
|
293
|
+
const content = line.content;
|
|
294
|
+
|
|
295
|
+
// Array header
|
|
296
|
+
if (this._isArrayHeader(content)) {
|
|
297
|
+
const header = this._parseArrayHeader(content);
|
|
298
|
+
if (header.key !== null) {
|
|
299
|
+
result[header.key] = this._decodeArrayFromHeader(i, parsedLines);
|
|
300
|
+
i++;
|
|
301
|
+
while (i < parsedLines.length && parsedLines[i].depth > baseDepth) i++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Key-value
|
|
307
|
+
if (this._isKeyValueLine(content)) {
|
|
308
|
+
const [key, value, nextIndex] = this._decodeKeyValue(line, parsedLines, i, baseDepth);
|
|
309
|
+
result[key] = value;
|
|
310
|
+
i = nextIndex;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
i++;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_decodeKeyValue(line, parsedLines, index, baseDepth) {
|
|
321
|
+
const content = line.content;
|
|
322
|
+
const keyData = this._parseKey(content);
|
|
323
|
+
const key = keyData.key;
|
|
324
|
+
const rest = content.slice(keyData.end).trim();
|
|
325
|
+
|
|
326
|
+
// Reference value
|
|
327
|
+
if (rest !== '' && rest.startsWith('@')) {
|
|
328
|
+
return [key, this._resolveReference(rest), index + 1];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (rest !== '') {
|
|
332
|
+
return [key, this._parsePrimitive(rest), index + 1];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Nested object
|
|
336
|
+
if (index + 1 < parsedLines.length && parsedLines[index + 1].depth > baseDepth) {
|
|
337
|
+
const value = this._decodeObject(baseDepth + 1, parsedLines, index + 1);
|
|
338
|
+
let nextIndex = index + 1;
|
|
339
|
+
while (nextIndex < parsedLines.length && parsedLines[nextIndex].depth > baseDepth) {
|
|
340
|
+
nextIndex++;
|
|
341
|
+
}
|
|
342
|
+
return [key, value, nextIndex];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return [key, [], index + 1];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_decodeArrayFromHeader(index, parsedLines) {
|
|
349
|
+
const line = parsedLines[index];
|
|
350
|
+
const header = this._parseArrayHeader(line.content);
|
|
351
|
+
const baseDepth = line.depth;
|
|
352
|
+
|
|
353
|
+
if (header.length === 0) return [];
|
|
354
|
+
|
|
355
|
+
if (header.inlineValues !== null && header.fields === null) {
|
|
356
|
+
return this._parseDelimitedValues(header.inlineValues, header.delimiter);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (header.fields !== null) {
|
|
360
|
+
return this._decodeTabularArray(index, parsedLines, baseDepth, header.length, header.fields, header.delimiter);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return this._decodeExpandedArray(index, parsedLines, baseDepth, header.length);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_decodeTabularArray(headerIdx, parsedLines, baseDepth, expected, fields, delim) {
|
|
367
|
+
const result = [];
|
|
368
|
+
let i = headerIdx + 1;
|
|
369
|
+
|
|
370
|
+
while (i < parsedLines.length && result.length < expected) {
|
|
371
|
+
if (parsedLines[i].depth !== baseDepth + 1) break;
|
|
372
|
+
const values = this._parseDelimitedValues(parsedLines[i].content, delim);
|
|
373
|
+
const row = {};
|
|
374
|
+
for (let j = 0; j < fields.length; j++) {
|
|
375
|
+
row[fields[j]] = values[j] ?? null;
|
|
376
|
+
}
|
|
377
|
+
result.push(row);
|
|
378
|
+
i++;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_decodeExpandedArray(headerIdx, parsedLines, baseDepth, expected) {
|
|
385
|
+
const result = [];
|
|
386
|
+
let i = headerIdx + 1;
|
|
387
|
+
|
|
388
|
+
while (i < parsedLines.length && result.length < expected) {
|
|
389
|
+
const line = parsedLines[i];
|
|
390
|
+
if (line.depth !== baseDepth + 1) break;
|
|
391
|
+
|
|
392
|
+
if (line.content.startsWith(LIST_PREFIX)) {
|
|
393
|
+
const itemContent = line.content.slice(LIST_PREFIX.length);
|
|
394
|
+
|
|
395
|
+
// Schema/response/security reference as list item
|
|
396
|
+
if (itemContent.startsWith('@s:') || itemContent.startsWith('@r:') || itemContent.startsWith('@sec:')) {
|
|
397
|
+
result.push(this._resolveReference(itemContent));
|
|
398
|
+
i++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (this._isKeyValueLine(itemContent)) {
|
|
403
|
+
const obj = this._decodeListItemObject(line, parsedLines, i, baseDepth);
|
|
404
|
+
result.push(obj);
|
|
405
|
+
i++;
|
|
406
|
+
while (i < parsedLines.length && parsedLines[i].depth > baseDepth + 1) i++;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (this._isArrayHeader(itemContent)) {
|
|
411
|
+
const itemHeader = this._parseArrayHeader(itemContent);
|
|
412
|
+
if (itemHeader.inlineValues !== null) {
|
|
413
|
+
result.push(this._parseDelimitedValues(itemHeader.inlineValues, itemHeader.delimiter));
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
result.push(this._parsePrimitive(itemContent));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
i++;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
_decodeListItemObject(line, parsedLines, index, baseDepth) {
|
|
426
|
+
const itemContent = line.content.slice(LIST_PREFIX.length);
|
|
427
|
+
const keyData = this._parseKey(itemContent);
|
|
428
|
+
const key = keyData.key;
|
|
429
|
+
const rest = itemContent.slice(keyData.end).trim();
|
|
430
|
+
|
|
431
|
+
const result = {};
|
|
432
|
+
const contDepth = baseDepth + 2;
|
|
433
|
+
|
|
434
|
+
if (rest !== '' && rest.startsWith('@')) {
|
|
435
|
+
result[key] = this._resolveReference(rest);
|
|
436
|
+
} else if (rest !== '') {
|
|
437
|
+
result[key] = this._parsePrimitive(rest);
|
|
438
|
+
} else if (index + 1 < parsedLines.length && parsedLines[index + 1].depth >= contDepth) {
|
|
439
|
+
result[key] = this._decodeObject(contDepth, parsedLines, index + 1);
|
|
440
|
+
} else {
|
|
441
|
+
result[key] = [];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Parse continuation fields
|
|
445
|
+
let i = index + 1;
|
|
446
|
+
while (i < parsedLines.length) {
|
|
447
|
+
const nextLine = parsedLines[i];
|
|
448
|
+
if (nextLine.depth < contDepth) break;
|
|
449
|
+
if (nextLine.depth === contDepth) {
|
|
450
|
+
if (nextLine.content.startsWith(LIST_PREFIX)) break;
|
|
451
|
+
|
|
452
|
+
// Array header in continuation
|
|
453
|
+
if (this._isArrayHeader(nextLine.content)) {
|
|
454
|
+
const header = this._parseArrayHeader(nextLine.content);
|
|
455
|
+
if (header.key !== null) {
|
|
456
|
+
result[header.key] = this._decodeArrayFromHeader(i, parsedLines);
|
|
457
|
+
i++;
|
|
458
|
+
while (i < parsedLines.length && parsedLines[i].depth > contDepth) i++;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (this._isKeyValueLine(nextLine.content)) {
|
|
463
|
+
const [k, v, nextIdx] = this._decodeKeyValue(nextLine, parsedLines, i, contDepth);
|
|
464
|
+
result[k] = v;
|
|
465
|
+
i = nextIdx;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
i++;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// --- Parsing helpers ---
|
|
476
|
+
|
|
477
|
+
_parseArrayHeader(content) {
|
|
478
|
+
let key = null;
|
|
479
|
+
const bracketStart = content.indexOf(OBRACK);
|
|
480
|
+
|
|
481
|
+
if (bracketStart > 0) {
|
|
482
|
+
const rawKey = content.slice(0, bracketStart).trim();
|
|
483
|
+
key = this._parseStringLiteral(rawKey);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const bracketEnd = content.indexOf(CBRACK, bracketStart);
|
|
487
|
+
if (bracketEnd === -1) throw new Error('Invalid array header: missing ]');
|
|
488
|
+
|
|
489
|
+
let bracketContent = content.slice(bracketStart + 1, bracketEnd);
|
|
490
|
+
|
|
491
|
+
let delimiter = COMMA;
|
|
492
|
+
if (bracketContent.endsWith(TAB)) {
|
|
493
|
+
delimiter = TAB;
|
|
494
|
+
bracketContent = bracketContent.slice(0, -1);
|
|
495
|
+
} else if (bracketContent.endsWith(PIPE)) {
|
|
496
|
+
delimiter = PIPE;
|
|
497
|
+
bracketContent = bracketContent.slice(0, -1);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const length = parseInt(bracketContent, 10);
|
|
501
|
+
let fields = null;
|
|
502
|
+
let braceStart = content.indexOf(OBRACE, bracketEnd);
|
|
503
|
+
let colonIndex = content.indexOf(COLON, bracketEnd);
|
|
504
|
+
|
|
505
|
+
if (braceStart !== -1 && (colonIndex === -1 || braceStart < colonIndex)) {
|
|
506
|
+
const braceEnd = content.indexOf(CBRACE, braceStart);
|
|
507
|
+
if (braceEnd !== -1) {
|
|
508
|
+
const fieldsContent = content.slice(braceStart + 1, braceEnd);
|
|
509
|
+
fields = this._parseDelimitedValues(fieldsContent, delimiter);
|
|
510
|
+
colonIndex = content.indexOf(COLON, braceEnd);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let inlineValues = null;
|
|
515
|
+
if (colonIndex !== -1) {
|
|
516
|
+
const afterColon = content.slice(colonIndex + 1).trim();
|
|
517
|
+
if (afterColon !== '') {
|
|
518
|
+
inlineValues = afterColon;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return { key, length, delimiter, fields, inlineValues };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
_parseDelimitedValues(input, delimiter) {
|
|
526
|
+
const values = [];
|
|
527
|
+
let buffer = '';
|
|
528
|
+
let inQuotes = false;
|
|
529
|
+
let braceDepth = 0;
|
|
530
|
+
|
|
531
|
+
for (let i = 0; i < input.length; i++) {
|
|
532
|
+
const char = input[i];
|
|
533
|
+
|
|
534
|
+
if (char === BACKSLASH && inQuotes && i + 1 < input.length) {
|
|
535
|
+
buffer += char + input[i + 1];
|
|
536
|
+
i++;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (char === DQUOTE) {
|
|
541
|
+
inQuotes = !inQuotes;
|
|
542
|
+
buffer += char;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!inQuotes) {
|
|
547
|
+
if (char === OBRACE) braceDepth++;
|
|
548
|
+
if (char === CBRACE) braceDepth--;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (char === delimiter && !inQuotes && braceDepth === 0) {
|
|
552
|
+
values.push(this._parsePrimitive(buffer.trim()));
|
|
553
|
+
buffer = '';
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
buffer += char;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (buffer !== '' || values.length > 0) {
|
|
561
|
+
values.push(this._parsePrimitive(buffer.trim()));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return values;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
_parsePrimitive(token) {
|
|
568
|
+
const trimmed = token.trim();
|
|
569
|
+
if (trimmed === '') return '';
|
|
570
|
+
if (trimmed === '[]') return [];
|
|
571
|
+
|
|
572
|
+
if (trimmed[0] === DQUOTE) {
|
|
573
|
+
return this._parseStringLiteral(trimmed);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (trimmed === 'true') return true;
|
|
577
|
+
if (trimmed === 'false') return false;
|
|
578
|
+
if (trimmed === 'null') return null;
|
|
579
|
+
|
|
580
|
+
// Strict decimal regex matching PHP is_numeric (no hex, no binary, no Infinity)
|
|
581
|
+
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(trimmed)) {
|
|
582
|
+
if (trimmed.includes('.') || trimmed.includes('e') || trimmed.includes('E')) {
|
|
583
|
+
return parseFloat(trimmed);
|
|
584
|
+
}
|
|
585
|
+
return parseInt(trimmed, 10);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return trimmed;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
_parseStringLiteral(token) {
|
|
592
|
+
const trimmed = token.trim();
|
|
593
|
+
if (trimmed === '' || trimmed[0] !== DQUOTE) return trimmed;
|
|
594
|
+
|
|
595
|
+
const closingQuote = this._findClosingQuote(trimmed, 0);
|
|
596
|
+
if (closingQuote === -1) throw new Error('Unterminated string');
|
|
597
|
+
|
|
598
|
+
return this._unescapeString(trimmed.slice(1, closingQuote));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
_findClosingQuote(str, start) {
|
|
602
|
+
let i = start + 1;
|
|
603
|
+
while (i < str.length) {
|
|
604
|
+
if (str[i] === BACKSLASH && i + 1 < str.length) { i += 2; continue; }
|
|
605
|
+
if (str[i] === DQUOTE) return i;
|
|
606
|
+
i++;
|
|
607
|
+
}
|
|
608
|
+
return -1;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
_unescapeString(str) {
|
|
612
|
+
let result = str.replace(/\\\\/g, '\x00BACKSLASH\x00');
|
|
613
|
+
result = result.replace(/\\"/g, '"');
|
|
614
|
+
result = result.replace(/\\n/g, '\n');
|
|
615
|
+
result = result.replace(/\\r/g, '\r');
|
|
616
|
+
result = result.replace(/\\t/g, '\t');
|
|
617
|
+
result = result.replace(/\\;/g, ';');
|
|
618
|
+
result = result.replace(/\x00BACKSLASH\x00/g, '\\');
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
_parseKey(content) {
|
|
623
|
+
if (content[0] === DQUOTE) {
|
|
624
|
+
const closingQuote = this._findClosingQuote(content, 0);
|
|
625
|
+
if (closingQuote === -1) throw new Error('Unterminated quoted key');
|
|
626
|
+
const key = this._unescapeString(content.slice(1, closingQuote));
|
|
627
|
+
const end = closingQuote + 1;
|
|
628
|
+
if (end >= content.length || content[end] !== COLON) {
|
|
629
|
+
throw new Error('Missing colon after key');
|
|
630
|
+
}
|
|
631
|
+
return { key, end: end + 1 };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const colonPos = content.indexOf(COLON);
|
|
635
|
+
if (colonPos === -1) throw new Error('Missing colon after key');
|
|
636
|
+
|
|
637
|
+
return { key: content.slice(0, colonPos).trim(), end: colonPos + 1 };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
_findKeyColon(str) {
|
|
641
|
+
let inQuotes = false;
|
|
642
|
+
let braceDepth = 0;
|
|
643
|
+
|
|
644
|
+
for (let i = 0; i < str.length; i++) {
|
|
645
|
+
const char = str[i];
|
|
646
|
+
if (char === BACKSLASH && inQuotes && i + 1 < str.length) { i++; continue; }
|
|
647
|
+
if (char === DQUOTE) { inQuotes = !inQuotes; continue; }
|
|
648
|
+
if (!inQuotes) {
|
|
649
|
+
if (char === OBRACE) braceDepth++;
|
|
650
|
+
if (char === CBRACE) braceDepth--;
|
|
651
|
+
if (char === COLON && braceDepth === 0) return i;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return -1;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
_splitTopLevel(input, delimiter) {
|
|
659
|
+
const parts = [];
|
|
660
|
+
let buffer = '';
|
|
661
|
+
let inQuotes = false;
|
|
662
|
+
let braceDepth = 0;
|
|
663
|
+
let bracketDepth = 0;
|
|
664
|
+
|
|
665
|
+
for (let i = 0; i < input.length; i++) {
|
|
666
|
+
const char = input[i];
|
|
667
|
+
if (char === BACKSLASH && inQuotes && i + 1 < input.length) {
|
|
668
|
+
buffer += char + input[i + 1];
|
|
669
|
+
i++;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (char === DQUOTE) inQuotes = !inQuotes;
|
|
673
|
+
if (!inQuotes) {
|
|
674
|
+
if (char === OBRACE) braceDepth++;
|
|
675
|
+
if (char === CBRACE) braceDepth--;
|
|
676
|
+
if (char === OBRACK) bracketDepth++;
|
|
677
|
+
if (char === CBRACK) bracketDepth--;
|
|
678
|
+
}
|
|
679
|
+
if (char === delimiter && !inQuotes && braceDepth === 0 && bracketDepth === 0) {
|
|
680
|
+
parts.push(buffer);
|
|
681
|
+
buffer = '';
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
buffer += char;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (buffer !== '') parts.push(buffer);
|
|
688
|
+
return parts;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
_findMatchingBracket(str, start) {
|
|
692
|
+
let depth = 0;
|
|
693
|
+
let inQuotes = false;
|
|
694
|
+
|
|
695
|
+
for (let i = start; i < str.length; i++) {
|
|
696
|
+
const char = str[i];
|
|
697
|
+
if (char === BACKSLASH && inQuotes && i + 1 < str.length) { i++; continue; }
|
|
698
|
+
if (char === DQUOTE) { inQuotes = !inQuotes; continue; }
|
|
699
|
+
if (!inQuotes) {
|
|
700
|
+
if (char === OBRACK) depth++;
|
|
701
|
+
if (char === CBRACK) {
|
|
702
|
+
depth--;
|
|
703
|
+
if (depth === 0) return i;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return -1;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
_extractBraceContent(input) {
|
|
712
|
+
let depth = 0;
|
|
713
|
+
let start = -1;
|
|
714
|
+
let inQuotes = false;
|
|
715
|
+
|
|
716
|
+
for (let i = 0; i < input.length; i++) {
|
|
717
|
+
const char = input[i];
|
|
718
|
+
if (char === BACKSLASH && inQuotes && i + 1 < input.length) { i++; continue; }
|
|
719
|
+
if (char === DQUOTE) { inQuotes = !inQuotes; continue; }
|
|
720
|
+
if (!inQuotes) {
|
|
721
|
+
if (char === OBRACE) {
|
|
722
|
+
if (depth === 0) start = i;
|
|
723
|
+
depth++;
|
|
724
|
+
}
|
|
725
|
+
if (char === CBRACE) {
|
|
726
|
+
depth--;
|
|
727
|
+
if (depth === 0) {
|
|
728
|
+
return input.slice(start + 1, i);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return '';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
_isArrayHeader(content) {
|
|
738
|
+
const bracketPos = content.indexOf(OBRACK);
|
|
739
|
+
const colonPos = content.indexOf(COLON);
|
|
740
|
+
return bracketPos !== -1 && colonPos !== -1 && bracketPos < colonPos;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
_isKeyValueLine(content) {
|
|
744
|
+
return content.includes(COLON);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
_setDotPath(obj, path, val) {
|
|
748
|
+
const keys = path.split('.');
|
|
749
|
+
let ref = obj;
|
|
750
|
+
for (let i = 0; i < keys.length; i++) {
|
|
751
|
+
if (i === keys.length - 1) {
|
|
752
|
+
ref[keys[i]] = val;
|
|
753
|
+
} else {
|
|
754
|
+
if (!Object.hasOwn(ref, keys[i]) || typeof ref[keys[i]] !== 'object' || ref[keys[i]] === null) {
|
|
755
|
+
ref[keys[i]] = {};
|
|
756
|
+
}
|
|
757
|
+
ref = ref[keys[i]];
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|