@safeaccess/inline 0.1.1 → 0.1.2
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/.gitattributes +1 -1
- package/CHANGELOG.md +10 -5
- package/LICENSE +1 -1
- package/README.md +56 -14
- package/dist/accessors/abstract-accessor.d.ts +22 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +22 -56
- package/dist/inline.js +39 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +3 -1
- package/dist/security/security-guard.js +5 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +23 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +42 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +7 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +332 -0
- package/tests/parser/xml-parser.test.ts +10 -334
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +3 -484
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -111,13 +111,15 @@ describe(`${YamlParser.name} > block scalars`, () => {
|
|
|
111
111
|
it('parses literal block scalar |', () => {
|
|
112
112
|
const yaml = 'text: |\n hello\n world';
|
|
113
113
|
const result = makeParser().parse(yaml);
|
|
114
|
-
|
|
114
|
+
// YAML spec: clip chomping adds trailing newline (matches PHP yaml_parse)
|
|
115
|
+
expect(result['text']).toBe('hello\nworld\n');
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
it('parses folded block scalar >', () => {
|
|
118
119
|
const yaml = 'text: >\n hello\n world';
|
|
119
120
|
const result = makeParser().parse(yaml);
|
|
120
|
-
|
|
121
|
+
// YAML spec: clip chomping adds trailing newline (matches PHP yaml_parse)
|
|
122
|
+
expect(result['text']).toBe('hello world\n');
|
|
121
123
|
});
|
|
122
124
|
});
|
|
123
125
|
|
|
@@ -130,11 +132,11 @@ describe(`${YamlParser.name} > inline flow`, () => {
|
|
|
130
132
|
expect(result['tags']).toEqual(['a', 'b', 'c']);
|
|
131
133
|
});
|
|
132
134
|
|
|
133
|
-
it('
|
|
134
|
-
//
|
|
135
|
+
it('parses unquoted inline array as flow sequence (matches PHP yaml_parse)', () => {
|
|
136
|
+
// YAML flow sequences support unquoted scalar values
|
|
135
137
|
const yaml = 'tags: [a, b, c]';
|
|
136
138
|
const result = makeParser().parse(yaml);
|
|
137
|
-
expect(result['tags']).
|
|
139
|
+
expect(result['tags']).toEqual(['a', 'b', 'c']);
|
|
138
140
|
});
|
|
139
141
|
|
|
140
142
|
it('falls back to raw string for unparseable inline flow', () => {
|
|
@@ -145,7 +147,7 @@ describe(`${YamlParser.name} > inline flow`, () => {
|
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
149
|
|
|
148
|
-
describe(`${YamlParser.name} > security
|
|
150
|
+
describe(`${YamlParser.name} > security - unsafe constructs`, () => {
|
|
149
151
|
it('throws YamlParseException for !! tags', () => {
|
|
150
152
|
expect(() => makeParser().parse('key: !!python/object foo')).toThrow(YamlParseException);
|
|
151
153
|
});
|
|
@@ -223,7 +225,7 @@ describe(`${YamlParser.name} > security — unsafe constructs`, () => {
|
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
it('does not throw for ! inside double-quoted string', () => {
|
|
226
|
-
// quoted string
|
|
228
|
+
// quoted string - regex should not match
|
|
227
229
|
expect(() => makeParser().parse('msg: "hello world"')).not.toThrow();
|
|
228
230
|
});
|
|
229
231
|
});
|
|
@@ -311,7 +313,7 @@ describe(`${YamlParser.name} > top-level result type`, () => {
|
|
|
311
313
|
});
|
|
312
314
|
});
|
|
313
315
|
|
|
314
|
-
describe(`${YamlParser.name} > indentation
|
|
316
|
+
describe(`${YamlParser.name} > indentation - over-indented key at block start`, () => {
|
|
315
317
|
it('ignores an over-indented key appearing before a properly-indented sibling', () => {
|
|
316
318
|
expect(makeParser().parse('outer:\n over_indented: ignored\n normal: value')).toEqual({
|
|
317
319
|
outer: { normal: 'value' },
|
|
@@ -346,12 +348,14 @@ describe(`${YamlParser.name} > inline flow objects`, () => {
|
|
|
346
348
|
});
|
|
347
349
|
|
|
348
350
|
describe(`${YamlParser.name} > block scalar trailing whitespace`, () => {
|
|
349
|
-
it('
|
|
350
|
-
|
|
351
|
+
it('clip-chomps trailing blank lines from literal block scalar', () => {
|
|
352
|
+
// YAML spec: clip removes trailing blanks, adds single newline (matches PHP yaml_parse)
|
|
353
|
+
expect(makeParser().parse('text: |\n hello\n\n')).toEqual({ text: 'hello\n' });
|
|
351
354
|
});
|
|
352
355
|
|
|
353
|
-
it('
|
|
354
|
-
|
|
356
|
+
it('clip-chomps trailing blank lines from folded block scalar', () => {
|
|
357
|
+
// YAML spec: clip removes trailing blanks, adds single newline (matches PHP yaml_parse)
|
|
358
|
+
expect(makeParser().parse('text: >\n hello\n\n')).toEqual({ text: 'hello\n' });
|
|
355
359
|
});
|
|
356
360
|
});
|
|
357
361
|
|
|
@@ -426,7 +430,7 @@ describe(`${YamlParser.name} > mergeChildLines sibling rows`, () => {
|
|
|
426
430
|
});
|
|
427
431
|
});
|
|
428
432
|
|
|
429
|
-
describe(`${YamlParser.name} > scalar types
|
|
433
|
+
describe(`${YamlParser.name} > scalar types - extended`, () => {
|
|
430
434
|
it('casts multi-digit float values', () => {
|
|
431
435
|
expect(makeParser().parse('ratio: 10.5')).toEqual({ ratio: 10.5 });
|
|
432
436
|
});
|
|
@@ -440,24 +444,846 @@ describe(`${YamlParser.name} > scalar types — extended`, () => {
|
|
|
440
444
|
});
|
|
441
445
|
});
|
|
442
446
|
|
|
443
|
-
describe(`${YamlParser.name} > block scalar
|
|
447
|
+
describe(`${YamlParser.name} > block scalar - blank lines and folded`, () => {
|
|
444
448
|
it('preserves a blank line in the middle of a literal block scalar', () => {
|
|
449
|
+
// YAML spec: clip chomping adds trailing newline (matches PHP yaml_parse)
|
|
445
450
|
expect(makeParser().parse('text: |\n hello\n\n world')).toEqual({
|
|
446
|
-
text: 'hello\n\nworld',
|
|
451
|
+
text: 'hello\n\nworld\n',
|
|
447
452
|
});
|
|
448
453
|
});
|
|
449
454
|
|
|
450
|
-
it('
|
|
455
|
+
it('terminates block scalar at less-indented comment line', () => {
|
|
456
|
+
// YAML spec: a line at lower indentation terminates the block scalar.
|
|
457
|
+
// PHP yaml_parse() errors on this input; JS captures content before the comment.
|
|
451
458
|
expect(makeParser().parse('text: |\n first\n# root comment\n second')).toEqual({
|
|
452
|
-
text: 'first\
|
|
459
|
+
text: 'first\n',
|
|
453
460
|
});
|
|
454
461
|
});
|
|
455
462
|
});
|
|
456
463
|
|
|
457
|
-
describe(`${YamlParser.name} > inline flow
|
|
464
|
+
describe(`${YamlParser.name} > inline flow - key with space before colon`, () => {
|
|
458
465
|
it('parses inline object with space before colon in key', () => {
|
|
459
466
|
expect(makeParser().parse("config: {key : 'value'}")).toEqual({
|
|
460
467
|
config: { key: 'value' },
|
|
461
468
|
});
|
|
462
469
|
});
|
|
463
470
|
});
|
|
471
|
+
|
|
472
|
+
describe(`${YamlParser.name} > nesting depth guard`, () => {
|
|
473
|
+
it('parses YAML within the default depth limit', () => {
|
|
474
|
+
const yaml = 'a:\n b:\n c:\n d: value';
|
|
475
|
+
const parser = new YamlParser();
|
|
476
|
+
const result = parser.parse(yaml);
|
|
477
|
+
expect((result['a'] as Record<string, unknown>)['b']).toEqual({ c: { d: 'value' } });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('throws YamlParseException when nesting exceeds maxDepth', () => {
|
|
481
|
+
const yaml = 'a:\n b:\n c:\n d:\n e: value';
|
|
482
|
+
const parser = new YamlParser(3);
|
|
483
|
+
expect(() => parser.parse(yaml)).toThrow(YamlParseException);
|
|
484
|
+
expect(() => parser.parse(yaml)).toThrow('YAML nesting depth 4 exceeds maximum of 3.');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('allows nesting exactly at maxDepth boundary', () => {
|
|
488
|
+
const yaml = 'a:\n b:\n c: value';
|
|
489
|
+
const parser = new YamlParser(3);
|
|
490
|
+
const result = parser.parse(yaml);
|
|
491
|
+
expect((result['a'] as Record<string, unknown>)['b']).toEqual({ c: 'value' });
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('throws YamlParseException for deep sequence nesting', () => {
|
|
495
|
+
const yaml = '-\n -\n -\n - value';
|
|
496
|
+
const parser = new YamlParser(2);
|
|
497
|
+
expect(() => parser.parse(yaml)).toThrow(YamlParseException);
|
|
498
|
+
expect(() => parser.parse(yaml)).toThrow('YAML nesting depth 3 exceeds maximum of 2.');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('throws YamlParseException for mixed map and sequence depth', () => {
|
|
502
|
+
const yaml = 'items:\n -\n nested:\n deep: value';
|
|
503
|
+
const parser = new YamlParser(2);
|
|
504
|
+
expect(() => parser.parse(yaml)).toThrow(YamlParseException);
|
|
505
|
+
expect(() => parser.parse(yaml)).toThrow('YAML nesting depth 3 exceeds maximum of 2.');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('uses the configured maxDepth not the default 512', () => {
|
|
509
|
+
const yaml = 'a:\n b: value';
|
|
510
|
+
const parser = new YamlParser(0);
|
|
511
|
+
expect(() => parser.parse(yaml)).toThrow(YamlParseException);
|
|
512
|
+
expect(() => parser.parse(yaml)).toThrow('YAML nesting depth 1 exceeds maximum of 0.');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('accepts the default 512 maxDepth for normal YAML', () => {
|
|
516
|
+
const parser = new YamlParser();
|
|
517
|
+
const result = parser.parse('root:\n child: value');
|
|
518
|
+
expect((result['root'] as Record<string, unknown>)['child']).toBe('value');
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe(`${YamlParser.name} > block scalar chomping modifiers`, () => {
|
|
523
|
+
it('strip chomping (|-) removes trailing newline from literal', () => {
|
|
524
|
+
const yaml = 'text: |-\n hello\n world';
|
|
525
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'hello\nworld' });
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('keep chomping (|+) preserves trailing blank lines in literal', () => {
|
|
529
|
+
const yaml = 'text: |+\n hello\n world\n\n';
|
|
530
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'hello\nworld\n\n' });
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('strip chomping (>-) removes trailing newline from folded', () => {
|
|
534
|
+
const yaml = 'text: >-\n hello\n world';
|
|
535
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'hello world' });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('keep chomping (>+) preserves trailing blank lines in folded', () => {
|
|
539
|
+
const yaml = 'text: >+\n hello\n world\n\n';
|
|
540
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'hello world\n\n' });
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('default literal chomping (|) adds exactly one trailing newline', () => {
|
|
544
|
+
const yaml = 'text: |\n line';
|
|
545
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'line\n' });
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('default folded chomping (>) adds exactly one trailing newline', () => {
|
|
549
|
+
const yaml = 'text: >\n line';
|
|
550
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'line\n' });
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('strip chomping result does NOT end with newline', () => {
|
|
554
|
+
const result = makeParser().parse('text: |-\n line');
|
|
555
|
+
expect((result['text'] as string).endsWith('\n')).toBe(false);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('keep chomping result ends with preserved trailing blanks', () => {
|
|
559
|
+
const result = makeParser().parse('text: |+\n line\n\n\n');
|
|
560
|
+
expect((result['text'] as string).endsWith('\n\n\n')).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('literal block with empty block produces just a newline for clip', () => {
|
|
564
|
+
const yaml = 'text: |\nnext: val';
|
|
565
|
+
const result = makeParser().parse(yaml);
|
|
566
|
+
expect(result['text']).toBe('\n');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('folded block with blank line between content preserves paragraph break', () => {
|
|
570
|
+
const yaml = 'text: >\n para1\n\n para2';
|
|
571
|
+
const result = makeParser().parse(yaml);
|
|
572
|
+
expect(result['text']).toBe('para1\npara2\n');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('folded block joins consecutive non-empty lines with space', () => {
|
|
576
|
+
const yaml = 'text: >\n a\n b\n c';
|
|
577
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'a b c\n' });
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('folded block does not add space after first line when result was empty', () => {
|
|
581
|
+
const yaml = 'text: >\n first';
|
|
582
|
+
const result = makeParser().parse(yaml);
|
|
583
|
+
expect((result['text'] as string).startsWith(' ')).toBe(false);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('folded block prevEmpty resets after non-empty line following blank', () => {
|
|
587
|
+
const yaml = 'text: >\n a\n\n b\n c';
|
|
588
|
+
const result = makeParser().parse(yaml);
|
|
589
|
+
expect(result['text']).toBe('a\nb c\n');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('literal block auto-detects indent from first content line', () => {
|
|
593
|
+
const yaml = 'text: |\n four_spaces';
|
|
594
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'four_spaces\n' });
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('literal block stops at line with less indent than detected', () => {
|
|
598
|
+
const yaml = 'text: |\n deep\n shallow\nnext: val';
|
|
599
|
+
const result = makeParser().parse(yaml);
|
|
600
|
+
expect(result['text']).toBe('deep\n');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('block scalar uses trimStart to compute indent not trim', () => {
|
|
604
|
+
const yaml = 'text: |\n hello ';
|
|
605
|
+
const result = makeParser().parse(yaml);
|
|
606
|
+
expect(result['text']).toBe('hello \n');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe(`${YamlParser.name} > flow sequence edge cases`, () => {
|
|
611
|
+
it('parses empty flow sequence []', () => {
|
|
612
|
+
expect(makeParser().parse('items: []')).toEqual({ items: [] });
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('parses flow sequence with single item', () => {
|
|
616
|
+
expect(makeParser().parse('items: [one]')).toEqual({ items: ['one'] });
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('trims whitespace from flow sequence items', () => {
|
|
620
|
+
expect(makeParser().parse('items: [ a , b , c ]')).toEqual({ items: ['a', 'b', 'c'] });
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('parses flow sequence with numeric values', () => {
|
|
624
|
+
expect(makeParser().parse('nums: [1, 2, 3]')).toEqual({ nums: [1, 2, 3] });
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('parses flow sequence with boolean values', () => {
|
|
628
|
+
expect(makeParser().parse('flags: [true, false]')).toEqual({ flags: [true, false] });
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('parses flow sequence with null values', () => {
|
|
632
|
+
expect(makeParser().parse('vals: [null, ~]')).toEqual({ vals: [null, null] });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('parses flow sequence with quoted strings containing commas', () => {
|
|
636
|
+
expect(makeParser().parse('items: ["a,b", "c,d"]')).toEqual({ items: ['a,b', 'c,d'] });
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('parses flow sequence with nested brackets', () => {
|
|
640
|
+
expect(makeParser().parse('items: [[1, 2], [3]]')).toEqual({ items: ['[1, 2]', '[3]'] });
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('does not return empty string items from trailing whitespace', () => {
|
|
644
|
+
const result = makeParser().parse('items: [a, b]');
|
|
645
|
+
expect((result['items'] as unknown[]).length).toBe(2);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('inner trim handles flow sequence with only whitespace inside as empty', () => {
|
|
649
|
+
expect(makeParser().parse('items: [ ]')).toEqual({ items: [] });
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
describe(`${YamlParser.name} > flow map edge cases`, () => {
|
|
654
|
+
it('parses empty flow map {}', () => {
|
|
655
|
+
expect(makeParser().parse('config: {}')).toEqual({ config: {} });
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('parses flow map with single key-value', () => {
|
|
659
|
+
expect(makeParser().parse('config: {a: 1}')).toEqual({ config: { a: 1 } });
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('trims whitespace from flow map keys and values', () => {
|
|
663
|
+
expect(makeParser().parse('m: { key : val }')).toEqual({ m: { key: 'val' } });
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('skips flow map entries without a colon', () => {
|
|
667
|
+
expect(makeParser().parse('m: {novalue, a: 1}')).toEqual({ m: { a: 1 } });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('parses flow map with numeric value', () => {
|
|
671
|
+
expect(makeParser().parse('m: {port: 8080}')).toEqual({ m: { port: 8080 } });
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('parses flow map with boolean value', () => {
|
|
675
|
+
expect(makeParser().parse('m: {active: true}')).toEqual({ m: { active: true } });
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('parses flow map with null value', () => {
|
|
679
|
+
expect(makeParser().parse('m: {val: null}')).toEqual({ m: { val: null } });
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('inner trim handles flow map with only whitespace inside as empty', () => {
|
|
683
|
+
expect(makeParser().parse('config: { }')).toEqual({ config: {} });
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('slices off the braces to get inner content', () => {
|
|
687
|
+
expect(makeParser().parse('m: {x: y}')).toEqual({ m: { x: 'y' } });
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe(`${YamlParser.name} > splitFlowItems edge cases`, () => {
|
|
692
|
+
it('respects nested braces when splitting flow items', () => {
|
|
693
|
+
expect(makeParser().parse('m: {outer: {inner: val}}')).toEqual({
|
|
694
|
+
m: { outer: '{inner: val}' },
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('respects single-quoted strings containing commas', () => {
|
|
699
|
+
expect(makeParser().parse("items: ['a,b', 'c']")).toEqual({ items: ['a,b', 'c'] });
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('closing quotes end the quoted region', () => {
|
|
703
|
+
expect(makeParser().parse('items: ["x", y]')).toEqual({ items: ['x', 'y'] });
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('closing bracket decrements depth', () => {
|
|
707
|
+
expect(makeParser().parse('items: [{a: 1}, b]')).toEqual({ items: ['{a: 1}', 'b'] });
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('does not push empty trailing items', () => {
|
|
711
|
+
const result = makeParser().parse('items: [a, b, ]');
|
|
712
|
+
expect((result['items'] as unknown[]).length).toBe(2);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('handles multiple nested brackets at different depths', () => {
|
|
716
|
+
expect(makeParser().parse('items: [[1, [2]], 3]')).toEqual({
|
|
717
|
+
items: ['[1, [2]]', 3],
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('preserves nested braces in flow items', () => {
|
|
722
|
+
expect(makeParser().parse('items: [{a: {b: c}}, d]')).toEqual({
|
|
723
|
+
items: ['{a: {b: c}}', 'd'],
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
describe(`${YamlParser.name} > castScalar - boolean variants`, () => {
|
|
729
|
+
it('casts yes as true', () => {
|
|
730
|
+
expect(makeParser().parse('val: yes')).toEqual({ val: true });
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('casts Yes as true', () => {
|
|
734
|
+
expect(makeParser().parse('val: Yes')).toEqual({ val: true });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('casts on as true', () => {
|
|
738
|
+
expect(makeParser().parse('val: on')).toEqual({ val: true });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('casts On as true', () => {
|
|
742
|
+
expect(makeParser().parse('val: On')).toEqual({ val: true });
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('casts no as false', () => {
|
|
746
|
+
expect(makeParser().parse('val: no')).toEqual({ val: false });
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('casts No as false', () => {
|
|
750
|
+
expect(makeParser().parse('val: No')).toEqual({ val: false });
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('casts off as false', () => {
|
|
754
|
+
expect(makeParser().parse('val: off')).toEqual({ val: false });
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('casts Off as false', () => {
|
|
758
|
+
expect(makeParser().parse('val: Off')).toEqual({ val: false });
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('casts TRUE as true', () => {
|
|
762
|
+
expect(makeParser().parse('val: TRUE')).toEqual({ val: true });
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('casts FALSE as false', () => {
|
|
766
|
+
expect(makeParser().parse('val: FALSE')).toEqual({ val: false });
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
describe(`${YamlParser.name} > castScalar - null variants`, () => {
|
|
771
|
+
it('casts Null as null', () => {
|
|
772
|
+
expect(makeParser().parse('val: Null')).toEqual({ val: null });
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('casts NULL as null', () => {
|
|
776
|
+
expect(makeParser().parse('val: NULL')).toEqual({ val: null });
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
describe(`${YamlParser.name} > castScalar - numeric edge cases`, () => {
|
|
781
|
+
it('parses octal value 0o777', () => {
|
|
782
|
+
expect(makeParser().parse('val: 0o777')).toEqual({ val: 511 });
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('parses octal value 0o10', () => {
|
|
786
|
+
expect(makeParser().parse('val: 0o10')).toEqual({ val: 8 });
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('parses hex value 0xFF', () => {
|
|
790
|
+
expect(makeParser().parse('val: 0xFF')).toEqual({ val: 255 });
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('parses hex value 0x1A', () => {
|
|
794
|
+
expect(makeParser().parse('val: 0x1A')).toEqual({ val: 26 });
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('parses .inf as Infinity', () => {
|
|
798
|
+
expect(makeParser().parse('val: .inf')).toEqual({ val: Infinity });
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('parses +.inf as Infinity', () => {
|
|
802
|
+
expect(makeParser().parse('val: +.inf')).toEqual({ val: Infinity });
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('parses -.inf as -Infinity', () => {
|
|
806
|
+
expect(makeParser().parse('val: -.inf')).toEqual({ val: -Infinity });
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('parses .nan as NaN', () => {
|
|
810
|
+
const result = makeParser().parse('val: .nan');
|
|
811
|
+
expect(result['val']).toBeNaN();
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('parses scientific notation 1.5e10', () => {
|
|
815
|
+
expect(makeParser().parse('val: 1.5e10')).toEqual({ val: 1.5e10 });
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('parses scientific notation with negative exponent 2.5e-3', () => {
|
|
819
|
+
expect(makeParser().parse('val: 2.5e-3')).toEqual({ val: 0.0025 });
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('parses scientific notation with positive exponent 1.0E+5', () => {
|
|
823
|
+
expect(makeParser().parse('val: 1.0E+5')).toEqual({ val: 100000 });
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('parses .Inf (capitalized) as Infinity', () => {
|
|
827
|
+
expect(makeParser().parse('val: .Inf')).toEqual({ val: Infinity });
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('parses -.Inf (capitalized) as -Infinity', () => {
|
|
831
|
+
expect(makeParser().parse('val: -.Inf')).toEqual({ val: -Infinity });
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('parses .NaN (capitalized) as NaN', () => {
|
|
835
|
+
const result = makeParser().parse('val: .NaN');
|
|
836
|
+
expect(result['val']).toBeNaN();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('parses zero as integer 0', () => {
|
|
840
|
+
expect(makeParser().parse('val: 0')).toEqual({ val: 0 });
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('does not parse 0o89 as octal (invalid octal digits)', () => {
|
|
844
|
+
expect(makeParser().parse('val: 0o89')).toEqual({ val: '0o89' });
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('does not parse 0xZZ as hex (invalid hex digits)', () => {
|
|
848
|
+
expect(makeParser().parse('val: 0xZZ')).toEqual({ val: '0xZZ' });
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('parses negative float -3.14', () => {
|
|
852
|
+
expect(makeParser().parse('val: -3.14')).toEqual({ val: -3.14 });
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('does not parse value with dot but no decimal digits as float', () => {
|
|
856
|
+
expect(makeParser().parse('val: 3.')).toEqual({ val: '3.' });
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('parses integer with leading zero as string if not 0', () => {
|
|
860
|
+
expect(makeParser().parse('val: 00123')).toEqual({ val: '00123' });
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('distinguishes .inf from + prefixed', () => {
|
|
864
|
+
const r1 = makeParser().parse('a: .inf');
|
|
865
|
+
const r2 = makeParser().parse('a: +.inf');
|
|
866
|
+
expect(r1['a']).toBe(Infinity);
|
|
867
|
+
expect(r2['a']).toBe(Infinity);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
describe(`${YamlParser.name} > castScalar - quoted string length 2`, () => {
|
|
872
|
+
it('parses empty double-quoted string ""', () => {
|
|
873
|
+
expect(makeParser().parse('val: ""')).toEqual({ val: '' });
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('parses empty single-quoted string', () => {
|
|
877
|
+
expect(makeParser().parse("val: ''")).toEqual({ val: '' });
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('does not unquote single-char string "x (missing end quote)', () => {
|
|
881
|
+
expect(makeParser().parse('val: "x')).toEqual({ val: '"x' });
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('single-quoted escape: two consecutive single quotes become one', () => {
|
|
885
|
+
expect(makeParser().parse("val: 'it''s'")).toEqual({ val: "it's" });
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
describe(`${YamlParser.name} > unescapeDoubleQuoted - escape sequences`, () => {
|
|
890
|
+
it('unescapes \\n to newline', () => {
|
|
891
|
+
expect(makeParser().parse('val: "hello\\nworld"')).toEqual({ val: 'hello\nworld' });
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('unescapes \\t to tab', () => {
|
|
895
|
+
expect(makeParser().parse('val: "col1\\tcol2"')).toEqual({ val: 'col1\tcol2' });
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('unescapes \\r to carriage return', () => {
|
|
899
|
+
expect(makeParser().parse('val: "line\\rend"')).toEqual({ val: 'line\rend' });
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('unescapes \\\\ to backslash', () => {
|
|
903
|
+
expect(makeParser().parse('val: "path\\\\dir"')).toEqual({ val: 'path\\dir' });
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('unescapes \\" to double quote', () => {
|
|
907
|
+
expect(makeParser().parse('val: "say \\"hello\\""')).toEqual({ val: 'say "hello"' });
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('unescapes \\0 to null char', () => {
|
|
911
|
+
expect(makeParser().parse('val: "null\\0char"')).toEqual({ val: 'null\0char' });
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it('unescapes \\a to bell', () => {
|
|
915
|
+
expect(makeParser().parse('val: "bell\\a"')).toEqual({ val: 'bell\x07' });
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('unescapes \\b to backspace', () => {
|
|
919
|
+
expect(makeParser().parse('val: "back\\bspace"')).toEqual({ val: 'back\x08space' });
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('unescapes \\f to form feed', () => {
|
|
923
|
+
expect(makeParser().parse('val: "feed\\f"')).toEqual({ val: 'feed\x0C' });
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('unescapes \\v to vertical tab', () => {
|
|
927
|
+
expect(makeParser().parse('val: "vtab\\v"')).toEqual({ val: 'vtab\x0B' });
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
describe(`${YamlParser.name} > stripInlineComment edge cases`, () => {
|
|
932
|
+
it('strips inline comment after a value', () => {
|
|
933
|
+
expect(makeParser().parse('key: value # comment')).toEqual({ key: 'value' });
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it('does not strip hash without preceding space', () => {
|
|
937
|
+
expect(makeParser().parse('key: value#notcomment')).toEqual({ key: 'value#notcomment' });
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('does not strip hash inside double-quoted string', () => {
|
|
941
|
+
expect(makeParser().parse('key: "val # ue"')).toEqual({ key: 'val # ue' });
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('does not strip hash inside single-quoted string', () => {
|
|
945
|
+
expect(makeParser().parse("key: 'val # ue'")).toEqual({ key: 'val # ue' });
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('strips comment after closing quote of double-quoted value', () => {
|
|
949
|
+
expect(makeParser().parse('key: "val" # comment')).toEqual({ key: 'val' });
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('strips comment after closing quote of single-quoted value', () => {
|
|
953
|
+
expect(makeParser().parse("key: 'val' # comment")).toEqual({ key: 'val' });
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('returns empty string for empty raw value', () => {
|
|
957
|
+
expect(makeParser().parse('key: ')).toEqual({ key: null });
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it('handles value that starts with quote but has no close quote', () => {
|
|
961
|
+
expect(makeParser().parse('key: "unclosed value')).toEqual({ key: '"unclosed value' });
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('handles value with unmatched single-quote keeping hash as part of value', () => {
|
|
965
|
+
expect(makeParser().parse("key: it's # comment")).toEqual({ key: "it's # comment" });
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('handles double-quote toggle - does not strip hash inside quoted region', () => {
|
|
969
|
+
expect(makeParser().parse('key: a "b # c" d')).toEqual({ key: 'a "b # c" d' });
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('strips comment that appears after double-quoted region has closed', () => {
|
|
973
|
+
expect(makeParser().parse('key: "word" rest # comment')).toEqual({ key: '"word" rest' });
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('returns original value when no hash character is present', () => {
|
|
977
|
+
expect(makeParser().parse('key: nohashhere')).toEqual({ key: 'nohashhere' });
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
describe(`${YamlParser.name} > resolveValue - block scalar detection regex`, () => {
|
|
982
|
+
it('detects | as block scalar not a string value', () => {
|
|
983
|
+
const yaml = 'text: |\n content';
|
|
984
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content\n' });
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('detects |- as block scalar', () => {
|
|
988
|
+
const yaml = 'text: |-\n content';
|
|
989
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content' });
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('detects |+ as block scalar', () => {
|
|
993
|
+
const yaml = 'text: |+\n content\n';
|
|
994
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content\n' });
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('detects > as block scalar not a string value', () => {
|
|
998
|
+
const yaml = 'text: >\n content';
|
|
999
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content\n' });
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it('detects >- as block scalar', () => {
|
|
1003
|
+
const yaml = 'text: >-\n content';
|
|
1004
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content' });
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it('detects >+ as block scalar', () => {
|
|
1008
|
+
const yaml = 'text: >+\n content\n';
|
|
1009
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'content\n' });
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('does not treat |x as block scalar (invalid chomping)', () => {
|
|
1013
|
+
const yaml = 'text: |x';
|
|
1014
|
+
expect(makeParser().parse(yaml)).toEqual({ text: '|x' });
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('does not treat >x as block scalar (invalid chomping)', () => {
|
|
1018
|
+
const yaml = 'text: >x';
|
|
1019
|
+
expect(makeParser().parse(yaml)).toEqual({ text: '>x' });
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it('resolveValue uses trimmed value to check block scalar, not raw', () => {
|
|
1023
|
+
const yaml = 'text: | \n content';
|
|
1024
|
+
const result = makeParser().parse(yaml);
|
|
1025
|
+
expect(result['text']).toBe('content\n');
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it('resolveValue passes correct folded flag for > indicator', () => {
|
|
1029
|
+
const yaml = 'a: >\n x\n y';
|
|
1030
|
+
const result = makeParser().parse(yaml);
|
|
1031
|
+
expect(result['a']).toBe('x y\n');
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('resolveValue passes correct folded flag for | indicator', () => {
|
|
1035
|
+
const yaml = 'a: |\n x\n y';
|
|
1036
|
+
const result = makeParser().parse(yaml);
|
|
1037
|
+
expect(result['a']).toBe('x\ny\n');
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
describe(`${YamlParser.name} > resolveValue - flow detection`, () => {
|
|
1042
|
+
it('detects value starting with [ and ending with ] as flow sequence', () => {
|
|
1043
|
+
expect(makeParser().parse('items: [a]')).toEqual({ items: ['a'] });
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it('detects value starting with { and ending with } as flow map', () => {
|
|
1047
|
+
expect(makeParser().parse('m: {a: 1}')).toEqual({ m: { a: 1 } });
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('does not treat [unclosed as flow sequence', () => {
|
|
1051
|
+
expect(makeParser().parse('val: [unclosed')).toEqual({ val: '[unclosed' });
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('does not treat {unclosed as flow map', () => {
|
|
1055
|
+
expect(makeParser().parse('val: {unclosed')).toEqual({ val: '{unclosed' });
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it('does not treat value ending with ] but not starting with [ as flow', () => {
|
|
1059
|
+
expect(makeParser().parse('val: notarray]')).toEqual({ val: 'notarray]' });
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('does not treat value ending with } but not starting with { as flow', () => {
|
|
1063
|
+
expect(makeParser().parse('val: notmap}')).toEqual({ val: 'notmap}' });
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
describe(`${YamlParser.name} > parseLines - map key regex anchoring`, () => {
|
|
1068
|
+
it('map key regex requires start anchor: value with internal k:v not parsed as map', () => {
|
|
1069
|
+
const yaml = 'items:\n - first:second';
|
|
1070
|
+
const result = makeParser().parse(yaml);
|
|
1071
|
+
expect(result).toEqual({ items: [{ first: 'second' }] });
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it('map key regex requires end anchor: full line must match', () => {
|
|
1075
|
+
const yaml = 'full: line value';
|
|
1076
|
+
expect(makeParser().parse(yaml)).toEqual({ full: 'line value' });
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it('key with spaces before colon is captured correctly', () => {
|
|
1080
|
+
const yaml = 'my key: my value';
|
|
1081
|
+
expect(makeParser().parse(yaml)).toEqual({ 'my key': 'my value' });
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
describe(`${YamlParser.name} > parseLines - indentation handling`, () => {
|
|
1086
|
+
it('skips lines with greater indent than baseIndent', () => {
|
|
1087
|
+
const yaml = 'root:\n key: val\n overindented: ignored\n key2: val2';
|
|
1088
|
+
const parsed = makeParser().parse(yaml);
|
|
1089
|
+
expect((parsed['root'] as Record<string, unknown>)['key']).toBe('val');
|
|
1090
|
+
expect((parsed['root'] as Record<string, unknown>)['key2']).toBe('val2');
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
describe(`${YamlParser.name} > assertNoUnsafeConstructs - regex precision`, () => {
|
|
1095
|
+
it('rejects tag with single ! followed by word char (not just !!)', () => {
|
|
1096
|
+
expect(() => makeParser().parse('key: !tagged value')).toThrow(/tag/i);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('rejects !! tag at start of value', () => {
|
|
1100
|
+
expect(() => makeParser().parse('key: !!binary abc')).toThrow(/tag/i);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it('does not throw for exclamation mark inside single-quoted string', () => {
|
|
1104
|
+
expect(() => makeParser().parse("key: 'hello!'")).not.toThrow();
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('does not throw for exclamation mark inside double-quoted string', () => {
|
|
1108
|
+
expect(() => makeParser().parse('key: "hello!"')).not.toThrow();
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('rejects anchor &name with multiple word chars', () => {
|
|
1112
|
+
expect(() => makeParser().parse('key: &anchor val')).toThrow(/anchor/i);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it('rejects alias *name with multiple word chars', () => {
|
|
1116
|
+
expect(() => makeParser().parse('key: *alias')).toThrow(/alias/i);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it('rejects merge key << with optional whitespace before colon', () => {
|
|
1120
|
+
expect(() => makeParser().parse(' << : val')).toThrow(/merge/i);
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
describe(`${YamlParser.name} > sequence item with nested block under bare dash`, () => {
|
|
1125
|
+
it('bare dash with nested sequence child returns parsed child block', () => {
|
|
1126
|
+
const yaml = 'items:\n -\n - nested1\n - nested2';
|
|
1127
|
+
const result = makeParser().parse(yaml);
|
|
1128
|
+
const items = result['items'] as unknown[];
|
|
1129
|
+
expect(items[0]).toEqual(['nested1', 'nested2']);
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
describe(`${YamlParser.name} > resolveValue rawValue trimming`, () => {
|
|
1134
|
+
it('strips inline comment from value before checking block scalar', () => {
|
|
1135
|
+
const yaml = 'key: value # inline comment';
|
|
1136
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'value' });
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('trims rawValue before processing', () => {
|
|
1140
|
+
const yaml = 'key: value ';
|
|
1141
|
+
expect(makeParser().parse(yaml)).toEqual({ key: 'value' });
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
describe(`${YamlParser.name} > castScalar - float requires dot`, () => {
|
|
1146
|
+
it('float regex requires a dot in the value', () => {
|
|
1147
|
+
expect(makeParser().parse('val: 123')).toEqual({ val: 123 });
|
|
1148
|
+
expect(typeof makeParser().parse('val: 123')['val']).toBe('number');
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('float regex matches value with only fractional part .5', () => {
|
|
1152
|
+
expect(makeParser().parse('val: .5')).toEqual({ val: 0.5 });
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
describe(`${YamlParser.name} > splitFlowItems - comma and depth tracking`, () => {
|
|
1157
|
+
it('only splits on comma at depth 0', () => {
|
|
1158
|
+
const result = makeParser().parse('items: [{a: 1, b: 2}, c]');
|
|
1159
|
+
const items = result['items'] as unknown[];
|
|
1160
|
+
expect(items).toHaveLength(2);
|
|
1161
|
+
expect(items[0]).toBe('{a: 1, b: 2}');
|
|
1162
|
+
expect(items[1]).toBe('c');
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
it('depth increments for [ and {', () => {
|
|
1166
|
+
expect(makeParser().parse('items: [[a, b]]')).toEqual({ items: ['[a, b]'] });
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it('depth decrements for ] and }', () => {
|
|
1170
|
+
const result = makeParser().parse('items: [{x: 1}, y]');
|
|
1171
|
+
expect((result['items'] as unknown[]).length).toBe(2);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('tracks quote state to avoid splitting inside double-quoted commas', () => {
|
|
1175
|
+
expect(makeParser().parse('items: ["a,b"]')).toEqual({ items: ['a,b'] });
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it('tracks quote state to avoid splitting inside single-quoted commas', () => {
|
|
1179
|
+
expect(makeParser().parse("items: ['a,b']")).toEqual({ items: ['a,b'] });
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it('resets quote state when closing quote is found', () => {
|
|
1183
|
+
expect(makeParser().parse('items: ["a", b]')).toEqual({ items: ['a', 'b'] });
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it('appends regular chars to current item', () => {
|
|
1187
|
+
expect(makeParser().parse('items: [abc]')).toEqual({ items: ['abc'] });
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
describe(`${YamlParser.name} > stripInlineComment - quote state toggling`, () => {
|
|
1192
|
+
it('single-quote toggles inSingle state correctly', () => {
|
|
1193
|
+
expect(makeParser().parse("key: 'has # hash'")).toEqual({ key: 'has # hash' });
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it('double-quote toggles inDouble state correctly', () => {
|
|
1197
|
+
expect(makeParser().parse('key: "has # hash"')).toEqual({ key: 'has # hash' });
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
it('single-quote inside double-quoted does not toggle inSingle', () => {
|
|
1201
|
+
expect(makeParser().parse('key: "it\'s # fine"')).toEqual({ key: "it's # fine" });
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('double-quote inside single-quoted does not toggle inDouble', () => {
|
|
1205
|
+
expect(makeParser().parse('key: \'say "hi" # ok\'')).toEqual({ key: 'say "hi" # ok' });
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
it('hash preceded by space outside quotes is treated as comment start', () => {
|
|
1209
|
+
expect(makeParser().parse('key: abc # comment')).toEqual({ key: 'abc' });
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it('hash at position 0 without preceding space is not stripped as comment', () => {
|
|
1213
|
+
expect(makeParser().parse('key: #value')).toEqual({ key: '#value' });
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
it('returns trimmed value when closing quote is followed by non-hash non-empty', () => {
|
|
1217
|
+
expect(makeParser().parse('key: "word" extra')).toEqual({ key: '"word" extra' });
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('handles value with alternating quotes protecting hash', () => {
|
|
1221
|
+
expect(makeParser().parse('key: \'a "b" c\' # comment')).toEqual({ key: 'a "b" c' });
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
describe(`${YamlParser.name} > block scalar - empty and edge`, () => {
|
|
1226
|
+
it('empty block lines are pushed as empty strings', () => {
|
|
1227
|
+
const yaml = 'text: |\n a\n\n b';
|
|
1228
|
+
const result = makeParser().parse(yaml);
|
|
1229
|
+
expect(result['text']).toBe('a\n\nb\n');
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
it('block scalar with strip chomping on empty content returns empty', () => {
|
|
1233
|
+
const yaml = 'text: |-\nnext: val';
|
|
1234
|
+
const result = makeParser().parse(yaml);
|
|
1235
|
+
expect(result['text']).toBe('');
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it('block scalar with keep chomping preserves all trailing newlines', () => {
|
|
1239
|
+
const yaml = 'text: |+\n line1\n\n\n';
|
|
1240
|
+
const result = makeParser().parse(yaml);
|
|
1241
|
+
expect(result['text']).toBe('line1\n\n\n');
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('block scalar endsWith check: result not already ending with newline gets one added', () => {
|
|
1245
|
+
const yaml = 'text: |\n singleline';
|
|
1246
|
+
expect(makeParser().parse(yaml)).toEqual({ text: 'singleline\n' });
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
describe(`${YamlParser.name} > flow map - colonPos edge cases`, () => {
|
|
1251
|
+
it('flow map with value containing colon splits on first colon', () => {
|
|
1252
|
+
expect(makeParser().parse('m: {url: http://x}')).toEqual({
|
|
1253
|
+
m: { url: 'http://x' },
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('flow map key is trimmed of whitespace', () => {
|
|
1258
|
+
expect(makeParser().parse('m: { key : val}')).toEqual({ m: { key: 'val' } });
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('flow map value is trimmed of whitespace', () => {
|
|
1262
|
+
expect(makeParser().parse('m: {key: val }')).toEqual({ m: { key: 'val' } });
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
describe(`${YamlParser.name} > mergeChildLines - resolveValue passthrough`, () => {
|
|
1267
|
+
it('mergeChildLines calls resolveValue for nested block scalar', () => {
|
|
1268
|
+
const yaml = 'items:\n - name: Alice\n bio: |\n hello\n world';
|
|
1269
|
+
const result = makeParser().parse(yaml);
|
|
1270
|
+
const items = result['items'] as Record<string, unknown>[];
|
|
1271
|
+
expect(items[0]['bio']).toBe('hello\nworld\n');
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
describe(`${YamlParser.name} > parseLines - sequence and map detection`, () => {
|
|
1276
|
+
it('isSequence flag is set when first item is "- " prefix', () => {
|
|
1277
|
+
expect(makeParser().parse('items:\n - a\n - b')).toEqual({ items: ['a', 'b'] });
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
it('returns arrResult when isSequence is true', () => {
|
|
1281
|
+
const result = makeParser().parse('items:\n - x');
|
|
1282
|
+
expect(Array.isArray(result['items'])).toBe(true);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
it('returns mapResult when isSequence is false', () => {
|
|
1286
|
+
const result = makeParser().parse('a: 1\nb: 2');
|
|
1287
|
+
expect(Array.isArray(result)).toBe(false);
|
|
1288
|
+
});
|
|
1289
|
+
});
|