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.
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
+ }