ng-form-foundry-transformers 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,18 +5,24 @@ const thesaurus_1 = require("../../core/thesaurus");
5
5
  const infer_1 = require("../../core/infer");
6
6
  const json_schema_1 = require("../../core/json-schema");
7
7
  const bigint_1 = require("../../core/bigint");
8
+ const schema_keys_1 = require("../../core/schema-keys");
9
+ const merge_inferred_1 = require("../../core/merge-inferred");
10
+ const shape_check_1 = require("../../core/shape-check");
8
11
  /**
9
12
  * A JSON {@link Transformer}: turn a JSON config document into a form and write
10
13
  * the edited value back to JSON. The form is built by the same format-agnostic
11
14
  * `core` builders the YAML transformer uses — only parsing (`JSON.parse`) and
12
15
  * serialization (`JSON.stringify`) are JSON-specific.
13
16
  *
14
- * Standard JSON has no comments, so revert re-serializes the edited value,
15
- * preserving the source's indent width and trailing newline. Key order follows
16
- * the form value (which follows the schema, derived from the original), so it is
17
- * preserved for untouched keys. For JSON **with comments** (JSONC), parse and
18
- * edit it as YAML with {@link import('../yaml/yaml-transformer').yamlTransformer}
19
- * instead `JSON.parse` rejects comments.
17
+ * Standard JSON has no comments, so revert re-serializes, preserving the
18
+ * source's indent width and trailing newline. In schema-driven mode with
19
+ * `unknownKeys: 'preserve'` or `'edit'` the value is merged over the original
20
+ * data at schema-born paths ({@link mergeSchemaBorn}), so uncovered keys and
21
+ * the original key order survive; otherwise the edited value is serialized
22
+ * as-is, key order following the form value. For JSON **with comments**
23
+ * (JSONC), parse and edit it as YAML with
24
+ * {@link import('../yaml/yaml-transformer').yamlTransformer} instead —
25
+ * `JSON.parse` rejects comments.
20
26
  *
21
27
  * Integers beyond 2^53 can't survive a `number` round-trip, so they are carried
22
28
  * as strings in the form value and re-emitted verbatim as unquoted numbers (the
@@ -28,26 +34,84 @@ exports.jsonTransformer = {
28
34
  const bigInts = [];
29
35
  const parsed = JSON.parse(source, bigIntReviver);
30
36
  const data = collectBigInts(parsed ?? {}, [], bigInts);
31
- const schema = options?.schema
37
+ const unknownKeys = options?.unknownKeys ?? 'preserve';
38
+ const fromJsonSchema = options?.schema
32
39
  ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
33
- : (0, infer_1.inferNodeGroup)(data, options?.rootName);
40
+ : undefined;
41
+ const schema = fromJsonSchema && unknownKeys === 'edit'
42
+ ? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, infer_1.inferNodeGroup)(data, options?.rootName))
43
+ : fromJsonSchema ?? (0, infer_1.inferNodeGroup)(data, options?.rootName);
44
+ // A container-shape disagreement between document and schema would erase
45
+ // the section on save: refuse up front (see assertSchemaShapes).
46
+ if (fromJsonSchema)
47
+ (0, shape_check_1.assertSchemaShapes)(data, fromJsonSchema);
34
48
  const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
49
+ // 'preserve' gates the revert on the JSON Schema's NodeGroup; 'edit' on
50
+ // the merged one (schema-born ≈ everything, but keys no form field can
51
+ // carry stay protected); 'drop' and inferred mode leave the value
52
+ // authoritative for the whole document.
53
+ const preserveUnknown = fromJsonSchema != null && unknownKeys !== 'drop';
35
54
  const binding = {
36
55
  indent: detectIndent(source),
37
56
  trailingNewline: source.endsWith('\n'),
38
57
  bigInts,
58
+ original: preserveUnknown ? data : undefined,
59
+ schema: preserveUnknown ? schema : undefined,
39
60
  };
40
61
  return { schema: labeled, binding, initialValue: data };
41
62
  },
42
63
  toSource(value, binding) {
64
+ const merged = binding.schema && binding.original !== undefined
65
+ ? mergeSchemaBorn(binding.original, value, (0, schema_keys_1.childrenOf)(binding.schema))
66
+ : value;
43
67
  const paths = new Set(binding.bigInts);
44
- const prepared = paths.size ? markBigInts(value, [], paths) : value;
68
+ const prepared = paths.size ? markBigInts(merged, [], paths) : merged;
45
69
  let text = JSON.stringify(prepared, null, binding.indent);
46
70
  if (paths.size)
47
71
  text = text.replace(BIGINT_MARK_RE, '$1');
48
72
  return binding.trailingNewline ? text + '\n' : text;
49
73
  },
50
74
  };
75
+ /**
76
+ * Merge the edited value over the original data, value-authoritative for
77
+ * schema-born keys only: an uncovered key keeps its original value and its
78
+ * position in the key order; a covered key takes the edited value (recursing
79
+ * where both sides are objects, and per index into arrays of groups); a
80
+ * covered key absent from the value is omitted (a presence toggle off);
81
+ * covered keys new to the document append after the originals.
82
+ */
83
+ function mergeSchemaBorn(original, value, schema) {
84
+ if (!isPlainObject(original) || !isPlainObject(value) || !schema)
85
+ return value;
86
+ // Null-prototype output and own-key checks: document keys are arbitrary, so
87
+ // `__proto__` must stay a data key and `toString`/`constructor` must not
88
+ // resolve through the prototype chain (which would drop or fabricate keys).
89
+ const out = Object.create(null);
90
+ for (const key of Object.keys(original)) {
91
+ if (!schema.has(key)) {
92
+ out[key] = original[key]; // not schema-born: verbatim, in place
93
+ }
94
+ else if (hasOwn(value, key)) {
95
+ out[key] = mergeChild(original[key], value[key], schema.get(key));
96
+ } // covered + absent: deleted
97
+ }
98
+ for (const key of Object.keys(value)) {
99
+ if (!(key in out) && !hasOwn(original, key) && schema.has(key))
100
+ out[key] = value[key];
101
+ }
102
+ return out;
103
+ }
104
+ /** Per-key recursion: objects merge by their child schema, group arrays per index. */
105
+ function mergeChild(original, value, schema) {
106
+ if (schema && isPlainObject(original) && isPlainObject(value)) {
107
+ return mergeSchemaBorn(original, value, (0, schema_keys_1.childrenOf)(schema));
108
+ }
109
+ const itemSchema = (0, schema_keys_1.itemSchemaOf)(schema);
110
+ if (itemSchema && Array.isArray(original) && Array.isArray(value)) {
111
+ return value.map((item, i) => i < original.length ? mergeChild(original[i], item, itemSchema) : item);
112
+ }
113
+ return value;
114
+ }
51
115
  /** Indent width (in spaces) of the first indented line, or 2 if none/tabs. */
52
116
  function detectIndent(source) {
53
117
  const match = source.match(/\n( +)\S/);
@@ -102,7 +166,8 @@ function collectBigInts(node, path, out) {
102
166
  if (Array.isArray(node))
103
167
  return node.map((v, i) => collectBigInts(v, [...path, i], out));
104
168
  if (isPlainObject(node)) {
105
- const res = {};
169
+ // Null prototype: a `__proto__` document key must stay a data key.
170
+ const res = Object.create(null);
106
171
  for (const key of Object.keys(node))
107
172
  res[key] = collectBigInts(node[key], [...path, key], out);
108
173
  return res;
@@ -121,7 +186,7 @@ function markBigInts(node, path, paths) {
121
186
  if (Array.isArray(node))
122
187
  return node.map((v, i) => markBigInts(v, [...path, i], paths));
123
188
  if (isPlainObject(node)) {
124
- const res = {};
189
+ const res = Object.create(null);
125
190
  for (const key of Object.keys(node))
126
191
  res[key] = markBigInts(node[key], [...path, key], paths);
127
192
  return res;
@@ -131,3 +196,7 @@ function markBigInts(node, path, paths) {
131
196
  function isPlainObject(v) {
132
197
  return typeof v === 'object' && v !== null && !Array.isArray(v);
133
198
  }
199
+ /** Own-key membership, immune to `Object.prototype` members and null-prototype objects. */
200
+ function hasOwn(obj, key) {
201
+ return Object.prototype.hasOwnProperty.call(obj, key);
202
+ }
@@ -1,2 +1,3 @@
1
1
  export { libconfigTransformer, type LibconfigBinding, type LibconfigOptions, } from './libconfig-transformer';
2
2
  export { parseLibconfig, LibconfigParseError } from './parser';
3
+ export { SchemaShapeError } from '../../core/shape-check';
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LibconfigParseError = exports.parseLibconfig = exports.libconfigTransformer = void 0;
3
+ exports.SchemaShapeError = exports.LibconfigParseError = exports.parseLibconfig = exports.libconfigTransformer = void 0;
4
4
  var libconfig_transformer_1 = require("./libconfig-transformer");
5
5
  Object.defineProperty(exports, "libconfigTransformer", { enumerable: true, get: function () { return libconfig_transformer_1.libconfigTransformer; } });
6
6
  var parser_1 = require("./parser");
7
7
  Object.defineProperty(exports, "parseLibconfig", { enumerable: true, get: function () { return parser_1.parseLibconfig; } });
8
8
  Object.defineProperty(exports, "LibconfigParseError", { enumerable: true, get: function () { return parser_1.LibconfigParseError; } });
9
+ // Thrown by schema-driven toSchema; re-exported so browser consumers of this
10
+ // subpath can catch it by instance.
11
+ var shape_check_1 = require("../../core/shape-check");
12
+ Object.defineProperty(exports, "SchemaShapeError", { enumerable: true, get: function () { return shape_check_1.SchemaShapeError; } });
@@ -12,7 +12,7 @@
12
12
  * are then shown read-only. A JSON Schema in the options unlocks typed empty
13
13
  * collections, presence toggles for optional settings, enums, and ranges.
14
14
  */
15
- import type { FormValue, Thesaurus } from '../../core/schema';
15
+ import type { FormValue, NodeGroup, Thesaurus } from '../../core/schema';
16
16
  import type { TransformResult } from '../../core/transformer';
17
17
  import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
18
18
  import { type CfgGroup } from './parser';
@@ -34,6 +34,20 @@ export interface LibconfigOptions {
34
34
  * line verbatim and edits only this file's own settings.
35
35
  */
36
36
  includes?: 'reject' | 'opaque';
37
+ /**
38
+ * Schema-driven mode only: what happens to settings the JSON Schema does
39
+ * not cover. `'preserve'` (default) keeps them byte-verbatim in place —
40
+ * the form never carried them, so a partial schema edits its slice without
41
+ * erasing the rest. `'drop'` makes the edited value authoritative for the
42
+ * whole document, deleting uncovered settings — for consumers whose schema
43
+ * is intentionally complete (sanitizing a config, enforcing a strict
44
+ * template). `'edit'` surfaces them instead: uncovered settings render as
45
+ * editable fields typed by the document's own literals (the inferred
46
+ * schema merged under the JSON Schema — see `mergeInferred`), so nothing
47
+ * is invisible and the value covers the whole document. Ignored without a
48
+ * `schema`, where the inferred value covers every setting anyway.
49
+ */
50
+ unknownKeys?: 'preserve' | 'drop' | 'edit';
37
51
  /**
38
52
  * Display metadata (`label`/`description`/choice `caseLabels`) injected into
39
53
  * the produced schema, schema-driven or inferred alike — see
@@ -42,10 +56,22 @@ export interface LibconfigOptions {
42
56
  */
43
57
  thesaurus?: Thesaurus;
44
58
  }
45
- /** The revert context: the original text and its positioned AST. */
59
+ /**
60
+ * The revert context: the original text, its positioned AST, and — in
61
+ * schema-driven mode with `unknownKeys: 'preserve'` or `'edit'` — the
62
+ * NodeGroup the form was built from (the JSON Schema's own under
63
+ * `'preserve'`, the inferred-merged one under `'edit'`), so `toSource` knows
64
+ * which paths are schema-born. Settings outside those paths were never
65
+ * carried by the form and survive verbatim; under `'edit'` that is only the
66
+ * keys no form field can carry (e.g. a key inside a covered choice's group
67
+ * that no case names). Absent `schema` (inferred mode, or
68
+ * `unknownKeys: 'drop'`), the value covers the whole document and is
69
+ * authoritative everywhere.
70
+ */
46
71
  export interface LibconfigBinding {
47
72
  source: string;
48
73
  root: CfgGroup;
74
+ schema?: NodeGroup;
49
75
  }
50
76
  export declare const libconfigTransformer: {
51
77
  id: string;
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.libconfigTransformer = void 0;
4
4
  const thesaurus_1 = require("../../core/thesaurus");
5
5
  const json_schema_1 = require("../../core/json-schema");
6
+ const merge_inferred_1 = require("../../core/merge-inferred");
7
+ const schema_keys_1 = require("../../core/schema-keys");
8
+ const shape_check_1 = require("../../core/shape-check");
6
9
  const parser_1 = require("./parser");
7
10
  const schema_1 = require("./schema");
8
11
  const revert_1 = require("./revert");
@@ -10,14 +13,44 @@ exports.libconfigTransformer = {
10
13
  id: 'libconfig',
11
14
  toSchema(source, options) {
12
15
  const root = (0, parser_1.parseLibconfig)(source, { includes: options?.includes });
13
- const schema = options?.schema
16
+ const unknownKeys = options?.unknownKeys ?? 'preserve';
17
+ const fromJsonSchema = options?.schema
14
18
  ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
15
- : (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
19
+ : undefined;
20
+ const schema = fromJsonSchema && unknownKeys === 'edit'
21
+ ? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__'))
22
+ : fromJsonSchema ?? (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
23
+ // Schema-driven leaves display in the base the document wrote them in —
24
+ // the JSON Schema cannot know it, the document does (see annotateSchemaRadix).
25
+ if (fromJsonSchema)
26
+ (0, schema_1.annotateSchemaRadix)(root, schema);
16
27
  const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
17
28
  const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
18
- return { schema: labeled, binding: { source, root }, initialValue };
29
+ // A container-shape disagreement (group vs list vs scalar) produces a
30
+ // form that cannot carry the section and would erase it on save: refuse
31
+ // up front so the consumer can fall back to inferred editing.
32
+ if (fromJsonSchema)
33
+ (0, shape_check_1.assertSchemaShapes)(initialValue, fromJsonSchema);
34
+ if (fromJsonSchema && unknownKeys === 'edit') {
35
+ // Uncovered empty collections merged as read-only carries: their value
36
+ // must be the verbatim slice, not the typed [] the extraction produced.
37
+ (0, schema_1.carryUncoveredEmpties)(root, source, initialValue, (0, schema_keys_1.childrenOf)(fromJsonSchema));
38
+ }
39
+ return {
40
+ schema: labeled,
41
+ binding: {
42
+ source,
43
+ root,
44
+ // 'preserve' gates the revert on the JSON Schema's NodeGroup; 'edit'
45
+ // on the merged one (schema-born ≈ everything, but settings no form
46
+ // field can carry stay protected); 'drop' and inferred mode leave
47
+ // the value authoritative for the whole document.
48
+ schema: fromJsonSchema && unknownKeys !== 'drop' ? schema : undefined,
49
+ },
50
+ initialValue,
51
+ };
19
52
  },
20
53
  toSource(value, binding) {
21
- return (0, revert_1.applyValueToSource)(binding.source, binding.root, value);
54
+ return (0, revert_1.applyValueToSource)(binding.source, binding.root, value, binding.schema);
22
55
  },
23
56
  };
@@ -13,6 +13,18 @@
13
13
  * literal re-emits in hex at its original digit width, an int64 keeps its
14
14
  * `L`/`LL` suffix, and values beyond 2^53 travel as exact decimal strings.
15
15
  */
16
+ import type { NodeGroup } from '../../core/schema';
16
17
  import type { CfgGroup } from './parser';
17
- /** Apply `value` onto the parsed `root` of `source`, returning the new text. */
18
- export declare function applyValueToSource(source: string, root: CfgGroup, value: Record<string, unknown>): string;
18
+ /**
19
+ * Apply `value` onto the parsed `root` of `source`, returning the new text.
20
+ *
21
+ * Without `schema` (inferred mode) the value is authoritative for the whole
22
+ * document: it was extracted from every setting, so a key it lacks is a
23
+ * deletion. With `schema` (the NodeGroup the form was built from) the value
24
+ * is authoritative for **schema-born paths only** — the form never carried
25
+ * the other settings, so they survive byte-verbatim in their original
26
+ * positions. A partial schema thus edits its slice of an OAI/srsRAN config
27
+ * without erasing keys the schema does not enumerate; absence of a
28
+ * schema-born key still deletes (a presence toggle turned off).
29
+ */
30
+ export declare function applyValueToSource(source: string, root: CfgGroup, value: Record<string, unknown>, schema?: NodeGroup): string;
@@ -1,33 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyValueToSource = applyValueToSource;
4
+ const schema_keys_1 = require("../../core/schema-keys");
4
5
  const schema_1 = require("./schema");
5
- /** Apply `value` onto the parsed `root` of `source`, returning the new text. */
6
- function applyValueToSource(source, root, value) {
6
+ /**
7
+ * Apply `value` onto the parsed `root` of `source`, returning the new text.
8
+ *
9
+ * Without `schema` (inferred mode) the value is authoritative for the whole
10
+ * document: it was extracted from every setting, so a key it lacks is a
11
+ * deletion. With `schema` (the NodeGroup the form was built from) the value
12
+ * is authoritative for **schema-born paths only** — the form never carried
13
+ * the other settings, so they survive byte-verbatim in their original
14
+ * positions. A partial schema thus edits its slice of an OAI/srsRAN config
15
+ * without erasing keys the schema does not enumerate; absence of a
16
+ * schema-born key still deletes (a presence toggle turned off).
17
+ */
18
+ function applyValueToSource(source, root, value, schema) {
7
19
  const edits = [];
8
- patchGroup(source, root, value, edits);
20
+ patchGroup(source, root, value, edits, schema && (0, schema_keys_1.childrenOf)(schema));
9
21
  edits.sort((a, b) => b.start - a.start);
10
22
  let out = source;
11
23
  for (const e of edits)
12
24
  out = out.slice(0, e.start) + e.text + out.slice(e.end);
13
25
  return out;
14
26
  }
15
- function patchGroup(src, group, value, edits) {
27
+ function patchGroup(src, group, value, edits, schema) {
16
28
  for (const setting of group.settings) {
17
- if (!value || !(setting.name in value)) {
29
+ if (schema && !schema.has(setting.name))
30
+ continue; // not schema-born: verbatim
31
+ // Own-key check: `in` would resolve setting names like `toString` through
32
+ // the prototype chain, making them undeletable (and their "value" a function).
33
+ if (!value || !Object.prototype.hasOwnProperty.call(value, setting.name)) {
18
34
  edits.push(deleteSetting(src, setting));
19
35
  continue;
20
36
  }
21
- patchValue(src, setting.value, value[setting.name], edits);
37
+ patchValue(src, setting.value, value[setting.name], edits, schema?.get(setting.name));
22
38
  }
23
39
  const known = new Set(group.settings.map((s) => s.name));
24
- // null/undefined means "absent", never a value: such keys are not added.
25
- const added = Object.keys(value ?? {}).filter((k) => !known.has(k) && value[k] != null);
40
+ // null/undefined means "absent", never a value: such keys are not added
41
+ // and under a schema, only schema-born keys may be added at all.
42
+ const added = Object.keys(value ?? {}).filter((k) => !known.has(k) && value[k] != null && (!schema || schema.has(k)));
26
43
  if (added.length) {
27
44
  edits.push(insertionEdit(src, group, added.map((k) => `${k} = ${serialize(value[k], '')};`)));
28
45
  }
29
46
  }
30
- function patchValue(src, node, value, edits) {
47
+ function patchValue(src, node, value, edits, schema) {
31
48
  switch (node.kind) {
32
49
  case 'scalar': {
33
50
  if (node.value === value)
@@ -41,7 +58,7 @@ function patchValue(src, node, value, edits) {
41
58
  }
42
59
  case 'group': {
43
60
  if (isRecord(value))
44
- return patchGroup(src, node, value, edits);
61
+ return patchGroup(src, node, value, edits, schema && (0, schema_keys_1.childrenOf)(schema));
45
62
  edits.push({ ...node.span, text: serialize(value, '') }); // shape change: regenerate
46
63
  return;
47
64
  }
@@ -61,7 +78,7 @@ function patchValue(src, node, value, edits) {
61
78
  case 'list': {
62
79
  const shape = (0, schema_1.listShape)(node);
63
80
  if (shape === 'groups' && Array.isArray(value)) {
64
- return patchElements(src, node.elements, node, value, edits);
81
+ return patchElements(src, node.elements, node, value, edits, (0, schema_keys_1.itemSchemaOf)(schema));
65
82
  }
66
83
  if (shape === 'scalars' && Array.isArray(value)) {
67
84
  return patchElements(src, node.elements, node, value, edits);
@@ -89,10 +106,10 @@ function patchValue(src, node, value, edits) {
89
106
  }
90
107
  }
91
108
  /** Element-wise patch of an array/list: edit in place, then grow or shrink. */
92
- function patchElements(src, elements, collection, value, edits) {
109
+ function patchElements(src, elements, collection, value, edits, itemSchema) {
93
110
  const shared = Math.min(elements.length, value.length);
94
111
  for (let i = 0; i < shared; i++)
95
- patchValue(src, elements[i], value[i], edits);
112
+ patchValue(src, elements[i], value[i], edits, itemSchema);
96
113
  if (value.length > elements.length) {
97
114
  const items = value.slice(elements.length).map((v) => serialize(v, ''));
98
115
  const at = elements.length ? elements[elements.length - 1].span.end : collection.innerSpan.start;
@@ -180,7 +197,10 @@ function emitScalar(value, node) {
180
197
  if ((node.type === 'int' || node.type === 'int64') && Number.isInteger(value)) {
181
198
  return intLiteral(value, node.int);
182
199
  }
183
- return floatLiteral(value); // a fractional edit into an int slot: emit honestly
200
+ // A type-changing edit (e.g. a quoted "0xe00" slot edited to a number
201
+ // under an integer schema): integral values emit an int literal —
202
+ // float form is reserved for genuinely fractional values.
203
+ return Number.isInteger(value) ? String(value) : floatLiteral(value);
184
204
  }
185
205
  return serialize(value, '');
186
206
  }
@@ -18,7 +18,8 @@
18
18
  * transformer options) replaces this inference entirely and makes typed
19
19
  * empty collections editable.
20
20
  */
21
- import type { NodeGroup } from '../../core/schema';
21
+ import type { NodeGroup, NodeType } from '../../core/schema';
22
+ import { type SchemaKeys } from '../../core/schema-keys';
22
23
  import { type CfgGroup, type CfgList, type CfgValue } from './parser';
23
24
  /** Infer the root NodeGroup for a parsed document. */
24
25
  export declare function libconfigToNodeGroup(root: CfgGroup, source: string, name: string): NodeGroup;
@@ -36,4 +37,27 @@ type ListShape = 'groups' | 'scalars' | 'empty' | 'heterogeneous';
36
37
  export declare function listShape(list: CfgList): ListShape;
37
38
  /** The verbatim source slice of a node. */
38
39
  export declare function raw(value: CfgValue, source: string): string;
40
+ /**
41
+ * Stamp the document's non-decimal integer presentation onto a
42
+ * **schema-driven** NodeGroup as `radix` display hints — the JSON Schema has
43
+ * no radix vocabulary, so the document is the only authority on how a value
44
+ * is written. Covered number/string leaves whose literal is hex/octal/binary
45
+ * (and homogeneous collections thereof) display in that base in every
46
+ * `unknownKeys` mode, matching what inference does for uncovered fields.
47
+ * Fills gaps only: a `radix` already on the leaf (e.g. carried by
48
+ * `mergeInferred` under `'edit'`) wins. Keyed containers — groups, choices,
49
+ * maps — resolve through {@link childrenOf}, so case fields and map entries
50
+ * annotate too.
51
+ */
52
+ export declare function annotateSchemaRadix(node: CfgValue, schema: NodeType): void;
53
+ /**
54
+ * `unknownKeys: 'edit'` extraction fixup. The whole-document extraction runs
55
+ * with `emptyAsArrays` (schema-covered empty collections are typed and
56
+ * editable), but collections the JSON Schema does **not** cover merge as
57
+ * read-only raw-carry leaves — their value must be the verbatim source
58
+ * slice, not `[]`. Walks the document alongside the original (pre-merge)
59
+ * schema coverage and restores the carry on every uncovered empty
60
+ * collection, at any depth.
61
+ */
62
+ export declare function carryUncoveredEmpties(group: CfgGroup, source: string, value: unknown, keys: SchemaKeys): void;
39
63
  export {};
@@ -4,6 +4,9 @@ exports.libconfigToNodeGroup = libconfigToNodeGroup;
4
4
  exports.extractValue = extractValue;
5
5
  exports.listShape = listShape;
6
6
  exports.raw = raw;
7
+ exports.annotateSchemaRadix = annotateSchemaRadix;
8
+ exports.carryUncoveredEmpties = carryUncoveredEmpties;
9
+ const schema_keys_1 = require("../../core/schema-keys");
7
10
  const parser_1 = require("./parser");
8
11
  /** Digits only — the constraint for int64 values carried as strings. */
9
12
  const INTEGER_STRING_PATTERN = '^[-+]?[0-9]+$';
@@ -184,3 +187,118 @@ function rawLeaf(name, why) {
184
187
  function raw(value, source) {
185
188
  return source.slice(value.span.start, value.span.end);
186
189
  }
190
+ /**
191
+ * Stamp the document's non-decimal integer presentation onto a
192
+ * **schema-driven** NodeGroup as `radix` display hints — the JSON Schema has
193
+ * no radix vocabulary, so the document is the only authority on how a value
194
+ * is written. Covered number/string leaves whose literal is hex/octal/binary
195
+ * (and homogeneous collections thereof) display in that base in every
196
+ * `unknownKeys` mode, matching what inference does for uncovered fields.
197
+ * Fills gaps only: a `radix` already on the leaf (e.g. carried by
198
+ * `mergeInferred` under `'edit'`) wins. Keyed containers — groups, choices,
199
+ * maps — resolve through {@link childrenOf}, so case fields and map entries
200
+ * annotate too.
201
+ */
202
+ function annotateSchemaRadix(node, schema) {
203
+ switch (schema.kind) {
204
+ case 'leaf': {
205
+ // Only number/string leaves render radix; a boolean/enum leaf covering
206
+ // an int literal is a scalar-type mismatch the form handles otherwise.
207
+ if (schema.type !== 'number' && schema.type !== 'string')
208
+ return;
209
+ if (node.kind === 'scalar' && node.int && node.int.radix !== 10 && !schema.radix) {
210
+ schema.radix = node.int.radix;
211
+ }
212
+ return;
213
+ }
214
+ case 'leafList': {
215
+ if (schema.radix)
216
+ return;
217
+ if ((node.kind === 'array' || node.kind === 'list') && node.elements.every((e) => e.kind === 'scalar')) {
218
+ const radix = sharedRadix(node.elements);
219
+ if (radix)
220
+ schema.radix = radix;
221
+ }
222
+ return;
223
+ }
224
+ case 'nodeGroupList': {
225
+ if (node.kind !== 'list')
226
+ return;
227
+ for (const el of node.elements)
228
+ annotateSchemaRadix(el, schema.type);
229
+ return;
230
+ }
231
+ default: {
232
+ // nodeGroup / choice / map — keyed containers over a document group.
233
+ if (node.kind !== 'group')
234
+ return;
235
+ const keys = (0, schema_keys_1.childrenOf)(schema);
236
+ if (!keys)
237
+ return;
238
+ for (const setting of node.settings) {
239
+ const child = keys.get(setting.name);
240
+ if (child)
241
+ annotateSchemaRadix(setting.value, child);
242
+ }
243
+ return;
244
+ }
245
+ }
246
+ }
247
+ /**
248
+ * `unknownKeys: 'edit'` extraction fixup. The whole-document extraction runs
249
+ * with `emptyAsArrays` (schema-covered empty collections are typed and
250
+ * editable), but collections the JSON Schema does **not** cover merge as
251
+ * read-only raw-carry leaves — their value must be the verbatim source
252
+ * slice, not `[]`. Walks the document alongside the original (pre-merge)
253
+ * schema coverage and restores the carry on every uncovered empty
254
+ * collection, at any depth.
255
+ */
256
+ function carryUncoveredEmpties(group, source, value, keys) {
257
+ if (!isRecord(value))
258
+ return;
259
+ for (const setting of group.settings) {
260
+ if (keys && !keys.has(setting.name)) {
261
+ restoreEmptyCarries(setting.value, source, value, setting.name);
262
+ continue;
263
+ }
264
+ const child = keys?.get(setting.name);
265
+ const v = value[setting.name];
266
+ if (setting.value.kind === 'group' && child) {
267
+ carryUncoveredEmpties(setting.value, source, v, (0, schema_keys_1.childrenOf)(child));
268
+ }
269
+ else if (setting.value.kind === 'list' && Array.isArray(v)) {
270
+ const itemSchema = (0, schema_keys_1.itemSchemaOf)(child);
271
+ if (!itemSchema)
272
+ continue;
273
+ const itemKeys = (0, schema_keys_1.childrenOf)(itemSchema);
274
+ setting.value.elements.forEach((el, i) => {
275
+ if (el.kind === 'group')
276
+ carryUncoveredEmpties(el, source, v[i], itemKeys);
277
+ });
278
+ }
279
+ }
280
+ }
281
+ /** In a fully uncovered subtree, every empty collection reverts to its carry. */
282
+ function restoreEmptyCarries(node, source, parent, key) {
283
+ if ((node.kind === 'array' || node.kind === 'list') && node.elements.length === 0) {
284
+ parent[key] = raw(node, source);
285
+ return;
286
+ }
287
+ if (node.kind === 'group') {
288
+ const v = parent[key];
289
+ if (!isRecord(v))
290
+ return;
291
+ for (const s of node.settings)
292
+ restoreEmptyCarries(s.value, source, v, s.name);
293
+ return;
294
+ }
295
+ if (node.kind === 'list') {
296
+ const v = parent[key];
297
+ if (!Array.isArray(v))
298
+ return;
299
+ node.elements.forEach((el, i) => restoreEmptyCarries(el, source, v, i));
300
+ }
301
+ }
302
+ function isRecord(v) {
303
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
304
+ }
@@ -7,5 +7,6 @@
7
7
  * `core` — they are format-agnostic and shared with the JSON transformer.
8
8
  */
9
9
  export { yamlTransformer } from './yaml-transformer';
10
- export type { YamlOptions } from './yaml-transformer';
10
+ export type { YamlOptions, YamlBinding } from './yaml-transformer';
11
11
  export { applyValueToDocument } from './revert';
12
+ export { SchemaShapeError } from '../../core/shape-check';
@@ -8,8 +8,12 @@
8
8
  * `core` — they are format-agnostic and shared with the JSON transformer.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.applyValueToDocument = exports.yamlTransformer = void 0;
11
+ exports.SchemaShapeError = exports.applyValueToDocument = exports.yamlTransformer = void 0;
12
12
  var yaml_transformer_1 = require("./yaml-transformer");
13
13
  Object.defineProperty(exports, "yamlTransformer", { enumerable: true, get: function () { return yaml_transformer_1.yamlTransformer; } });
14
14
  var revert_1 = require("./revert");
15
15
  Object.defineProperty(exports, "applyValueToDocument", { enumerable: true, get: function () { return revert_1.applyValueToDocument; } });
16
+ // Thrown by schema-driven toSchema; re-exported so subpath consumers can
17
+ // catch it by instance.
18
+ var shape_check_1 = require("../../core/shape-check");
19
+ Object.defineProperty(exports, "SchemaShapeError", { enumerable: true, get: function () { return shape_check_1.SchemaShapeError; } });
@@ -1,4 +1,5 @@
1
1
  import { type Document } from 'yaml';
2
+ import type { NodeGroup } from '../../core/schema';
2
3
  /**
3
4
  * Apply an edited form value onto a parsed YAML {@link Document} in place,
4
5
  * preserving comments and formatting on every node that survives the edit.
@@ -10,9 +11,15 @@ import { type Document } from 'yaml';
10
11
  * {@link import('./json-schema')}. Callers should clone the document first if the
11
12
  * original must be preserved.
12
13
  *
14
+ * With `schema` (the NodeGroup the form was built from) the
15
+ * value is authoritative for **schema-born paths only**: keys the schema does
16
+ * not cover — at any depth — are preserved verbatim, their comments included.
17
+ * Without it the value covers the whole document and a key it lacks is a
18
+ * deletion.
19
+ *
13
20
  * Map keys are matched by the *same string form* `doc.toJS()` produced when the
14
21
  * form value was built, so non-string keys (a `80:` port map, a `true:` flag)
15
22
  * reconcile against their original typed key nodes instead of being appended as
16
23
  * duplicate string-keyed pairs.
17
24
  */
18
- export declare function applyValueToDocument(doc: Document, value: unknown): void;
25
+ export declare function applyValueToDocument(doc: Document, value: unknown, schema?: NodeGroup): void;