layout-data 0.0.0-stage → 0.3.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.
@@ -0,0 +1,23 @@
1
+ // Dispatch of events that does not stop at a listener that throws
2
+ // (doc/spec-data-api.md §4.4, doc/design.md §5.1). Internal.
3
+
4
+ /**
5
+ * Calls every listener of an event, in the order they subscribed, and pushes
6
+ * what a listener throws to `errors` instead of letting it stop the others.
7
+ * A `once` listener removes itself as usual.
8
+ *
9
+ * @param {import("node:events").EventEmitter | undefined} emitter - Nothing happens for undefined.
10
+ * @param {string} event
11
+ * @param {any[]} args
12
+ * @param {unknown[]} errors
13
+ */
14
+ export function emit_all(emitter, event, args, errors) {
15
+ if (!emitter) return;
16
+ for (const listener of emitter.rawListeners(event)) {
17
+ try {
18
+ listener.apply(emitter, args);
19
+ } catch (error) {
20
+ errors.push(error);
21
+ }
22
+ }
23
+ }
package/src/endian.js ADDED
@@ -0,0 +1,197 @@
1
+ // Conversion between the device byte order of an IO buffer and the big-endian
2
+ // buffer of an Ldata, per compiled layout entry (doc/spec-data-api.md §2.9,
3
+ // doc/design.md §6). Device byte order acts per 16-bit register and per 32-bit
4
+ // value; the IO buffer is indexed like the Ldata buffer. Only io_index_table is
5
+ // exported by the package: Ldata calls the other functions with its own buffer.
6
+ import { compiled_of } from "./registry.js";
7
+
8
+ /** @import { Endian, Layout } from "./typedefs.js" */
9
+ /** @import { Layout_Entry } from "./internal_types.js" */
10
+
11
+ // The device byte orders; there are no aliases.
12
+ const ENDIANS = ["BE", "LE", "BEBS", "LEBS"];
13
+
14
+ // Endians that swap the two bytes of a 16-bit register.
15
+ const BYTE_SWAP_ENDIANS = ["LE", "BEBS"];
16
+
17
+ /**
18
+ * Throws for a byte order that is not one of BE, LE, BEBS and LEBS, instead of
19
+ * treating it as BE and corrupting the data silently.
20
+ *
21
+ * @param {string} endian
22
+ * @throws {RangeError}
23
+ */
24
+ export function check_endian(endian) {
25
+ if (!ENDIANS.includes(endian)) {
26
+ throw new RangeError(`unknown byte order ${endian}: use BE, LE, BEBS or LEBS`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Converts the endianness of a 2-byte value between the specified endian format and Big Endian (BE).
32
+ *
33
+ * This function performs bidirectional conversion:
34
+ * - If the data is not in BE, it converts it to BE.
35
+ * - If the data is in BE, it converts it to the specified endian format.
36
+ *
37
+ * @param {Uint8Array} buffer_from - The source buffer containing the data to be converted.
38
+ * @param {Uint8Array} buffer_to - The destination buffer where the converted data will be written.
39
+ * @param {number} index - The starting index in the buffers for the 2-byte value.
40
+ * @param {Endian} endian - The endian format to convert to/from.
41
+ */
42
+ function convert_endian_2byte(buffer_from, buffer_to, index, endian) {
43
+ if (BYTE_SWAP_ENDIANS.includes(endian)) { // LE, BEBS: BA
44
+ buffer_to[index] = buffer_from[index + 1];
45
+ buffer_to[index + 1] = buffer_from[index];
46
+ } else { // BE, LEBS: AB
47
+ buffer_to[index] = buffer_from[index];
48
+ buffer_to[index + 1] = buffer_from[index + 1];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Converts the endianness of a 4-byte value between the specified endian format and Big Endian (BE).
54
+ *
55
+ * This function performs bidirectional conversion:
56
+ * - If the data is not in BE, it converts it to BE.
57
+ * - If the data is in BE, it converts it to the specified endian format.
58
+ *
59
+ * The function handles different endian formats:
60
+ * - LE (Little Endian): DCBA <-> ABCD
61
+ * - BEBS (Big Endian Byte Swap): BADC <-> ABCD
62
+ * - LEBS (Little Endian Byte Swap): CDAB <-> ABCD
63
+ *
64
+ * @param {Uint8Array} buffer_from - The source buffer containing the data to be converted.
65
+ * @param {Uint8Array} buffer_to - The destination buffer where the converted data will be written.
66
+ * @param {number} index - The starting index in the buffers for the 4-byte value.
67
+ * @param {Endian} endian - The endian format to convert to/from.
68
+ */
69
+ function convert_endian_4bytes(buffer_from, buffer_to, index, endian) {
70
+ switch (endian) {
71
+ case "LE": { // DCBA <-> ABCD
72
+ buffer_to[index] = buffer_from[index + 3];
73
+ buffer_to[index + 1] = buffer_from[index + 2];
74
+ buffer_to[index + 2] = buffer_from[index + 1];
75
+ buffer_to[index + 3] = buffer_from[index];
76
+ break;
77
+ }
78
+ case "BEBS": { // BADC <-> ABCD
79
+ buffer_to[index] = buffer_from[index + 1];
80
+ buffer_to[index + 1] = buffer_from[index];
81
+ buffer_to[index + 2] = buffer_from[index + 3];
82
+ buffer_to[index + 3] = buffer_from[index + 2];
83
+ break;
84
+ }
85
+ case "LEBS": { // CDAB <-> ABCD
86
+ buffer_to[index] = buffer_from[index + 2];
87
+ buffer_to[index + 1] = buffer_from[index + 3];
88
+ buffer_to[index + 2] = buffer_from[index];
89
+ buffer_to[index + 3] = buffer_from[index + 1];
90
+ break;
91
+ }
92
+ default: { // BE
93
+ for (let k = 0; k < 4; k++) buffer_to[index + k] = buffer_from[index + k];
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Index in a device-ordered IO buffer of the byte of a single-byte entry
100
+ * (Bool, Byte, SInt, USInt): when the endian swaps the register bytes, the
101
+ * byte sits in the other half of its register.
102
+ *
103
+ * @param {number} byte - Byte offset of the entry.
104
+ * @param {Endian} endian - Device byte order.
105
+ * @returns {number}
106
+ */
107
+ export function io_index(byte, endian) {
108
+ return BYTE_SWAP_ENDIANS.includes(endian) ? byte ^ 1 : byte;
109
+ }
110
+
111
+ /**
112
+ * The index in a device-ordered IO buffer of every byte of a data block: the
113
+ * byte `i` of the block is the byte `table[i]` of the IO buffer. The table
114
+ * makes loading and storing a whole block a plain byte copy, and it agrees
115
+ * with `load_entry` / `store_entry` for every entry (a Bool or a single-byte
116
+ * entry maps as `io_index`, a multi-byte entry as its endian).
117
+ *
118
+ * @param {Layout} layout - The compiled layout of the data block.
119
+ * @param {Endian} endian - Device byte order: BE, LE, BEBS or LEBS.
120
+ * @returns {Int32Array}
121
+ * @throws {RangeError} For another byte order.
122
+ */
123
+ export function io_index_table(layout, endian) {
124
+ check_endian(endian);
125
+ const table = new Int32Array(layout.size);
126
+ for (let i = 0; i < table.length; i++) table[i] = io_index(i, endian);
127
+ for (const { byte, size, type } of compiled_of(layout).entries.values()) {
128
+ if (size !== 4 || type === "bool") continue;
129
+ for (let k = 0; k < 4; k++) {
130
+ switch (endian) {
131
+ case "LE": table[byte + k] = byte + 3 - k; break;
132
+ case "BEBS": table[byte + k] = byte + (k ^ 1); break;
133
+ case "LEBS": table[byte + k] = byte + ((k + 2) % 4); break;
134
+ default: table[byte + k] = byte + k;
135
+ }
136
+ }
137
+ }
138
+ return table;
139
+ }
140
+
141
+ /**
142
+ * Byte range [start, end) an entry occupies in a device-ordered IO buffer.
143
+ *
144
+ * @param {Layout_Entry} entry
145
+ * @param {Endian} endian - Device byte order.
146
+ * @returns {{start: number, end: number}}
147
+ */
148
+ export function io_range(entry, endian) {
149
+ const start = entry.size === 1 ? io_index(entry.byte, endian) : entry.byte;
150
+ return { start, end: start + entry.size };
151
+ }
152
+
153
+ /**
154
+ * Copies an entry from a device-ordered IO buffer into a big-endian buffer.
155
+ * A Bool only changes its own bit. No change is detected here, and the byte
156
+ * order and the range in the IO buffer are not checked (Ldata does).
157
+ *
158
+ * @param {Layout_Entry} entry
159
+ * @param {Uint8Array} io_buffer - The device-ordered source.
160
+ * @param {Uint8Array} buffer - The big-endian destination.
161
+ * @param {Endian} endian - Device byte order.
162
+ */
163
+ export function load_entry(entry, io_buffer, buffer, endian) {
164
+ const { byte, size, mask } = entry;
165
+ if (entry.type === "bool") {
166
+ const value = io_buffer[io_index(byte, endian)] & mask;
167
+ buffer[byte] = value ? buffer[byte] | mask : buffer[byte] & ~mask;
168
+ } else if (size === 1) {
169
+ buffer[byte] = io_buffer[io_index(byte, endian)];
170
+ } else if (size === 2) {
171
+ convert_endian_2byte(io_buffer, buffer, byte, endian);
172
+ } else {
173
+ convert_endian_4bytes(io_buffer, buffer, byte, endian);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Copies an entry from a big-endian buffer into a device-ordered IO buffer.
179
+ *
180
+ * @param {Layout_Entry} entry
181
+ * @param {Uint8Array} buffer - The big-endian source.
182
+ * @param {Uint8Array} io_buffer - The device-ordered destination.
183
+ * @param {Endian} endian - Device byte order.
184
+ */
185
+ export function store_entry(entry, buffer, io_buffer, endian) {
186
+ const { byte, size, mask } = entry;
187
+ if (entry.type === "bool") {
188
+ const index = io_index(byte, endian);
189
+ io_buffer[index] = buffer[byte] & mask ? io_buffer[index] | mask : io_buffer[index] & ~mask;
190
+ } else if (size === 1) {
191
+ io_buffer[io_index(byte, endian)] = buffer[byte];
192
+ } else if (size === 2) {
193
+ convert_endian_2byte(buffer, io_buffer, byte, endian);
194
+ } else {
195
+ convert_endian_4bytes(buffer, io_buffer, byte, endian);
196
+ }
197
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Public interface of the package (doc/design.md §1).
2
+ export { Ldata } from "./Ldata.js";
3
+ export { Ltag } from "./Ltag.js";
4
+ export { register_layout } from "./layout.js";
5
+ export { get_layout } from "./registry.js";
6
+ export { io_index_table } from "./endian.js";
7
+ export { render_types } from "./types.js";
8
+ export * from "./typedefs.js";
@@ -0,0 +1,47 @@
1
+ // Type definitions of the internals. This file has no run-time content, and
2
+ // index.js does not export it: the compiled data of a layout is not part of
3
+ // the public interface (doc/spec-data-api.md §6.6).
4
+
5
+ /**
6
+ * Codec of one tag type on a big-endian buffer (doc/design.md §4).
7
+ *
8
+ * @typedef {object} Codec
9
+ * @property {number} size - Size in bytes.
10
+ * @property {(value: any) => string | null} check - The reason a value is refused, null if it can be written.
11
+ * @property {(buffer: Buffer, byte: number, mask: number) => number | boolean} get
12
+ * @property {(buffer: Buffer, byte: number, mask: number, value: number | boolean) => void} set
13
+ */
14
+
15
+ /**
16
+ * One tag of a compiled layout.
17
+ *
18
+ * @typedef {object} Layout_Entry
19
+ * @property {string} name
20
+ * @property {import("./typedefs.js").Tag_Type} type - Lower case type name.
21
+ * @property {number | boolean} init_value
22
+ * @property {number} byte - Byte offset.
23
+ * @property {number} index - Position among the entries of the layout.
24
+ * @property {number} size - Size in bytes.
25
+ * @property {number} mask - Bit mask of a Bool, 0xff otherwise.
26
+ * @property {Codec} codec
27
+ */
28
+
29
+ /**
30
+ * The expanded structure of a data block (the result of build_structure).
31
+ *
32
+ * @typedef {object} Data_Struct
33
+ * @property {string} name
34
+ * @property {number} length - Total length in bits.
35
+ * @property {import("./typedefs.js").Layout_Item[]} items
36
+ */
37
+
38
+ /**
39
+ * The compiled data of a layout, kept apart from the public Layout
40
+ * (doc/design.md §3).
41
+ *
42
+ * @typedef {object} Compiled_Layout
43
+ * @property {Map<string, Layout_Entry>} entries
44
+ * @property {Layout_Entry[][]} tags_at_byte - The entries covering each byte.
45
+ */
46
+
47
+ export {};
package/src/layout.js ADDED
@@ -0,0 +1,303 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseDocument } from "yaml";
3
+ import { CODECS } from "./codecs.js";
4
+ import { register, registered_of, set_compiled } from "./registry.js";
5
+ import { build_structure, validate_structure, TYPE_BITS } from "./structure.js";
6
+
7
+ /** @import { Layout_Source, Registered_Layouts, Layout } from "./typedefs.js" */
8
+ /** @import { Data_Struct } from "./internal_types.js" */
9
+
10
+ /**
11
+ * The canonical text of a value: object keys sorted, `comment` left out (it
12
+ * does not change how a layout works) and the type names in lower case.
13
+ */
14
+ function canonical(value, key) {
15
+ if (Array.isArray(value)) return `[${value.map((v) => canonical(v)).join(",")}]`;
16
+ if (value !== null && typeof value === "object") {
17
+ const fields = Object.keys(value).filter((k) => k !== "comment" && value[k] !== undefined).sort();
18
+ return `{${fields.map((k) => `${JSON.stringify(k)}:${canonical(value[k], k)}`).join(",")}}`;
19
+ }
20
+ if (key === "type" && typeof value === "string") return JSON.stringify(value.toLowerCase());
21
+ return JSON.stringify(value) ?? "null";
22
+ }
23
+
24
+ /**
25
+ * Compiles an expanded structure (the result of build_structure) into a Layout
26
+ * (doc/design.md §3) and registers it under its name, the one source of the
27
+ * layouts an Ldata is created from. Registering a structure whose canonical
28
+ * text equals the registered one of that name gives the registered Layout, so
29
+ * the same layout described twice is compiled once; another structure under a
30
+ * used name throws. Throws on an unknown type or a duplicated tag name; the
31
+ * other layout rules are checked by validate_structure. Internal.
32
+ *
33
+ * @param {Data_Struct} data_struct
34
+ * @returns {Layout}
35
+ * @throws {Error} When the name is used by another layout.
36
+ */
37
+ export function register_structure(data_struct) {
38
+ const { name } = data_struct;
39
+ if (typeof name !== "string" || name === "") throw new Error("a layout needs a name");
40
+ const key = key_of(data_struct);
41
+ const known = registered_of(name);
42
+ if (known) {
43
+ if (known.key !== key) throw new Error(`another layout is already named ${name}`);
44
+ return known.layout;
45
+ }
46
+ const layout = compile_structure(data_struct);
47
+ register(name, { key, layout });
48
+ return layout;
49
+ }
50
+
51
+ function key_of(data_struct) {
52
+ return canonical({ name: data_struct.name, length: data_struct.length, items: data_struct.items });
53
+ }
54
+
55
+ function compile_structure(data_struct) {
56
+ const size = data_struct.length >> 3;
57
+ const entries = new Map();
58
+ const tags_at_byte = Array.from({ length: size }, () => []);
59
+ for (const item of data_struct.items) {
60
+ const { name, offset, init_value } = item;
61
+ const type = item.type.toLowerCase();
62
+ const codec = CODECS[type];
63
+ const fail = (message) => {
64
+ throw new Error(`${data_struct.name}.${name}: ${message}`);
65
+ };
66
+ if (!codec) fail(`unknown tag type ${item.type}`);
67
+ if (entries.has(name)) fail("duplicated tag name");
68
+ // defensive checks of what an Ldata relies on; validate_structure reports them all
69
+ if (type !== "bool" && offset % 8 !== 0) fail(`bit offset ${offset % 8} on a ${item.type}`);
70
+ if (codec.size > 1 && (offset >> 3) % 2 !== 0) fail(`${item.type} at the odd byte ${offset >> 3}`);
71
+ if (!["number", "boolean"].includes(typeof init_value)) fail(`init_value ${init_value} is not a number or boolean`);
72
+ const byte = offset >> 3;
73
+ const entry = {
74
+ name, type, init_value, byte,
75
+ index: entries.size,
76
+ size: codec.size,
77
+ mask: type === "bool" ? 1 << (offset % 8) : 0xff,
78
+ codec,
79
+ };
80
+ entries.set(name, entry);
81
+ // entries keep the item order: a combined word comes before its bits
82
+ for (let i = byte; i < byte + codec.size && i < size; i++) tags_at_byte[i].push(entry);
83
+ }
84
+
85
+ // the public layout holds the name, the sizes and frozen copies of the items;
86
+ // the entries and the lookup table stay out of reach (doc/spec-data-api.md §6.6)
87
+ const layout = Object.freeze({
88
+ name: data_struct.name,
89
+ length: data_struct.length,
90
+ size,
91
+ items: Object.freeze(data_struct.items.map(public_tag)),
92
+ });
93
+ set_compiled(layout, { entries, tags_at_byte });
94
+ return layout;
95
+ }
96
+
97
+ /**
98
+ * The frozen description of a tag of the public layout: the documented fields
99
+ * only, the length filled in from the type when the item has none.
100
+ */
101
+ function public_tag(item) {
102
+ const tag = {
103
+ name: item.name,
104
+ type: item.type,
105
+ offset: item.offset,
106
+ length: item.length ?? TYPE_BITS[item.type.toLowerCase()],
107
+ init_value: item.init_value,
108
+ };
109
+ if (item.comment !== undefined) tag.comment = item.comment;
110
+ tag.coupling = Object.freeze([...(item.coupling ?? [])]);
111
+ return Object.freeze(tag);
112
+ }
113
+
114
+ /**
115
+ * Resolves the fragment references of a list of items: `coupling` and
116
+ * `includes` given as a fragment name are replaced by a copy of that
117
+ * fragment, recursively. Omitted lengths are filled in from the type.
118
+ */
119
+ function resolve_items(list, fragments, stack, where, errors) {
120
+ if (!Array.isArray(list)) {
121
+ errors.push(`${where}: expected a list of items`);
122
+ return [];
123
+ }
124
+ const resolve_ref = (ref, field) => {
125
+ if (Array.isArray(ref)) return resolve_items(ref, fragments, stack, where, errors);
126
+ if (typeof ref !== "string") {
127
+ errors.push(`${where}: ${field} must be a fragment name or a list of items`);
128
+ return [];
129
+ }
130
+ if (stack.includes(ref)) {
131
+ errors.push(`${where}: circular fragment reference ${[...stack, ref].join(" -> ")}`);
132
+ return [];
133
+ }
134
+ if (!Object.hasOwn(fragments, ref)) {
135
+ errors.push(`${where}: unknown fragment ${ref}`);
136
+ return [];
137
+ }
138
+ return resolve_items(fragments[ref], fragments, [...stack, ref], `fragment ${ref}`, errors);
139
+ };
140
+ return list.map((_item) => {
141
+ const item = { ..._item };
142
+ if (item.coupling !== undefined) item.coupling = resolve_ref(item.coupling, "coupling");
143
+ if (item.includes !== undefined) item.includes = resolve_ref(item.includes, "includes");
144
+ const bits = TYPE_BITS[item.type?.toLowerCase()];
145
+ if (item.length === undefined && bits !== undefined) item.length = bits;
146
+ return item;
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Reads the sources of the layouts (doc/design.md §3), merges their layouts
152
+ * and fragments into one description and registers it with
153
+ * register_layout_data. Each source is the path of a YAML layout file or a
154
+ * parsed description; a list of sources is merged, so a layout may name a
155
+ * fragment of any source whatever the order. The root key `layouts` of a
156
+ * source maps layout names to item lists; every other root key is a fragment
157
+ * referenced by name.
158
+ *
159
+ * @param {Layout_Source | Layout_Source[]} sources
160
+ * @returns {Registered_Layouts}
161
+ * @throws {Error} With `errors`, every violation found, when the sources are not valid.
162
+ */
163
+ export function register_sources(sources) {
164
+ const errors = [];
165
+ const list = Array.isArray(sources) ? sources : [sources];
166
+ if (list.length === 0) errors.push("no source is given");
167
+ const origins = [];
168
+ const layout_defs = {};
169
+ const fragment_defs = {};
170
+ // the origin of each layout and fragment, to name both in a duplicate
171
+ const owners = { layout: new Map(), fragment: new Map() };
172
+ const add = (origin, root) => {
173
+ origins.push(origin);
174
+ if (root === null || typeof root !== "object" || Array.isArray(root)) {
175
+ errors.push(`${origin}: the root must be a mapping`);
176
+ return;
177
+ }
178
+ const { layouts, ...fragments } = root;
179
+ if (layouts !== undefined && (layouts === null || typeof layouts !== "object" || Array.isArray(layouts))) {
180
+ errors.push(`${origin}: the root key \`layouts\` must map layout names to item lists`);
181
+ return;
182
+ }
183
+ const merge = (kind, defs, into) => {
184
+ for (const [name, value] of Object.entries(defs)) {
185
+ if (owners[kind].has(name)) {
186
+ errors.push(`${kind} ${name} is defined in ${owners[kind].get(name)} and ${origin}`);
187
+ continue;
188
+ }
189
+ owners[kind].set(name, origin);
190
+ into[name] = value;
191
+ }
192
+ };
193
+ merge("layout", layouts ?? {}, layout_defs);
194
+ merge("fragment", fragments, fragment_defs);
195
+ };
196
+ list.forEach((source, index) => {
197
+ const label = Array.isArray(sources) ? `layout description #${index + 1}` : "layout description";
198
+ if (Array.isArray(source)) {
199
+ errors.push(`${label}: a list of sources must not contain a list`);
200
+ } else if (typeof source === "string") {
201
+ let text;
202
+ try {
203
+ text = readFileSync(source, "utf8");
204
+ } catch (error) {
205
+ errors.push(error.message);
206
+ return;
207
+ }
208
+ const doc = parseDocument(text, { version: "1.2" });
209
+ if (doc.errors.length > 0) errors.push(...doc.errors.map((e) => `${source}: ${e.message}`));
210
+ else add(source, doc.toJS());
211
+ } else {
212
+ add(label, source);
213
+ }
214
+ });
215
+ if (errors.length === 0 && Object.keys(layout_defs).length === 0) errors.push("no layout is defined in the sources");
216
+ if (errors.length > 0) throw layout_error("layout sources", errors);
217
+ return register_layout_data({ layouts: layout_defs, ...fragment_defs }, origins.join(", "));
218
+ }
219
+
220
+ /** Set once a call of register_layout has registered the layouts. */
221
+ let registered_once = false;
222
+
223
+ /**
224
+ * Registers the layouts of the sources, once per process: a layout is
225
+ * described, registered under its name, and then used by that name
226
+ * (doc/spec-data-api.md §6.4). Nothing is registered when a source is not
227
+ * valid, and such a call does not use up the single call.
228
+ *
229
+ * @param {Layout_Source | Layout_Source[]} sources - The path of a YAML layout file, a parsed description, or a list of them.
230
+ * @returns {Registered_Layouts}
231
+ * @throws {Error} With `errors`, every violation found, when the sources are not valid.
232
+ * @throws {Error} When the layouts were registered by an earlier call.
233
+ */
234
+ export function register_layout(sources) {
235
+ if (registered_once) throw new Error("register_layout can be called only once");
236
+ const result = register_sources(sources);
237
+ registered_once = true;
238
+ return result;
239
+ }
240
+
241
+ function layout_error(subject, errors) {
242
+ const error = new Error(`invalid ${subject}:\n${errors.join("\n")}`);
243
+ error.errors = errors;
244
+ return error;
245
+ }
246
+
247
+ /**
248
+ * The conflicts of structures with the layouts registered under their names.
249
+ *
250
+ * @param {Record<string, Data_Struct>} structs - By layout name.
251
+ * @returns {string[]}
252
+ */
253
+ function find_conflicts(structs) {
254
+ const conflicts = [];
255
+ for (const [name, struct] of Object.entries(structs)) {
256
+ const known = registered_of(name);
257
+ if (known && known.key !== key_of(struct)) conflicts.push(`layout ${name}: another layout is already named ${name}`);
258
+ }
259
+ return conflicts;
260
+ }
261
+
262
+ /**
263
+ * Registers the layouts of one description that is parsed and complete: the
264
+ * root key `layouts` and the fragments the layouts name. Internal.
265
+ *
266
+ * @param {Record<string, any>} root - The parsed description: `layouts` and the fragments.
267
+ * @param {string} [origin] - Names the source in errors.
268
+ * @returns {Registered_Layouts}
269
+ * @throws {Error} With `errors`, every violation found, when the description is not valid.
270
+ */
271
+ export function register_layout_data(root, origin = "the given object") {
272
+ const fail = (errors) => {
273
+ throw layout_error(`layout description from ${origin}`, errors);
274
+ };
275
+ if (root === null || typeof root !== "object" || Array.isArray(root)) fail(["the root must be a mapping"]);
276
+ const { layouts: layout_defs, ...fragment_defs } = root;
277
+ if (layout_defs === null || typeof layout_defs !== "object" || Array.isArray(layout_defs)) {
278
+ fail(["the root key `layouts` must map layout names to item lists"]);
279
+ }
280
+
281
+ const errors = [];
282
+ const fragments = {};
283
+ for (const [name, list] of Object.entries(fragment_defs)) {
284
+ fragments[name] = resolve_items(list, fragment_defs, [name], `fragment ${name}`, errors);
285
+ }
286
+ const structs = {};
287
+ for (const [name, list] of Object.entries(layout_defs)) {
288
+ const items = resolve_items(list, fragment_defs, [], `layout ${name}`, errors);
289
+ structs[name] = build_structure({ name }, items);
290
+ }
291
+ if (errors.length === 0) {
292
+ for (const struct of Object.values(structs)) errors.push(...validate_structure(struct));
293
+ }
294
+ if (errors.length === 0) {
295
+ // all or none: a conflict is found before any of the layouts is registered
296
+ errors.push(...find_conflicts(structs));
297
+ }
298
+ if (errors.length > 0) fail(errors);
299
+
300
+ const layouts = {};
301
+ for (const [name, struct] of Object.entries(structs)) layouts[name] = register_structure(struct);
302
+ return { layouts, fragments };
303
+ }