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.
- package/README.md +34 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +3 -1
- package/dist/core/infer.d.ts +3 -2
- package/dist/core/infer.js +7 -4
- package/dist/core/merge-inferred.d.ts +20 -0
- package/dist/core/merge-inferred.js +46 -0
- package/dist/core/schema-keys.d.ts +40 -0
- package/dist/core/schema-keys.js +67 -0
- package/dist/core/shape-check.d.ts +40 -0
- package/dist/core/shape-check.js +147 -0
- package/dist/transformers/json/index.d.ts +1 -0
- package/dist/transformers/json/index.js +5 -1
- package/dist/transformers/json/json-transformer.d.ts +35 -7
- package/dist/transformers/json/json-transformer.js +80 -11
- package/dist/transformers/libconfig/index.d.ts +1 -0
- package/dist/transformers/libconfig/index.js +5 -1
- package/dist/transformers/libconfig/libconfig-transformer.d.ts +28 -2
- package/dist/transformers/libconfig/libconfig-transformer.js +37 -4
- package/dist/transformers/libconfig/revert.d.ts +14 -2
- package/dist/transformers/libconfig/revert.js +34 -14
- package/dist/transformers/libconfig/schema.d.ts +25 -1
- package/dist/transformers/libconfig/schema.js +118 -0
- package/dist/transformers/yaml/index.d.ts +2 -1
- package/dist/transformers/yaml/index.js +5 -1
- package/dist/transformers/yaml/revert.d.ts +8 -1
- package/dist/transformers/yaml/revert.js +43 -11
- package/dist/transformers/yaml/yaml-transformer.d.ts +32 -5
- package/dist/transformers/yaml/yaml-transformer.js +59 -27
- package/package.json +1 -1
|
@@ -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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
-
:
|
|
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(
|
|
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
|
-
|
|
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,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
|
-
/**
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
/**
|
|
18
|
-
|
|
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
|
-
/**
|
|
6
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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;
|