ng-form-foundry-transformers 0.3.2 → 0.3.4

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 CHANGED
@@ -16,6 +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) | **beta** — warns on first use |
19
20
 
20
21
  The YAML and JSON transformers share the same format-agnostic form builders in
21
22
  `core` (`inferNodeGroup`, `jsonSchemaToNodeGroup`) — a JSON Schema is an *option*
@@ -80,9 +81,16 @@ covers **draft 2020-12** (back-compatible with draft-07): `object` → nodeGroup
80
81
  a `map` for `additionalProperties`/`patternProperties`), `array` → nodeGroupList /
81
82
  leafList, `anyOf`/`oneOf` → choice (or a nullable leaf for `[T, null]`), `$ref` →
82
83
  `$defs`/`definitions` resolved inline (local **or cross-file** via
83
- `options.refDocuments`, matched by `$id`), `const` → a read-only leaf, and the string /
84
- number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`, `format`, …)
85
- carried onto the leaves as validators.
84
+ `schemaOptions.refDocuments`, matched by `$id`), `const` → a read-only leaf, and the
85
+ string / number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`,
86
+ `format`, …) carried onto the leaves as validators, with
87
+ `minProperties`/`maxProperties` becoming present-children bounds on closed
88
+ objects (`minPresent`/`maxPresent`) or entry bounds on open maps.
89
+ Non-`required` properties become **`presence` nodes** — absent from the form
90
+ and the serialized value until
91
+ enabled, so the output validates against the source schema (opt out with
92
+ `schemaOptions: { optionalPresence: false }`); a required property mapping to a
93
+ choice is marked `mandatory`.
86
94
 
87
95
  ## JSON transformer
88
96
 
@@ -98,6 +106,42 @@ const { schema, binding, initialValue } = jsonTransformer.toSchema(configText);
98
106
  const out = jsonTransformer.toSource({ ...initialValue, replicas: 5 }, binding);
99
107
  ```
100
108
 
109
+ ## libconfig transformer (beta)
110
+
111
+ Edit a **libconfig document** — the `.cfg`/`.conf` format of srsRAN, OAI, and
112
+ other C/C++ software using [libconfig](https://hyperrealm.github.io/libconfig/).
113
+ **Beta**: it logs a one-time warning on first use; diff `toSource` output
114
+ against the original before deploying a write-back.
115
+
116
+ ```ts
117
+ import { libconfigTransformer } from 'ng-form-foundry-transformers';
118
+
119
+ const { schema, binding, initialValue } = libconfigTransformer.toSchema(cfgText);
120
+ // … build the form, let the user edit …
121
+ const editedCfg = libconfigTransformer.toSource(editedValue, binding);
122
+ ```
123
+
124
+ The revert **splices edited spans into the original text**, so comments, key
125
+ order, `=` vs `:`, terminators, and delimiter style survive verbatim on every
126
+ unedited byte. Emission is **type-preserving** — libconfig is statically typed,
127
+ so a float slot edited to an integral value emits `21.0` not `21`, hex re-emits
128
+ in hex at its original width, an int64 keeps its `L` suffix, and values beyond
129
+ 2^53 travel as exact decimal strings (the same bigint strategy as YAML/JSON).
130
+
131
+ Without a JSON Schema, the form is inferred from the document's own typed
132
+ literals; a list of groups infers the union of entry keys (keys missing from
133
+ some entries become presence toggles). **Empty and heterogeneous collections
134
+ are then shown read-only** — libconfig gives no element type to edit by; pass a
135
+ JSON Schema (`{ schema, schemaOptions? }`) to type them and make them editable.
136
+ `@include` is rejected by default (the form would show less than the C reader
137
+ sees); `{ includes: 'opaque' }` keeps the directive line verbatim and edits only
138
+ the file's own settings. Newly added settings are value-typed (an integral
139
+ number emits as an int) — route float-typed additions through a JSON Schema
140
+ slot that exists in the source, or accept the int typing.
141
+
142
+ The format, the parser design, and the full round-trip semantics are described
143
+ in [`docs/libconfig.md`](docs/libconfig.md).
144
+
101
145
  ## YANG transformer
102
146
 
103
147
  Turn a **YANG** model into an ng-form-foundry schema, then revert the edited form
@@ -187,6 +231,23 @@ npm test # node:test on the compiled output (no Python needed)
187
231
 
188
232
  ## Status
189
233
 
234
+ `0.3.4` — the **libconfig transformer debuts as a beta** (one-time console
235
+ warning on first use; lossless span-splicing round-trip with scalar types
236
+ preserved — see [`docs/libconfig.md`](docs/libconfig.md)), and
237
+ `minProperties`/`maxProperties` on closed objects now map to the library's
238
+ `minPresent`/`maxPresent` present-children bounds, so an emptied
239
+ all-optional object (e.g. an A1 `qosObjectives`) is flagged client-side
240
+ instead of only by the server's validator.
241
+
242
+ `0.3.3` — schema-valid round-trips for JSON-Schema-driven forms. Non-`required`
243
+ properties now map to `presence` nodes (absent until enabled; opt out with
244
+ `schemaOptions: { optionalPresence: false }`), a required property mapping to a
245
+ choice is flagged `mandatory`, and the YAML/JSON transformers gained
246
+ `options.schemaOptions` — which also makes `refDocuments` (cross-file `$ref`)
247
+ reachable through them for the first time. Pairs with the library's
248
+ required-aware choice-case inference, empty list seeding, and
249
+ materialized-validity rules.
250
+
190
251
  `0.3.2` — documentation release alongside the library's `serializeForm`: the
191
252
  YAML/JSON `toSource` examples now take the wire value (choice `__case`
192
253
  discriminators stripped); no transformer code changes.
@@ -47,6 +47,15 @@ export interface JsonSchemaOptions {
47
47
  * document then resolve against it.
48
48
  */
49
49
  refDocuments?: JsonSchema[];
50
+ /**
51
+ * Map non-required properties to `presence: true` nodes (default `true`):
52
+ * absent from the form and the serialized value until the user enables them,
53
+ * which is what `required` + `additionalProperties: false` schemas demand —
54
+ * an untouched optional materialized as `null` fails validation against the
55
+ * very schema the form came from. Set `false` to materialize every property
56
+ * unconditionally.
57
+ */
58
+ optionalPresence?: boolean;
50
59
  }
51
60
  type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
52
61
  /**
@@ -58,8 +67,11 @@ type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'bo
58
67
  * `additionalProperties`/`patternProperties`); `anyOf`/`oneOf` → a {@link Choice}
59
68
  * (or a nullable leaf for the `[T, null]` shape); `array` → nodeGroupList or
60
69
  * leafList; a scalar (with `enum`, `const`, `type: [T, 'null']`, and the string /
61
- * number constraints) → a typed leaf. `required` marks child leaves, `title`
62
- * becomes the label, `default`/`const` carry a scalar value.
70
+ * number constraints) → a typed leaf. `required` marks leaves required and a
71
+ * choice `mandatory`; a property *not* in `required` becomes a `presence` node
72
+ * — absent from the form value until enabled — unless
73
+ * {@link JsonSchemaOptions.optionalPresence} is `false`. `title` becomes the
74
+ * label, `default`/`const` carry a scalar value.
63
75
  */
64
76
  export declare function jsonSchemaToNodeGroup(schema: JsonSchema, name?: string, options?: JsonSchemaOptions): NodeGroup;
65
77
  export {};
@@ -11,17 +11,37 @@ const ROOT = '__root__';
11
11
  * `additionalProperties`/`patternProperties`); `anyOf`/`oneOf` → a {@link Choice}
12
12
  * (or a nullable leaf for the `[T, null]` shape); `array` → nodeGroupList or
13
13
  * leafList; a scalar (with `enum`, `const`, `type: [T, 'null']`, and the string /
14
- * number constraints) → a typed leaf. `required` marks child leaves, `title`
15
- * becomes the label, `default`/`const` carry a scalar value.
14
+ * number constraints) → a typed leaf. `required` marks leaves required and a
15
+ * choice `mandatory`; a property *not* in `required` becomes a `presence` node
16
+ * — absent from the form value until enabled — unless
17
+ * {@link JsonSchemaOptions.optionalPresence} is `false`. `title` becomes the
18
+ * label, `default`/`const` carry a scalar value.
16
19
  */
17
20
  function jsonSchemaToNodeGroup(schema, name = ROOT, options) {
18
21
  const documents = [schema, ...(options?.refDocuments ?? [])];
19
22
  const resolver = new RefResolver(schema, documents);
23
+ const ctx = { optionalPresence: options?.optionalPresence ?? true };
20
24
  const { schema: root, scope } = resolver.resolve(schema);
21
- const group = objectToNodeGroup(root, name, scope, root.title);
25
+ const group = objectToNodeGroup(root, name, scope, ctx, root.title);
22
26
  group.root = true;
23
27
  return group;
24
28
  }
29
+ /**
30
+ * Apply {@link JsonSchemaOptions.optionalPresence} to one *property* node: a
31
+ * non-required property becomes a presence node, so it stays absent from the
32
+ * form value until enabled. Only called at property positions
33
+ * ({@link objectToNodeGroup} children and {@link branchToCase} fields) — a map's
34
+ * `value` template, an array's item type, and a leaf-bodied case are not
35
+ * properties and must never be marked. Lists cannot carry `presence`.
36
+ */
37
+ function markOptionalProperty(node, required, ctx) {
38
+ if (!ctx.optionalPresence || required)
39
+ return node;
40
+ if (node.kind === 'leaf' || node.kind === 'nodeGroup' || node.kind === 'choice' || node.kind === 'map') {
41
+ node.presence = true;
42
+ }
43
+ return node;
44
+ }
25
45
  /**
26
46
  * Resolves `$ref` chains, following them across documents. A resolver is bound to
27
47
  * one document (for `#/…` local refs); crossing into another document via a
@@ -80,15 +100,22 @@ function resolveFragment(doc, fragment) {
80
100
  return node;
81
101
  }
82
102
  // --- objects / maps -----------------------------------------------------------
83
- function objectToNodeGroup(schema, name, resolver, label) {
103
+ function objectToNodeGroup(schema, name, resolver, ctx, label) {
84
104
  const required = new Set(schema.required ?? []);
85
105
  const children = {};
86
106
  for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
87
- children[key] = schemaToNode(key, propSchema, required.has(key), resolver);
107
+ const node = schemaToNode(key, propSchema, required.has(key), resolver, ctx);
108
+ children[key] = markOptionalProperty(node, required.has(key), ctx);
88
109
  }
89
110
  const group = { kind: 'nodeGroup', name, children };
90
111
  if (label)
91
112
  group.label = label;
113
+ // On a closed object these bound the count of *present* keys — meaningful
114
+ // because non-required properties are presence nodes (optionalPresence).
115
+ if (typeof schema.minProperties === 'number')
116
+ group.minPresent = schema.minProperties;
117
+ if (typeof schema.maxProperties === 'number')
118
+ group.maxPresent = schema.maxProperties;
92
119
  return group;
93
120
  }
94
121
  /** An `object` with open keys — `additionalProperties: <schema>` or `patternProperties`, and no fixed `properties`. */
@@ -100,7 +127,7 @@ function isOpenMap(schema) {
100
127
  const hasPattern = !!schema.patternProperties && Object.keys(schema.patternProperties).length > 0;
101
128
  return additionalIsSchema || hasPattern;
102
129
  }
103
- function objectToMap(name, schema, resolver) {
130
+ function objectToMap(name, schema, resolver, ctx) {
104
131
  let valueSchema = {};
105
132
  let keyPattern;
106
133
  const pattern = schema.patternProperties && Object.entries(schema.patternProperties)[0];
@@ -110,7 +137,7 @@ function objectToMap(name, schema, resolver) {
110
137
  else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
111
138
  valueSchema = schema.additionalProperties;
112
139
  }
113
- const node = { kind: 'map', name, value: schemaToNode('value', valueSchema, false, resolver) };
140
+ const node = { kind: 'map', name, value: schemaToNode('value', valueSchema, false, resolver, ctx) };
114
141
  if (schema.title)
115
142
  node.label = schema.title;
116
143
  if (keyPattern)
@@ -123,7 +150,7 @@ function objectToMap(name, schema, resolver) {
123
150
  }
124
151
  // --- anyOf / oneOf → choice ---------------------------------------------------
125
152
  /** The `anyOf`/`oneOf` `[T, null]` shape collapses to a nullable leaf; anything else is a choice. */
126
- function branchesToNode(name, schema, rawBranches, required, resolver) {
153
+ function branchesToNode(name, schema, rawBranches, required, resolver, ctx) {
127
154
  const branches = rawBranches.map((b) => resolver.resolve(b));
128
155
  const nonNull = branches.filter((b) => b.schema.type !== 'null');
129
156
  const sole = nonNull.length === 1 ? nonNull[0] : undefined;
@@ -136,11 +163,13 @@ function branchesToNode(name, schema, rawBranches, required, resolver) {
136
163
  const caseLabels = {};
137
164
  branches.forEach(({ schema: branch, scope }, index) => {
138
165
  const caseName = `case${index}`;
139
- cases[caseName] = branchToCase(branch, scope);
166
+ cases[caseName] = branchToCase(branch, scope, ctx);
140
167
  if (branch.title)
141
168
  caseLabels[caseName] = branch.title;
142
169
  });
143
170
  const choice = { kind: 'choice', name, cases };
171
+ if (required)
172
+ choice.mandatory = true;
144
173
  if (schema.title)
145
174
  choice.label = schema.title;
146
175
  if (Object.keys(caseLabels).length)
@@ -148,29 +177,30 @@ function branchesToNode(name, schema, rawBranches, required, resolver) {
148
177
  return choice;
149
178
  }
150
179
  /** A resolved branch → a case body: a field record for an object branch, else a single node. */
151
- function branchToCase(branch, scope) {
180
+ function branchToCase(branch, scope, ctx) {
152
181
  if (primaryType(branch) === 'object' && branch.properties) {
153
182
  const required = new Set(branch.required ?? []);
154
183
  const fields = {};
155
184
  for (const [key, propSchema] of Object.entries(branch.properties)) {
156
- fields[key] = schemaToNode(key, propSchema, required.has(key), scope);
185
+ const node = schemaToNode(key, propSchema, required.has(key), scope, ctx);
186
+ fields[key] = markOptionalProperty(node, required.has(key), ctx);
157
187
  }
158
188
  return fields;
159
189
  }
160
- return schemaToNode('value', branch, false, scope);
190
+ return schemaToNode('value', branch, false, scope, ctx);
161
191
  }
162
192
  // --- dispatch -----------------------------------------------------------------
163
- function schemaToNode(name, rawSchema, required, resolver) {
193
+ function schemaToNode(name, rawSchema, required, resolver, ctx) {
164
194
  const { schema, scope } = resolver.resolve(rawSchema);
165
195
  const branches = schema.anyOf ?? schema.oneOf;
166
196
  if (branches && branches.length) {
167
- return branchesToNode(name, schema, branches, required, scope);
197
+ return branchesToNode(name, schema, branches, required, scope, ctx);
168
198
  }
169
199
  const type = primaryType(schema);
170
200
  if (type === 'object' || (type === undefined && (schema.properties || isOpenMap(schema)))) {
171
201
  if (isOpenMap(schema))
172
- return objectToMap(name, schema, scope);
173
- return objectToNodeGroup(schema, name, scope, schema.title);
202
+ return objectToMap(name, schema, scope, ctx);
203
+ return objectToNodeGroup(schema, name, scope, ctx, schema.title);
174
204
  }
175
205
  if (type === 'array') {
176
206
  const { schema: items, scope: itemScope } = scope.resolve(schema.items ?? {});
@@ -178,7 +208,7 @@ function schemaToNode(name, rawSchema, required, resolver) {
178
208
  const node = {
179
209
  kind: 'nodeGroupList',
180
210
  name,
181
- type: objectToNodeGroup(items, name, itemScope, items.title),
211
+ type: objectToNodeGroup(items, name, itemScope, ctx, items.title),
182
212
  };
183
213
  if (schema.title)
184
214
  node.label = schema.title;
@@ -59,6 +59,13 @@ export interface NodeGroup {
59
59
  * serializes as `{}`, absent is omitted.
60
60
  */
61
61
  presence?: boolean;
62
+ /**
63
+ * Minimum / maximum number of keys present in the group's value (JSON Schema
64
+ * `minProperties`/`maxProperties` on a closed object with presence-optional
65
+ * children); the library validates the enabled-children count against them.
66
+ */
67
+ minPresent?: number;
68
+ maxPresent?: number;
62
69
  children: Record<string, NodeType>;
63
70
  }
64
71
  export interface NodeGroupList {
@@ -86,7 +93,10 @@ export interface Choice {
86
93
  /** Display labels for cases, keyed by case name. */
87
94
  caseLabels?: Record<string, string>;
88
95
  default?: string;
96
+ /** A case must be selected for the form to be valid (a required property). */
89
97
  mandatory?: boolean;
98
+ /** Optional choice whose presence is itself data (mirrors {@link NodeGroup.presence}). */
99
+ presence?: boolean;
90
100
  }
91
101
  /**
92
102
  * An open, arbitrary-keyed record: unlike {@link NodeGroup} (a fixed key set), a
package/dist/index.d.ts CHANGED
@@ -12,6 +12,9 @@
12
12
  * driven or inferred from the data, comment-preserving on revert.
13
13
  * - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
14
14
  * as `yaml`, indent preserved.
15
+ * - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
16
+ * libconfig ({@link libconfigTransformer}); **BETA**, warns once on
17
+ * first use; comment- and type-preserving span splicing on revert.
15
18
  *
16
19
  * Look transformers up at runtime through {@link TransformerRegistry}, or import
17
20
  * the one you need directly.
@@ -20,3 +23,4 @@ export * from './core';
20
23
  export * from './transformers/yang';
21
24
  export * from './transformers/yaml';
22
25
  export * from './transformers/json';
26
+ export * from './transformers/libconfig';
package/dist/index.js CHANGED
@@ -13,6 +13,9 @@
13
13
  * driven or inferred from the data, comment-preserving on revert.
14
14
  * - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
15
15
  * as `yaml`, indent preserved.
16
+ * - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
17
+ * libconfig ({@link libconfigTransformer}); **BETA**, warns once on
18
+ * first use; comment- and type-preserving span splicing on revert.
16
19
  *
17
20
  * Look transformers up at runtime through {@link TransformerRegistry}, or import
18
21
  * the one you need directly.
@@ -38,3 +41,4 @@ __exportStar(require("./core"), exports);
38
41
  __exportStar(require("./transformers/yang"), exports);
39
42
  __exportStar(require("./transformers/yaml"), exports);
40
43
  __exportStar(require("./transformers/json"), exports);
44
+ __exportStar(require("./transformers/libconfig"), exports); // BETA — warns once on first use
@@ -1,6 +1,6 @@
1
1
  import type { FormValue } from '../../core/schema';
2
2
  import type { TransformResult } from '../../core/transformer';
3
- import { type JsonSchema } from '../../core/json-schema';
3
+ import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
4
4
  /** Options for {@link jsonTransformer}'s `toSchema`. */
5
5
  export interface JsonOptions {
6
6
  /**
@@ -10,6 +10,8 @@ export interface JsonOptions {
10
10
  schema?: JsonSchema;
11
11
  /** Name for the root node group. Defaults to `__root__`. */
12
12
  rootName?: string;
13
+ /** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
14
+ schemaOptions?: JsonSchemaOptions;
13
15
  }
14
16
  /** How the source was formatted, so `toSource` re-emits it the same way. */
15
17
  export interface JsonFormat {
@@ -28,7 +28,7 @@ exports.jsonTransformer = {
28
28
  const parsed = JSON.parse(source, bigIntReviver);
29
29
  const data = collectBigInts(parsed ?? {}, [], bigInts);
30
30
  const schema = options?.schema
31
- ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName)
31
+ ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
32
32
  : (0, infer_1.inferNodeGroup)(data, options?.rootName);
33
33
  const binding = {
34
34
  indent: detectIndent(source),
@@ -0,0 +1,2 @@
1
+ export { libconfigTransformer, resetLibconfigBetaWarning, type LibconfigBinding, type LibconfigOptions, } from './libconfig-transformer';
2
+ export { parseLibconfig, LibconfigParseError } from './parser';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LibconfigParseError = exports.parseLibconfig = exports.resetLibconfigBetaWarning = exports.libconfigTransformer = void 0;
4
+ var libconfig_transformer_1 = require("./libconfig-transformer");
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
+ var parser_1 = require("./parser");
8
+ Object.defineProperty(exports, "parseLibconfig", { enumerable: true, get: function () { return parser_1.parseLibconfig; } });
9
+ Object.defineProperty(exports, "LibconfigParseError", { enumerable: true, get: function () { return parser_1.LibconfigParseError; } });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A libconfig {@link Transformer} (BETA): turn a libconfig document
3
+ * (srsRAN/OAI-style `.cfg`/`.conf`) into a form and write the edited value
4
+ * back with comments, formatting, and — critically — scalar *types*
5
+ * preserved (libconfig is statically typed; see {@link import('./revert')}).
6
+ *
7
+ * The `binding` is the original source plus its positioned AST; `toSource`
8
+ * splices edited spans into the source, so unedited bytes survive verbatim.
9
+ *
10
+ * Without a JSON Schema the form is inferred from the document's own typed
11
+ * literals ({@link import('./schema')}); empty and heterogeneous collections
12
+ * are then shown read-only. A JSON Schema in the options unlocks typed empty
13
+ * collections, presence toggles for optional settings, enums, and ranges.
14
+ */
15
+ import type { FormValue } from '../../core/schema';
16
+ import type { TransformResult } from '../../core/transformer';
17
+ import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
18
+ import { type CfgGroup } from './parser';
19
+ /** Options for {@link libconfigTransformer}'s `toSchema`. */
20
+ export interface LibconfigOptions {
21
+ /**
22
+ * A JSON Schema describing the config. When given, the form is built from it
23
+ * (types, required/presence, enums, empty-collection element types); when
24
+ * omitted, the form is inferred from the document's typed literals.
25
+ */
26
+ schema?: JsonSchema;
27
+ /** Name for the root node group. Defaults to `__root__`. */
28
+ rootName?: string;
29
+ /** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
30
+ schemaOptions?: JsonSchemaOptions;
31
+ /**
32
+ * `@include` handling: `'reject'` (default) errors, because the form would
33
+ * silently show less than the C reader sees; `'opaque'` keeps the directive
34
+ * line verbatim and edits only this file's own settings.
35
+ */
36
+ includes?: 'reject' | 'opaque';
37
+ }
38
+ /** The revert context: the original text and its positioned AST. */
39
+ export interface LibconfigBinding {
40
+ source: string;
41
+ root: CfgGroup;
42
+ }
43
+ /** Test seam: makes the one-time beta warning observable per test. */
44
+ export declare function resetLibconfigBetaWarning(): void;
45
+ export declare const libconfigTransformer: {
46
+ id: string;
47
+ toSchema(source: string, options?: LibconfigOptions): TransformResult<LibconfigBinding>;
48
+ toSource(value: FormValue, binding: LibconfigBinding): string;
49
+ };
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.libconfigTransformer = void 0;
4
+ exports.resetLibconfigBetaWarning = resetLibconfigBetaWarning;
5
+ const json_schema_1 = require("../../core/json-schema");
6
+ const parser_1 = require("./parser");
7
+ const schema_1 = require("./schema");
8
+ const revert_1 = require("./revert");
9
+ let warnedBeta = false;
10
+ function warnBeta() {
11
+ if (warnedBeta)
12
+ return;
13
+ warnedBeta = true;
14
+ console.warn('[ng-form-foundry-transformers] The libconfig transformer is a BETA feature. ' +
15
+ 'Verify every write-back (diff toSource output against the original file) before deploying it.');
16
+ }
17
+ /** Test seam: makes the one-time beta warning observable per test. */
18
+ function resetLibconfigBetaWarning() {
19
+ warnedBeta = false;
20
+ }
21
+ exports.libconfigTransformer = {
22
+ id: 'libconfig',
23
+ toSchema(source, options) {
24
+ warnBeta();
25
+ const root = (0, parser_1.parseLibconfig)(source, { includes: options?.includes });
26
+ const schema = options?.schema
27
+ ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
28
+ : (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
29
+ const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
30
+ return { schema, binding: { source, root }, initialValue };
31
+ },
32
+ toSource(value, binding) {
33
+ warnBeta();
34
+ return (0, revert_1.applyValueToSource)(binding.source, binding.root, value);
35
+ },
36
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A lossless libconfig parser: source text → positioned AST.
3
+ *
4
+ * Every node carries its `[start, end)` span into the original text, and every
5
+ * scalar carries the emission metadata (`radix`, `L` suffix, float-ness, raw
6
+ * lexeme) that {@link import('./revert')} needs to write an edited value back
7
+ * without re-typing the setting for the consuming C program — libconfig is
8
+ * statically typed, so `20` and `20.0` are different types to
9
+ * `config_lookup_*`. Comments and formatting are not modeled: the revert
10
+ * splices only edited value spans, so every unedited byte survives verbatim.
11
+ *
12
+ * Implemented independently from the libconfig manual
13
+ * (https://hyperrealm.github.io/libconfig/libconfig_manual.html); the grammar
14
+ * covers: groups `{}`, arrays `[]` (homogeneous scalars), lists `()`
15
+ * (heterogeneous), settings `name = value;` / `name : value` with an optional
16
+ * `;`/`,` terminator, int (decimal/hex/binary/octal), int64 (`L`/`LL`
17
+ * suffix), float, bool (case-insensitive), strings with escapes and adjacent
18
+ * literal concatenation, `#`/`//`/`/* *​/` comments, and `@include` lines.
19
+ */
20
+ /** Half-open source range of a node, in code-unit offsets. */
21
+ export interface Span {
22
+ start: number;
23
+ end: number;
24
+ }
25
+ /** How an integer literal was written, so an edit re-emits the same style. */
26
+ export interface IntMeta {
27
+ radix: 2 | 8 | 10 | 16;
28
+ /** `L`/`LL` suffix, verbatim, or empty. */
29
+ suffix: string;
30
+ /** Hex/binary/octal digit count of the literal, for width-preserving edits. */
31
+ digits: number;
32
+ }
33
+ export type CfgValue = CfgScalar | CfgGroup | CfgArray | CfgList;
34
+ export interface CfgScalar {
35
+ kind: 'scalar';
36
+ span: Span;
37
+ /**
38
+ * `int` and `float` hold a JS number; `int64` holds the exact decimal digits
39
+ * as a string when the value is outside the safe integer range, a number
40
+ * otherwise (the string carry mirrors `core/bigint.ts`).
41
+ */
42
+ type: 'int' | 'int64' | 'float' | 'bool' | 'string';
43
+ value: number | string | boolean;
44
+ int?: IntMeta;
45
+ }
46
+ export interface CfgSetting {
47
+ name: string;
48
+ /** The whole setting: name through terminator (exclusive of trailing comment). */
49
+ span: Span;
50
+ value: CfgValue;
51
+ }
52
+ export interface CfgGroup {
53
+ kind: 'group';
54
+ span: Span;
55
+ /** Between the braces (the whole file for the root) — the insertion window. */
56
+ innerSpan: Span;
57
+ settings: CfgSetting[];
58
+ }
59
+ export interface CfgArray {
60
+ kind: 'array';
61
+ span: Span;
62
+ innerSpan: Span;
63
+ elements: CfgScalar[];
64
+ }
65
+ export interface CfgList {
66
+ kind: 'list';
67
+ span: Span;
68
+ innerSpan: Span;
69
+ elements: CfgValue[];
70
+ }
71
+ /** Options observed by the parser (a subset of the transformer's options). */
72
+ export interface ParseOptions {
73
+ /**
74
+ * `'reject'` (default) errors on an `@include` line — the parsed form would
75
+ * silently show less than the C reader sees. `'opaque'` skips the directive:
76
+ * the line survives verbatim in the source and the included settings stay
77
+ * invisible to the form.
78
+ */
79
+ includes?: 'reject' | 'opaque';
80
+ }
81
+ /** A parse failure, pointing at the offending line and column. */
82
+ export declare class LibconfigParseError extends Error {
83
+ readonly line: number;
84
+ readonly column: number;
85
+ constructor(message: string, line: number, column: number);
86
+ }
87
+ /** Parse a libconfig document into its root group (spans index into `source`). */
88
+ export declare function parseLibconfig(source: string, options?: ParseOptions): CfgGroup;
89
+ /** Scalar type family for array homogeneity: int and int64 mix, float does not. */
90
+ export declare function family(type: CfgScalar['type']): 'integer' | 'float' | 'bool' | 'string';