ng-form-foundry-transformers 0.4.0 → 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.
- package/README.md +19 -4
- package/dist/core/schema.d.ts +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/transformers/libconfig/index.d.ts +1 -1
- package/dist/transformers/libconfig/index.js +1 -2
- package/dist/transformers/libconfig/libconfig-transformer.d.ts +1 -3
- package/dist/transformers/libconfig/libconfig-transformer.js +0 -15
- package/dist/transformers/libconfig/parser.d.ts +8 -2
- package/dist/transformers/libconfig/parser.js +48 -27
- package/dist/transformers/libconfig/revert.js +46 -19
- package/dist/transformers/libconfig/schema.js +29 -6
- package/dist/transformers/yaml/yaml-transformer.js +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ or a CLI just as easily.
|
|
|
16
16
|
| `yang` | YANG model (via an engine) | RFC 7951 instance data | available |
|
|
17
17
|
| `yaml` | YAML config (optionally a JSON Schema) | YAML (comments preserved) | available |
|
|
18
18
|
| `json` | JSON config (optionally a JSON Schema) | JSON (indent preserved) | available |
|
|
19
|
-
| `libconfig` | libconfig document (optionally a JSON Schema) | libconfig (comments **and scalar types** preserved) |
|
|
19
|
+
| `libconfig` | libconfig document (optionally a JSON Schema) | libconfig (comments **and scalar types** preserved) | available |
|
|
20
20
|
|
|
21
21
|
The YAML and JSON transformers share the same format-agnostic form builders in
|
|
22
22
|
`core` (`inferNodeGroup`, `jsonSchemaToNodeGroup`) — a JSON Schema is an *option*
|
|
@@ -169,12 +169,10 @@ const { schema, binding, initialValue } = jsonTransformer.toSchema(configText);
|
|
|
169
169
|
const out = jsonTransformer.toSource({ ...initialValue, replicas: 5 }, binding);
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
## libconfig transformer
|
|
172
|
+
## libconfig transformer
|
|
173
173
|
|
|
174
174
|
Edit a **libconfig document** — the `.cfg`/`.conf` format of srsRAN, OAI, and
|
|
175
175
|
other C/C++ software using [libconfig](https://hyperrealm.github.io/libconfig/).
|
|
176
|
-
**Beta**: it logs a one-time warning on first use; diff `toSource` output
|
|
177
|
-
against the original before deploying a write-back.
|
|
178
176
|
|
|
179
177
|
```ts
|
|
180
178
|
import { libconfigTransformer } from 'ng-form-foundry-transformers';
|
|
@@ -294,6 +292,23 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
294
292
|
|
|
295
293
|
## Status
|
|
296
294
|
|
|
295
|
+
`0.5.0` — the **libconfig transformer is stable**: the one-time beta warning
|
|
296
|
+
is gone (and with it the `resetLibconfigBetaWarning` helper), and the format's
|
|
297
|
+
guarantees and known limitations are documented in the transformers guide.
|
|
298
|
+
Non-decimal integer literals (`0x`, `0b`, `0o`/`0q`) now set a `radix` display
|
|
299
|
+
hint on the generated schema — the paired `ng-form-foundry` release renders
|
|
300
|
+
those fields as hex/octal/binary editors, and the YAML transformer does the
|
|
301
|
+
same for its hex and octal literals.
|
|
302
|
+
|
|
303
|
+
`0.4.1` — libconfig hardening after a conformance battle test against the C
|
|
304
|
+
library's own scanner, test corpus, and real srsRAN/OAI configs: `0q`/`0Q`
|
|
305
|
+
octal, verbatim radix-prefix preservation on edits, sign accepted on decimal
|
|
306
|
+
literals only, negative edits into hex/bin/oct slots emit decimal, exact
|
|
307
|
+
round-trips for collections mixing safe and beyond-2^53 integers, `null`
|
|
308
|
+
rejected instead of written as `true`, a 256-level nesting cap, strict `\x`
|
|
309
|
+
escapes, `@include` legal in value positions, and modified read-only carries
|
|
310
|
+
throw instead of corrupting.
|
|
311
|
+
|
|
297
312
|
`0.4.0` — version-alignment release with the workspace (the paired
|
|
298
313
|
`ng-form-foundry` 0.4.0 adds appearance-driven field layout); no transformer
|
|
299
314
|
changes.
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -32,6 +32,13 @@ export interface Leaf {
|
|
|
32
32
|
min?: number;
|
|
33
33
|
max?: number;
|
|
34
34
|
multipleOf?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Present the value in this base (16 hex, 8 octal, 2 binary) instead of
|
|
37
|
+
* decimal — set when the source document wrote the literal that way. Purely
|
|
38
|
+
* a display hint: the value itself stays a plain number (or, on a string
|
|
39
|
+
* leaf, the exact decimal-digit carry of an integer beyond ±2^53).
|
|
40
|
+
*/
|
|
41
|
+
radix?: 2 | 8 | 16;
|
|
35
42
|
/** The value may be `null` (JSON Schema `type: [T, 'null']`). */
|
|
36
43
|
nullable?: boolean;
|
|
37
44
|
/** Optional scalar whose presence is itself data (on/off toggle). */
|
|
@@ -47,6 +54,8 @@ export interface LeafList {
|
|
|
47
54
|
label?: string;
|
|
48
55
|
minItems?: number;
|
|
49
56
|
maxItems?: number;
|
|
57
|
+
/** Present every item in this base — see {@link Leaf.radix}. */
|
|
58
|
+
radix?: 2 | 8 | 16;
|
|
50
59
|
}
|
|
51
60
|
export interface NodeGroup {
|
|
52
61
|
kind: 'nodeGroup';
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
|
|
14
14
|
* as `yaml`, indent preserved.
|
|
15
15
|
* - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
|
|
16
|
-
* libconfig ({@link libconfigTransformer});
|
|
17
|
-
*
|
|
16
|
+
* libconfig ({@link libconfigTransformer}); comment- and
|
|
17
|
+
* type-preserving span splicing on revert.
|
|
18
18
|
*
|
|
19
19
|
* Look transformers up at runtime through {@link TransformerRegistry}, or import
|
|
20
20
|
* the one you need directly.
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
|
|
15
15
|
* as `yaml`, indent preserved.
|
|
16
16
|
* - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
|
|
17
|
-
* libconfig ({@link libconfigTransformer});
|
|
18
|
-
*
|
|
17
|
+
* libconfig ({@link libconfigTransformer}); comment- and
|
|
18
|
+
* type-preserving span splicing on revert.
|
|
19
19
|
*
|
|
20
20
|
* Look transformers up at runtime through {@link TransformerRegistry}, or import
|
|
21
21
|
* the one you need directly.
|
|
@@ -41,4 +41,4 @@ __exportStar(require("./core"), exports);
|
|
|
41
41
|
__exportStar(require("./transformers/yang"), exports);
|
|
42
42
|
__exportStar(require("./transformers/yaml"), exports);
|
|
43
43
|
__exportStar(require("./transformers/json"), exports);
|
|
44
|
-
__exportStar(require("./transformers/libconfig"), exports);
|
|
44
|
+
__exportStar(require("./transformers/libconfig"), exports);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { libconfigTransformer,
|
|
1
|
+
export { libconfigTransformer, type LibconfigBinding, type LibconfigOptions, } from './libconfig-transformer';
|
|
2
2
|
export { parseLibconfig, LibconfigParseError } from './parser';
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LibconfigParseError = exports.parseLibconfig = exports.
|
|
3
|
+
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
|
-
Object.defineProperty(exports, "resetLibconfigBetaWarning", { enumerable: true, get: function () { return libconfig_transformer_1.resetLibconfigBetaWarning; } });
|
|
7
6
|
var parser_1 = require("./parser");
|
|
8
7
|
Object.defineProperty(exports, "parseLibconfig", { enumerable: true, get: function () { return parser_1.parseLibconfig; } });
|
|
9
8
|
Object.defineProperty(exports, "LibconfigParseError", { enumerable: true, get: function () { return parser_1.LibconfigParseError; } });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A libconfig {@link Transformer}
|
|
2
|
+
* A libconfig {@link Transformer}: turn a libconfig document
|
|
3
3
|
* (srsRAN/OAI-style `.cfg`/`.conf`) into a form and write the edited value
|
|
4
4
|
* back with comments, formatting, and — critically — scalar *types*
|
|
5
5
|
* preserved (libconfig is statically typed; see {@link import('./revert')}).
|
|
@@ -47,8 +47,6 @@ export interface LibconfigBinding {
|
|
|
47
47
|
source: string;
|
|
48
48
|
root: CfgGroup;
|
|
49
49
|
}
|
|
50
|
-
/** Test seam: makes the one-time beta warning observable per test. */
|
|
51
|
-
export declare function resetLibconfigBetaWarning(): void;
|
|
52
50
|
export declare const libconfigTransformer: {
|
|
53
51
|
id: string;
|
|
54
52
|
toSchema(source: string, options?: LibconfigOptions): TransformResult<LibconfigBinding>;
|
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.libconfigTransformer = void 0;
|
|
4
|
-
exports.resetLibconfigBetaWarning = resetLibconfigBetaWarning;
|
|
5
4
|
const thesaurus_1 = require("../../core/thesaurus");
|
|
6
5
|
const json_schema_1 = require("../../core/json-schema");
|
|
7
6
|
const parser_1 = require("./parser");
|
|
8
7
|
const schema_1 = require("./schema");
|
|
9
8
|
const revert_1 = require("./revert");
|
|
10
|
-
let warnedBeta = false;
|
|
11
|
-
function warnBeta() {
|
|
12
|
-
if (warnedBeta)
|
|
13
|
-
return;
|
|
14
|
-
warnedBeta = true;
|
|
15
|
-
console.warn('[ng-form-foundry-transformers] The libconfig transformer is a BETA feature. ' +
|
|
16
|
-
'Verify every write-back (diff toSource output against the original file) before deploying it.');
|
|
17
|
-
}
|
|
18
|
-
/** Test seam: makes the one-time beta warning observable per test. */
|
|
19
|
-
function resetLibconfigBetaWarning() {
|
|
20
|
-
warnedBeta = false;
|
|
21
|
-
}
|
|
22
9
|
exports.libconfigTransformer = {
|
|
23
10
|
id: 'libconfig',
|
|
24
11
|
toSchema(source, options) {
|
|
25
|
-
warnBeta();
|
|
26
12
|
const root = (0, parser_1.parseLibconfig)(source, { includes: options?.includes });
|
|
27
13
|
const schema = options?.schema
|
|
28
14
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
@@ -32,7 +18,6 @@ exports.libconfigTransformer = {
|
|
|
32
18
|
return { schema: labeled, binding: { source, root }, initialValue };
|
|
33
19
|
},
|
|
34
20
|
toSource(value, binding) {
|
|
35
|
-
warnBeta();
|
|
36
21
|
return (0, revert_1.applyValueToSource)(binding.source, binding.root, value);
|
|
37
22
|
},
|
|
38
23
|
};
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
* (https://hyperrealm.github.io/libconfig/libconfig_manual.html); the grammar
|
|
14
14
|
* covers: groups `{}`, arrays `[]` (homogeneous scalars), lists `()`
|
|
15
15
|
* (heterogeneous), settings `name = value;` / `name : value` with an optional
|
|
16
|
-
* `;`/`,` terminator, int (decimal
|
|
16
|
+
* `;`/`,` terminator, int (decimal, `0x` hex, `0b` binary, `0o`/`0q` octal —
|
|
17
|
+
* a sign is legal on decimal only, as in the C scanner), int64 (`L`/`LL`
|
|
17
18
|
* suffix), float, bool (case-insensitive), strings with escapes and adjacent
|
|
18
|
-
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
19
|
+
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
20
|
+
* (handled as trivia, so they are legal anywhere the C scanner accepts them).
|
|
21
|
+
* Nesting is capped at {@link MAX_DEPTH} levels so hostile documents fail
|
|
22
|
+
* with a parse error instead of exhausting the JS stack.
|
|
19
23
|
*/
|
|
20
24
|
/** Half-open source range of a node, in code-unit offsets. */
|
|
21
25
|
export interface Span {
|
|
@@ -29,6 +33,8 @@ export interface IntMeta {
|
|
|
29
33
|
suffix: string;
|
|
30
34
|
/** Hex/binary/octal digit count of the literal, for width-preserving edits. */
|
|
31
35
|
digits: number;
|
|
36
|
+
/** Radix prefix verbatim (`0x`, `0B`, `0q`, …), empty for decimal. */
|
|
37
|
+
prefix: string;
|
|
32
38
|
}
|
|
33
39
|
export type CfgValue = CfgScalar | CfgGroup | CfgArray | CfgList;
|
|
34
40
|
export interface CfgScalar {
|
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
* (https://hyperrealm.github.io/libconfig/libconfig_manual.html); the grammar
|
|
15
15
|
* covers: groups `{}`, arrays `[]` (homogeneous scalars), lists `()`
|
|
16
16
|
* (heterogeneous), settings `name = value;` / `name : value` with an optional
|
|
17
|
-
* `;`/`,` terminator, int (decimal
|
|
17
|
+
* `;`/`,` terminator, int (decimal, `0x` hex, `0b` binary, `0o`/`0q` octal —
|
|
18
|
+
* a sign is legal on decimal only, as in the C scanner), int64 (`L`/`LL`
|
|
18
19
|
* suffix), float, bool (case-insensitive), strings with escapes and adjacent
|
|
19
|
-
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
20
|
+
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
21
|
+
* (handled as trivia, so they are legal anywhere the C scanner accepts them).
|
|
22
|
+
* Nesting is capped at {@link MAX_DEPTH} levels so hostile documents fail
|
|
23
|
+
* with a parse error instead of exhausting the JS stack.
|
|
20
24
|
*/
|
|
21
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
26
|
exports.LibconfigParseError = void 0;
|
|
@@ -33,18 +37,22 @@ class LibconfigParseError extends Error {
|
|
|
33
37
|
}
|
|
34
38
|
exports.LibconfigParseError = LibconfigParseError;
|
|
35
39
|
const NAME_RE = /^[A-Za-z*][-A-Za-z0-9_*.]*/;
|
|
36
|
-
|
|
40
|
+
// Like the C scanner: octal is `0o` or `0q`, and only decimal takes a sign.
|
|
41
|
+
const INT_RE = /^(0[xX][0-9A-Fa-f]+|0[bB][01]+|0[oOqQ][0-7]+|[-+]?[0-9]+)(LL?)?/;
|
|
37
42
|
const FLOAT_RE = /^[-+]?(([0-9]*\.[0-9]+|[0-9]+\.[0-9]*)([eE][-+]?[0-9]+)?|[0-9]+[eE][-+]?[0-9]+)/;
|
|
38
43
|
const BOOL_RE = /^(true|false)(?![-A-Za-z0-9_*.])/i;
|
|
39
44
|
/** Parse a libconfig document into its root group (spans index into `source`). */
|
|
40
45
|
function parseLibconfig(source, options) {
|
|
41
46
|
return new Parser(source, options?.includes ?? 'reject').parseRoot();
|
|
42
47
|
}
|
|
48
|
+
/** Deepest legal value nesting — far past real configs, well inside JS stack. */
|
|
49
|
+
const MAX_DEPTH = 256;
|
|
43
50
|
class Parser {
|
|
44
51
|
constructor(src, includes) {
|
|
45
52
|
this.src = src;
|
|
46
53
|
this.includes = includes;
|
|
47
54
|
this.pos = 0;
|
|
55
|
+
this.depth = 0;
|
|
48
56
|
}
|
|
49
57
|
parseRoot() {
|
|
50
58
|
const settings = this.parseSettings(undefined);
|
|
@@ -67,10 +75,6 @@ class Parser {
|
|
|
67
75
|
}
|
|
68
76
|
if (closer && this.src[this.pos] === closer)
|
|
69
77
|
return settings;
|
|
70
|
-
if (this.src[this.pos] === '@') {
|
|
71
|
-
this.handleInclude();
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
78
|
settings.push(this.parseSetting(settings));
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -94,14 +98,21 @@ class Parser {
|
|
|
94
98
|
return { name, span: { start, end: this.pos }, value };
|
|
95
99
|
}
|
|
96
100
|
parseValue() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (++this.depth > MAX_DEPTH)
|
|
102
|
+
this.fail(`nesting deeper than ${MAX_DEPTH} levels`);
|
|
103
|
+
try {
|
|
104
|
+
const c = this.src[this.pos];
|
|
105
|
+
if (c === '{')
|
|
106
|
+
return this.parseGroup();
|
|
107
|
+
if (c === '[')
|
|
108
|
+
return this.parseCollection('[', ']', 'array');
|
|
109
|
+
if (c === '(')
|
|
110
|
+
return this.parseCollection('(', ')', 'list');
|
|
111
|
+
return this.parseScalar();
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this.depth--;
|
|
115
|
+
}
|
|
105
116
|
}
|
|
106
117
|
parseGroup() {
|
|
107
118
|
const start = this.pos++;
|
|
@@ -181,15 +192,16 @@ class Parser {
|
|
|
181
192
|
return this.fail('expected a value');
|
|
182
193
|
}
|
|
183
194
|
intScalar(lexeme, span) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
const
|
|
195
|
+
const suffix = /LL?$/i.exec(lexeme)?.[0] ?? '';
|
|
196
|
+
const body = suffix ? lexeme.slice(0, -suffix.length) : lexeme;
|
|
197
|
+
const prefix = /^0[xXbBoOqQ]/.test(body) ? body.slice(0, 2) : '';
|
|
198
|
+
const p = prefix.toLowerCase();
|
|
199
|
+
const radix = p === '0x' ? 16 : p === '0b' ? 2 : p === '' ? 10 : 8;
|
|
200
|
+
const digits = prefix ? body.slice(2) : body.replace(/^[-+]/, '');
|
|
201
|
+
const sign = body[0] === '-' ? '-' : '';
|
|
202
|
+
// `0q`/`0O` normalize to a prefix BigInt understands; decimal keeps its sign.
|
|
203
|
+
const big = prefix ? BigInt((radix === 8 ? '0o' : p) + digits) : BigInt(sign + digits);
|
|
204
|
+
const meta = { radix, suffix, digits: digits.length, prefix };
|
|
193
205
|
// Values past 2^53 ride as exact decimal strings (the core bigint strategy);
|
|
194
206
|
// the suffix and radix stay in the metadata for the write-back.
|
|
195
207
|
const safe = big >= BigInt(Number.MIN_SAFE_INTEGER) && big <= BigInt(Number.MAX_SAFE_INTEGER);
|
|
@@ -228,8 +240,11 @@ class Parser {
|
|
|
228
240
|
}
|
|
229
241
|
if (c === '\\') {
|
|
230
242
|
const esc = this.src[this.pos + 1];
|
|
231
|
-
if (esc === 'x') {
|
|
232
|
-
|
|
243
|
+
if (esc === 'x' || esc === 'X') {
|
|
244
|
+
const hex = this.src.slice(this.pos + 2, this.pos + 4);
|
|
245
|
+
if (!/^[0-9A-Fa-f]{2}$/.test(hex))
|
|
246
|
+
this.fail('\\x escape needs two hex digits');
|
|
247
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
233
248
|
this.pos += 4;
|
|
234
249
|
continue;
|
|
235
250
|
}
|
|
@@ -253,7 +268,7 @@ class Parser {
|
|
|
253
268
|
}
|
|
254
269
|
this.pos = lineEnd === -1 ? this.src.length : lineEnd + 1;
|
|
255
270
|
}
|
|
256
|
-
/** Skip whitespace
|
|
271
|
+
/** Skip whitespace, all three comment forms, and `@include` lines. */
|
|
257
272
|
skipTrivia() {
|
|
258
273
|
for (;;) {
|
|
259
274
|
const c = this.src[this.pos];
|
|
@@ -261,6 +276,12 @@ class Parser {
|
|
|
261
276
|
this.pos++;
|
|
262
277
|
continue;
|
|
263
278
|
}
|
|
279
|
+
// The C scanner handles `@include` at token level, so it is legal in any
|
|
280
|
+
// trivia position (even between list elements), not just between settings.
|
|
281
|
+
if (c === '@' && this.src.startsWith('@include', this.pos)) {
|
|
282
|
+
this.handleInclude();
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
264
285
|
if (c === '#' || (c === '/' && this.src[this.pos + 1] === '/')) {
|
|
265
286
|
const nl = this.src.indexOf('\n', this.pos);
|
|
266
287
|
this.pos = nl === -1 ? this.src.length : nl;
|
|
@@ -21,7 +21,8 @@ function patchGroup(src, group, value, edits) {
|
|
|
21
21
|
patchValue(src, setting.value, value[setting.name], edits);
|
|
22
22
|
}
|
|
23
23
|
const known = new Set(group.settings.map((s) => s.name));
|
|
24
|
-
|
|
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);
|
|
25
26
|
if (added.length) {
|
|
26
27
|
edits.push(insertionEdit(src, group, added.map((k) => `${k} = ${serialize(value[k], '')};`)));
|
|
27
28
|
}
|
|
@@ -31,6 +32,10 @@ function patchValue(src, node, value, edits) {
|
|
|
31
32
|
case 'scalar': {
|
|
32
33
|
if (node.value === value)
|
|
33
34
|
return;
|
|
35
|
+
// The collection-wide bigint carry hands safe elements back as digit
|
|
36
|
+
// strings; a string spelling the same value is not an edit.
|
|
37
|
+
if (typeof value === 'string' && typeof node.value === 'number' && value === String(node.value))
|
|
38
|
+
return;
|
|
34
39
|
edits.push({ ...node.span, text: emitScalar(value, node) });
|
|
35
40
|
return;
|
|
36
41
|
}
|
|
@@ -43,9 +48,13 @@ function patchValue(src, node, value, edits) {
|
|
|
43
48
|
case 'array': {
|
|
44
49
|
if (Array.isArray(value))
|
|
45
50
|
return patchElements(src, node.elements, node, value, edits);
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
// A string here is the read-only raw carry of an untyped empty array:
|
|
52
|
+
// unchanged it stays verbatim, edited it is an error, never a splice.
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
if (value === (0, schema_1.raw)(node, src))
|
|
55
|
+
return;
|
|
56
|
+
throw new Error('this collection is read-only (no element type to edit by): the edited text was not applied');
|
|
57
|
+
}
|
|
49
58
|
edits.push({ ...node.span, text: serialize(value, '') });
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
@@ -64,11 +73,15 @@ function patchValue(src, node, value, edits) {
|
|
|
64
73
|
}
|
|
65
74
|
return;
|
|
66
75
|
}
|
|
67
|
-
// Heterogeneous (read-only raw carry) or unchanged empty:
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
// Heterogeneous (read-only raw carry) or unchanged empty: the carry
|
|
77
|
+
// stays verbatim, an edited carry is an error, a non-string value is a
|
|
78
|
+
// genuine shape change and regenerates.
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
if (value === (0, schema_1.raw)(node, src))
|
|
81
|
+
return;
|
|
82
|
+
throw new Error('this list is read-only (heterogeneous): the edited text was not applied');
|
|
83
|
+
}
|
|
84
|
+
if (value !== undefined) {
|
|
72
85
|
edits.push({ ...node.span, text: serialize(value, '') });
|
|
73
86
|
}
|
|
74
87
|
return;
|
|
@@ -146,11 +159,17 @@ function emitScalar(value, node) {
|
|
|
146
159
|
if (typeof value === 'boolean')
|
|
147
160
|
return value ? 'true' : 'false';
|
|
148
161
|
if (typeof value === 'string') {
|
|
149
|
-
|
|
150
|
-
if (node.type === 'int64' && typeof node.value === 'string') {
|
|
162
|
+
if (node.type === 'int' || node.type === 'int64') {
|
|
151
163
|
if (!/^[-+]?[0-9]+$/.test(value)) {
|
|
152
|
-
throw new Error(`'${value}' is not an integer: this setting
|
|
164
|
+
throw new Error(`'${value}' is not an integer: this setting is integer-typed`);
|
|
153
165
|
}
|
|
166
|
+
// The int64 string carry: exact digits back, with the original suffix.
|
|
167
|
+
if (typeof node.value === 'string')
|
|
168
|
+
return value + (node.int?.suffix ?? '');
|
|
169
|
+
// A safe element of a carried collection: back to its own literal style.
|
|
170
|
+
const n = Number(value);
|
|
171
|
+
if (Number.isSafeInteger(n))
|
|
172
|
+
return intLiteral(n, node.int);
|
|
154
173
|
return value + (node.int?.suffix ?? '');
|
|
155
174
|
}
|
|
156
175
|
return quote(value);
|
|
@@ -165,13 +184,17 @@ function emitScalar(value, node) {
|
|
|
165
184
|
}
|
|
166
185
|
return serialize(value, '');
|
|
167
186
|
}
|
|
168
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Integer in the source's radix, width, and prefix spelling, suffix preserved.
|
|
189
|
+
* Negatives always emit decimal: the C scanner accepts no sign on a
|
|
190
|
+
* hex/binary/octal literal, so `-0x1A` would not load.
|
|
191
|
+
*/
|
|
169
192
|
function intLiteral(value, meta) {
|
|
170
|
-
if (!meta || meta.radix === 10)
|
|
193
|
+
if (!meta || meta.radix === 10 || value < 0)
|
|
171
194
|
return String(value) + (meta?.suffix ?? '');
|
|
172
|
-
const prefix = meta.radix === 16 ? '0x' : meta.radix === 2 ? '0b' : '0o';
|
|
173
|
-
const digits =
|
|
174
|
-
return
|
|
195
|
+
const prefix = meta.prefix || (meta.radix === 16 ? '0x' : meta.radix === 2 ? '0b' : '0o');
|
|
196
|
+
const digits = value.toString(meta.radix).toUpperCase().padStart(meta.digits, '0');
|
|
197
|
+
return prefix + digits + meta.suffix;
|
|
175
198
|
}
|
|
176
199
|
/** A float literal that stays a float: integral values gain `.0`. */
|
|
177
200
|
function floatLiteral(value) {
|
|
@@ -206,9 +229,13 @@ function quote(value) {
|
|
|
206
229
|
* grown collection). Typing is value-driven here: integral numbers emit as
|
|
207
230
|
* ints, fractional as floats — a host that needs a float-typed `21.0` for a
|
|
208
231
|
* fresh setting should send `21.0`-producing edits through an existing float
|
|
209
|
-
* slot or accept the int typing
|
|
232
|
+
* slot or accept the int typing. null/undefined have no libconfig literal
|
|
233
|
+
* and throw.
|
|
210
234
|
*/
|
|
211
235
|
function serialize(value, indent) {
|
|
236
|
+
if (value == null) {
|
|
237
|
+
throw new Error('null/undefined has no libconfig representation: omit the setting instead');
|
|
238
|
+
}
|
|
212
239
|
if (typeof value === 'boolean')
|
|
213
240
|
return value ? 'true' : 'false';
|
|
214
241
|
if (typeof value === 'number')
|
|
@@ -226,7 +253,7 @@ function serialize(value, indent) {
|
|
|
226
253
|
.join(' ');
|
|
227
254
|
return `{ ${body} }`;
|
|
228
255
|
}
|
|
229
|
-
|
|
256
|
+
throw new Error(`a ${typeof value} value has no libconfig representation`);
|
|
230
257
|
}
|
|
231
258
|
/** An integer string that exceeds the double-safe range (the bigint carry). */
|
|
232
259
|
function isBigIntString(value) {
|
|
@@ -80,14 +80,21 @@ function scalarLeaf(name, scalar) {
|
|
|
80
80
|
case 'float':
|
|
81
81
|
return { kind: 'leaf', name, type: 'number' };
|
|
82
82
|
case 'int':
|
|
83
|
-
return { kind: 'leaf', name, type: 'number', integer: true };
|
|
83
|
+
return withRadix({ kind: 'leaf', name, type: 'number', integer: true }, scalar);
|
|
84
84
|
case 'int64':
|
|
85
85
|
// Beyond 2^53 the value rides as an exact decimal string.
|
|
86
86
|
return typeof scalar.value === 'string'
|
|
87
|
-
? { kind: 'leaf', name, type: 'string', pattern: INTEGER_STRING_PATTERN }
|
|
88
|
-
: { kind: 'leaf', name, type: 'number', integer: true };
|
|
87
|
+
? withRadix({ kind: 'leaf', name, type: 'string', pattern: INTEGER_STRING_PATTERN }, scalar)
|
|
88
|
+
: withRadix({ kind: 'leaf', name, type: 'number', integer: true }, scalar);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
/** Carry a non-decimal literal's base onto the leaf as its display radix. */
|
|
92
|
+
function withRadix(leaf, scalar) {
|
|
93
|
+
const radix = scalar.int?.radix;
|
|
94
|
+
if (radix && radix !== 10)
|
|
95
|
+
leaf.radix = radix;
|
|
96
|
+
return leaf;
|
|
97
|
+
}
|
|
91
98
|
/**
|
|
92
99
|
* A homogeneous scalar collection → leafList. One int64 element beyond 2^53
|
|
93
100
|
* degrades the whole list to string carry: a leafList holds one scalar type,
|
|
@@ -97,10 +104,23 @@ function listLeaf(name, elements) {
|
|
|
97
104
|
if (elements.length === 0)
|
|
98
105
|
return rawLeaf(name, 'empty collection');
|
|
99
106
|
const f = (0, parser_1.family)(elements[0].type);
|
|
107
|
+
const radix = f === 'integer' ? sharedRadix(elements) : undefined;
|
|
100
108
|
if (f === 'integer' && elements.some((e) => typeof e.value === 'string')) {
|
|
101
|
-
return { kind: 'leafList', name, type: 'string' };
|
|
109
|
+
return { kind: 'leafList', name, type: 'string', ...(radix && { radix }) };
|
|
102
110
|
}
|
|
103
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'leafList',
|
|
113
|
+
name,
|
|
114
|
+
type: f === 'integer' || f === 'float' ? 'number' : f === 'bool' ? 'boolean' : 'string',
|
|
115
|
+
...(radix && { radix }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** The display radix shared by every element, when uniform and non-decimal. */
|
|
119
|
+
function sharedRadix(elements) {
|
|
120
|
+
const first = elements[0]?.int?.radix ?? 10;
|
|
121
|
+
if (first === 10)
|
|
122
|
+
return undefined;
|
|
123
|
+
return elements.every((e) => (e.int?.radix ?? 10) === first) ? first : undefined;
|
|
104
124
|
}
|
|
105
125
|
function arrayValue(elements) {
|
|
106
126
|
const stringCarry = elements.length > 0 &&
|
|
@@ -149,12 +169,15 @@ function listShape(list) {
|
|
|
149
169
|
}
|
|
150
170
|
/** Read-only leaf carrying the collection's verbatim source text. */
|
|
151
171
|
function rawLeaf(name, why) {
|
|
172
|
+
const hint = why === 'empty collection'
|
|
173
|
+
? 'Provide a JSON Schema to type its elements and make it editable.'
|
|
174
|
+
: 'It stays read-only: no single element type exists to edit by.';
|
|
152
175
|
return {
|
|
153
176
|
kind: 'leaf',
|
|
154
177
|
name,
|
|
155
178
|
type: 'string',
|
|
156
179
|
readOnly: true,
|
|
157
|
-
description: `Shown verbatim (${why})
|
|
180
|
+
description: `Shown verbatim (${why}). ${hint}`,
|
|
158
181
|
};
|
|
159
182
|
}
|
|
160
183
|
/** The verbatim source slice of a node. */
|
|
@@ -30,6 +30,8 @@ exports.yamlTransformer = {
|
|
|
30
30
|
const schema = options?.schema
|
|
31
31
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
32
32
|
: (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
33
|
+
if (!options?.schema && doc.contents)
|
|
34
|
+
annotateRadix(doc.contents, schema);
|
|
33
35
|
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
34
36
|
return { schema: labeled, binding: doc, initialValue: data };
|
|
35
37
|
},
|
|
@@ -41,6 +43,67 @@ exports.yamlTransformer = {
|
|
|
41
43
|
// `satisfies` verifies conformance to the catalog contract while keeping the
|
|
42
44
|
// concrete sync return types, so direct callers need no `await`.
|
|
43
45
|
};
|
|
46
|
+
const RADIX_BY_FORMAT = { BIN: 2, OCT: 8, HEX: 16 };
|
|
47
|
+
/**
|
|
48
|
+
* Copy non-decimal integer presentation (`0x`/`0o`/`0b` literals) from the
|
|
49
|
+
* parsed document onto the inferred schema as leaf/leafList `radix` display
|
|
50
|
+
* hints — the plain-data inference cannot see them, because `toJS()` drops the
|
|
51
|
+
* scalar format. The revert needs nothing: scalars are mutated in place, so an
|
|
52
|
+
* edited value re-emits in its literal's own base. Across group-list items the
|
|
53
|
+
* first non-decimal occurrence wins — one shared item schema cannot vary the
|
|
54
|
+
* display per item. Schema-driven mode is untouched (JSON Schema has no radix
|
|
55
|
+
* vocabulary).
|
|
56
|
+
*/
|
|
57
|
+
function annotateRadix(node, schema) {
|
|
58
|
+
switch (schema.kind) {
|
|
59
|
+
case 'leaf': {
|
|
60
|
+
const radix = scalarRadix(node);
|
|
61
|
+
if (radix && !schema.radix)
|
|
62
|
+
schema.radix = radix;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
case 'leafList': {
|
|
66
|
+
if (!(0, yaml_1.isSeq)(node) || schema.radix)
|
|
67
|
+
return;
|
|
68
|
+
const radixes = node.items.map(scalarRadix);
|
|
69
|
+
if (radixes[0] && radixes.every((r) => r === radixes[0]))
|
|
70
|
+
schema.radix = radixes[0];
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case 'nodeGroup': {
|
|
74
|
+
if (!(0, yaml_1.isMap)(node))
|
|
75
|
+
return;
|
|
76
|
+
for (const pair of node.items) {
|
|
77
|
+
const child = schema.children[pairKey(pair.key)];
|
|
78
|
+
if (child)
|
|
79
|
+
annotateRadix(pair.value, child);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
case 'nodeGroupList': {
|
|
84
|
+
if (!(0, yaml_1.isSeq)(node))
|
|
85
|
+
return;
|
|
86
|
+
for (const item of node.items)
|
|
87
|
+
annotateRadix(item, schema.type);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return; // choice/map never come out of plain-data inference
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The display radix of a non-decimal integer scalar node, else undefined. */
|
|
95
|
+
function scalarRadix(node) {
|
|
96
|
+
if (!(0, yaml_1.isScalar)(node))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (typeof node.value !== 'number' && typeof node.value !== 'bigint')
|
|
99
|
+
return undefined;
|
|
100
|
+
return RADIX_BY_FORMAT[node.format ?? ''];
|
|
101
|
+
}
|
|
102
|
+
/** The string a key node takes as a JS object key, matching `doc.toJS()`. */
|
|
103
|
+
function pairKey(key) {
|
|
104
|
+
const v = (0, yaml_1.isScalar)(key) ? key.value : key;
|
|
105
|
+
return v == null ? '' : String(v);
|
|
106
|
+
}
|
|
44
107
|
/**
|
|
45
108
|
* Replace every BigInt from an `intAsBigInt` parse with a form-value scalar: a
|
|
46
109
|
* plain `number` when it fits the safe range, otherwise its decimal string (so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ng-form-foundry-transformers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Framework-agnostic Node + TypeScript catalog of source-format transformers: turn a YANG model (and more) into an ng-form-foundry schema and revert the edited form value back to the source format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ng-form-foundry",
|