@safeaccess/inline 0.1.1 → 0.1.3

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.
Files changed (179) hide show
  1. package/.gitattributes +1 -1
  2. package/CHANGELOG.md +23 -5
  3. package/LICENSE +1 -1
  4. package/README.md +79 -21
  5. package/dist/accessors/abstract-accessor.d.ts +24 -10
  6. package/dist/accessors/abstract-accessor.js +21 -8
  7. package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
  8. package/dist/accessors/abstract-integration-accessor.js +23 -0
  9. package/dist/accessors/formats/any-accessor.d.ts +10 -8
  10. package/dist/accessors/formats/any-accessor.js +9 -8
  11. package/dist/accessors/formats/array-accessor.d.ts +2 -0
  12. package/dist/accessors/formats/array-accessor.js +2 -0
  13. package/dist/accessors/formats/env-accessor.d.ts +2 -0
  14. package/dist/accessors/formats/env-accessor.js +2 -0
  15. package/dist/accessors/formats/ini-accessor.d.ts +2 -0
  16. package/dist/accessors/formats/ini-accessor.js +2 -0
  17. package/dist/accessors/formats/json-accessor.d.ts +2 -0
  18. package/dist/accessors/formats/json-accessor.js +2 -0
  19. package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
  20. package/dist/accessors/formats/ndjson-accessor.js +2 -0
  21. package/dist/accessors/formats/object-accessor.d.ts +2 -0
  22. package/dist/accessors/formats/object-accessor.js +2 -0
  23. package/dist/accessors/formats/xml-accessor.d.ts +2 -0
  24. package/dist/accessors/formats/xml-accessor.js +2 -0
  25. package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
  26. package/dist/accessors/formats/yaml-accessor.js +4 -2
  27. package/dist/cache/simple-path-cache.d.ts +51 -0
  28. package/dist/cache/simple-path-cache.js +72 -0
  29. package/dist/contracts/accessors-interface.d.ts +2 -0
  30. package/dist/contracts/factory-accessors-interface.d.ts +2 -0
  31. package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
  32. package/dist/contracts/filter-evaluator-interface.js +1 -0
  33. package/dist/contracts/parse-integration-interface.d.ts +2 -0
  34. package/dist/contracts/parser-interface.d.ts +92 -0
  35. package/dist/contracts/parser-interface.js +1 -0
  36. package/dist/contracts/path-cache-interface.d.ts +7 -6
  37. package/dist/contracts/readable-accessors-interface.d.ts +11 -6
  38. package/dist/contracts/security-guard-interface.d.ts +2 -0
  39. package/dist/contracts/security-parser-interface.d.ts +2 -0
  40. package/dist/contracts/validatable-parser-interface.d.ts +59 -0
  41. package/dist/contracts/validatable-parser-interface.js +1 -0
  42. package/dist/contracts/writable-accessors-interface.d.ts +5 -0
  43. package/dist/core/accessor-factory.d.ts +124 -0
  44. package/dist/core/accessor-factory.js +157 -0
  45. package/dist/core/dot-notation-parser.d.ts +34 -5
  46. package/dist/core/dot-notation-parser.js +51 -10
  47. package/dist/core/inline-builder-accessor.d.ts +82 -0
  48. package/dist/core/inline-builder-accessor.js +107 -0
  49. package/dist/exceptions/accessor-exception.d.ts +9 -0
  50. package/dist/exceptions/accessor-exception.js +9 -0
  51. package/dist/exceptions/invalid-format-exception.d.ts +5 -0
  52. package/dist/exceptions/invalid-format-exception.js +5 -0
  53. package/dist/exceptions/parser-exception.d.ts +4 -0
  54. package/dist/exceptions/parser-exception.js +4 -0
  55. package/dist/exceptions/path-not-found-exception.d.ts +4 -0
  56. package/dist/exceptions/path-not-found-exception.js +4 -0
  57. package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
  58. package/dist/exceptions/readonly-violation-exception.js +4 -0
  59. package/dist/exceptions/security-exception.d.ts +6 -0
  60. package/dist/exceptions/security-exception.js +6 -0
  61. package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
  62. package/dist/exceptions/unsupported-type-exception.js +4 -0
  63. package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
  64. package/dist/exceptions/yaml-parse-exception.js +4 -0
  65. package/dist/index.js +2 -1
  66. package/dist/inline.d.ts +26 -56
  67. package/dist/inline.js +43 -111
  68. package/dist/parser/xml-parser.js +23 -10
  69. package/dist/parser/yaml-parser.d.ts +54 -7
  70. package/dist/parser/yaml-parser.js +268 -51
  71. package/dist/path-query/segment-filter-parser.d.ts +142 -0
  72. package/dist/path-query/segment-filter-parser.js +384 -0
  73. package/dist/path-query/segment-parser.d.ts +98 -0
  74. package/dist/path-query/segment-parser.js +283 -0
  75. package/dist/path-query/segment-path-resolver.d.ts +149 -0
  76. package/dist/path-query/segment-path-resolver.js +351 -0
  77. package/dist/path-query/segment-type.d.ts +85 -0
  78. package/dist/path-query/segment-type.js +35 -0
  79. package/dist/security/forbidden-keys.d.ts +2 -2
  80. package/dist/security/forbidden-keys.js +5 -5
  81. package/dist/security/security-guard.d.ts +4 -1
  82. package/dist/security/security-guard.js +7 -2
  83. package/dist/security/security-parser.d.ts +10 -1
  84. package/dist/security/security-parser.js +10 -1
  85. package/dist/type-format.d.ts +2 -0
  86. package/dist/type-format.js +2 -0
  87. package/package.json +11 -3
  88. package/src/accessors/abstract-accessor.ts +25 -19
  89. package/src/accessors/abstract-integration-accessor.ts +27 -0
  90. package/src/accessors/formats/any-accessor.ts +11 -11
  91. package/src/accessors/formats/array-accessor.ts +2 -0
  92. package/src/accessors/formats/env-accessor.ts +2 -0
  93. package/src/accessors/formats/ini-accessor.ts +2 -0
  94. package/src/accessors/formats/json-accessor.ts +2 -0
  95. package/src/accessors/formats/ndjson-accessor.ts +2 -0
  96. package/src/accessors/formats/object-accessor.ts +2 -0
  97. package/src/accessors/formats/xml-accessor.ts +2 -0
  98. package/src/accessors/formats/yaml-accessor.ts +4 -2
  99. package/src/cache/simple-path-cache.ts +77 -0
  100. package/src/contracts/accessors-interface.ts +2 -0
  101. package/src/contracts/factory-accessors-interface.ts +2 -0
  102. package/src/contracts/filter-evaluator-interface.ts +30 -0
  103. package/src/contracts/parse-integration-interface.ts +2 -0
  104. package/src/contracts/parser-interface.ts +114 -0
  105. package/src/contracts/path-cache-interface.ts +8 -6
  106. package/src/contracts/readable-accessors-interface.ts +11 -6
  107. package/src/contracts/security-guard-interface.ts +2 -0
  108. package/src/contracts/security-parser-interface.ts +2 -0
  109. package/src/contracts/validatable-parser-interface.ts +64 -0
  110. package/src/contracts/writable-accessors-interface.ts +5 -0
  111. package/src/core/accessor-factory.ts +173 -0
  112. package/src/core/dot-notation-parser.ts +74 -11
  113. package/src/core/inline-builder-accessor.ts +163 -0
  114. package/src/exceptions/accessor-exception.ts +9 -0
  115. package/src/exceptions/invalid-format-exception.ts +5 -0
  116. package/src/exceptions/parser-exception.ts +4 -0
  117. package/src/exceptions/path-not-found-exception.ts +4 -0
  118. package/src/exceptions/readonly-violation-exception.ts +4 -0
  119. package/src/exceptions/security-exception.ts +6 -0
  120. package/src/exceptions/unsupported-type-exception.ts +4 -0
  121. package/src/exceptions/yaml-parse-exception.ts +4 -0
  122. package/src/index.ts +3 -1
  123. package/src/inline.ts +46 -120
  124. package/src/parser/xml-parser.ts +31 -10
  125. package/src/parser/yaml-parser.ts +310 -45
  126. package/src/path-query/segment-filter-parser.ts +444 -0
  127. package/src/path-query/segment-parser.ts +321 -0
  128. package/src/path-query/segment-path-resolver.ts +521 -0
  129. package/src/path-query/segment-type.ts +82 -0
  130. package/src/security/forbidden-keys.ts +5 -5
  131. package/src/security/security-guard.ts +10 -2
  132. package/src/security/security-parser.ts +18 -3
  133. package/src/type-format.ts +2 -0
  134. package/stryker.config.json +8 -10
  135. package/tests/accessors/abstract-accessor.test.ts +217 -0
  136. package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
  137. package/tests/accessors/formats/any-accessor.test.ts +57 -0
  138. package/tests/accessors/formats/array-accessor.test.ts +42 -0
  139. package/tests/accessors/formats/env-accessor.test.ts +103 -0
  140. package/tests/accessors/formats/ini-accessor.test.ts +186 -0
  141. package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
  142. package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
  143. package/tests/accessors/formats/object-accessor.test.ts +172 -0
  144. package/tests/accessors/formats/xml-accessor.test.ts +162 -0
  145. package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
  146. package/tests/cache/simple-path-cache.test.ts +168 -0
  147. package/tests/core/accessor-factory.test.ts +157 -0
  148. package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
  149. package/tests/core/dot-notation-parser.test.ts +0 -288
  150. package/tests/core/inline-builder-accessor.test.ts +114 -0
  151. package/tests/exceptions/accessor-exception.test.ts +28 -0
  152. package/tests/exceptions/invalid-format-exception.test.ts +31 -0
  153. package/tests/exceptions/path-not-found-exception.test.ts +33 -0
  154. package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
  155. package/tests/exceptions/security-exception.test.ts +33 -0
  156. package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
  157. package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
  158. package/tests/mocks/fake-path-cache.ts +4 -3
  159. package/tests/parity-from.test.ts +118 -0
  160. package/tests/parity.test.ts +227 -10
  161. package/tests/parser/xml-parser-mutations.test.ts +579 -0
  162. package/tests/parser/xml-parser-scanner.test.ts +379 -0
  163. package/tests/parser/xml-parser.test.ts +17 -330
  164. package/tests/parser/yaml-parser-mutations.test.ts +750 -0
  165. package/tests/parser/yaml-parser.test.ts +844 -18
  166. package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
  167. package/tests/path-query/segment-filter-parser.test.ts +1091 -0
  168. package/tests/path-query/segment-parser-mutations.test.ts +539 -0
  169. package/tests/path-query/segment-parser.test.ts +606 -0
  170. package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
  171. package/tests/path-query/segment-path-resolver.test.ts +1009 -0
  172. package/tests/security/security-guard-advanced.test.ts +413 -0
  173. package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
  174. package/tests/security/security-guard.test.ts +8 -479
  175. package/tests/security/security-parser.test.ts +18 -14
  176. package/vitest.config.ts +3 -3
  177. package/benchmarks/get.bench.ts +0 -26
  178. package/benchmarks/parse.bench.ts +0 -41
  179. 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
- expect(result['text']).toBe('hello\nworld');
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
- expect(result['text']).toBe('hello world');
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('falls back to raw string for unquoted inline array (not JSON-compatible)', () => {
134
- // unquoted identifiers are not valid JSON → fallback to raw string
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']).toBe('[a, b, c]');
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 unsafe constructs`, () => {
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 regex should not match
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 over-indented key at block start`, () => {
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('strips trailing newline from literal block scalar with trailing blank line', () => {
350
- expect(makeParser().parse('text: |\n hello\n\n')).toEqual({ text: 'hello' });
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('strips trailing space from folded block scalar with trailing blank line', () => {
354
- expect(makeParser().parse('text: >\n hello\n\n')).toEqual({ text: 'hello' });
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 extended`, () => {
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 blank lines and folded`, () => {
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('drops a root-level comment line inside a literal block scalar', () => {
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\nsecond',
459
+ text: 'first\n',
453
460
  });
454
461
  });
455
462
  });
456
463
 
457
- describe(`${YamlParser.name} > inline flow key with space before colon`, () => {
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
+ });