layout-data 0.2.0 → 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.
- package/CHANGELOG.md +30 -0
- package/README.md +1 -3
- package/dist/types/Ldata.d.ts +16 -5
- package/package.json +10 -8
- package/src/Ldata.js +541 -0
- package/src/Ltag.js +87 -0
- package/src/codecs.js +64 -0
- package/src/dispatch.js +23 -0
- package/src/endian.js +197 -0
- package/src/index.js +8 -0
- package/src/internal_types.js +47 -0
- package/src/layout.js +303 -0
- package/src/observer.js +147 -0
- package/src/registry.js +83 -0
- package/src/reserved.js +35 -0
- package/src/structure.js +238 -0
- package/src/typedefs.js +143 -0
- package/src/types.js +74 -0
- package/dist/layout-data.js +0 -8320
package/src/structure.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Expansion and validation of data block structures (doc/spec.md §3,
|
|
2
|
+
// doc/design.md §3.1). Part of the data layer.
|
|
3
|
+
import { FLOAT32_MAX } from "./codecs.js";
|
|
4
|
+
import { is_reserved_name } from "./reserved.js";
|
|
5
|
+
|
|
6
|
+
/** @import { Layout_Item } from "./typedefs.js" */
|
|
7
|
+
/** @import { Data_Struct } from "./internal_types.js" */
|
|
8
|
+
|
|
9
|
+
function add_item(data_struct, item, base_offset, name_prefix) {
|
|
10
|
+
const items = data_struct.items;
|
|
11
|
+
// every entry that is not a pure inclusion is a tag, whatever its name: duplicated
|
|
12
|
+
// and invalid names are kept and reported by validate_structure
|
|
13
|
+
if (item.includes === undefined || item.name !== undefined) items.push(item);
|
|
14
|
+
if (name_prefix && typeof item.name === "string") item.name = name_prefix + item.name;
|
|
15
|
+
|
|
16
|
+
item.offset = item.offset + base_offset;
|
|
17
|
+
const length = item.offset + item.length;
|
|
18
|
+
if (data_struct.length < length) data_struct.length = length;
|
|
19
|
+
|
|
20
|
+
const includes = item.includes;
|
|
21
|
+
if (includes && Array.isArray(includes)) {
|
|
22
|
+
for (const _item of includes) {
|
|
23
|
+
add_item(data_struct, { ..._item }, item.offset, item.name_prefix);
|
|
24
|
+
}
|
|
25
|
+
// The "includes" and "coupling" attributes can only be chosen one at a time.
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const coupling = [];
|
|
30
|
+
const _coupling = item.coupling;
|
|
31
|
+
if (_coupling && Array.isArray(_coupling)) {
|
|
32
|
+
for (const _cp of _coupling) {
|
|
33
|
+
const cp = { ..._cp };
|
|
34
|
+
add_item(data_struct, cp, item.offset, item.name_prefix);
|
|
35
|
+
cp.coupling ??= [];
|
|
36
|
+
cp.coupling.push(item.name);
|
|
37
|
+
coupling.push(cp);
|
|
38
|
+
}
|
|
39
|
+
share_init_value(item, coupling);
|
|
40
|
+
}
|
|
41
|
+
item.coupling = coupling.map((item) => item.name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Items whose init_value is given both on the word and on some of its bits,
|
|
45
|
+
// with the names of those bits; reported by validate_structure.
|
|
46
|
+
const init_conflicts = new WeakMap();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Only one side of a combined word gives the init values (doc/spec.md §3):
|
|
50
|
+
* either the word, whose bits then take theirs from it, or the bits, from
|
|
51
|
+
* which the word is composed (a bit without init_value is false).
|
|
52
|
+
*/
|
|
53
|
+
function share_init_value(word, bits) {
|
|
54
|
+
const given = bits.filter((bit) => bit.init_value !== undefined);
|
|
55
|
+
if (word.init_value !== undefined && given.length > 0) {
|
|
56
|
+
// keep the given values (the bits are written after the word) and
|
|
57
|
+
// report the conflict; the other bits still take theirs from the word
|
|
58
|
+
init_conflicts.set(word, given.map((bit) => bit.name));
|
|
59
|
+
}
|
|
60
|
+
const size = word.length >> 3;
|
|
61
|
+
// big-endian word: byte k of the word holds bits (size - 1 - k) * 8 ..
|
|
62
|
+
// a bit outside the word has no value in it (reported by validate_structure)
|
|
63
|
+
const bit_value = (bit) => {
|
|
64
|
+
const relative = bit.offset - word.offset;
|
|
65
|
+
if (!(relative >= 0 && relative < word.length)) return 0;
|
|
66
|
+
return 2 ** ((size - 1 - (relative >> 3)) * 8 + relative % 8);
|
|
67
|
+
};
|
|
68
|
+
if (word.init_value === undefined) {
|
|
69
|
+
word.init_value = 0;
|
|
70
|
+
for (const bit of bits) {
|
|
71
|
+
bit.init_value ??= false;
|
|
72
|
+
if (bit.init_value === true) word.init_value += bit_value(bit);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (typeof word.init_value !== "number") return;
|
|
77
|
+
for (const bit of bits) {
|
|
78
|
+
if (bit.init_value !== undefined) continue;
|
|
79
|
+
const value = bit_value(bit);
|
|
80
|
+
bit.init_value = value > 0 && Math.floor(word.init_value / value) % 2 === 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Expands a list of items (resolving `includes` and `coupling`, making offsets
|
|
86
|
+
* absolute) into the given structure. Internal. It changes the items it is given.
|
|
87
|
+
*
|
|
88
|
+
* @param {{name: string}} data_struct - The structure to fill in; only the name is given.
|
|
89
|
+
* @param {Layout_Item[]} items
|
|
90
|
+
* @returns {Data_Struct} The same object, now complete.
|
|
91
|
+
*/
|
|
92
|
+
export function build_structure(data_struct, items) {
|
|
93
|
+
data_struct.length = 0;
|
|
94
|
+
data_struct.items = [];
|
|
95
|
+
if (!items) return data_struct;
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
add_item(data_struct, item, 0);
|
|
98
|
+
}
|
|
99
|
+
return data_struct;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bit length of each tag type (lower case names).
|
|
104
|
+
*
|
|
105
|
+
* @type {Record<string, number>}
|
|
106
|
+
*/
|
|
107
|
+
export const TYPE_BITS = {
|
|
108
|
+
bool: 1,
|
|
109
|
+
byte: 8, sint: 8, usint: 8,
|
|
110
|
+
int: 16, uint: 16, word: 16,
|
|
111
|
+
dint: 32, udint: 32, dword: 32, real: 32,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A tag name: letters, digits and `_`, not starting with a digit, an
|
|
116
|
+
* identifier in every language (doc/spec-data-api.md §6.2).
|
|
117
|
+
*/
|
|
118
|
+
const TAG_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
119
|
+
|
|
120
|
+
// value range of each integer type
|
|
121
|
+
const INTEGER_RANGES = {
|
|
122
|
+
byte: [0, 0xff], usint: [0, 0xff], sint: [-0x80, 0x7f],
|
|
123
|
+
int: [-0x8000, 0x7fff], uint: [0, 0xffff], word: [0, 0xffff],
|
|
124
|
+
dint: [-0x80000000, 0x7fffffff], udint: [0, 0xffffffff], dword: [0, 0xffffffff],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function check_init_value(item, type) {
|
|
128
|
+
const value = item.init_value;
|
|
129
|
+
if (type === "bool") return typeof value === "boolean";
|
|
130
|
+
if (typeof value !== "number") return false;
|
|
131
|
+
if (type === "real") return Number.isFinite(value) && Math.abs(value) <= FLOAT32_MAX;
|
|
132
|
+
const [min, max] = INTEGER_RANGES[type];
|
|
133
|
+
return Number.isInteger(value) && value >= min && value <= max;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validates a structure built by build_structure against the layout rules
|
|
138
|
+
* (doc/spec.md §3, doc/design.md §3.1). Internal.
|
|
139
|
+
*
|
|
140
|
+
* @param {Data_Struct} data_struct
|
|
141
|
+
* @returns {string[]} descriptions of all violations, empty when the layout is valid
|
|
142
|
+
*/
|
|
143
|
+
export function validate_structure(data_struct) {
|
|
144
|
+
const errors = [];
|
|
145
|
+
const struct_name = data_struct.name;
|
|
146
|
+
const report = (item, message) => errors.push(`${struct_name}.${item.name}: ${message}`);
|
|
147
|
+
const items = data_struct.items;
|
|
148
|
+
|
|
149
|
+
// names
|
|
150
|
+
const by_name = new Map();
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
if (by_name.has(item.name)) report(item, "duplicated name");
|
|
153
|
+
else by_name.set(item.name, item);
|
|
154
|
+
if (typeof item.name !== "string" || !TAG_NAME.test(item.name)) {
|
|
155
|
+
report(item, "name is not an identifier (letters, digits and \"_\", not starting with a digit)");
|
|
156
|
+
} else if (is_reserved_name(item.name)) {
|
|
157
|
+
report(item, "name is a reserved name");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// type, length, alignment and init_value
|
|
162
|
+
const valid_items = [];
|
|
163
|
+
for (const item of items) {
|
|
164
|
+
const type = item.type?.toLowerCase();
|
|
165
|
+
if (!(type in TYPE_BITS)) {
|
|
166
|
+
report(item, `unknown type ${item.type}`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (item.length !== TYPE_BITS[type]) {
|
|
170
|
+
report(item, `length ${item.length} does not match type ${item.type}`);
|
|
171
|
+
}
|
|
172
|
+
if (type !== "bool") {
|
|
173
|
+
if (item.offset % 8 !== 0) {
|
|
174
|
+
report(item, `offset ${item.offset} is not on a byte boundary`);
|
|
175
|
+
} else if (TYPE_BITS[type] > 8 && (item.offset >> 3) % 2 !== 0) {
|
|
176
|
+
report(item, `byte offset ${item.offset >> 3} is not even`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const conflict = init_conflicts.get(item);
|
|
180
|
+
if (conflict) {
|
|
181
|
+
report(item, `init_value is given both on the word and on its bits ${conflict.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
if (!check_init_value(item, type)) {
|
|
184
|
+
report(item, `init_value ${item.init_value} is not valid for type ${item.type}`);
|
|
185
|
+
}
|
|
186
|
+
valid_items.push({ item, type, end: item.offset + TYPE_BITS[type] });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// non-Bool tags must not overlap
|
|
190
|
+
const words = valid_items
|
|
191
|
+
.filter(({ type }) => type !== "bool")
|
|
192
|
+
.sort((a, b) => a.item.offset - b.item.offset);
|
|
193
|
+
let last = null;
|
|
194
|
+
for (const word of words) {
|
|
195
|
+
if (last && word.item.offset < last.end) {
|
|
196
|
+
report(word.item, `overlaps ${last.item.name}`);
|
|
197
|
+
}
|
|
198
|
+
if (!last || word.end > last.end) last = word;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// each bit belongs to at most one Bool, and a Bool inside a non-Bool tag
|
|
202
|
+
// must be one of its coupled bits
|
|
203
|
+
const bits = new Map();
|
|
204
|
+
for (const { item, type } of valid_items) {
|
|
205
|
+
if (type !== "bool") continue;
|
|
206
|
+
const other = bits.get(item.offset);
|
|
207
|
+
if (other) report(item, `shares bit ${item.offset} with ${other.name}`);
|
|
208
|
+
else bits.set(item.offset, item);
|
|
209
|
+
for (const word of words) {
|
|
210
|
+
if (item.offset < word.item.offset || item.offset >= word.end) continue;
|
|
211
|
+
if (!item.coupling?.includes(word.item.name)) {
|
|
212
|
+
report(item, `lies inside ${word.item.name} but is not one of its coupled bits`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// coupled bits of a combined tag must lie inside it
|
|
218
|
+
for (const { item, type, end } of valid_items) {
|
|
219
|
+
if (type === "bool" || !item.coupling?.length) continue;
|
|
220
|
+
for (const bit_name of item.coupling) {
|
|
221
|
+
const bit = by_name.get(bit_name);
|
|
222
|
+
if (!bit) {
|
|
223
|
+
report(item, `coupled bit ${bit_name} does not exist`);
|
|
224
|
+
} else if (bit.type?.toLowerCase() !== "bool") {
|
|
225
|
+
report(item, `coupled tag ${bit_name} is not a Bool`);
|
|
226
|
+
} else if (bit.offset < item.offset || bit.offset >= end) {
|
|
227
|
+
report(item, `coupled bit ${bit_name} lies outside it`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// whole registers
|
|
233
|
+
if (data_struct.length % 16 !== 0) {
|
|
234
|
+
errors.push(`${struct_name}: length ${data_struct.length} bits is not a whole number of registers`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
package/src/typedefs.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Type definitions of the public interface. This file has no run-time content:
|
|
2
|
+
// the JSDoc here is the source of the generated declaration file (dist/types).
|
|
3
|
+
// The types of the internals are in internal_types.js and are not exported.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An entry of a layout description that defines a tag (doc/spec-data-api.md §6.2).
|
|
7
|
+
*
|
|
8
|
+
* @typedef {object} Tag_Item
|
|
9
|
+
* @property {string} name - Tag name.
|
|
10
|
+
* @property {string} type - Tag type, case-insensitive (see Tag_Type).
|
|
11
|
+
* @property {number} offset - Bit offset, relative to the parent entry until expanded.
|
|
12
|
+
* @property {number} [length] - Length in bits, filled in from the type when omitted.
|
|
13
|
+
* @property {number | boolean} [init_value] - Initial value.
|
|
14
|
+
* @property {string} [comment] - Description, rendered into the generated types.
|
|
15
|
+
* @property {boolean} [is_combined] - Descriptive mark of a combined word; the linkage is decided by `coupling`.
|
|
16
|
+
* @property {Tag_Item[] | string} [coupling] - The bits of a combined word, as an inline list or the name of a fragment.
|
|
17
|
+
* @property {never} [includes] - A tag entry does not include entries.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An entry of a layout description that splices in other entries (doc/spec-data-api.md §6.2).
|
|
22
|
+
*
|
|
23
|
+
* @typedef {object} Include_Item
|
|
24
|
+
* @property {Layout_Item[] | string} includes - The entries to include, as an inline list or the name of a fragment.
|
|
25
|
+
* @property {number} offset - Bit offset by which the included entries are shifted as a whole.
|
|
26
|
+
* @property {string} [name_prefix] - Prefix of the names of the included entries.
|
|
27
|
+
* @property {never} [name] - An inclusion defines no tag.
|
|
28
|
+
* @property {never} [type] - An inclusion defines no tag.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One entry of a layout description, as written by the user in a layout file
|
|
33
|
+
* or given to register_layout (doc/spec-data-api.md §6).
|
|
34
|
+
*
|
|
35
|
+
* @typedef {Tag_Item | Include_Item} Layout_Item
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A layout description: `layouts` maps layout names to lists of entries, and
|
|
40
|
+
* every other key is a fragment, a list of entries that can be named
|
|
41
|
+
* (doc/spec-data-api.md §6.1).
|
|
42
|
+
*
|
|
43
|
+
* @typedef {{ layouts?: Record<string, Layout_Item[]> } & Record<string, Layout_Item[] | Record<string, Layout_Item[]> | undefined>} Layout_Description
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One tag of a compiled layout (doc/spec-data-api.md §6.6), read-only:
|
|
48
|
+
* `name`; `type`, as written in the description; `offset`, the absolute bit
|
|
49
|
+
* offset; `length` in bits; `init_value`; `comment`, if given; `coupling`,
|
|
50
|
+
* the tag names of the bits of a combined word, or of the combined words a
|
|
51
|
+
* bit belongs to.
|
|
52
|
+
*
|
|
53
|
+
* @typedef {Readonly<{
|
|
54
|
+
* name: string,
|
|
55
|
+
* type: string,
|
|
56
|
+
* offset: number,
|
|
57
|
+
* length: number,
|
|
58
|
+
* init_value: number | boolean,
|
|
59
|
+
* comment?: string,
|
|
60
|
+
* coupling: readonly string[],
|
|
61
|
+
* }>} Layout_Tag
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A compiled layout (doc/spec-data-api.md §6.6), read-only and deeply frozen:
|
|
66
|
+
* `name`; `length` in bits; `size` in bytes; `items`, the tags in layout
|
|
67
|
+
* order (a combined word before its bits).
|
|
68
|
+
*
|
|
69
|
+
* @typedef {Readonly<{
|
|
70
|
+
* name: string,
|
|
71
|
+
* length: number,
|
|
72
|
+
* size: number,
|
|
73
|
+
* items: readonly Layout_Tag[],
|
|
74
|
+
* }>} Layout
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Device byte order of an IO buffer (doc/spec-data-api.md §2.9).
|
|
79
|
+
*
|
|
80
|
+
* @typedef {"BE" | "LE" | "BEBS" | "LEBS"} Endian
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A source of layouts: the path of a YAML 1.2 layout file, or a description
|
|
85
|
+
* that is already parsed (the same structure as the file, doc/spec-data-api.md §6.1).
|
|
86
|
+
*
|
|
87
|
+
* @typedef {string | Layout_Description} Layout_Source
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The result of registering a layout description.
|
|
92
|
+
*
|
|
93
|
+
* @typedef {object} Registered_Layouts
|
|
94
|
+
* @property {Record<string, Layout>} layouts
|
|
95
|
+
* @property {Record<string, Layout_Item[]>} fragments
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Type of a tag, in lower case: the type of `Ltag.type` (doc/spec-data-api.md §1).
|
|
100
|
+
*
|
|
101
|
+
* @typedef {"bool" | "byte" | "usint" | "sint" | "int" | "uint" | "word" | "dint" | "udint" | "dword" | "real"} Tag_Type
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The listeners of the events of an Ldata that the library emits.
|
|
106
|
+
*
|
|
107
|
+
* @typedef {object} Data_Events
|
|
108
|
+
* @property {(name: string, old_value: number | boolean, new_value: number | boolean) => void} change
|
|
109
|
+
* @property {(data: import("./Ldata.js").Ldata) => void} write
|
|
110
|
+
* @property {(kind: "read" | "write", tags: import("./Ltag.js").Ltag[]) => void} access
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A class-level access listener: called for the accesses to every Ldata
|
|
115
|
+
* (doc/spec-data-api.md §2.8).
|
|
116
|
+
*
|
|
117
|
+
* @typedef {(data: import("./Ldata.js").Ldata, kind: "read" | "write", tags: import("./Ltag.js").Ltag[]) => void} Class_Access_Listener
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The listener of an event of an Ldata: typed for `change` and `write`, any
|
|
122
|
+
* listener for another name (doc/spec-data-api.md §2.7).
|
|
123
|
+
*
|
|
124
|
+
* @template {string | symbol} E
|
|
125
|
+
* @typedef {E extends keyof Data_Events ? Data_Events[E] : (...args: any[]) => void} Data_Listener
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The listeners of the events of an Ltag that the library emits.
|
|
130
|
+
*
|
|
131
|
+
* @typedef {object} Tag_Events
|
|
132
|
+
* @property {(old_value: number | boolean, new_value: number | boolean) => void} change
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The listener of an event of an Ltag: typed for `change`, any listener for
|
|
137
|
+
* another name (doc/spec-data-api.md §3).
|
|
138
|
+
*
|
|
139
|
+
* @template {string | symbol} E
|
|
140
|
+
* @typedef {E extends keyof Tag_Events ? Tag_Events[E] : (...args: any[]) => void} Tag_Listener
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
export {};
|
package/src/types.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Renders TypeScript declarations of registered layouts, for editor completion
|
|
2
|
+
// of the tag accessors (doc/spec-data-api.md §6.7, doc/design.md §4).
|
|
3
|
+
|
|
4
|
+
/** A name that can be part of a type name: an identifier of JavaScript, in any script. */
|
|
5
|
+
const IDENTIFIER = /^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders a text as a doc comment, one line or several, with the `*` and `/`
|
|
9
|
+
* pair that would end the comment early escaped.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @param {string} indent
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
function doc_comment(text, indent) {
|
|
16
|
+
const lines = String(text).replaceAll("*/", "*\\/").split(/\r?\n/);
|
|
17
|
+
if (lines.length === 1) return [`${indent}/** ${lines[0]} */`];
|
|
18
|
+
return [`${indent}/**`, ...lines.map((line) => `${indent} * ${line}`.trimEnd()), `${indent} */`];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renders a declaration file with, for each layout, an interface of its tags
|
|
23
|
+
* (`<Layout>_Tags`, one property per tag) and the type of its data block
|
|
24
|
+
* (`<Layout>_Ldata`). The layout name is used as it is, with the case it has,
|
|
25
|
+
* so that the type names are those of `new Ldata("<Layout>", ...)` and layouts
|
|
26
|
+
* with different names never get the same type name.
|
|
27
|
+
*
|
|
28
|
+
* In JavaScript a JSDoc cast names the type of the value, which an editor
|
|
29
|
+
* uses for completion and, with `checkJs`, to check the tags:
|
|
30
|
+
*
|
|
31
|
+
* ```js
|
|
32
|
+
* const pump = /** @type {import("./layout_types.js").Pump_Ldata} *\/ (new Ldata("Pump", "pump_1"));
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @param {Record<string, import("./typedefs.js").Layout>} layouts - Registered layouts by name.
|
|
36
|
+
* @param {object} [options]
|
|
37
|
+
* @param {string} [options.source] - The layout file, named in the header; the header names none when it is omitted.
|
|
38
|
+
* @param {string} [options.ldata_module] - Module specifier of Ldata, relative to the output file; "layout-data" by default.
|
|
39
|
+
* @returns {string}
|
|
40
|
+
* @throws {Error} With `errors`, when the name of a layout is not an identifier, which a type name needs.
|
|
41
|
+
*/
|
|
42
|
+
export function render_types(layouts, { source, ldata_module = "layout-data" } = {}) {
|
|
43
|
+
const names = Object.keys(layouts);
|
|
44
|
+
const errors = names
|
|
45
|
+
.filter((name) => !IDENTIFIER.test(name))
|
|
46
|
+
.map((name) => `layout ${JSON.stringify(name)}: the name is not an identifier, so it cannot be part of a type name`);
|
|
47
|
+
if (errors.length > 0) {
|
|
48
|
+
const error = new Error(`cannot render the types of the layouts:\n${errors.join("\n")}`);
|
|
49
|
+
error.errors = errors;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lines = [
|
|
54
|
+
source === undefined ? "// Generated by render_types. Do not edit." : `// Generated from ${source}. Do not edit.`,
|
|
55
|
+
`import type { Ldata } from ${JSON.stringify(ldata_module)};`,
|
|
56
|
+
];
|
|
57
|
+
if (names.length > 0) {
|
|
58
|
+
lines.push(
|
|
59
|
+
"//",
|
|
60
|
+
"// In JavaScript, a JSDoc cast names the type of the value:",
|
|
61
|
+
`// const data = /** @type {import("<this file>").${names[0]}_Ldata} */ (new Ldata("${names[0]}", "data_1"));`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
for (const [name, layout] of Object.entries(layouts)) {
|
|
65
|
+
lines.push("", `/** Tags of the ${name} data block. */`, `export interface ${name}_Tags {`);
|
|
66
|
+
for (const item of layout.items) {
|
|
67
|
+
if (item.comment) lines.push(...doc_comment(item.comment, "\t"));
|
|
68
|
+
const ts_type = item.type.toLowerCase() === "bool" ? "boolean" : "number";
|
|
69
|
+
lines.push(`\t${item.name}: ${ts_type};`);
|
|
70
|
+
}
|
|
71
|
+
lines.push("}", "", `/** A ${name} data block, with its tags as properties. */`, `export type ${name}_Ldata = Ldata & ${name}_Tags;`);
|
|
72
|
+
}
|
|
73
|
+
return `${lines.join("\n")}\n`;
|
|
74
|
+
}
|