@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
@@ -7,14 +7,26 @@ import { YamlParseException } from '../exceptions/yaml-parse-exception.js';
7
7
  * tags (!! and !), anchors (&), aliases (*), and merge keys (<<).
8
8
  *
9
9
  * Does not depend on external YAML libraries, making the package portable.
10
+ *
11
+ * @internal
10
12
  */
11
13
  export class YamlParser {
14
+ private readonly maxDepth: number;
15
+
16
+ /**
17
+ * @param maxDepth - Maximum allowed nesting depth during parsing.
18
+ * Defaults to 512 to match SecurityParser.maxDepth.
19
+ */
20
+ constructor(maxDepth: number = 512) {
21
+ this.maxDepth = maxDepth;
22
+ }
23
+
12
24
  /**
13
25
  * Parse a YAML string into a plain object.
14
26
  *
15
27
  * @param yaml - Raw YAML content.
16
28
  * @returns Parsed data structure.
17
- * @throws {YamlParseException} When unsafe constructs or syntax errors are found.
29
+ * @throws {YamlParseException} When unsafe constructs, syntax errors, or nesting depth exceeded.
18
30
  *
19
31
  * @example
20
32
  * new YamlParser().parse('key: value'); // { key: 'value' }
@@ -22,7 +34,7 @@ export class YamlParser {
22
34
  parse(yaml: string): Record<string, unknown> {
23
35
  const lines = yaml.replace(/\r\n/g, '\n').split('\n');
24
36
  this.assertNoUnsafeConstructs(lines);
25
- const result = this.parseLines(lines, 0, 0, lines.length);
37
+ const result = this.parseLines(lines, 0, 0, lines.length, 0);
26
38
  if (Array.isArray(result)) {
27
39
  return {};
28
40
  }
@@ -73,9 +85,23 @@ export class YamlParser {
73
85
  * @param baseIndent - Indentation level for this block.
74
86
  * @param start - First line index (inclusive).
75
87
  * @param end - Last line index (exclusive).
88
+ * @param depth - Current nesting depth.
76
89
  * @returns Parsed value.
90
+ *
91
+ * @throws {YamlParseException} When nesting depth exceeds the configured maximum.
77
92
  */
78
- private parseLines(lines: string[], baseIndent: number, start: number, end: number): unknown {
93
+ private parseLines(
94
+ lines: string[],
95
+ baseIndent: number,
96
+ start: number,
97
+ end: number,
98
+ depth: number = 0,
99
+ ): unknown {
100
+ if (depth > this.maxDepth) {
101
+ throw new YamlParseException(
102
+ `YAML nesting depth ${depth} exceeds maximum of ${this.maxDepth}.`,
103
+ );
104
+ }
79
105
  const mapResult: Record<string, unknown> = {};
80
106
  const arrResult: unknown[] = [];
81
107
  let isSequence = false;
@@ -120,15 +146,18 @@ export class YamlParser {
120
146
  i,
121
147
  childIndent,
122
148
  childEnd,
149
+ depth,
123
150
  );
124
- this.mergeChildLines(lines, i + 1, childEnd, childIndent, subMap);
151
+ this.mergeChildLines(lines, i + 1, childEnd, childIndent, subMap, depth);
125
152
  arrResult.push(subMap);
126
153
  i = childEnd;
127
154
  } else if (itemContent === '') {
128
155
  const childIndent = currentIndent + 2;
129
156
  const childEnd = this.findBlockEnd(lines, childIndent, i + 1, end);
130
157
  if (childEnd > i + 1) {
131
- arrResult.push(this.parseLines(lines, childIndent, i + 1, childEnd));
158
+ arrResult.push(
159
+ this.parseLines(lines, childIndent, i + 1, childEnd, depth + 1),
160
+ );
132
161
  i = childEnd;
133
162
  } else {
134
163
  arrResult.push(null);
@@ -148,7 +177,14 @@ export class YamlParser {
148
177
  const rawValue = mapMatch[2] as string;
149
178
  const childIndent = currentIndent + 2;
150
179
  const childEnd = this.findBlockEnd(lines, childIndent, i + 1, end);
151
- mapResult[key] = this.resolveValue(rawValue, lines, i, childIndent, childEnd);
180
+ mapResult[key] = this.resolveValue(
181
+ rawValue,
182
+ lines,
183
+ i,
184
+ childIndent,
185
+ childEnd,
186
+ depth,
187
+ );
152
188
  i = childEnd;
153
189
  continue;
154
190
  }
@@ -171,6 +207,8 @@ export class YamlParser {
171
207
  * @param end - Last child line index (exclusive).
172
208
  * @param childIndent - Expected indentation for child lines.
173
209
  * @param map - Map to merge values into.
210
+ * @param depth - Current nesting depth.
211
+ * @throws {YamlParseException} When resolved values exceed the configured nesting depth.
174
212
  */
175
213
  private mergeChildLines(
176
214
  lines: string[],
@@ -178,6 +216,7 @@ export class YamlParser {
178
216
  end: number,
179
217
  childIndent: number,
180
218
  map: Record<string, unknown>,
219
+ depth: number,
181
220
  ): void {
182
221
  let ci = start;
183
222
  while (ci < end) {
@@ -206,6 +245,7 @@ export class YamlParser {
206
245
  ci,
207
246
  childCurrentIndent + 2,
208
247
  nextChildEnd,
248
+ depth,
209
249
  );
210
250
  ci = nextChildEnd;
211
251
  continue;
@@ -224,7 +264,9 @@ export class YamlParser {
224
264
  * @param lineIndex - Line where the value was found.
225
265
  * @param childIndent - Expected indentation for child block.
226
266
  * @param childEnd - End index of the child block.
267
+ * @param depth - Current nesting depth.
227
268
  * @returns Resolved typed value.
269
+ * @throws {YamlParseException} When child block nesting exceeds the configured maximum.
228
270
  */
229
271
  private resolveValue(
230
272
  rawValue: string,
@@ -232,41 +274,49 @@ export class YamlParser {
232
274
  lineIndex: number,
233
275
  childIndent: number,
234
276
  childEnd: number,
277
+ depth: number = 0,
235
278
  ): unknown {
236
- const trimmed = rawValue.trim();
279
+ const trimmed = this.stripInlineComment(rawValue.trim());
237
280
 
238
- // Block scalar literal (|) or folded (>)
239
- if (trimmed === '|' || trimmed === '>') {
281
+ // Block scalar literal (|, |-, |+) or folded (>, >-, >+)
282
+ if (/^\|[+-]?$/.test(trimmed) || /^>[+-]?$/.test(trimmed)) {
240
283
  return this.parseBlockScalar(
241
284
  lines,
242
285
  lineIndex + 1,
243
286
  childEnd,
244
287
  childIndent,
245
- trimmed === '>',
288
+ trimmed.startsWith('>'),
289
+ trimmed,
246
290
  );
247
291
  }
248
292
 
249
- // Inline flow (array or map) starting with [ or {
250
- if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
251
- return this.parseInlineFlow(trimmed);
293
+ // Inline flow sequence [a, b, c]
294
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
295
+ return this.parseFlowSequence(trimmed);
296
+ }
297
+
298
+ // Inline flow map {a: b, c: d}
299
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
300
+ return this.parseFlowMap(trimmed);
252
301
  }
253
302
 
254
303
  // Nested block
255
304
  if (trimmed === '' && childEnd > lineIndex + 1) {
256
- return this.parseLines(lines, childIndent, lineIndex + 1, childEnd);
305
+ return this.parseLines(lines, childIndent, lineIndex + 1, childEnd, depth + 1);
257
306
  }
258
307
 
259
308
  return this.castScalar(trimmed);
260
309
  }
261
310
 
262
311
  /**
263
- * Parse a YAML block scalar (literal | or folded >).
312
+ * Parse a YAML block scalar (literal | or folded >) with chomping modifiers.
264
313
  *
265
314
  * @param lines - All lines of the YAML document.
266
315
  * @param start - First line of the scalar block (inclusive).
267
316
  * @param end - Last line of the scalar block (exclusive).
268
- * @param indent - Minimum indentation for scalar lines.
317
+ * @param indent - Expected minimum indentation for scalar lines.
269
318
  * @param folded - Whether to fold lines (> style) or keep literal (| style).
319
+ * @param chomping - Chomping indicator (|, |-, |+, >, >-, >+).
270
320
  * @returns The assembled string.
271
321
  */
272
322
  private parseBlockScalar(
@@ -275,65 +325,280 @@ export class YamlParser {
275
325
  end: number,
276
326
  indent: number,
277
327
  folded: boolean,
328
+ chomping: string = '|',
278
329
  ): string {
279
- const parts: string[] = [];
330
+ const blockLines: string[] = [];
331
+ let actualIndent: number | null = null;
332
+
280
333
  for (let i = start; i < end; i++) {
281
334
  const line = lines[i] as string;
282
- const trimmed = line.trimStart();
283
- const lineIndent = line.length - trimmed.length;
284
- if (lineIndent >= indent || trimmed === '') {
285
- parts.push(trimmed);
335
+
336
+ if (line.trim() === '') {
337
+ blockLines.push('');
338
+ continue;
339
+ }
340
+
341
+ const lineIndent = line.length - line.trimStart().length;
342
+ if (actualIndent === null) {
343
+ actualIndent = lineIndent;
344
+ }
345
+
346
+ if (lineIndent < actualIndent) {
347
+ break;
348
+ }
349
+
350
+ blockLines.push(line.substring(actualIndent));
351
+ }
352
+
353
+ // Remove trailing empty lines for clip (default) and strip (-) modes.
354
+ // Keep (+) mode preserves all trailing blank lines.
355
+ if (!chomping.endsWith('+')) {
356
+ while (blockLines.length > 0 && blockLines[blockLines.length - 1] === '') {
357
+ blockLines.pop();
286
358
  }
287
359
  }
288
360
 
361
+ let result: string;
289
362
  if (folded) {
290
- return parts.join(' ').trim();
363
+ result = '';
364
+ let prevEmpty = false;
365
+ for (const bl of blockLines) {
366
+ if (bl === '') {
367
+ result += '\n';
368
+ prevEmpty = true;
369
+ } else {
370
+ if (result !== '' && !prevEmpty && !result.endsWith('\n')) {
371
+ result += ' ';
372
+ }
373
+ result += bl;
374
+ prevEmpty = false;
375
+ }
376
+ }
377
+ } else {
378
+ result = blockLines.join('\n');
379
+ }
380
+
381
+ // Default YAML chomping: add trailing newline unless strip (-)
382
+ if (!chomping.endsWith('-') && !result.endsWith('\n')) {
383
+ result += '\n';
291
384
  }
292
385
 
293
- return parts.join('\n').trimEnd();
386
+ return result;
294
387
  }
295
388
 
296
389
  /**
297
- * Parse an inline flow collection (JSON-like array or object).
390
+ * Parse a YAML flow sequence ([a, b, c]) into an array.
298
391
  *
299
- * @param raw - Raw inline flow string.
300
- * @returns Parsed value or the raw string if parsing fails.
392
+ * @param value - Raw flow sequence string including brackets.
393
+ * @returns Parsed sequence values.
301
394
  */
302
- private parseInlineFlow(raw: string): unknown {
303
- try {
304
- // Convert YAML flow to JSON by single-quoting to double-quoting
305
- const jsonLike = raw.replace(/'/g, '"').replace(/(\w+)\s*:/g, '"$1":');
306
- return JSON.parse(jsonLike);
307
- } catch {
308
- // Fallback: return raw string
309
- return raw;
395
+ private parseFlowSequence(value: string): unknown[] {
396
+ const inner = value.slice(1, -1).trim();
397
+ if (inner === '') {
398
+ return [];
310
399
  }
400
+
401
+ const items = this.splitFlowItems(inner);
402
+ return items.map((item) => this.castScalar(item.trim()));
403
+ }
404
+
405
+ /**
406
+ * Parse a YAML flow map ({a: b, c: d}) into a record.
407
+ *
408
+ * @param value - Raw flow map string including braces.
409
+ * @returns Parsed key-value pairs.
410
+ */
411
+ private parseFlowMap(value: string): Record<string, unknown> {
412
+ const inner = value.slice(1, -1).trim();
413
+ if (inner === '') {
414
+ return {};
415
+ }
416
+
417
+ const result: Record<string, unknown> = {};
418
+ const items = this.splitFlowItems(inner);
419
+ for (const item of items) {
420
+ const trimmedItem = item.trim();
421
+ const colonPos = trimmedItem.indexOf(':');
422
+ if (colonPos === -1) {
423
+ continue;
424
+ }
425
+ const key = trimmedItem.substring(0, colonPos).trim();
426
+ const val = trimmedItem.substring(colonPos + 1).trim();
427
+ result[key] = this.castScalar(val);
428
+ }
429
+
430
+ return result;
431
+ }
432
+
433
+ /**
434
+ * Split flow-syntax items by comma, respecting nested brackets and quotes.
435
+ *
436
+ * @param inner - Content between outer brackets/braces.
437
+ * @returns Individual item strings.
438
+ */
439
+ private splitFlowItems(inner: string): string[] {
440
+ const items: string[] = [];
441
+ let depth = 0;
442
+ let current = '';
443
+ let inQuote = false;
444
+ let quoteChar = '';
445
+
446
+ for (let i = 0; i < inner.length; i++) {
447
+ const ch = inner[i] as string;
448
+
449
+ if (inQuote) {
450
+ current += ch;
451
+ if (ch === quoteChar) {
452
+ inQuote = false;
453
+ }
454
+ continue;
455
+ }
456
+
457
+ if (ch === '"' || ch === "'") {
458
+ inQuote = true;
459
+ quoteChar = ch;
460
+ current += ch;
461
+ continue;
462
+ }
463
+
464
+ if (ch === '[' || ch === '{') {
465
+ depth++;
466
+ current += ch;
467
+ continue;
468
+ }
469
+
470
+ if (ch === ']' || ch === '}') {
471
+ depth--;
472
+ current += ch;
473
+ continue;
474
+ }
475
+
476
+ if (ch === ',' && depth === 0) {
477
+ items.push(current);
478
+ current = '';
479
+ continue;
480
+ }
481
+
482
+ current += ch;
483
+ }
484
+
485
+ if (current.trim() !== '') {
486
+ items.push(current);
487
+ }
488
+
489
+ return items;
311
490
  }
312
491
 
313
492
  /**
314
493
  * Cast a scalar string to its native type.
315
494
  *
495
+ * Handles quoted strings, null, boolean, integer (decimal/octal/hex),
496
+ * float, infinity, and NaN values.
497
+ *
316
498
  * @param value - Raw scalar string.
317
499
  * @returns Typed value (null, boolean, number, or string).
318
500
  */
319
501
  private castScalar(value: string): unknown {
320
- if (value === '' || value === 'null' || value === '~') return null;
321
- if (value === 'true') return true;
322
- if (value === 'false') return false;
502
+ value = value.trim();
503
+
504
+ // Quoted strings
505
+ if (value.length >= 2) {
506
+ if (value.startsWith('"') && value.endsWith('"')) {
507
+ return this.unescapeDoubleQuoted(value.slice(1, -1));
508
+ }
509
+ if (value.startsWith("'") && value.endsWith("'")) {
510
+ return value.slice(1, -1).replace(/''/g, "'");
511
+ }
512
+ }
323
513
 
324
- // Quoted string
514
+ // Null
325
515
  if (
326
- (value.startsWith('"') && value.endsWith('"')) ||
327
- (value.startsWith("'") && value.endsWith("'"))
516
+ value === '' ||
517
+ value === '~' ||
518
+ value === 'null' ||
519
+ value === 'Null' ||
520
+ value === 'NULL'
328
521
  ) {
329
- return value.slice(1, -1);
522
+ return null;
330
523
  }
331
524
 
332
- // Integer
333
- if (/^-?\d+$/.test(value)) return parseInt(value, 10);
525
+ // Boolean
526
+ const lower = value.toLowerCase();
527
+ if (lower === 'true' || lower === 'yes' || lower === 'on') return true;
528
+ if (lower === 'false' || lower === 'no' || lower === 'off') return false;
529
+
530
+ // Integer patterns
531
+ if (/^-?(?:0|[1-9]\d*)$/.test(value)) return parseInt(value, 10);
532
+ if (/^0o[0-7]+$/i.test(value)) return parseInt(value.slice(2), 8);
533
+ if (/^0x[0-9a-fA-F]+$/.test(value)) return parseInt(value, 16);
334
534
 
335
- // Float
336
- if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
535
+ // Float patterns
536
+ if (/^-?(?:0|[1-9]\d*)?(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value) && value.includes('.')) {
537
+ return parseFloat(value);
538
+ }
539
+ if (lower === '.inf' || lower === '+.inf') return Infinity;
540
+ if (lower === '-.inf') return -Infinity;
541
+ if (lower === '.nan') return NaN;
542
+
543
+ return value;
544
+ }
545
+
546
+ /**
547
+ * Unescape YAML double-quoted string escape sequences.
548
+ *
549
+ * @param value - String content between double quotes.
550
+ * @returns Unescaped string.
551
+ */
552
+ private unescapeDoubleQuoted(value: string): string {
553
+ return value
554
+ .replace(/\\n/g, '\n')
555
+ .replace(/\\t/g, '\t')
556
+ .replace(/\\r/g, '\r')
557
+ .replace(/\\\\/g, '\\')
558
+ .replace(/\\"/g, '"')
559
+ .replace(/\\0/g, '\0')
560
+ .replace(/\\a/g, '\x07')
561
+ .replace(/\\b/g, '\x08')
562
+ .replace(/\\f/g, '\x0C')
563
+ .replace(/\\v/g, '\x0B');
564
+ }
565
+
566
+ /**
567
+ * Strip inline comments from a value string, respecting quoted regions.
568
+ *
569
+ * @param value - Raw value potentially containing inline comments.
570
+ * @returns Value with inline comments removed.
571
+ */
572
+ private stripInlineComment(value: string): string {
573
+ value = value.trim();
574
+ if (value === '') {
575
+ return '';
576
+ }
577
+
578
+ // Don't strip from quoted strings
579
+ if (value[0] === '"' || value[0] === "'") {
580
+ const closePos = value.indexOf(value[0], 1);
581
+ if (closePos !== -1) {
582
+ const afterQuote = value.substring(closePos + 1).trim();
583
+ if (afterQuote === '' || afterQuote[0] === '#') {
584
+ return value.substring(0, closePos + 1);
585
+ }
586
+ }
587
+ }
588
+
589
+ // Strip # comments (but not inside strings)
590
+ let inSingle = false;
591
+ let inDouble = false;
592
+ for (let i = 0; i < value.length; i++) {
593
+ const ch = value[i];
594
+ if (ch === "'" && !inDouble) {
595
+ inSingle = !inSingle;
596
+ } else if (ch === '"' && !inSingle) {
597
+ inDouble = !inDouble;
598
+ } else if (ch === '#' && !inSingle && !inDouble && i > 0 && value[i - 1] === ' ') {
599
+ return value.substring(0, i).trim();
600
+ }
601
+ }
337
602
 
338
603
  return value;
339
604
  }