@loadbare/app 0.4.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/LICENSE +201 -0
- package/README.md +95 -0
- package/dist/build/assemble.d.ts +31 -0
- package/dist/build/assemble.d.ts.map +1 -0
- package/dist/build/assemble.js +53 -0
- package/dist/build/cli.d.ts +30 -0
- package/dist/build/cli.d.ts.map +1 -0
- package/dist/build/cli.js +107 -0
- package/dist/build/elements.d.ts +61 -0
- package/dist/build/elements.d.ts.map +1 -0
- package/dist/build/elements.js +158 -0
- package/dist/build/expand.d.ts +45 -0
- package/dist/build/expand.d.ts.map +1 -0
- package/dist/build/expand.js +386 -0
- package/dist/build/format.d.ts +28 -0
- package/dist/build/format.d.ts.map +1 -0
- package/dist/build/format.js +42 -0
- package/dist/build/locations.d.ts +87 -0
- package/dist/build/locations.d.ts.map +1 -0
- package/dist/build/locations.js +173 -0
- package/dist/build/package-root.d.ts +9 -0
- package/dist/build/package-root.d.ts.map +1 -0
- package/dist/build/package-root.js +24 -0
- package/dist/build/pages.d.ts +25 -0
- package/dist/build/pages.d.ts.map +1 -0
- package/dist/build/pages.js +54 -0
- package/dist/build/styles.d.ts +13 -0
- package/dist/build/styles.d.ts.map +1 -0
- package/dist/build/styles.js +18 -0
- package/dist/client.js +522 -0
- package/dist/core/lb-constants.d.ts +23 -0
- package/dist/core/lb-constants.d.ts.map +1 -0
- package/dist/core/lb-constants.js +95 -0
- package/dist/core/lb-types.d.ts +88 -0
- package/dist/core/lb-types.d.ts.map +1 -0
- package/dist/core/lb-types.js +5 -0
- package/dist/demo-static/src/widgets/app-box.d.ts +15 -0
- package/dist/demo-static/src/widgets/app-box.d.ts.map +1 -0
- package/dist/demo-static/src/widgets/app-box.js +19 -0
- package/dist/hub/lb-apply.d.ts +13 -0
- package/dist/hub/lb-apply.d.ts.map +1 -0
- package/dist/hub/lb-apply.js +77 -0
- package/dist/hub/lb-hub.d.ts +2 -0
- package/dist/hub/lb-hub.d.ts.map +1 -0
- package/dist/hub/lb-hub.js +242 -0
- package/dist/hub/lb-rows.d.ts +18 -0
- package/dist/hub/lb-rows.d.ts.map +1 -0
- package/dist/hub/lb-rows.js +106 -0
- package/dist/server/lb-express.d.ts +28 -0
- package/dist/server/lb-express.d.ts.map +1 -0
- package/dist/server/lb-express.js +77 -0
- package/dist/server/lb-server.d.ts +174 -0
- package/dist/server/lb-server.d.ts.map +1 -0
- package/dist/server/lb-server.js +79 -0
- package/dist/tests/assemble.test.d.ts +8 -0
- package/dist/tests/assemble.test.d.ts.map +1 -0
- package/dist/tests/assemble.test.js +51 -0
- package/dist/tests/elements.test.d.ts +8 -0
- package/dist/tests/elements.test.d.ts.map +1 -0
- package/dist/tests/elements.test.js +111 -0
- package/dist/tests/expand.test.d.ts +10 -0
- package/dist/tests/expand.test.d.ts.map +1 -0
- package/dist/tests/expand.test.js +226 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/elements.js +3 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +2 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.js +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +2 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.js +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +3 -0
- package/dist/tests/golden.test.d.ts +19 -0
- package/dist/tests/golden.test.d.ts.map +1 -0
- package/dist/tests/golden.test.js +60 -0
- package/dist/tests/helpers/console.d.ts +20 -0
- package/dist/tests/helpers/console.d.ts.map +1 -0
- package/dist/tests/helpers/console.js +28 -0
- package/dist/tests/helpers/dom.d.ts +18 -0
- package/dist/tests/helpers/dom.d.ts.map +1 -0
- package/dist/tests/helpers/dom.js +22 -0
- package/dist/tests/helpers/window.d.ts +43 -0
- package/dist/tests/helpers/window.d.ts.map +1 -0
- package/dist/tests/helpers/window.js +78 -0
- package/dist/tests/lb-apply.test.d.ts +8 -0
- package/dist/tests/lb-apply.test.d.ts.map +1 -0
- package/dist/tests/lb-apply.test.js +153 -0
- package/dist/tests/lb-express.test.d.ts +14 -0
- package/dist/tests/lb-express.test.d.ts.map +1 -0
- package/dist/tests/lb-express.test.js +238 -0
- package/dist/tests/lb-input.test.d.ts +9 -0
- package/dist/tests/lb-input.test.d.ts.map +1 -0
- package/dist/tests/lb-input.test.js +78 -0
- package/dist/tests/lb-list.test.d.ts +12 -0
- package/dist/tests/lb-list.test.d.ts.map +1 -0
- package/dist/tests/lb-list.test.js +44 -0
- package/dist/tests/lb-options.test.d.ts +10 -0
- package/dist/tests/lb-options.test.d.ts.map +1 -0
- package/dist/tests/lb-options.test.js +121 -0
- package/dist/tests/lb-picker.test.d.ts +14 -0
- package/dist/tests/lb-picker.test.d.ts.map +1 -0
- package/dist/tests/lb-picker.test.js +59 -0
- package/dist/tests/lb-rows.test.d.ts +12 -0
- package/dist/tests/lb-rows.test.d.ts.map +1 -0
- package/dist/tests/lb-rows.test.js +336 -0
- package/dist/tests/lb-select.test.d.ts +9 -0
- package/dist/tests/lb-select.test.d.ts.map +1 -0
- package/dist/tests/lb-select.test.js +71 -0
- package/dist/tests/lb-server.test.d.ts +9 -0
- package/dist/tests/lb-server.test.d.ts.map +1 -0
- package/dist/tests/lb-server.test.js +495 -0
- package/dist/tests/lb-table.test.d.ts +15 -0
- package/dist/tests/lb-table.test.d.ts.map +1 -0
- package/dist/tests/lb-table.test.js +205 -0
- package/dist/tests/pages.test.d.ts +6 -0
- package/dist/tests/pages.test.d.ts.map +1 -0
- package/dist/tests/pages.test.js +98 -0
- package/dist/tests/styles.test.d.ts +7 -0
- package/dist/tests/styles.test.d.ts.map +1 -0
- package/dist/tests/styles.test.js +73 -0
- package/dist/widgets/index.d.ts +7 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/dist/widgets/index.js +6 -0
- package/dist/widgets/lb-input.d.ts +2 -0
- package/dist/widgets/lb-input.d.ts.map +1 -0
- package/dist/widgets/lb-input.js +48 -0
- package/dist/widgets/lb-list.d.ts +2 -0
- package/dist/widgets/lb-list.d.ts.map +1 -0
- package/dist/widgets/lb-list.js +17 -0
- package/dist/widgets/lb-options.d.ts +26 -0
- package/dist/widgets/lb-options.d.ts.map +1 -0
- package/dist/widgets/lb-options.js +72 -0
- package/dist/widgets/lb-picker.d.ts +2 -0
- package/dist/widgets/lb-picker.d.ts.map +1 -0
- package/dist/widgets/lb-picker.js +25 -0
- package/dist/widgets/lb-select.d.ts +2 -0
- package/dist/widgets/lb-select.d.ts.map +1 -0
- package/dist/widgets/lb-select.js +43 -0
- package/dist/widgets/lb-table.d.ts +2 -0
- package/dist/widgets/lb-table.d.ts.map +1 -0
- package/dist/widgets/lb-table.js +113 -0
- package/docs/application-chrome.md +36 -0
- package/docs/building-html-pages.md +130 -0
- package/docs/getting-started.md +120 -0
- package/docs/guide.md +1164 -0
- package/docs/hosting.md +218 -0
- package/docs/latent-risks.md +20 -0
- package/docs/theory.md +226 -0
- package/package.json +85 -0
- package/widgets/index.ts +6 -0
- package/widgets/lb-input.html +1 -0
- package/widgets/lb-input.ts +64 -0
- package/widgets/lb-list.html +1 -0
- package/widgets/lb-list.ts +21 -0
- package/widgets/lb-options.html +4 -0
- package/widgets/lb-options.ts +88 -0
- package/widgets/lb-picker.html +7 -0
- package/widgets/lb-picker.ts +27 -0
- package/widgets/lb-select.html +4 -0
- package/widgets/lb-select.ts +55 -0
- package/widgets/lb-table.html +8 -0
- package/widgets/lb-table.ts +126 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validating that a custom element a build actually uses can do something —
|
|
3
|
+
* and finding the script to import for it when it can.
|
|
4
|
+
*
|
|
5
|
+
* A used tag is valid if it has a script, a definition, or both:
|
|
6
|
+
* - a script registers the class that upgrades it. It can come from a
|
|
7
|
+
* built-in source (this package's own widgets/ or hub/ — hub/ is where
|
|
8
|
+
* lb-hub itself lives, discovered exactly the same way as any built-in
|
|
9
|
+
* widget, no special case for it here), an application's own — found
|
|
10
|
+
* anywhere in its source tree by build/locations.ts, not confined to a
|
|
11
|
+
* directory — or a third party's, declared by tag in `elements.ts`.
|
|
12
|
+
* - a definition (an `.html` file, built-in or local) expands the tag's
|
|
13
|
+
* content at build time. A widget with only a definition and no script
|
|
14
|
+
* is markup only: expansion already gave it everything it will ever
|
|
15
|
+
* have, and the tag survives just as an inert wrapper (see expand.ts).
|
|
16
|
+
*
|
|
17
|
+
* A tag with neither — no script anywhere and no definition — would do
|
|
18
|
+
* nothing were it ever to reach the browser, so that is an error.
|
|
19
|
+
*
|
|
20
|
+
* Every directory and file here is a parameter, resolved once by
|
|
21
|
+
* build/locations.ts — this module never reconstructs one of its own, and
|
|
22
|
+
* the application's own widgets are handed over already found, not scanned
|
|
23
|
+
* again here.
|
|
24
|
+
*/
|
|
25
|
+
import { readdir } from "node:fs/promises";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
import { CUSTOM_ELEMENT_TAG } from "./locations.js";
|
|
29
|
+
/** This package's own built-in sources are still a directory scan — that layout is fixed and internal, not discovered from an application's tree. */
|
|
30
|
+
async function scanBuiltinDir(dir) {
|
|
31
|
+
const found = new Map();
|
|
32
|
+
let names;
|
|
33
|
+
try {
|
|
34
|
+
names = await readdir(dir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
const entry = (tag) => {
|
|
40
|
+
let w = found.get(tag);
|
|
41
|
+
if (!w) {
|
|
42
|
+
w = {};
|
|
43
|
+
found.set(tag, w);
|
|
44
|
+
}
|
|
45
|
+
return w;
|
|
46
|
+
};
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
if (name.endsWith(".ts")) {
|
|
49
|
+
entry(name.slice(0, -".ts".length).toLowerCase()).script = path.join(dir, name);
|
|
50
|
+
}
|
|
51
|
+
else if (name.endsWith(".html")) {
|
|
52
|
+
entry(name.slice(0, -".html".length).toLowerCase()).html = path.join(dir, name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `elements.ts`, executed rather than parsed — it runs through the same
|
|
59
|
+
* loader as the rest of the build, so it is ordinary TypeScript, not a data
|
|
60
|
+
* format with its own rules.
|
|
61
|
+
*/
|
|
62
|
+
export async function loadElementManifest(file) {
|
|
63
|
+
let mod;
|
|
64
|
+
try {
|
|
65
|
+
// pathToFileURL requires an absolute path; the caller's may not be one.
|
|
66
|
+
mod = (await import(pathToFileURL(path.resolve(file)).href));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (err.code === "ERR_MODULE_NOT_FOUND")
|
|
70
|
+
return {};
|
|
71
|
+
throw new Error(`elements: '${file}' failed to load: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
const manifest = (mod.default ?? {});
|
|
74
|
+
for (const [tag, spec] of Object.entries(manifest)) {
|
|
75
|
+
if (!CUSTOM_ELEMENT_TAG.test(tag)) {
|
|
76
|
+
throw new Error(`elements.ts: '${tag}' is not a valid custom element name`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof spec !== "string") {
|
|
79
|
+
throw new Error(`elements.ts: '${tag}' must map to an import specifier string, got ${typeof spec}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return manifest;
|
|
83
|
+
}
|
|
84
|
+
export async function resolveElements({ used, widgets, elementsFile, builtinWidgetsDir, builtinHubDir, packageName, }) {
|
|
85
|
+
// lb-hub is found here the same way as any other built-in: hub/ is just a
|
|
86
|
+
// second built-in source, named specifiers differently because it is the
|
|
87
|
+
// package's main entry point rather than a widgets/ subpath.
|
|
88
|
+
const builtinSources = [
|
|
89
|
+
{
|
|
90
|
+
dir: builtinWidgetsDir,
|
|
91
|
+
specifier: (tag) => `${packageName}/widgets/${tag}`,
|
|
92
|
+
},
|
|
93
|
+
{ dir: builtinHubDir, specifier: () => packageName },
|
|
94
|
+
];
|
|
95
|
+
const builtinDirs = await Promise.all(builtinSources.map((source) => scanBuiltinDir(source.dir)));
|
|
96
|
+
const manifest = elementsFile ? await loadElementManifest(elementsFile) : {};
|
|
97
|
+
const resolved = new Map();
|
|
98
|
+
for (const tag of used) {
|
|
99
|
+
const hits = [];
|
|
100
|
+
let hasDefinition = false;
|
|
101
|
+
builtinSources.forEach((source, i) => {
|
|
102
|
+
const entry = builtinDirs[i].get(tag);
|
|
103
|
+
if (entry?.script) {
|
|
104
|
+
hits.push([
|
|
105
|
+
source.dir,
|
|
106
|
+
{ kind: "specifier", value: source.specifier(tag) },
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
if (entry?.html)
|
|
110
|
+
hasDefinition = true;
|
|
111
|
+
});
|
|
112
|
+
const local = widgets.get(tag);
|
|
113
|
+
if (local?.script) {
|
|
114
|
+
hits.push(["src", { kind: "file", value: local.script }]);
|
|
115
|
+
}
|
|
116
|
+
if (local?.html)
|
|
117
|
+
hasDefinition = true;
|
|
118
|
+
if (tag in manifest) {
|
|
119
|
+
hits.push([elementsFile, { kind: "specifier", value: manifest[tag] }]);
|
|
120
|
+
}
|
|
121
|
+
if (hits.length > 1) {
|
|
122
|
+
throw new Error(`build: <${tag}> is declared in more than one place: ` +
|
|
123
|
+
hits.map(([where]) => where).join(", "));
|
|
124
|
+
}
|
|
125
|
+
if (hits.length === 1) {
|
|
126
|
+
resolved.set(tag, hits[0][1]);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// No script anywhere. Valid only if a definition gave the tag its
|
|
130
|
+
// content — otherwise nothing, ever, would upgrade or fill it, and the
|
|
131
|
+
// tag would do nothing were it to reach the browser.
|
|
132
|
+
if (!hasDefinition) {
|
|
133
|
+
throw new Error(`build: <${tag}> has neither a script nor a definition, so it would ` +
|
|
134
|
+
`do nothing (checked built-in widgets, the src tree, and elements.ts)`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { resolved };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The generated client entry point — every custom element the assembled
|
|
141
|
+
* document actually uses, and nothing else. Written to `outDir` so a
|
|
142
|
+
* relative import to a local widget resolves the ordinary way; a specifier
|
|
143
|
+
* (built-in or third-party) is written as the bare string it is.
|
|
144
|
+
*/
|
|
145
|
+
export function clientEntrySource(resolved, outDir) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
for (const tag of [...resolved.keys()].sort()) {
|
|
148
|
+
const entry = resolved.get(tag);
|
|
149
|
+
if (entry.kind === "specifier") {
|
|
150
|
+
lines.push(`import ${JSON.stringify(entry.value)};`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const rel = path.relative(outDir, entry.value).split(path.sep).join("/");
|
|
154
|
+
lines.push(`import ${JSON.stringify(rel.startsWith(".") ? rel : `./${rel}`)};`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return lines.join("\n") + "\n";
|
|
158
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expansion — see docs/expansion.md.
|
|
3
|
+
*
|
|
4
|
+
* A page author writes one tag and gets the tree it stands for. Expansion
|
|
5
|
+
* runs at build time, before any request exists, so it has no data. That is
|
|
6
|
+
* why there is no control flow here: nothing to branch on.
|
|
7
|
+
*
|
|
8
|
+
* Substitution is applied to a parsed tree with setAttribute and node values,
|
|
9
|
+
* never to a string. A value applied through the DOM API cannot be reparsed
|
|
10
|
+
* as markup, so injection is impossible by construction.
|
|
11
|
+
*/
|
|
12
|
+
export type Definitions = Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Load one definition per file, named by the tag it defines.
|
|
15
|
+
*
|
|
16
|
+
* Later directories win, so an application can override a built-in widget.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadDefinitions(dirs: string[]): Promise<Definitions>;
|
|
19
|
+
/**
|
|
20
|
+
* Overlay definitions read from explicit files, each already keyed by the
|
|
21
|
+
* tag it defines — for a source, like an application's own `src` tree, that
|
|
22
|
+
* is discovered file by file rather than listed by directory. A tag here
|
|
23
|
+
* overrides the same tag in `defs`, the same override `loadDefinitions`
|
|
24
|
+
* gives a later directory over an earlier one.
|
|
25
|
+
*/
|
|
26
|
+
export declare function addDefinitions(defs: Definitions, files: Map<string, string>): Promise<Definitions>;
|
|
27
|
+
/**
|
|
28
|
+
* Every hyphenated tag name left in a document, template content included.
|
|
29
|
+
*
|
|
30
|
+
* Meant to run on already-expanded output: an `LB-*` tag with a definition
|
|
31
|
+
* is gone by then, replaced by the tree it stands for, so what remains is
|
|
32
|
+
* whatever is left for the browser to upgrade itself — a built-in widget
|
|
33
|
+
* class, an application's own custom element, or one from a package. A
|
|
34
|
+
* hyphen in the tag name is the one thing a custom element is guaranteed to
|
|
35
|
+
* have, per the Custom Elements spec, and nothing native has one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function customElementTags(html: string): Set<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Expand every widget tag in an authored page. Throws rather than shipping.
|
|
40
|
+
*
|
|
41
|
+
* Definitions must come from `loadDefinitions`, which is where the graph is
|
|
42
|
+
* checked for cycles. Handed a cyclic map directly, this recurses forever.
|
|
43
|
+
*/
|
|
44
|
+
export declare function expand(source: string, defs: Definitions): string;
|
|
45
|
+
//# sourceMappingURL=expand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../build/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA0CH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA8BjD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAmB1E;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,OAAO,CAAC,WAAW,CAAC,CAStB;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAS3D;AAkPD;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAIhE"}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expansion — see docs/expansion.md.
|
|
3
|
+
*
|
|
4
|
+
* A page author writes one tag and gets the tree it stands for. Expansion
|
|
5
|
+
* runs at build time, before any request exists, so it has no data. That is
|
|
6
|
+
* why there is no control flow here: nothing to branch on.
|
|
7
|
+
*
|
|
8
|
+
* Substitution is applied to a parsed tree with setAttribute and node values,
|
|
9
|
+
* never to a string. A value applied through the DOM API cannot be reparsed
|
|
10
|
+
* as markup, so injection is impossible by construction.
|
|
11
|
+
*/
|
|
12
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
+
import { JSDOM } from "jsdom";
|
|
14
|
+
import { ATTR_SLOT, ATTR_TEMPLATE, HUB_TAG_NAME, } from "../core/lb-constants.js";
|
|
15
|
+
/**
|
|
16
|
+
* The framework's reserved namespace. Every one of these must resolve to a
|
|
17
|
+
* definition — a typo here is caught immediately rather than shipped as an
|
|
18
|
+
* inert tag. A tag outside this namespace still expands when a definition
|
|
19
|
+
* exists for it (an application is free to name its own widgets anything),
|
|
20
|
+
* but is not required to have one: with no definition, it is assumed to be
|
|
21
|
+
* a plain custom element, registered by script alone.
|
|
22
|
+
*/
|
|
23
|
+
const WIDGET_TAG = /^LB-/;
|
|
24
|
+
/**
|
|
25
|
+
* The one LB-* tag that is not a widget: the hub itself, always present in
|
|
26
|
+
* the chrome, registered by script rather than by definition. Expanding the
|
|
27
|
+
* chrome (see build/assemble.ts) would otherwise demand a definition for it
|
|
28
|
+
* that could never exist.
|
|
29
|
+
*/
|
|
30
|
+
const HUB_TAG = HUB_TAG_NAME.toUpperCase();
|
|
31
|
+
/** A placeholder is an entire attribute value or an entire text node. */
|
|
32
|
+
const PLACEHOLDER = /^\{\{([A-Za-z][\w-]*)\}\}$/;
|
|
33
|
+
/**
|
|
34
|
+
* Marks an attribute as input to expansion. Three namespaces share the tag:
|
|
35
|
+
* `lb-` is the hub's, `exp-` is expansion's, and everything unprefixed is
|
|
36
|
+
* HTML's and means what HTML says it means. So an author is free to write
|
|
37
|
+
* `class`, `title` or `hidden` on any widget, whatever its parameters happen
|
|
38
|
+
* to be named. The prefix marks the slot on the tag, where the ambiguity was;
|
|
39
|
+
* inside a definition `{{ }}` already says the same thing, so `exp-label`
|
|
40
|
+
* fills `{{label}}`.
|
|
41
|
+
*/
|
|
42
|
+
const EXP_PREFIX = "exp-";
|
|
43
|
+
const dom = new JSDOM("");
|
|
44
|
+
const doc = dom.window.document;
|
|
45
|
+
const { Element, HTMLTemplateElement, Node } = dom.window;
|
|
46
|
+
function parse(source) {
|
|
47
|
+
const holder = doc.createElement("div");
|
|
48
|
+
holder.innerHTML = source;
|
|
49
|
+
return holder;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A tree and the content of every template in it, however deep.
|
|
53
|
+
*
|
|
54
|
+
* A template's children are parsed into a fragment rather than into the tree,
|
|
55
|
+
* so `querySelectorAll` does not reach them. Everything expansion does has to
|
|
56
|
+
* reach them anyway: a definition may supply its own row template, and
|
|
57
|
+
* `lb-picker` does, so `{{key}}` written inside one has to be a
|
|
58
|
+
* placeholder rather than literal text that ships. `walk` already descends
|
|
59
|
+
* into template content to expand widget tags; this is the same rule stated
|
|
60
|
+
* once for every pass that reads a tree.
|
|
61
|
+
*/
|
|
62
|
+
function* scopes(root) {
|
|
63
|
+
yield root;
|
|
64
|
+
for (const el of root.querySelectorAll("template")) {
|
|
65
|
+
yield* scopes(el.content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load one definition per file, named by the tag it defines.
|
|
70
|
+
*
|
|
71
|
+
* Later directories win, so an application can override a built-in widget.
|
|
72
|
+
*/
|
|
73
|
+
export async function loadDefinitions(dirs) {
|
|
74
|
+
const defs = {};
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
let names;
|
|
77
|
+
try {
|
|
78
|
+
names = await readdir(dir);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
if (!name.endsWith(".html"))
|
|
85
|
+
continue;
|
|
86
|
+
const file = `${dir}/${name}`;
|
|
87
|
+
const source = await readFile(file, "utf-8");
|
|
88
|
+
checkNames(file, source);
|
|
89
|
+
defs[name.slice(0, -".html".length).toUpperCase()] = source;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
checkAcyclic(defs);
|
|
93
|
+
return defs;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Overlay definitions read from explicit files, each already keyed by the
|
|
97
|
+
* tag it defines — for a source, like an application's own `src` tree, that
|
|
98
|
+
* is discovered file by file rather than listed by directory. A tag here
|
|
99
|
+
* overrides the same tag in `defs`, the same override `loadDefinitions`
|
|
100
|
+
* gives a later directory over an earlier one.
|
|
101
|
+
*/
|
|
102
|
+
export async function addDefinitions(defs, files) {
|
|
103
|
+
const merged = { ...defs };
|
|
104
|
+
for (const [tag, file] of files) {
|
|
105
|
+
const source = await readFile(file, "utf-8");
|
|
106
|
+
checkNames(file, source);
|
|
107
|
+
merged[tag.toUpperCase()] = source;
|
|
108
|
+
}
|
|
109
|
+
checkAcyclic(merged);
|
|
110
|
+
return merged;
|
|
111
|
+
}
|
|
112
|
+
/** Every placeholder a definition declares. */
|
|
113
|
+
function placeholdersOf(source) {
|
|
114
|
+
const names = [];
|
|
115
|
+
for (const scope of scopes(parse(source))) {
|
|
116
|
+
for (const el of scope.querySelectorAll("*")) {
|
|
117
|
+
for (const attr of el.attributes) {
|
|
118
|
+
const match = PLACEHOLDER.exec(attr.value.trim());
|
|
119
|
+
if (match)
|
|
120
|
+
names.push(match[1]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const walker = doc.createTreeWalker(scope, 4 /* TEXT */);
|
|
124
|
+
while (walker.nextNode()) {
|
|
125
|
+
const match = PLACEHOLDER.exec(walker.currentNode.data.trim());
|
|
126
|
+
if (match)
|
|
127
|
+
names.push(match[1]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A placeholder must be spelled the way a tag can supply it.
|
|
134
|
+
*
|
|
135
|
+
* HTML lowercases attribute names, so `exp-inputClass` reaches expansion as
|
|
136
|
+
* `exp-inputclass` and could never fill `{{inputClass}}`. A definition is the
|
|
137
|
+
* only file that can hold such a name and the only one that can fix it, so it
|
|
138
|
+
* is rejected here, once, rather than at every page that uses the widget.
|
|
139
|
+
*
|
|
140
|
+
* Recognition stays broad on purpose. Narrowing PLACEHOLDER to lowercase would
|
|
141
|
+
* make `{{inputClass}}` not a placeholder at all, and ship it to the browser as
|
|
142
|
+
* literal text — the same silent failure somewhere new.
|
|
143
|
+
*/
|
|
144
|
+
function checkNames(file, source) {
|
|
145
|
+
for (const name of placeholdersOf(source)) {
|
|
146
|
+
if (!/[A-Z]/.test(name))
|
|
147
|
+
continue;
|
|
148
|
+
const kebab = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
149
|
+
throw new Error(`expand: ${file} declares {{${name}}}, but HTML lowercases attribute ` +
|
|
150
|
+
`names, so no tag can supply it. Write {{${kebab}}}.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Every hyphenated tag name left in a document, template content included.
|
|
155
|
+
*
|
|
156
|
+
* Meant to run on already-expanded output: an `LB-*` tag with a definition
|
|
157
|
+
* is gone by then, replaced by the tree it stands for, so what remains is
|
|
158
|
+
* whatever is left for the browser to upgrade itself — a built-in widget
|
|
159
|
+
* class, an application's own custom element, or one from a package. A
|
|
160
|
+
* hyphen in the tag name is the one thing a custom element is guaranteed to
|
|
161
|
+
* have, per the Custom Elements spec, and nothing native has one.
|
|
162
|
+
*/
|
|
163
|
+
export function customElementTags(html) {
|
|
164
|
+
const tags = new Set();
|
|
165
|
+
for (const scope of scopes(parse(html))) {
|
|
166
|
+
for (const el of scope.querySelectorAll("*")) {
|
|
167
|
+
const tag = el.tagName.toLowerCase();
|
|
168
|
+
if (tag.includes("-"))
|
|
169
|
+
tags.add(tag);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return tags;
|
|
173
|
+
}
|
|
174
|
+
/** Which widgets a definition uses. Undefined tags are caught during expansion. */
|
|
175
|
+
function referencesOf(source) {
|
|
176
|
+
const refs = new Set();
|
|
177
|
+
for (const scope of scopes(parse(source))) {
|
|
178
|
+
for (const el of scope.querySelectorAll("*")) {
|
|
179
|
+
const tag = el.tagName.toUpperCase();
|
|
180
|
+
if (WIDGET_TAG.test(tag))
|
|
181
|
+
refs.add(tag);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...refs];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Expansion is a fixpoint, so it terminates only on an acyclic graph. A depth
|
|
188
|
+
* cap would hide the bug behind a limit instead of reporting it.
|
|
189
|
+
*/
|
|
190
|
+
function checkAcyclic(defs) {
|
|
191
|
+
const refs = new Map();
|
|
192
|
+
for (const [tag, source] of Object.entries(defs)) {
|
|
193
|
+
refs.set(tag, referencesOf(source));
|
|
194
|
+
}
|
|
195
|
+
const done = new Set();
|
|
196
|
+
const path = [];
|
|
197
|
+
const visit = (tag) => {
|
|
198
|
+
if (done.has(tag))
|
|
199
|
+
return;
|
|
200
|
+
const cycle = path.indexOf(tag);
|
|
201
|
+
if (cycle !== -1) {
|
|
202
|
+
throw new Error(`expand: cycle in the definition graph: ` +
|
|
203
|
+
[...path.slice(cycle), tag].map((t) => t.toLowerCase()).join(" → "));
|
|
204
|
+
}
|
|
205
|
+
path.push(tag);
|
|
206
|
+
for (const ref of refs.get(tag) ?? [])
|
|
207
|
+
visit(ref);
|
|
208
|
+
path.pop();
|
|
209
|
+
done.add(tag);
|
|
210
|
+
};
|
|
211
|
+
for (const tag of refs.keys())
|
|
212
|
+
visit(tag);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Resolve a placeholder against what the author wrote. Unset means absent.
|
|
216
|
+
*
|
|
217
|
+
* Every name the definition asks for is recorded, whether or not it was
|
|
218
|
+
* supplied, so expansion can report a parameter the definition never declared.
|
|
219
|
+
*/
|
|
220
|
+
function resolve(text, attrs, asked) {
|
|
221
|
+
const match = PLACEHOLDER.exec(text.trim());
|
|
222
|
+
if (!match)
|
|
223
|
+
return text;
|
|
224
|
+
const name = match[1];
|
|
225
|
+
asked.add(name);
|
|
226
|
+
return attrs.get(name) ?? null;
|
|
227
|
+
}
|
|
228
|
+
function substitute(root, attrs, asked) {
|
|
229
|
+
for (const scope of scopes(root)) {
|
|
230
|
+
for (const el of scope.querySelectorAll("*")) {
|
|
231
|
+
for (const attr of [...el.attributes]) {
|
|
232
|
+
const value = resolve(attr.value, attrs, asked);
|
|
233
|
+
// Presence propagates: a placeholder with nothing behind it drops the
|
|
234
|
+
// attribute rather than emitting it empty, which is what HTML's own
|
|
235
|
+
// boolean-attribute semantics require.
|
|
236
|
+
if (value === null)
|
|
237
|
+
el.removeAttribute(attr.name);
|
|
238
|
+
else if (value !== attr.value)
|
|
239
|
+
el.setAttribute(attr.name, value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const walker = doc.createTreeWalker(scope, 4 /* TEXT */);
|
|
243
|
+
const texts = [];
|
|
244
|
+
while (walker.nextNode())
|
|
245
|
+
texts.push(walker.currentNode);
|
|
246
|
+
for (const text of texts) {
|
|
247
|
+
const raw = text.data;
|
|
248
|
+
if (!PLACEHOLDER.test(raw.trim()))
|
|
249
|
+
continue;
|
|
250
|
+
const value = resolve(raw, attrs, asked) ?? "";
|
|
251
|
+
// Keep the author's surrounding whitespace: "{{label}} <input>" should
|
|
252
|
+
// still separate the label from the control.
|
|
253
|
+
const lead = raw.slice(0, raw.length - raw.trimStart().length);
|
|
254
|
+
const tail = raw.slice(raw.trimEnd().length);
|
|
255
|
+
text.data = lead + value + tail;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Fill the definition's named destinations from the authored templates that
|
|
261
|
+
* name them, and hand back the content that is left for the slot.
|
|
262
|
+
*
|
|
263
|
+
* A destination is filled with the template's contents, not with the
|
|
264
|
+
* template, so what ships is ordinary markup in an ordinary place — a real
|
|
265
|
+
* `<tr>` inside a real `<thead>`, visible in view-source like everything
|
|
266
|
+
* else. The wrapper existed only to survive the parser and is gone.
|
|
267
|
+
*
|
|
268
|
+
* Unlike the slot, there may be several, because the reason for wrapping is
|
|
269
|
+
* per-destination: a table has a head and a body, and each needs its own
|
|
270
|
+
* markup past the same obstacle. They are matched by name, and an empty name
|
|
271
|
+
* is a name, so a widget with one destination costs the author nothing.
|
|
272
|
+
*/
|
|
273
|
+
function fillTemplates(tag, root, content) {
|
|
274
|
+
const destinations = new Map();
|
|
275
|
+
for (const el of root.querySelectorAll(`[${ATTR_TEMPLATE}]`)) {
|
|
276
|
+
const name = el.getAttribute(ATTR_TEMPLATE);
|
|
277
|
+
if (destinations.has(name)) {
|
|
278
|
+
throw new Error(`expand: <${tag.toLowerCase()}> declares two ${ATTR_TEMPLATE}="${name}" destinations`);
|
|
279
|
+
}
|
|
280
|
+
destinations.set(name, el);
|
|
281
|
+
el.removeAttribute(ATTR_TEMPLATE);
|
|
282
|
+
}
|
|
283
|
+
const rest = [];
|
|
284
|
+
const filled = new Set();
|
|
285
|
+
for (const node of content) {
|
|
286
|
+
const named = node instanceof HTMLTemplateElement && node.hasAttribute(ATTR_TEMPLATE);
|
|
287
|
+
if (!named) {
|
|
288
|
+
rest.push(node);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const template = node;
|
|
292
|
+
const name = template.getAttribute(ATTR_TEMPLATE);
|
|
293
|
+
const destination = destinations.get(name);
|
|
294
|
+
if (!destination) {
|
|
295
|
+
throw new Error(`expand: a <template ${ATTR_TEMPLATE}="${name}"> was written inside ` +
|
|
296
|
+
`<${tag.toLowerCase()}>, whose definition has no destination by that name.`);
|
|
297
|
+
}
|
|
298
|
+
if (filled.has(name)) {
|
|
299
|
+
throw new Error(`expand: <${tag.toLowerCase()}> was given two templates for '${name}'`);
|
|
300
|
+
}
|
|
301
|
+
filled.add(name);
|
|
302
|
+
destination.append(template.content);
|
|
303
|
+
}
|
|
304
|
+
return rest;
|
|
305
|
+
}
|
|
306
|
+
/** Place authored inner content in the single slot, if the definition has one. */
|
|
307
|
+
function fillSlot(tag, root, content) {
|
|
308
|
+
const slots = root.querySelectorAll(`[${ATTR_SLOT}]`);
|
|
309
|
+
if (slots.length > 1) {
|
|
310
|
+
throw new Error(`expand: <${tag.toLowerCase()}> declares more than one ${ATTR_SLOT}`);
|
|
311
|
+
}
|
|
312
|
+
if (slots.length === 0) {
|
|
313
|
+
// Whitespace is not content. An author who writes only named templates
|
|
314
|
+
// still leaves the newlines between them, and those are the file's
|
|
315
|
+
// formatting rather than something anybody meant to place.
|
|
316
|
+
const wrote = content.some((node) => node.nodeType !== Node.TEXT_NODE || node.data.trim());
|
|
317
|
+
if (wrote) {
|
|
318
|
+
throw new Error(`expand: content was written inside <${tag.toLowerCase()}>, whose definition has no ${ATTR_SLOT}`);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const slot = slots[0];
|
|
323
|
+
// The authored nodes move rather than copy. The caller replaces the tag's
|
|
324
|
+
// children with the expanded tree immediately after, so there is nothing
|
|
325
|
+
// left behind for a clone to protect.
|
|
326
|
+
slot.append(...content);
|
|
327
|
+
slot.removeAttribute(ATTR_SLOT);
|
|
328
|
+
}
|
|
329
|
+
function expandElement(el, defs) {
|
|
330
|
+
const tag = el.tagName.toUpperCase();
|
|
331
|
+
const source = defs[tag];
|
|
332
|
+
if (source === undefined) {
|
|
333
|
+
throw new Error(`expand: <${tag.toLowerCase()}> has no definition. Every hub tag must resolve.`);
|
|
334
|
+
}
|
|
335
|
+
// The tag survives expansion carrying every attribute the author wrote,
|
|
336
|
+
// `exp-` ones included. They cost nothing where they are, and what ships
|
|
337
|
+
// shows what was asked for beside what it produced.
|
|
338
|
+
const attrs = new Map();
|
|
339
|
+
for (const attr of el.attributes) {
|
|
340
|
+
if (!attr.name.startsWith(EXP_PREFIX))
|
|
341
|
+
continue;
|
|
342
|
+
attrs.set(attr.name.slice(EXP_PREFIX.length), attr.value);
|
|
343
|
+
}
|
|
344
|
+
const tree = parse(source);
|
|
345
|
+
const asked = new Set();
|
|
346
|
+
substitute(tree, attrs, asked);
|
|
347
|
+
// A parameter the definition never declared is a name that resolves to
|
|
348
|
+
// nothing. Unprefixed attributes could not support this check, because a
|
|
349
|
+
// stray one is indistinguishable from a deliberate HTML attribute.
|
|
350
|
+
for (const name of attrs.keys()) {
|
|
351
|
+
if (asked.has(name))
|
|
352
|
+
continue;
|
|
353
|
+
throw new Error(`expand: <${tag.toLowerCase()}> was given ${EXP_PREFIX}${name}, ` +
|
|
354
|
+
`but its definition has no {{${name}}}.`);
|
|
355
|
+
}
|
|
356
|
+
// Named destinations first: what they take is addressed to them, and only
|
|
357
|
+
// what is left is the slot's.
|
|
358
|
+
fillSlot(tag, tree, fillTemplates(tag, tree, [...el.childNodes]));
|
|
359
|
+
el.replaceChildren(...tree.childNodes);
|
|
360
|
+
}
|
|
361
|
+
function walk(root, defs) {
|
|
362
|
+
for (const child of [...root.children]) {
|
|
363
|
+
const tag = child.tagName.toUpperCase();
|
|
364
|
+
if (tag in defs || (WIDGET_TAG.test(tag) && tag !== HUB_TAG)) {
|
|
365
|
+
expandElement(child, defs);
|
|
366
|
+
}
|
|
367
|
+
// Definitions may use other widgets, so descend after expanding. The
|
|
368
|
+
// acyclicity check is what makes this terminate.
|
|
369
|
+
walk(child, defs);
|
|
370
|
+
// Template content is markup in a file like any other, and it is inert,
|
|
371
|
+
// so a row template ships already expanded.
|
|
372
|
+
if (child instanceof HTMLTemplateElement)
|
|
373
|
+
walk(child.content, defs);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Expand every widget tag in an authored page. Throws rather than shipping.
|
|
378
|
+
*
|
|
379
|
+
* Definitions must come from `loadDefinitions`, which is where the graph is
|
|
380
|
+
* checked for cycles. Handed a cyclic map directly, this recurses forever.
|
|
381
|
+
*/
|
|
382
|
+
export function expand(source, defs) {
|
|
383
|
+
const tree = parse(source);
|
|
384
|
+
walk(tree, defs);
|
|
385
|
+
return tree.innerHTML;
|
|
386
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-printing the assembled document — cosmetic, and deliberately so.
|
|
3
|
+
*
|
|
4
|
+
* Loadbare App ships the DOM the browser holds, so view-source is a real view of the
|
|
5
|
+
* application. That only pays if it is legible. Expansion produces correct
|
|
6
|
+
* markup but inherits its whitespace from the definition files, so the
|
|
7
|
+
* assembled document reads like a concatenation, which is what it is.
|
|
8
|
+
*
|
|
9
|
+
* Prettier is a nice-to-have here and is treated as one. It runs on the whole
|
|
10
|
+
* document once, after everything is assembled, and every way it can fail —
|
|
11
|
+
* throwing, or not being installed — leaves the unformatted string in place.
|
|
12
|
+
*
|
|
13
|
+
* Ignoring the throw is the point rather than an oversight. The correct output
|
|
14
|
+
* is what `expand` already returned; formatting only makes it pleasant. If a
|
|
15
|
+
* failure could fail the build, we would end up writing checks upstream whose
|
|
16
|
+
* only purpose was to keep the formatter happy, and an error message about
|
|
17
|
+
* markup the author never wrote is worse than an ugly line. So: nothing in this
|
|
18
|
+
* codebase exists to keep prettier from throwing. It either helps or it doesn't.
|
|
19
|
+
*
|
|
20
|
+
* The one failure that would retire this outright is whitespace. Whitespace is
|
|
21
|
+
* semantic in inline contexts — `<label>Name <input></label>` needs that space —
|
|
22
|
+
* and prettier has no display information for `lb-*` elements. If formatting
|
|
23
|
+
* is ever observed to change what renders, delete this file. A prettier page
|
|
24
|
+
* that shows the wrong thing is not a trade worth making.
|
|
25
|
+
*/
|
|
26
|
+
/** Format if we can, and return the input unchanged if we cannot. */
|
|
27
|
+
export declare function formatHtml(source: string): Promise<string>;
|
|
28
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../build/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,qEAAqE;AACrE,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBhE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-printing the assembled document — cosmetic, and deliberately so.
|
|
3
|
+
*
|
|
4
|
+
* Loadbare App ships the DOM the browser holds, so view-source is a real view of the
|
|
5
|
+
* application. That only pays if it is legible. Expansion produces correct
|
|
6
|
+
* markup but inherits its whitespace from the definition files, so the
|
|
7
|
+
* assembled document reads like a concatenation, which is what it is.
|
|
8
|
+
*
|
|
9
|
+
* Prettier is a nice-to-have here and is treated as one. It runs on the whole
|
|
10
|
+
* document once, after everything is assembled, and every way it can fail —
|
|
11
|
+
* throwing, or not being installed — leaves the unformatted string in place.
|
|
12
|
+
*
|
|
13
|
+
* Ignoring the throw is the point rather than an oversight. The correct output
|
|
14
|
+
* is what `expand` already returned; formatting only makes it pleasant. If a
|
|
15
|
+
* failure could fail the build, we would end up writing checks upstream whose
|
|
16
|
+
* only purpose was to keep the formatter happy, and an error message about
|
|
17
|
+
* markup the author never wrote is worse than an ugly line. So: nothing in this
|
|
18
|
+
* codebase exists to keep prettier from throwing. It either helps or it doesn't.
|
|
19
|
+
*
|
|
20
|
+
* The one failure that would retire this outright is whitespace. Whitespace is
|
|
21
|
+
* semantic in inline contexts — `<label>Name <input></label>` needs that space —
|
|
22
|
+
* and prettier has no display information for `lb-*` elements. If formatting
|
|
23
|
+
* is ever observed to change what renders, delete this file. A prettier page
|
|
24
|
+
* that shows the wrong thing is not a trade worth making.
|
|
25
|
+
*/
|
|
26
|
+
/** Format if we can, and return the input unchanged if we cannot. */
|
|
27
|
+
export async function formatHtml(source) {
|
|
28
|
+
try {
|
|
29
|
+
const prettier = await import("prettier");
|
|
30
|
+
return await prettier.format(source, {
|
|
31
|
+
parser: "html",
|
|
32
|
+
// The default. Prettier respects each element's CSS display default when
|
|
33
|
+
// deciding where whitespace may move, and treats elements it does not
|
|
34
|
+
// know — every hub widget — as inline, which is the conservative side.
|
|
35
|
+
htmlWhitespaceSensitivity: "css",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.warn(`loadbare: shipping unformatted markup, prettier declined: ${err}`);
|
|
40
|
+
return source;
|
|
41
|
+
}
|
|
42
|
+
}
|