@vessel-dsp/core 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +6 -0
  3. package/package.json +56 -0
  4. package/src/editor/commands.ts +344 -0
  5. package/src/editor/factory.ts +148 -0
  6. package/src/editor/history.ts +142 -0
  7. package/src/editor/index.ts +11 -0
  8. package/src/editor/layout.ts +207 -0
  9. package/src/formats/circuit-json/serializer.ts +1410 -0
  10. package/src/formats/document.ts +274 -0
  11. package/src/formats/interchange/parser.ts +1165 -0
  12. package/src/formats/interchange/serializer.ts +594 -0
  13. package/src/formats/ltspice/catalog.ts +181 -0
  14. package/src/formats/ltspice/encoding.ts +151 -0
  15. package/src/formats/ltspice/parser.ts +432 -0
  16. package/src/formats/ltspice/serializer.ts +169 -0
  17. package/src/formats/schx/catalog.ts +439 -0
  18. package/src/formats/schx/parser.ts +261 -0
  19. package/src/formats/schx/runtime-descriptors.ts +502 -0
  20. package/src/formats/schx/serializer.ts +211 -0
  21. package/src/formats/schx/transforms.ts +38 -0
  22. package/src/formats/spice/parser.ts +373 -0
  23. package/src/formats/spice/serializer.ts +43 -0
  24. package/src/index.ts +205 -0
  25. package/src/model/connectivity.ts +239 -0
  26. package/src/model/netlist.ts +375 -0
  27. package/src/model/properties.ts +101 -0
  28. package/src/model/quantity.ts +173 -0
  29. package/src/model/types.ts +309 -0
  30. package/src/model/validation.ts +985 -0
  31. package/src/model/wires.ts +86 -0
  32. package/src/panel/extract.ts +878 -0
  33. package/src/panel/index.ts +39 -0
  34. package/src/panel/knobs.ts +70 -0
  35. package/src/panel/protocol.ts +117 -0
  36. package/src/panel/types.ts +180 -0
  37. package/src/preview/bounds.ts +85 -0
  38. package/src/preview/box-layout.ts +24 -0
  39. package/src/preview/colors.ts +43 -0
  40. package/src/preview/hanging.ts +94 -0
  41. package/src/preview/junctions.ts +94 -0
  42. package/src/preview/label-layout.ts +90 -0
  43. package/src/preview/ports.ts +101 -0
  44. package/src/preview/renderable-wires.ts +113 -0
  45. package/src/preview/routing.ts +15 -0
  46. package/src/preview/snap.ts +104 -0
  47. package/src/preview/symbols/analog-switch.svg +17 -0
  48. package/src/preview/symbols/battery.svg +16 -0
  49. package/src/preview/symbols/bbd.svg +21 -0
  50. package/src/preview/symbols/bjt-npn.svg +16 -0
  51. package/src/preview/symbols/bjt-pnp.svg +17 -0
  52. package/src/preview/symbols/capacitor-electrolytic.svg +13 -0
  53. package/src/preview/symbols/capacitor.svg +12 -0
  54. package/src/preview/symbols/current-source.svg +14 -0
  55. package/src/preview/symbols/delay-ic.svg +22 -0
  56. package/src/preview/symbols/diode-schottky.svg +12 -0
  57. package/src/preview/symbols/diode-zener.svg +12 -0
  58. package/src/preview/symbols/diode.svg +13 -0
  59. package/src/preview/symbols/flipflop.svg +20 -0
  60. package/src/preview/symbols/ground.svg +12 -0
  61. package/src/preview/symbols/ic-block.svg +20 -0
  62. package/src/preview/symbols/ic.svg +19 -0
  63. package/src/preview/symbols/inductor.svg +11 -0
  64. package/src/preview/symbols/jack-input.svg +16 -0
  65. package/src/preview/symbols/jack-output.svg +16 -0
  66. package/src/preview/symbols/jfet-junction-n.svg +17 -0
  67. package/src/preview/symbols/jfet-n.svg +17 -0
  68. package/src/preview/symbols/jfet-p.svg +17 -0
  69. package/src/preview/symbols/label.svg +8 -0
  70. package/src/preview/symbols/led.svg +18 -0
  71. package/src/preview/symbols/mosfet-n.svg +21 -0
  72. package/src/preview/symbols/mosfet-p.svg +21 -0
  73. package/src/preview/symbols/named-wire.svg +11 -0
  74. package/src/preview/symbols/opamp.svg +21 -0
  75. package/src/preview/symbols/optocoupler.svg +30 -0
  76. package/src/preview/symbols/ota.svg +20 -0
  77. package/src/preview/symbols/pentode.svg +25 -0
  78. package/src/preview/symbols/photoresistor.svg +19 -0
  79. package/src/preview/symbols/port.svg +8 -0
  80. package/src/preview/symbols/potentiometer.svg +15 -0
  81. package/src/preview/symbols/power-amp.svg +20 -0
  82. package/src/preview/symbols/rail.svg +11 -0
  83. package/src/preview/symbols/regulator.svg +13 -0
  84. package/src/preview/symbols/relay.svg +20 -0
  85. package/src/preview/symbols/resistor.svg +11 -0
  86. package/src/preview/symbols/svg-content.ts +59 -0
  87. package/src/preview/symbols/switch-3pdt.svg +32 -0
  88. package/src/preview/symbols/switch-rotary.svg +23 -0
  89. package/src/preview/symbols/switch-spdt.svg +16 -0
  90. package/src/preview/symbols/switch-spst.svg +14 -0
  91. package/src/preview/symbols/switch-toggle.svg +14 -0
  92. package/src/preview/symbols/transformer.svg +17 -0
  93. package/src/preview/symbols/triode.svg +17 -0
  94. package/src/preview/symbols/tube-diode.svg +13 -0
  95. package/src/preview/symbols/unsupported.svg +8 -0
  96. package/src/preview/symbols/variable-resistor.svg +13 -0
  97. package/src/preview/symbols/voltage-source.svg +15 -0
  98. package/src/preview/symbols.ts +207 -0
  99. package/src/preview/wire-chains.ts +153 -0
@@ -0,0 +1,594 @@
1
+ import { getPinNode, resolveConnectivity, type Connectivity } from '../../model/connectivity';
2
+ import { isParsedQuantity, isPropertyObject } from '../../model/properties';
3
+ import type {
4
+ CircuitDocument,
5
+ CircuitDocumentDevice,
6
+ Component,
7
+ ControlApplicabilityPredicate,
8
+ ControlContext,
9
+ ControlGroup,
10
+ DeviceInterface,
11
+ DeviceInterfaceBinding,
12
+ DeviceInterfaceControl,
13
+ ControlInterface,
14
+ ControlInterfaceBinding,
15
+ ControlOutput,
16
+ DocumentSource,
17
+ PanelElementBinding,
18
+ PanelElementPlacement,
19
+ PanelFace,
20
+ PanelGridLayout,
21
+ PanelGridPosition,
22
+ PanelPlacementMetadata,
23
+ Point,
24
+ PropertyValue,
25
+ Terminal,
26
+ Warning,
27
+ Wire,
28
+ } from '../../model/types';
29
+
30
+ export type InterchangeSourceFormat = string;
31
+
32
+ export type SerializeInterchangeYamlOptions = Readonly<{
33
+ filename?: string;
34
+ source?: DocumentSource | null;
35
+ sourceFormat?: InterchangeSourceFormat | null;
36
+ }>;
37
+
38
+ type YamlScalar = string | number | boolean | null;
39
+ type YamlValue = YamlScalar | readonly YamlValue[] | { readonly [key: string]: YamlValue };
40
+ type MutableYamlObject = Record<string, YamlValue>;
41
+
42
+ export function serializeInterchangeYaml(
43
+ doc: CircuitDocument,
44
+ options: SerializeInterchangeYamlOptions = {},
45
+ ): string {
46
+ const connectivity = resolveConnectivity(doc);
47
+ const root: MutableYamlObject = {
48
+ schema: 'circuit-interchange/v2',
49
+ metadata: {
50
+ name: doc.metadata.name,
51
+ description: doc.metadata.description,
52
+ partNumber: doc.metadata.partNumber,
53
+ },
54
+ source: sourceBlock(doc.source, options),
55
+ };
56
+ if (doc.device !== undefined) {
57
+ root.device = deviceBlock(doc.device);
58
+ }
59
+ if (doc.controlGroups !== undefined) {
60
+ root.controlGroups = doc.controlGroups.map(controlGroupBlock);
61
+ }
62
+ if (doc.controlContexts !== undefined) {
63
+ root.controlContexts = doc.controlContexts.map(controlContextBlock);
64
+ }
65
+ if (doc.deviceInterface !== undefined) {
66
+ root.deviceInterface = deviceInterfaceBlock(doc.deviceInterface);
67
+ }
68
+ if (doc.panel !== undefined) {
69
+ root.panel = panelBlock(doc.panel);
70
+ }
71
+ if (doc.controlInterfaces !== undefined) {
72
+ root.controlInterfaces = doc.controlInterfaces.map(controlInterfaceBlock);
73
+ }
74
+ if (doc.controlOutputs !== undefined) {
75
+ root.controlOutputs = doc.controlOutputs.map(controlOutputBlock);
76
+ }
77
+ Object.assign(root, {
78
+ components: doc.components.map((component) => componentBlock(component, connectivity)),
79
+ nodes: nodeBlocks(connectivity),
80
+ wires: doc.wires.map(wireBlock),
81
+ directives: doc.directives,
82
+ diagnostics: doc.warnings.map(warningBlock),
83
+ rawAttributes: doc.rawAttributes,
84
+ });
85
+
86
+ return `${emitYaml(root, 0)}\n`;
87
+ }
88
+
89
+ function deviceBlock(device: CircuitDocumentDevice): MutableYamlObject {
90
+ const out: MutableYamlObject = {
91
+ kind: device.kind,
92
+ };
93
+ if (device.id !== undefined) {
94
+ out.id = device.id;
95
+ }
96
+ if (device.version !== undefined) {
97
+ out.version = device.version;
98
+ }
99
+ if (device.family !== undefined) {
100
+ out.family = device.family;
101
+ }
102
+ if (device.model !== undefined) {
103
+ out.model = device.model;
104
+ }
105
+ if (device.audioProcessing !== undefined) {
106
+ out.audioProcessing = device.audioProcessing;
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function controlGroupBlock(group: ControlGroup): MutableYamlObject {
112
+ const out: MutableYamlObject = {
113
+ id: group.id,
114
+ name: group.name,
115
+ role: group.role,
116
+ };
117
+ if (group.contextIds !== undefined) {
118
+ out.contextIds = group.contextIds;
119
+ }
120
+ if (group.description !== undefined) {
121
+ out.description = group.description;
122
+ }
123
+ return out;
124
+ }
125
+
126
+ function controlContextBlock(context: ControlContext): MutableYamlObject {
127
+ const out: MutableYamlObject = {
128
+ id: context.id,
129
+ name: context.name,
130
+ role: context.role,
131
+ };
132
+ if (context.description !== undefined) {
133
+ out.description = context.description;
134
+ }
135
+ return out;
136
+ }
137
+
138
+ function deviceInterfaceBlock(deviceInterface: DeviceInterface): MutableYamlObject {
139
+ return {
140
+ controls: deviceInterface.controls.map(deviceInterfaceControlBlock),
141
+ };
142
+ }
143
+
144
+ function deviceInterfaceControlBlock(control: DeviceInterfaceControl): MutableYamlObject {
145
+ const out: MutableYamlObject = {
146
+ id: control.id,
147
+ label: control.label,
148
+ kind: control.kind,
149
+ role: control.role,
150
+ };
151
+ if (control.groupId !== undefined) {
152
+ out.groupId = control.groupId;
153
+ }
154
+ if (control.order !== undefined) {
155
+ out.order = control.order;
156
+ }
157
+ if (control.binding !== undefined) {
158
+ out.binding = deviceInterfaceBindingBlock(control.binding);
159
+ }
160
+ if (control.appliesWhen !== undefined) {
161
+ out.appliesWhen = controlApplicabilityPredicateBlock(control.appliesWhen);
162
+ }
163
+ if (control.description !== undefined) {
164
+ out.description = control.description;
165
+ }
166
+ return out;
167
+ }
168
+
169
+ function deviceInterfaceBindingBlock(binding: DeviceInterfaceBinding): MutableYamlObject {
170
+ const out: MutableYamlObject = {
171
+ componentId: binding.componentId,
172
+ };
173
+ if (binding.controlId !== undefined) {
174
+ out.controlId = binding.controlId;
175
+ }
176
+ if (binding.controlName !== undefined) {
177
+ out.controlName = binding.controlName;
178
+ }
179
+ if (binding.property !== undefined) {
180
+ out.property = binding.property;
181
+ }
182
+ if (binding.externalInterfaceId !== undefined) {
183
+ out.externalInterfaceId = binding.externalInterfaceId;
184
+ }
185
+ return out;
186
+ }
187
+
188
+ function controlApplicabilityPredicateBlock(predicate: ControlApplicabilityPredicate): MutableYamlObject {
189
+ const out: MutableYamlObject = {};
190
+ if (predicate.allOf !== undefined) {
191
+ out.allOf = predicate.allOf;
192
+ }
193
+ if (predicate.anyOf !== undefined) {
194
+ out.anyOf = predicate.anyOf;
195
+ }
196
+ return out;
197
+ }
198
+
199
+ function controlInterfaceBlock(controlInterface: ControlInterface): MutableYamlObject {
200
+ const out: MutableYamlObject = {
201
+ id: controlInterface.id,
202
+ name: controlInterface.name,
203
+ role: controlInterface.role,
204
+ };
205
+ if (controlInterface.componentId !== undefined) {
206
+ out.componentId = controlInterface.componentId;
207
+ }
208
+ if (controlInterface.controlRole !== undefined) {
209
+ out.controlRole = controlInterface.controlRole;
210
+ }
211
+ if (controlInterface.interface !== undefined) {
212
+ out.interface = controlInterface.interface;
213
+ }
214
+ if (controlInterface.connector !== undefined) {
215
+ out.connector = controlInterface.connector;
216
+ }
217
+ if (controlInterface.assignmentHint !== undefined) {
218
+ out.assignmentHint = controlInterface.assignmentHint;
219
+ }
220
+ if (controlInterface.polarity !== undefined) {
221
+ out.polarity = controlInterface.polarity;
222
+ }
223
+ if (controlInterface.binding !== undefined) {
224
+ out.binding = controlInterfaceBindingBlock(controlInterface.binding);
225
+ }
226
+ if (controlInterface.description !== undefined) {
227
+ out.description = controlInterface.description;
228
+ }
229
+ return out;
230
+ }
231
+
232
+ function controlOutputBlock(controlOutput: ControlOutput): MutableYamlObject {
233
+ const out: MutableYamlObject = {
234
+ id: controlOutput.id,
235
+ name: controlOutput.name,
236
+ role: controlOutput.role,
237
+ };
238
+ if (controlOutput.connector !== undefined) {
239
+ out.connector = controlOutput.connector;
240
+ }
241
+ if (controlOutput.switchMode !== undefined) {
242
+ out.switchMode = controlOutput.switchMode;
243
+ }
244
+ if (controlOutput.polarity !== undefined) {
245
+ out.polarity = controlOutput.polarity;
246
+ }
247
+ if (controlOutput.inactiveValue !== undefined) {
248
+ out.inactiveValue = controlOutput.inactiveValue;
249
+ }
250
+ if (controlOutput.activeValue !== undefined) {
251
+ out.activeValue = controlOutput.activeValue;
252
+ }
253
+ if (controlOutput.componentId !== undefined) {
254
+ out.componentId = controlOutput.componentId;
255
+ }
256
+ if (controlOutput.description !== undefined) {
257
+ out.description = controlOutput.description;
258
+ }
259
+ return out;
260
+ }
261
+
262
+ function controlInterfaceBindingBlock(binding: ControlInterfaceBinding): MutableYamlObject {
263
+ const out: MutableYamlObject = {};
264
+ if (binding.sourceComponentId !== undefined) {
265
+ out.sourceComponentId = binding.sourceComponentId;
266
+ }
267
+ if (binding.controlId !== undefined) {
268
+ out.controlId = binding.controlId;
269
+ }
270
+ if (binding.controlName !== undefined) {
271
+ out.controlName = binding.controlName;
272
+ }
273
+ if (binding.property !== undefined) {
274
+ out.property = binding.property;
275
+ }
276
+ return out;
277
+ }
278
+
279
+ function sourceBlock(
280
+ documentSource: DocumentSource | undefined,
281
+ options: SerializeInterchangeYamlOptions,
282
+ ): MutableYamlObject {
283
+ const source: MutableYamlObject = {};
284
+ for (const [key, value] of Object.entries(documentSource ?? {})) {
285
+ source[key] = value;
286
+ }
287
+ for (const [key, value] of Object.entries(options.source ?? {})) {
288
+ source[key] = value;
289
+ }
290
+ if (options.sourceFormat !== undefined && options.sourceFormat !== null) {
291
+ source.format = options.sourceFormat;
292
+ }
293
+ if (options.sourceFormat === null) {
294
+ delete source.format;
295
+ }
296
+ if (options.filename !== undefined && options.filename.length > 0) {
297
+ source.filename = options.filename;
298
+ }
299
+ return source;
300
+ }
301
+
302
+ function panelBlock(panel: PanelPlacementMetadata): MutableYamlObject {
303
+ return {
304
+ faces: panel.faces.map(panelFaceBlock),
305
+ };
306
+ }
307
+
308
+ function panelFaceBlock(face: PanelFace): MutableYamlObject {
309
+ const out: MutableYamlObject = {
310
+ id: face.id,
311
+ };
312
+ if (face.label !== undefined) {
313
+ out.label = face.label;
314
+ }
315
+ out.layout = panelLayoutBlock(face.layout);
316
+ out.elements = face.elements.map(panelElementBlock);
317
+ return out;
318
+ }
319
+
320
+ function panelLayoutBlock(layout: PanelGridLayout): MutableYamlObject {
321
+ const out: MutableYamlObject = {
322
+ kind: layout.kind,
323
+ rows: layout.rows,
324
+ columns: layout.columns,
325
+ indexing: layout.indexing,
326
+ };
327
+ if (layout.rowOrder !== undefined) {
328
+ out.rowOrder = layout.rowOrder;
329
+ }
330
+ if (layout.columnOrder !== undefined) {
331
+ out.columnOrder = layout.columnOrder;
332
+ }
333
+ return out;
334
+ }
335
+
336
+ function panelElementBlock(element: PanelElementPlacement): MutableYamlObject {
337
+ const out: MutableYamlObject = {
338
+ bind: panelElementBindingBlock(element.bind),
339
+ kind: element.kind,
340
+ grid: panelGridPositionBlock(element.grid),
341
+ };
342
+ if (element.label !== undefined) {
343
+ out.label = element.label;
344
+ }
345
+ if (element.interfaceControlId !== undefined) {
346
+ out.interfaceControlId = element.interfaceControlId;
347
+ }
348
+ return out;
349
+ }
350
+
351
+ function panelElementBindingBlock(binding: PanelElementBinding): MutableYamlObject {
352
+ const out: MutableYamlObject = {
353
+ componentId: binding.componentId,
354
+ };
355
+ if (binding.controlId !== undefined) {
356
+ out.controlId = binding.controlId;
357
+ }
358
+ if (binding.controlName !== undefined) {
359
+ out.controlName = binding.controlName;
360
+ }
361
+ if (binding.property !== undefined) {
362
+ out.property = binding.property;
363
+ }
364
+ return out;
365
+ }
366
+
367
+ function panelGridPositionBlock(grid: PanelGridPosition): MutableYamlObject {
368
+ const out: MutableYamlObject = {
369
+ row: grid.row,
370
+ column: grid.column,
371
+ };
372
+ if (grid.rowSpan !== undefined) {
373
+ out.rowSpan = grid.rowSpan;
374
+ }
375
+ if (grid.columnSpan !== undefined) {
376
+ out.columnSpan = grid.columnSpan;
377
+ }
378
+ return out;
379
+ }
380
+
381
+ function componentBlock(component: Component, connectivity: Connectivity): MutableYamlObject {
382
+ return {
383
+ id: component.id,
384
+ kind: component.kind,
385
+ name: component.name,
386
+ sourceTypeName: component.sourceTypeName,
387
+ origin: pointBlock(component.origin),
388
+ rotation: component.rotation,
389
+ flipped: component.flipped,
390
+ terminals: component.terminals.map((terminal) => terminalBlock(component, terminal, connectivity)),
391
+ properties: propertiesBlock(component.properties),
392
+ };
393
+ }
394
+
395
+ function terminalBlock(
396
+ component: Component,
397
+ terminal: Terminal,
398
+ connectivity: Connectivity,
399
+ ): MutableYamlObject {
400
+ return {
401
+ name: terminal.name,
402
+ node: getPinNode(connectivity, {
403
+ componentId: component.id,
404
+ terminalName: terminal.name,
405
+ }) ?? null,
406
+ position: pointBlock(terminal.position),
407
+ };
408
+ }
409
+
410
+ function propertiesBlock(properties: Readonly<Record<string, PropertyValue>>): MutableYamlObject {
411
+ const out: MutableYamlObject = {};
412
+ for (const [key, value] of Object.entries(properties)) {
413
+ out[key] = propertyValueBlock(value);
414
+ }
415
+ return out;
416
+ }
417
+
418
+ function propertyValueBlock(value: PropertyValue): YamlValue {
419
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
420
+ return value;
421
+ }
422
+ if (Array.isArray(value)) {
423
+ return value.map(propertyValueBlock);
424
+ }
425
+ if (isPropertyObject(value)) {
426
+ return propertiesBlock(value);
427
+ }
428
+ if (!isParsedQuantity(value)) {
429
+ return null;
430
+ }
431
+ return {
432
+ raw: value.raw,
433
+ value: value.value,
434
+ unit: value.unit,
435
+ };
436
+ }
437
+
438
+ function nodeBlocks(connectivity: Connectivity): readonly MutableYamlObject[] {
439
+ return Array.from(connectivity.nodeMembers.entries())
440
+ .sort(([a], [b]) => a - b)
441
+ .map(([id, members]) => ({
442
+ id,
443
+ role: id === connectivity.groundNodeId ? 'ground' : 'signal',
444
+ members: members.map((member) => ({
445
+ componentId: member.componentId,
446
+ terminalName: member.terminalName,
447
+ })),
448
+ }));
449
+ }
450
+
451
+ function wireBlock(wire: Wire): MutableYamlObject {
452
+ return {
453
+ id: wire.id,
454
+ points: wire.endpoints.map(pointBlock),
455
+ };
456
+ }
457
+
458
+ function warningBlock(warning: Warning): MutableYamlObject {
459
+ const out: MutableYamlObject = {
460
+ code: warning.code,
461
+ message: warning.message,
462
+ };
463
+ if (warning.componentId !== undefined) {
464
+ out.componentId = warning.componentId;
465
+ }
466
+ if (warning.wireId !== undefined) {
467
+ out.wireId = warning.wireId;
468
+ }
469
+ return out;
470
+ }
471
+
472
+ function pointBlock(point: Point): MutableYamlObject {
473
+ return {
474
+ x: point.x,
475
+ y: point.y,
476
+ };
477
+ }
478
+
479
+ function emitYaml(value: YamlValue, indent: number): string {
480
+ if (isScalar(value)) {
481
+ return `${spaces(indent)}${formatScalar(value)}`;
482
+ }
483
+ if (isYamlArray(value)) {
484
+ return emitArray(value, indent);
485
+ }
486
+ return emitObject(value, indent);
487
+ }
488
+
489
+ function emitObject(value: { readonly [key: string]: YamlValue }, indent: number): string {
490
+ const entries = Object.entries(value);
491
+ if (entries.length === 0) {
492
+ return `${spaces(indent)}{}`;
493
+ }
494
+
495
+ const lines: string[] = [];
496
+ for (const [key, child] of entries) {
497
+ if (isScalar(child)) {
498
+ lines.push(`${spaces(indent)}${formatKey(key)}: ${formatScalar(child)}`);
499
+ continue;
500
+ }
501
+ if (isYamlArray(child)) {
502
+ if (child.length === 0) {
503
+ lines.push(`${spaces(indent)}${formatKey(key)}: []`);
504
+ } else {
505
+ lines.push(`${spaces(indent)}${formatKey(key)}:`);
506
+ lines.push(emitArray(child, indent + 2));
507
+ }
508
+ continue;
509
+ }
510
+ const nestedEntries = Object.entries(child);
511
+ if (nestedEntries.length === 0) {
512
+ lines.push(`${spaces(indent)}${formatKey(key)}: {}`);
513
+ } else {
514
+ lines.push(`${spaces(indent)}${formatKey(key)}:`);
515
+ lines.push(emitObject(child, indent + 2));
516
+ }
517
+ }
518
+ return lines.join('\n');
519
+ }
520
+
521
+ function emitArray(value: readonly YamlValue[], indent: number): string {
522
+ if (value.length === 0) {
523
+ return `${spaces(indent)}[]`;
524
+ }
525
+
526
+ const lines: string[] = [];
527
+ for (const item of value) {
528
+ if (isScalar(item)) {
529
+ lines.push(`${spaces(indent)}- ${formatScalar(item)}`);
530
+ continue;
531
+ }
532
+ if (isYamlArray(item)) {
533
+ lines.push(`${spaces(indent)}-`);
534
+ lines.push(emitArray(item, indent + 2));
535
+ continue;
536
+ }
537
+ const rendered = emitObject(item, indent + 2).split('\n');
538
+ const first = rendered[0];
539
+ if (first === undefined) {
540
+ lines.push(`${spaces(indent)}- {}`);
541
+ continue;
542
+ }
543
+ lines.push(`${spaces(indent)}- ${first.trimStart()}`);
544
+ for (const line of rendered.slice(1)) {
545
+ lines.push(line);
546
+ }
547
+ }
548
+ return lines.join('\n');
549
+ }
550
+
551
+ function isScalar(value: YamlValue): value is YamlScalar {
552
+ return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
553
+ }
554
+
555
+ function isYamlArray(value: YamlValue): value is readonly YamlValue[] {
556
+ return Array.isArray(value);
557
+ }
558
+
559
+ function formatKey(key: string): string {
560
+ return isPlainScalar(key) ? key : JSON.stringify(key);
561
+ }
562
+
563
+ function formatScalar(value: YamlScalar): string {
564
+ if (value === null) {
565
+ return 'null';
566
+ }
567
+ if (typeof value === 'number' || typeof value === 'boolean') {
568
+ return String(value);
569
+ }
570
+ if (isPlainScalar(value) && !isReservedScalar(value) && !looksLikeNumber(value)) {
571
+ return value;
572
+ }
573
+ return JSON.stringify(value);
574
+ }
575
+
576
+ function isPlainScalar(value: string): boolean {
577
+ return /^[A-Za-z_][A-Za-z0-9_./\-]*$/.test(value);
578
+ }
579
+
580
+ function isReservedScalar(value: string): boolean {
581
+ const lower = value.toLowerCase();
582
+ return lower === 'null' || lower === 'true' || lower === 'false';
583
+ }
584
+
585
+ function looksLikeNumber(value: string): boolean {
586
+ // Quote values that look like bare numbers (would be parsed as numbers by YAML)
587
+ // This includes scientific notation like "1e-12", "1.0e-7"
588
+ // But NOT version strings like "v1" or "1.0" that are meant to be strings
589
+ return /^-?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?$/.test(value);
590
+ }
591
+
592
+ function spaces(count: number): string {
593
+ return ' '.repeat(count);
594
+ }