@safeaccess/inline 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.gitattributes +16 -0
  2. package/.gitkeep +0 -0
  3. package/CHANGELOG.md +38 -0
  4. package/LICENSE +21 -0
  5. package/README.md +454 -0
  6. package/benchmarks/get.bench.ts +26 -0
  7. package/benchmarks/parse.bench.ts +41 -0
  8. package/dist/accessors/abstract-accessor.d.ts +213 -0
  9. package/dist/accessors/abstract-accessor.js +294 -0
  10. package/dist/accessors/formats/any-accessor.d.ts +35 -0
  11. package/dist/accessors/formats/any-accessor.js +44 -0
  12. package/dist/accessors/formats/array-accessor.d.ts +26 -0
  13. package/dist/accessors/formats/array-accessor.js +39 -0
  14. package/dist/accessors/formats/env-accessor.d.ts +27 -0
  15. package/dist/accessors/formats/env-accessor.js +64 -0
  16. package/dist/accessors/formats/ini-accessor.d.ts +41 -0
  17. package/dist/accessors/formats/ini-accessor.js +109 -0
  18. package/dist/accessors/formats/json-accessor.d.ts +26 -0
  19. package/dist/accessors/formats/json-accessor.js +56 -0
  20. package/dist/accessors/formats/ndjson-accessor.d.ts +28 -0
  21. package/dist/accessors/formats/ndjson-accessor.js +71 -0
  22. package/dist/accessors/formats/object-accessor.d.ts +48 -0
  23. package/dist/accessors/formats/object-accessor.js +90 -0
  24. package/dist/accessors/formats/xml-accessor.d.ts +27 -0
  25. package/dist/accessors/formats/xml-accessor.js +52 -0
  26. package/dist/accessors/formats/yaml-accessor.d.ts +29 -0
  27. package/dist/accessors/formats/yaml-accessor.js +46 -0
  28. package/dist/contracts/accessors-interface.d.ts +11 -0
  29. package/dist/contracts/accessors-interface.js +1 -0
  30. package/dist/contracts/factory-accessors-interface.d.ts +16 -0
  31. package/dist/contracts/factory-accessors-interface.js +1 -0
  32. package/dist/contracts/parse-integration-interface.d.ts +31 -0
  33. package/dist/contracts/parse-integration-interface.js +1 -0
  34. package/dist/contracts/path-cache-interface.d.ts +40 -0
  35. package/dist/contracts/path-cache-interface.js +1 -0
  36. package/dist/contracts/readable-accessors-interface.d.ts +79 -0
  37. package/dist/contracts/readable-accessors-interface.js +1 -0
  38. package/dist/contracts/security-guard-interface.d.ts +40 -0
  39. package/dist/contracts/security-guard-interface.js +1 -0
  40. package/dist/contracts/security-parser-interface.d.ts +67 -0
  41. package/dist/contracts/security-parser-interface.js +1 -0
  42. package/dist/contracts/writable-accessors-interface.d.ts +65 -0
  43. package/dist/contracts/writable-accessors-interface.js +1 -0
  44. package/dist/core/dot-notation-parser.d.ts +204 -0
  45. package/dist/core/dot-notation-parser.js +343 -0
  46. package/dist/exceptions/accessor-exception.d.ts +13 -0
  47. package/dist/exceptions/accessor-exception.js +16 -0
  48. package/dist/exceptions/invalid-format-exception.d.ts +14 -0
  49. package/dist/exceptions/invalid-format-exception.js +17 -0
  50. package/dist/exceptions/parser-exception.d.ts +14 -0
  51. package/dist/exceptions/parser-exception.js +17 -0
  52. package/dist/exceptions/path-not-found-exception.d.ts +14 -0
  53. package/dist/exceptions/path-not-found-exception.js +17 -0
  54. package/dist/exceptions/readonly-violation-exception.d.ts +15 -0
  55. package/dist/exceptions/readonly-violation-exception.js +18 -0
  56. package/dist/exceptions/security-exception.d.ts +18 -0
  57. package/dist/exceptions/security-exception.js +21 -0
  58. package/dist/exceptions/unsupported-type-exception.d.ts +14 -0
  59. package/dist/exceptions/unsupported-type-exception.js +17 -0
  60. package/dist/exceptions/yaml-parse-exception.d.ts +17 -0
  61. package/dist/exceptions/yaml-parse-exception.js +20 -0
  62. package/dist/index.d.ts +30 -0
  63. package/dist/index.js +30 -0
  64. package/dist/inline.d.ts +402 -0
  65. package/dist/inline.js +512 -0
  66. package/dist/parser/xml-parser.d.ts +46 -0
  67. package/dist/parser/xml-parser.js +288 -0
  68. package/dist/parser/yaml-parser.d.ts +94 -0
  69. package/dist/parser/yaml-parser.js +286 -0
  70. package/dist/security/forbidden-keys.d.ts +34 -0
  71. package/dist/security/forbidden-keys.js +80 -0
  72. package/dist/security/security-guard.d.ts +94 -0
  73. package/dist/security/security-guard.js +172 -0
  74. package/dist/security/security-parser.d.ts +130 -0
  75. package/dist/security/security-parser.js +192 -0
  76. package/dist/type-format.d.ts +28 -0
  77. package/dist/type-format.js +29 -0
  78. package/eslint.config.js +1 -0
  79. package/package.json +39 -0
  80. package/src/accessors/abstract-accessor.ts +353 -0
  81. package/src/accessors/formats/any-accessor.ts +51 -0
  82. package/src/accessors/formats/array-accessor.ts +45 -0
  83. package/src/accessors/formats/env-accessor.ts +79 -0
  84. package/src/accessors/formats/ini-accessor.ts +124 -0
  85. package/src/accessors/formats/json-accessor.ts +66 -0
  86. package/src/accessors/formats/ndjson-accessor.ts +82 -0
  87. package/src/accessors/formats/object-accessor.ts +100 -0
  88. package/src/accessors/formats/xml-accessor.ts +58 -0
  89. package/src/accessors/formats/yaml-accessor.ts +52 -0
  90. package/src/contracts/accessors-interface.ts +12 -0
  91. package/src/contracts/factory-accessors-interface.ts +16 -0
  92. package/src/contracts/parse-integration-interface.ts +32 -0
  93. package/src/contracts/path-cache-interface.ts +43 -0
  94. package/src/contracts/readable-accessors-interface.ts +88 -0
  95. package/src/contracts/security-guard-interface.ts +43 -0
  96. package/src/contracts/security-parser-interface.ts +74 -0
  97. package/src/contracts/writable-accessors-interface.ts +70 -0
  98. package/src/core/dot-notation-parser.ts +419 -0
  99. package/src/exceptions/accessor-exception.ts +16 -0
  100. package/src/exceptions/invalid-format-exception.ts +18 -0
  101. package/src/exceptions/parser-exception.ts +18 -0
  102. package/src/exceptions/path-not-found-exception.ts +18 -0
  103. package/src/exceptions/readonly-violation-exception.ts +19 -0
  104. package/src/exceptions/security-exception.ts +22 -0
  105. package/src/exceptions/unsupported-type-exception.ts +18 -0
  106. package/src/exceptions/yaml-parse-exception.ts +21 -0
  107. package/src/index.ts +46 -0
  108. package/src/inline.ts +570 -0
  109. package/src/parser/xml-parser.ts +334 -0
  110. package/src/parser/yaml-parser.ts +368 -0
  111. package/src/security/forbidden-keys.ts +81 -0
  112. package/src/security/security-guard.ts +195 -0
  113. package/src/security/security-parser.ts +233 -0
  114. package/src/type-format.ts +28 -0
  115. package/stryker.config.json +24 -0
  116. package/tests/accessors/accessors.test.ts +1017 -0
  117. package/tests/accessors/json-accessor.test.ts +171 -0
  118. package/tests/core/dot-notation-parser.test.ts +587 -0
  119. package/tests/exceptions/parser-exception.test.ts +31 -0
  120. package/tests/inline.test.ts +445 -0
  121. package/tests/mocks/fake-parse-integration.ts +24 -0
  122. package/tests/mocks/fake-path-cache.ts +31 -0
  123. package/tests/parity.test.ts +164 -0
  124. package/tests/parser/xml-parser.test.ts +618 -0
  125. package/tests/parser/yaml-parser.test.ts +463 -0
  126. package/tests/security/security-guard.test.ts +646 -0
  127. package/tests/security/security-parser.test.ts +391 -0
  128. package/tsconfig.json +16 -0
  129. package/vitest.config.ts +19 -0
@@ -0,0 +1,463 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { YamlParser } from '../../src/parser/yaml-parser.js';
3
+ import { YamlParseException } from '../../src/exceptions/yaml-parse-exception.js';
4
+
5
+ function makeParser(): YamlParser {
6
+ return new YamlParser();
7
+ }
8
+
9
+ describe(YamlParser.name, () => {
10
+ it('parses a simple key-value pair', () => {
11
+ expect(makeParser().parse('name: Alice')).toEqual({ name: 'Alice' });
12
+ });
13
+
14
+ it('returns empty object for empty string', () => {
15
+ expect(makeParser().parse('')).toEqual({});
16
+ });
17
+
18
+ it('returns empty object for comment-only input', () => {
19
+ expect(makeParser().parse('# comment only')).toEqual({});
20
+ });
21
+
22
+ it('parses multiple root-level keys', () => {
23
+ expect(makeParser().parse('a: 1\nb: 2')).toEqual({ a: 1, b: 2 });
24
+ });
25
+
26
+ it('parses nested keys', () => {
27
+ expect(makeParser().parse('user:\n name: Alice\n age: 30')).toEqual({
28
+ user: { name: 'Alice', age: 30 },
29
+ });
30
+ });
31
+
32
+ it('parses a sequence of scalars', () => {
33
+ const result = makeParser().parse('- a\n- b\n- c');
34
+ expect(result).toEqual({});
35
+ });
36
+ });
37
+
38
+ describe(`${YamlParser.name} > scalar types`, () => {
39
+ it('casts integer values', () => {
40
+ expect(makeParser().parse('count: 42')).toEqual({ count: 42 });
41
+ });
42
+
43
+ it('casts negative integers', () => {
44
+ expect(makeParser().parse('offset: -10')).toEqual({ offset: -10 });
45
+ });
46
+
47
+ it('casts float values', () => {
48
+ expect(makeParser().parse('ratio: 3.14')).toEqual({ ratio: 3.14 });
49
+ });
50
+
51
+ it('casts true boolean', () => {
52
+ expect(makeParser().parse('active: true')).toEqual({ active: true });
53
+ });
54
+
55
+ it('casts false boolean', () => {
56
+ expect(makeParser().parse('active: false')).toEqual({ active: false });
57
+ });
58
+
59
+ it('casts null value', () => {
60
+ expect(makeParser().parse('value: null')).toEqual({ value: null });
61
+ });
62
+
63
+ it('casts tilde ~ as null', () => {
64
+ expect(makeParser().parse('value: ~')).toEqual({ value: null });
65
+ });
66
+
67
+ it('casts empty value as null', () => {
68
+ expect(makeParser().parse('value: ')).toEqual({ value: null });
69
+ });
70
+
71
+ it('preserves double-quoted strings without stripping content', () => {
72
+ expect(makeParser().parse('msg: "hello world"')).toEqual({ msg: 'hello world' });
73
+ });
74
+
75
+ it('preserves single-quoted strings without stripping content', () => {
76
+ expect(makeParser().parse("msg: 'hello world'")).toEqual({ msg: 'hello world' });
77
+ });
78
+
79
+ it('preserves string values that look like numbers (quoted)', () => {
80
+ expect(makeParser().parse("code: '007'")).toEqual({ code: '007' });
81
+ });
82
+ });
83
+
84
+ describe(`${YamlParser.name} > CRLF handling`, () => {
85
+ it('parses CRLF line endings correctly', () => {
86
+ expect(makeParser().parse('a: 1\r\nb: 2')).toEqual({ a: 1, b: 2 });
87
+ });
88
+ });
89
+
90
+ describe(`${YamlParser.name} > sequences`, () => {
91
+ it('parses a sequence under a key', () => {
92
+ const result = makeParser().parse('items:\n - apple\n - banana');
93
+ expect(result).toEqual({ items: ['apple', 'banana'] });
94
+ });
95
+
96
+ it('parses a sequence with map items', () => {
97
+ const yaml = 'items:\n - name: Alice\n - name: Bob';
98
+ const result = makeParser().parse(yaml);
99
+ expect(result).toEqual({ items: [{ name: 'Alice' }, { name: 'Bob' }] });
100
+ });
101
+
102
+ it('parses empty sequence item as null', () => {
103
+ const yaml = 'items:\n -\n - b';
104
+ const result = makeParser().parse(yaml);
105
+ expect((result['items'] as unknown[])[0]).toBeNull();
106
+ expect((result['items'] as unknown[])[1]).toBe('b');
107
+ });
108
+ });
109
+
110
+ describe(`${YamlParser.name} > block scalars`, () => {
111
+ it('parses literal block scalar |', () => {
112
+ const yaml = 'text: |\n hello\n world';
113
+ const result = makeParser().parse(yaml);
114
+ expect(result['text']).toBe('hello\nworld');
115
+ });
116
+
117
+ it('parses folded block scalar >', () => {
118
+ const yaml = 'text: >\n hello\n world';
119
+ const result = makeParser().parse(yaml);
120
+ expect(result['text']).toBe('hello world');
121
+ });
122
+ });
123
+
124
+ describe(`${YamlParser.name} > inline flow`, () => {
125
+ it('parses simple inline array of quoted strings', () => {
126
+ // Only JSON-compatible inline flows are parsed correctly
127
+ const yaml = "tags: ['a', 'b', 'c']";
128
+ const result = makeParser().parse(yaml);
129
+ // single quotes converted to double for JSON.parse → valid JSON array
130
+ expect(result['tags']).toEqual(['a', 'b', 'c']);
131
+ });
132
+
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
+ const yaml = 'tags: [a, b, c]';
136
+ const result = makeParser().parse(yaml);
137
+ expect(result['tags']).toBe('[a, b, c]');
138
+ });
139
+
140
+ it('falls back to raw string for unparseable inline flow', () => {
141
+ // Intentionally malformed to trigger fallback
142
+ const yaml = 'tags: [a b c';
143
+ const result = makeParser().parse(yaml);
144
+ expect(result['tags']).toBe('[a b c');
145
+ });
146
+ });
147
+
148
+ describe(`${YamlParser.name} > security — unsafe constructs`, () => {
149
+ it('throws YamlParseException for !! tags', () => {
150
+ expect(() => makeParser().parse('key: !!python/object foo')).toThrow(YamlParseException);
151
+ });
152
+
153
+ it('throws with a message mentioning tags for !! syntax', () => {
154
+ expect(() => makeParser().parse('key: !!str value')).toThrow(/tag/i);
155
+ });
156
+
157
+ it('includes the line number in the !! tag error message', () => {
158
+ expect(() => makeParser().parse('\nkey: !!str value')).toThrow(/line 2/);
159
+ });
160
+
161
+ it('throws YamlParseException for ! tags', () => {
162
+ expect(() => makeParser().parse('key: !custom value')).toThrow(YamlParseException);
163
+ });
164
+
165
+ it('throws YamlParseException for anchors (&)', () => {
166
+ expect(() => makeParser().parse('a: &anchor hello')).toThrow(YamlParseException);
167
+ });
168
+
169
+ it('throws YamlParseException for anchor at start of line (no preceding space)', () => {
170
+ expect(() => makeParser().parse('&anchor: value')).toThrow(YamlParseException);
171
+ });
172
+
173
+ it('throws with a message mentioning anchors', () => {
174
+ expect(() => makeParser().parse('a: &anchor hello')).toThrow(/anchor/i);
175
+ });
176
+
177
+ it('includes the line number in the anchor error message', () => {
178
+ expect(() => makeParser().parse('\na: &anchor hello')).toThrow(/line 2/);
179
+ });
180
+
181
+ it('detects anchor on a line whose value ends with a hash character', () => {
182
+ expect(() => makeParser().parse('a: &anchor value#')).toThrow(YamlParseException);
183
+ });
184
+
185
+ it('throws YamlParseException for aliases (*)', () => {
186
+ expect(() => makeParser().parse('a: *alias')).toThrow(YamlParseException);
187
+ });
188
+
189
+ it('throws YamlParseException for alias at start of line (no preceding space)', () => {
190
+ expect(() => makeParser().parse('*alias: value')).toThrow(YamlParseException);
191
+ });
192
+
193
+ it('throws with a message mentioning aliases', () => {
194
+ expect(() => makeParser().parse('a: *alias')).toThrow(/alias/i);
195
+ });
196
+
197
+ it('includes the line number in the alias error message', () => {
198
+ expect(() => makeParser().parse('\na: *alias')).toThrow(/line 2/);
199
+ });
200
+
201
+ it('throws YamlParseException for merge keys (<<)', () => {
202
+ expect(() => makeParser().parse('<<: {a: 1}')).toThrow(YamlParseException);
203
+ });
204
+
205
+ it('throws YamlParseException for indented merge key', () => {
206
+ expect(() => makeParser().parse('mapping:\n <<: {a: 1}')).toThrow(YamlParseException);
207
+ });
208
+
209
+ it('throws YamlParseException for merge key with space before colon', () => {
210
+ expect(() => makeParser().parse('<< : {a: 1}')).toThrow(YamlParseException);
211
+ });
212
+
213
+ it('throws with a message mentioning merge keys', () => {
214
+ expect(() => makeParser().parse('<<: {a: 1}')).toThrow(/merge key/i);
215
+ });
216
+
217
+ it('includes the line number in the merge key error message', () => {
218
+ expect(() => makeParser().parse('\n<<: {a: 1}')).toThrow(/line 2/);
219
+ });
220
+
221
+ it('does not throw for <<: appearing inside a string value', () => {
222
+ expect(() => makeParser().parse('note: use <<: syntax')).not.toThrow();
223
+ });
224
+
225
+ it('does not throw for ! inside double-quoted string', () => {
226
+ // quoted string — regex should not match
227
+ expect(() => makeParser().parse('msg: "hello world"')).not.toThrow();
228
+ });
229
+ });
230
+
231
+ describe(`${YamlParser.name} > top-level non-map`, () => {
232
+ it('returns empty object when top-level is a sequence (not a map)', () => {
233
+ // Top-level sequences can't be a Record, so parse returns {}
234
+ const yaml = '- a\n- b';
235
+ const result = makeParser().parse(yaml);
236
+ expect(result).toEqual({});
237
+ });
238
+ });
239
+
240
+ describe(`${YamlParser.name} > mergeChildLines`, () => {
241
+ it('parses nested map inside sequence item with sibling keys', () => {
242
+ const yaml = 'users:\n - name: Alice\n role: admin\n - name: Bob\n role: user';
243
+ const result = makeParser().parse(yaml);
244
+ expect(result).toEqual({
245
+ users: [
246
+ { name: 'Alice', role: 'admin' },
247
+ { name: 'Bob', role: 'user' },
248
+ ],
249
+ });
250
+ });
251
+
252
+ it('preserves sibling key after a nested block within a sequence item', () => {
253
+ const yaml = 'list:\n - a: 1\n b:\n c: 2\n d: 3';
254
+ expect(makeParser().parse(yaml)).toEqual({ list: [{ a: 1, b: { c: 2 }, d: 3 }] });
255
+ });
256
+
257
+ it('skips a comment line containing a colon inside a sequence item child block', () => {
258
+ const yaml = 'items:\n - name: Alice\n # role: admin\n active: true';
259
+ expect(makeParser().parse(yaml)).toEqual({ items: [{ name: 'Alice', active: true }] });
260
+ });
261
+ });
262
+
263
+ describe(`${YamlParser.name} > comments`, () => {
264
+ it('skips inline comment lines', () => {
265
+ const yaml = '# comment\nname: Alice\n# another comment';
266
+ expect(makeParser().parse(yaml)).toEqual({ name: 'Alice' });
267
+ });
268
+
269
+ it('parses a key whose value ends with a hash character', () => {
270
+ expect(makeParser().parse('key: value#')).toEqual({ key: 'value#' });
271
+ });
272
+
273
+ it('includes child keys that follow a root-level comment inside a nested block', () => {
274
+ const yaml = 'parent:\n key1: value1\n# root comment\n key2: value2';
275
+ expect(makeParser().parse(yaml)).toEqual({ parent: { key1: 'value1', key2: 'value2' } });
276
+ });
277
+ });
278
+
279
+ describe(`${YamlParser.name} > nested blocks with blank lines and indentation`, () => {
280
+ it('handles empty lines inside a nested block', () => {
281
+ expect(makeParser().parse('user:\n name: Alice\n\n age: 30')).toEqual({
282
+ user: { name: 'Alice', age: 30 },
283
+ });
284
+ });
285
+
286
+ it('handles comment lines inside a nested block', () => {
287
+ expect(makeParser().parse('user:\n name: Alice\n # inline comment\n age: 30')).toEqual({
288
+ user: { name: 'Alice', age: 30 },
289
+ });
290
+ });
291
+
292
+ it('ignores over-indented lines relative to current block', () => {
293
+ const yaml = 'user:\n name: Alice\n extra: ignored\n age: 30';
294
+ const result = makeParser().parse(yaml);
295
+ expect(result).toEqual({ user: { name: 'Alice', age: 30 } });
296
+ });
297
+ });
298
+
299
+ describe(`${YamlParser.name} > sequence bare dash with block children`, () => {
300
+ it('parses bare dash followed by indented map as a map item', () => {
301
+ const yaml = 'items:\n -\n name: Alice\n role: admin\n - b';
302
+ const result = makeParser().parse(yaml);
303
+ expect((result['items'] as unknown[])[0]).toEqual({ name: 'Alice', role: 'admin' });
304
+ expect((result['items'] as unknown[])[1]).toBe('b');
305
+ });
306
+ });
307
+
308
+ describe(`${YamlParser.name} > top-level result type`, () => {
309
+ it('result is not an array when top-level is a sequence', () => {
310
+ expect(Array.isArray(makeParser().parse('- a\n- b'))).toBe(false);
311
+ });
312
+ });
313
+
314
+ describe(`${YamlParser.name} > indentation — over-indented key at block start`, () => {
315
+ it('ignores an over-indented key appearing before a properly-indented sibling', () => {
316
+ expect(makeParser().parse('outer:\n over_indented: ignored\n normal: value')).toEqual({
317
+ outer: { normal: 'value' },
318
+ });
319
+ });
320
+ });
321
+
322
+ describe(`${YamlParser.name} > sequence item content trimming`, () => {
323
+ it('trims extra leading space when sequence item has multiple spaces after dash', () => {
324
+ expect(makeParser().parse('items:\n - value')).toEqual({ items: ['value'] });
325
+ });
326
+ });
327
+
328
+ describe(`${YamlParser.name} > raw value whitespace`, () => {
329
+ it('strips trailing whitespace from a scalar value', () => {
330
+ expect(makeParser().parse('key: value ')).toEqual({ key: 'value' });
331
+ });
332
+ });
333
+
334
+ describe(`${YamlParser.name} > non-key lines ignored`, () => {
335
+ it('skips lines with no colon at root level and parses valid keys that follow', () => {
336
+ expect(makeParser().parse('just_a_value\nkey: valid')).toEqual({ key: 'valid' });
337
+ });
338
+ });
339
+
340
+ describe(`${YamlParser.name} > inline flow objects`, () => {
341
+ it('parses inline flow object with single-quoted values', () => {
342
+ expect(makeParser().parse("config: {host: 'localhost', port: '8080'}")).toEqual({
343
+ config: { host: 'localhost', port: '8080' },
344
+ });
345
+ });
346
+ });
347
+
348
+ 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
+ });
352
+
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' });
355
+ });
356
+ });
357
+
358
+ describe(`${YamlParser.name} > partially-quoted strings`, () => {
359
+ it('does not strip a string that starts with double-quote but lacks closing quote', () => {
360
+ expect(makeParser().parse('msg: "hello')).toEqual({ msg: '"hello' });
361
+ });
362
+
363
+ it('does not strip a string that ends with double-quote but lacks opening quote', () => {
364
+ expect(makeParser().parse('msg: hello"')).toEqual({ msg: 'hello"' });
365
+ });
366
+
367
+ it('does not strip a string that starts with single-quote but lacks closing quote', () => {
368
+ expect(makeParser().parse("msg: 'hello")).toEqual({ msg: "'hello" });
369
+ });
370
+
371
+ it('does not strip a string that ends with single-quote but lacks opening quote', () => {
372
+ expect(makeParser().parse("msg: hello'")).toEqual({ msg: "hello'" });
373
+ });
374
+
375
+ it('does not strip a string that starts and ends with different quote chars', () => {
376
+ expect(makeParser().parse('msg: "hello\'')).toEqual({ msg: '"hello\'' });
377
+ });
378
+ });
379
+
380
+ describe(`${YamlParser.name} > float parsing edge cases`, () => {
381
+ it('does not parse a string with leading digit group but extra suffix as float', () => {
382
+ expect(makeParser().parse('val: 3.14extra')).toEqual({ val: '3.14extra' });
383
+ });
384
+
385
+ it('does not parse a string with trailing decimal dot as float', () => {
386
+ expect(makeParser().parse('val: 3.')).toEqual({ val: '3.' });
387
+ });
388
+ });
389
+
390
+ describe(`${YamlParser.name} > mergeChildLines sibling rows`, () => {
391
+ it('parses only sibling keys at the correct indentation in mergeChildLines', () => {
392
+ const yaml =
393
+ 'items:\n - role: admin\n level: 1\n name: Alice\n - role: user\n name: Bob';
394
+ const result = makeParser().parse(yaml);
395
+ expect(result).toEqual({
396
+ items: [
397
+ { role: 'admin', level: 1, name: 'Alice' },
398
+ { role: 'user', name: 'Bob' },
399
+ ],
400
+ });
401
+ });
402
+
403
+ it('stops mergeChildLines when indentation drops below child indent', () => {
404
+ const yaml = 'items:\n - role: admin\n level: 1\n - role: user';
405
+ const result = makeParser().parse(yaml);
406
+ const items = result['items'] as unknown[];
407
+ expect((items[0] as Record<string, unknown>)['level']).toBe(1);
408
+ expect((items[1] as Record<string, unknown>)['level']).toBeUndefined();
409
+ });
410
+
411
+ it('ignores an over-indented sibling line in mergeChildLines', () => {
412
+ const yaml = 'items:\n - role: admin\n over: ignored\n level: 1\n - role: user';
413
+ const result = makeParser().parse(yaml);
414
+ const items = result['items'] as unknown[];
415
+ expect((items[0] as Record<string, unknown>)['level']).toBe(1);
416
+ expect((items[0] as Record<string, unknown>)['over']).toBeUndefined();
417
+ expect((items[1] as Record<string, unknown>)['role']).toBe('user');
418
+ });
419
+
420
+ it('skips a non-key-value sibling line in mergeChildLines', () => {
421
+ const yaml = 'items:\n - key: value\n baretext\n name: Alice';
422
+ const result = makeParser().parse(yaml);
423
+ const items = result['items'] as unknown[];
424
+ expect((items[0] as Record<string, unknown>)['key']).toBe('value');
425
+ expect((items[0] as Record<string, unknown>)['name']).toBe('Alice');
426
+ });
427
+ });
428
+
429
+ describe(`${YamlParser.name} > scalar types — extended`, () => {
430
+ it('casts multi-digit float values', () => {
431
+ expect(makeParser().parse('ratio: 10.5')).toEqual({ ratio: 10.5 });
432
+ });
433
+
434
+ it('does not parse a value with non-digit prefix as float', () => {
435
+ expect(makeParser().parse('val: abc3.14')).toEqual({ val: 'abc3.14' });
436
+ });
437
+
438
+ it('parses keys with multiple spaces after colon', () => {
439
+ expect(makeParser().parse('key: value')).toEqual({ key: 'value' });
440
+ });
441
+ });
442
+
443
+ describe(`${YamlParser.name} > block scalar — blank lines and folded`, () => {
444
+ it('preserves a blank line in the middle of a literal block scalar', () => {
445
+ expect(makeParser().parse('text: |\n hello\n\n world')).toEqual({
446
+ text: 'hello\n\nworld',
447
+ });
448
+ });
449
+
450
+ it('drops a root-level comment line inside a literal block scalar', () => {
451
+ expect(makeParser().parse('text: |\n first\n# root comment\n second')).toEqual({
452
+ text: 'first\nsecond',
453
+ });
454
+ });
455
+ });
456
+
457
+ describe(`${YamlParser.name} > inline flow — key with space before colon`, () => {
458
+ it('parses inline object with space before colon in key', () => {
459
+ expect(makeParser().parse("config: {key : 'value'}")).toEqual({
460
+ config: { key: 'value' },
461
+ });
462
+ });
463
+ });