hdoc-tools 0.61.0 → 0.62.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.
Files changed (45) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +89 -75
  3. package/editor/dist/assets/index-Blewr90z.css +1 -1
  4. package/editor/dist/assets/index-BtxvGZHW.js +111 -111
  5. package/editor/dist/assets/spell.worker-sryEKmOj.js +13 -13
  6. package/editor/dist/index.html +14 -14
  7. package/hdoc-build-db.js +275 -275
  8. package/hdoc-build-embeddings.js +202 -202
  9. package/hdoc-build-pdf.js +232 -232
  10. package/hdoc-bump.js +125 -125
  11. package/hdoc-create.js +110 -110
  12. package/hdoc-db.js +114 -114
  13. package/hdoc-help.js +60 -60
  14. package/hdoc-install-browser.js +145 -145
  15. package/hdoc-mermaid.js +204 -204
  16. package/hdoc-module.js +1102 -1102
  17. package/hdoc-validate-config.js +355 -355
  18. package/hdoc-validate-interbook.js +321 -321
  19. package/hdoc-validate.js +1231 -1231
  20. package/hdoc-ver.js +45 -45
  21. package/package.json +12 -1
  22. package/templates/doc-header-non-git.html +19 -19
  23. package/templates/doc-header.html +26 -26
  24. package/templates/init/.github/workflows/hdocbuild_onpull.yml +16 -16
  25. package/templates/init/.github/workflows/hdocbuild_onpush.yml +15 -15
  26. package/templates/init/LICENSE +21 -21
  27. package/templates/init/README.md +9 -9
  28. package/templates/init/_hdocbook/index.md +4 -4
  29. package/templates/init/gitignore +8 -8
  30. package/templates/init/resources/README.md +2 -2
  31. package/templates/pdf/css/custom-block.css +90 -90
  32. package/templates/pdf/css/fonts.css +221 -221
  33. package/templates/pdf/css/hdocs-pdf.css +495 -495
  34. package/templates/pdf/css/vars.css +404 -404
  35. package/templates/pdf/template-footer.html +19 -19
  36. package/templates/pdf/template-header.html +37 -37
  37. package/templates/pdf/template.html +20 -20
  38. package/templates/pdf-header-non-git.html +12 -12
  39. package/templates/pdf-header.html +16 -16
  40. package/ui/content/invalid-hdocbook-json.html +6 -6
  41. package/ui/content/invalid-hdocbook-json.md +7 -7
  42. package/ui/css/theme-default/styles/components/content.css +124 -124
  43. package/ui/css/theme-default/styles/components/sidebar.css +182 -182
  44. package/ui/css/theme-default/styles/htldoc.layouts.css +310 -310
  45. package/ui/index.html +419 -419
package/hdoc-mermaid.js CHANGED
@@ -1,204 +1,204 @@
1
- // In-process Mermaid -> SVG renderer.
2
- //
3
- // Replaces the previous `@mermaid-js/mermaid-cli` (mmdc) integration. Instead of
4
- // shelling out to the CLI (which spawned a fresh Chromium per diagram and pinned
5
- // puppeteer to <=24 via its peerDependency), this renders Mermaid using the
6
- // `mermaid` library directly inside a puppeteer browser supplied by the caller —
7
- // the very same browser hdoc-build already launches for PDF/link work.
8
- //
9
- // The render path (Interceptor + page evaluation) is ported from mermaid-cli
10
- // (MIT licensed): https://github.com/mermaid-js/mermaid-cli
11
- // Trimmed to the SVG-only case we need, plus ELK layout support (required by
12
- // books that use `layout: elk`). zenuml / tidy-tree / icon-packs / png / pdf are
13
- // intentionally dropped — no hdocbook content uses them.
14
-
15
- const { readFile, realpath } = require("node:fs/promises");
16
- const path = require("node:path");
17
- const url = require("node:url");
18
-
19
- // ESM bundle locations inside the installed packages. Both packages expose
20
- // "./*": "./*" in their exports map, so the dist files resolve directly.
21
- const mermaidESMPath = require.resolve("mermaid/dist/mermaid.esm.mjs");
22
- const elkESMPath = require.resolve(
23
- "@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.mjs",
24
- );
25
-
26
- /**
27
- * Guesses the MIME-type of a file based on its extension. Only the handful of
28
- * types the Mermaid ESM bundles actually request are supported.
29
- */
30
- function getContentTypeFromFileExtension(filePath) {
31
- const ext = path.extname(filePath).toLowerCase();
32
- switch (ext) {
33
- case ".css":
34
- // Force UTF-8 so puppeteer doesn't mis-parse as Latin-1.
35
- return "text/css;charset=UTF-8";
36
- case ".js":
37
- case ".mjs":
38
- return "application/javascript";
39
- case ".woff2":
40
- return "font/woff2";
41
- default:
42
- throw new Error(`Unsupported file extension for intercept: ${ext}`);
43
- }
44
- }
45
-
46
- /**
47
- * Puppeteer can't `import` ESM modules from `file://` URLs. Rather than stand up
48
- * a real HTTP server (ports/firewalls), we intercept requests to a dummy
49
- * `https://hdoc-mermaid-intercept.invalid` origin and serve the corresponding
50
- * local file. Ported verbatim from mermaid-cli's puppeteerIntercept.js.
51
- */
52
- class Interceptor {
53
- #INTERCEPT_ORIGIN = "https://hdoc-mermaid-intercept.invalid";
54
-
55
- /** @type {Set<string>} resolved (realpath) dirs allowed to be served. */
56
- #allowedDirs = new Set();
57
-
58
- async fileUrlToInterceptUrl(fileUrl, { allowParentDirectoryLevel = 1 } = {}) {
59
- fileUrl = new URL(fileUrl);
60
- if (fileUrl.protocol !== "file:") {
61
- throw new Error(`Invalid file URL: ${fileUrl}`);
62
- }
63
- let parentDirectory = await realpath(url.fileURLToPath(fileUrl));
64
- while (allowParentDirectoryLevel-- >= 0) {
65
- parentDirectory = path.dirname(parentDirectory);
66
- }
67
- this.#allowedDirs.add(parentDirectory);
68
- return `${this.#INTERCEPT_ORIGIN}${fileUrl.pathname}`;
69
- }
70
-
71
- async interceptUrlToFileUrl(interceptUrl) {
72
- interceptUrl = new URL(interceptUrl);
73
- if (interceptUrl.origin !== this.#INTERCEPT_ORIGIN) {
74
- throw new Error(`Invalid intercept URL: ${interceptUrl}`);
75
- }
76
- const fileUrl = new URL(
77
- interceptUrl.href.slice(this.#INTERCEPT_ORIGIN.length),
78
- "file://",
79
- );
80
- const filePath = await realpath(url.fileURLToPath(fileUrl));
81
- if (
82
- ![...this.#allowedDirs].some((dir) =>
83
- path.relative(filePath, dir).startsWith(".."),
84
- )
85
- ) {
86
- throw new Error(
87
- `Intercept URL is not in an allowed directory: ${interceptUrl}`,
88
- );
89
- }
90
- return fileUrl;
91
- }
92
-
93
- async #interceptRequestHandler(request) {
94
- try {
95
- if (request.url().startsWith(this.#INTERCEPT_ORIGIN)) {
96
- const fileUrl = await this.interceptUrlToFileUrl(request.url());
97
- return request.respond({
98
- status: 200,
99
- headers: { "Access-Control-Allow-Origin": "*" },
100
- contentType: getContentTypeFromFileExtension(
101
- url.fileURLToPath(fileUrl),
102
- ),
103
- body: await readFile(fileUrl),
104
- });
105
- }
106
- } catch (error) {
107
- console.error(
108
- `Error handling intercept request for ${request.url()}:`,
109
- error,
110
- );
111
- request.abort();
112
- return;
113
- }
114
- request.continue();
115
- }
116
-
117
- get interceptRequestHandler() {
118
- return this.#interceptRequestHandler.bind(this);
119
- }
120
- }
121
-
122
- /**
123
- * Render a single Mermaid definition to an SVG string.
124
- *
125
- * @param {import('puppeteer').Browser} browser - An open puppeteer browser.
126
- * @param {string} definition - Mermaid source (may include `--- config: ---` frontmatter).
127
- * @param {{ backgroundColor?: string }} [opts]
128
- * @returns {Promise<string>} the serialized `<svg>`.
129
- */
130
- async function render_svg(browser, definition, { backgroundColor = "transparent" } = {}) {
131
- const page = await browser.newPage();
132
- // Surface in-page warnings/errors (e.g. mermaid parse errors) to the build log.
133
- page.on("console", (msg) => {
134
- if (msg.type() === "error" || msg.type() === "warning") {
135
- console.warn(msg.text());
136
- }
137
- });
138
- try {
139
- await page.setContent(
140
- "<!doctype html><html><body><div id=\"container\"></div></body></html>",
141
- );
142
- await page.$eval(
143
- "body",
144
- (body, bg) => {
145
- body.style.background = bg;
146
- },
147
- backgroundColor,
148
- );
149
-
150
- const interceptor = new Interceptor();
151
- const mermaidUrl = await interceptor.fileUrlToInterceptUrl(
152
- url.pathToFileURL(mermaidESMPath),
153
- );
154
- const elkUrl = await interceptor.fileUrlToInterceptUrl(
155
- url.pathToFileURL(elkESMPath),
156
- );
157
-
158
- page.on("request", interceptor.interceptRequestHandler);
159
- await page.setRequestInterception(true);
160
-
161
- await page.$eval(
162
- "#container",
163
- async (container, { definition, mermaidUrl, elkUrl, backgroundColor }) => {
164
- const { default: mermaid } = await import(mermaidUrl);
165
- const { default: elkLayouts } = await import(elkUrl);
166
-
167
- await Promise.all(
168
- Array.from(document.fonts, (font) => font.load()),
169
- );
170
-
171
- mermaid.registerLayoutLoaders(elkLayouts);
172
- mermaid.initialize({ startOnLoad: false });
173
-
174
- // Throws on invalid diagrams — propagates out of $eval as a rejection.
175
- const { svg: svgText } = await mermaid.render(
176
- "hdoc-svg",
177
- definition,
178
- container,
179
- );
180
- container.innerHTML = svgText;
181
-
182
- const svg = container.getElementsByTagName?.("svg")?.[0];
183
- if (svg?.style) {
184
- svg.style.backgroundColor = backgroundColor;
185
- }
186
- },
187
- { definition, mermaidUrl, elkUrl, backgroundColor },
188
- );
189
-
190
- // Serialize via XMLSerializer so HTML <foreignObject> content (e.g. <br>)
191
- // becomes valid XML in the saved .svg file.
192
- const svgXML = await page.$eval("svg", (svg) => {
193
- // eslint-disable-next-line no-undef
194
- const xmlSerializer = new XMLSerializer();
195
- return xmlSerializer.serializeToString(svg);
196
- });
197
-
198
- return svgXML;
199
- } finally {
200
- await page.close();
201
- }
202
- }
203
-
204
- module.exports = { render_svg, Interceptor };
1
+ // In-process Mermaid -> SVG renderer.
2
+ //
3
+ // Replaces the previous `@mermaid-js/mermaid-cli` (mmdc) integration. Instead of
4
+ // shelling out to the CLI (which spawned a fresh Chromium per diagram and pinned
5
+ // puppeteer to <=24 via its peerDependency), this renders Mermaid using the
6
+ // `mermaid` library directly inside a puppeteer browser supplied by the caller —
7
+ // the very same browser hdoc-build already launches for PDF/link work.
8
+ //
9
+ // The render path (Interceptor + page evaluation) is ported from mermaid-cli
10
+ // (MIT licensed): https://github.com/mermaid-js/mermaid-cli
11
+ // Trimmed to the SVG-only case we need, plus ELK layout support (required by
12
+ // books that use `layout: elk`). zenuml / tidy-tree / icon-packs / png / pdf are
13
+ // intentionally dropped — no hdocbook content uses them.
14
+
15
+ const { readFile, realpath } = require("node:fs/promises");
16
+ const path = require("node:path");
17
+ const url = require("node:url");
18
+
19
+ // ESM bundle locations inside the installed packages. Both packages expose
20
+ // "./*": "./*" in their exports map, so the dist files resolve directly.
21
+ const mermaidESMPath = require.resolve("mermaid/dist/mermaid.esm.mjs");
22
+ const elkESMPath = require.resolve(
23
+ "@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.mjs",
24
+ );
25
+
26
+ /**
27
+ * Guesses the MIME-type of a file based on its extension. Only the handful of
28
+ * types the Mermaid ESM bundles actually request are supported.
29
+ */
30
+ function getContentTypeFromFileExtension(filePath) {
31
+ const ext = path.extname(filePath).toLowerCase();
32
+ switch (ext) {
33
+ case ".css":
34
+ // Force UTF-8 so puppeteer doesn't mis-parse as Latin-1.
35
+ return "text/css;charset=UTF-8";
36
+ case ".js":
37
+ case ".mjs":
38
+ return "application/javascript";
39
+ case ".woff2":
40
+ return "font/woff2";
41
+ default:
42
+ throw new Error(`Unsupported file extension for intercept: ${ext}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Puppeteer can't `import` ESM modules from `file://` URLs. Rather than stand up
48
+ * a real HTTP server (ports/firewalls), we intercept requests to a dummy
49
+ * `https://hdoc-mermaid-intercept.invalid` origin and serve the corresponding
50
+ * local file. Ported verbatim from mermaid-cli's puppeteerIntercept.js.
51
+ */
52
+ class Interceptor {
53
+ #INTERCEPT_ORIGIN = "https://hdoc-mermaid-intercept.invalid";
54
+
55
+ /** @type {Set<string>} resolved (realpath) dirs allowed to be served. */
56
+ #allowedDirs = new Set();
57
+
58
+ async fileUrlToInterceptUrl(fileUrl, { allowParentDirectoryLevel = 1 } = {}) {
59
+ fileUrl = new URL(fileUrl);
60
+ if (fileUrl.protocol !== "file:") {
61
+ throw new Error(`Invalid file URL: ${fileUrl}`);
62
+ }
63
+ let parentDirectory = await realpath(url.fileURLToPath(fileUrl));
64
+ while (allowParentDirectoryLevel-- >= 0) {
65
+ parentDirectory = path.dirname(parentDirectory);
66
+ }
67
+ this.#allowedDirs.add(parentDirectory);
68
+ return `${this.#INTERCEPT_ORIGIN}${fileUrl.pathname}`;
69
+ }
70
+
71
+ async interceptUrlToFileUrl(interceptUrl) {
72
+ interceptUrl = new URL(interceptUrl);
73
+ if (interceptUrl.origin !== this.#INTERCEPT_ORIGIN) {
74
+ throw new Error(`Invalid intercept URL: ${interceptUrl}`);
75
+ }
76
+ const fileUrl = new URL(
77
+ interceptUrl.href.slice(this.#INTERCEPT_ORIGIN.length),
78
+ "file://",
79
+ );
80
+ const filePath = await realpath(url.fileURLToPath(fileUrl));
81
+ if (
82
+ ![...this.#allowedDirs].some((dir) =>
83
+ path.relative(filePath, dir).startsWith(".."),
84
+ )
85
+ ) {
86
+ throw new Error(
87
+ `Intercept URL is not in an allowed directory: ${interceptUrl}`,
88
+ );
89
+ }
90
+ return fileUrl;
91
+ }
92
+
93
+ async #interceptRequestHandler(request) {
94
+ try {
95
+ if (request.url().startsWith(this.#INTERCEPT_ORIGIN)) {
96
+ const fileUrl = await this.interceptUrlToFileUrl(request.url());
97
+ return request.respond({
98
+ status: 200,
99
+ headers: { "Access-Control-Allow-Origin": "*" },
100
+ contentType: getContentTypeFromFileExtension(
101
+ url.fileURLToPath(fileUrl),
102
+ ),
103
+ body: await readFile(fileUrl),
104
+ });
105
+ }
106
+ } catch (error) {
107
+ console.error(
108
+ `Error handling intercept request for ${request.url()}:`,
109
+ error,
110
+ );
111
+ request.abort();
112
+ return;
113
+ }
114
+ request.continue();
115
+ }
116
+
117
+ get interceptRequestHandler() {
118
+ return this.#interceptRequestHandler.bind(this);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Render a single Mermaid definition to an SVG string.
124
+ *
125
+ * @param {import('puppeteer').Browser} browser - An open puppeteer browser.
126
+ * @param {string} definition - Mermaid source (may include `--- config: ---` frontmatter).
127
+ * @param {{ backgroundColor?: string }} [opts]
128
+ * @returns {Promise<string>} the serialized `<svg>`.
129
+ */
130
+ async function render_svg(browser, definition, { backgroundColor = "transparent" } = {}) {
131
+ const page = await browser.newPage();
132
+ // Surface in-page warnings/errors (e.g. mermaid parse errors) to the build log.
133
+ page.on("console", (msg) => {
134
+ if (msg.type() === "error" || msg.type() === "warning") {
135
+ console.warn(msg.text());
136
+ }
137
+ });
138
+ try {
139
+ await page.setContent(
140
+ "<!doctype html><html><body><div id=\"container\"></div></body></html>",
141
+ );
142
+ await page.$eval(
143
+ "body",
144
+ (body, bg) => {
145
+ body.style.background = bg;
146
+ },
147
+ backgroundColor,
148
+ );
149
+
150
+ const interceptor = new Interceptor();
151
+ const mermaidUrl = await interceptor.fileUrlToInterceptUrl(
152
+ url.pathToFileURL(mermaidESMPath),
153
+ );
154
+ const elkUrl = await interceptor.fileUrlToInterceptUrl(
155
+ url.pathToFileURL(elkESMPath),
156
+ );
157
+
158
+ page.on("request", interceptor.interceptRequestHandler);
159
+ await page.setRequestInterception(true);
160
+
161
+ await page.$eval(
162
+ "#container",
163
+ async (container, { definition, mermaidUrl, elkUrl, backgroundColor }) => {
164
+ const { default: mermaid } = await import(mermaidUrl);
165
+ const { default: elkLayouts } = await import(elkUrl);
166
+
167
+ await Promise.all(
168
+ Array.from(document.fonts, (font) => font.load()),
169
+ );
170
+
171
+ mermaid.registerLayoutLoaders(elkLayouts);
172
+ mermaid.initialize({ startOnLoad: false });
173
+
174
+ // Throws on invalid diagrams — propagates out of $eval as a rejection.
175
+ const { svg: svgText } = await mermaid.render(
176
+ "hdoc-svg",
177
+ definition,
178
+ container,
179
+ );
180
+ container.innerHTML = svgText;
181
+
182
+ const svg = container.getElementsByTagName?.("svg")?.[0];
183
+ if (svg?.style) {
184
+ svg.style.backgroundColor = backgroundColor;
185
+ }
186
+ },
187
+ { definition, mermaidUrl, elkUrl, backgroundColor },
188
+ );
189
+
190
+ // Serialize via XMLSerializer so HTML <foreignObject> content (e.g. <br>)
191
+ // becomes valid XML in the saved .svg file.
192
+ const svgXML = await page.$eval("svg", (svg) => {
193
+ // eslint-disable-next-line no-undef
194
+ const xmlSerializer = new XMLSerializer();
195
+ return xmlSerializer.serializeToString(svg);
196
+ });
197
+
198
+ return svgXML;
199
+ } finally {
200
+ await page.close();
201
+ }
202
+ }
203
+
204
+ module.exports = { render_svg, Interceptor };