hdoc-tools 0.60.1 → 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 (55) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +89 -75
  3. package/hdoc-build-db.js +275 -275
  4. package/hdoc-build-embeddings.js +202 -202
  5. package/hdoc-build-pdf.js +232 -232
  6. package/hdoc-build.js +14 -6
  7. package/hdoc-bump.js +4 -2
  8. package/hdoc-content-routes.js +143 -83
  9. package/hdoc-create.js +110 -108
  10. package/hdoc-db.js +114 -114
  11. package/hdoc-help.js +60 -60
  12. package/hdoc-init.js +103 -68
  13. package/hdoc-install-browser.js +145 -145
  14. package/hdoc-mermaid.js +204 -204
  15. package/hdoc-module.js +1102 -1079
  16. package/hdoc-serve.js +13 -7
  17. package/hdoc-stats.js +9 -9
  18. package/hdoc-validate-config.js +355 -329
  19. package/hdoc-validate-interbook.js +321 -0
  20. package/hdoc-validate.js +1231 -1158
  21. package/hdoc-ver.js +4 -2
  22. package/hdoc.js +12 -11
  23. package/npm-shrinkwrap.json +2 -2
  24. package/package.json +13 -2
  25. package/schemas/hdocbook-project.schema.json +20 -0
  26. package/schemas/hdocbook.schema.json +6 -2
  27. package/templates/doc-header-non-git.html +19 -19
  28. package/templates/doc-header.html +26 -26
  29. package/templates/init/.github/workflows/hdocbuild_onpull.yml +16 -16
  30. package/templates/init/.github/workflows/hdocbuild_onpush.yml +15 -15
  31. package/templates/init/LICENSE +21 -21
  32. package/templates/init/README.md +9 -9
  33. package/templates/init/_hdocbook/index.md +4 -4
  34. package/templates/init/gitignore +8 -8
  35. package/templates/init/resources/README.md +2 -2
  36. package/templates/pdf/css/custom-block.css +90 -90
  37. package/templates/pdf/css/fonts.css +221 -221
  38. package/templates/pdf/css/hdocs-pdf.css +495 -495
  39. package/templates/pdf/css/vars.css +404 -404
  40. package/templates/pdf/template-footer.html +19 -19
  41. package/templates/pdf/template-header.html +37 -37
  42. package/templates/pdf/template.html +20 -20
  43. package/templates/pdf-header-non-git.html +12 -12
  44. package/templates/pdf-header.html +16 -16
  45. package/ui/content/invalid-hdocbook-json.html +6 -6
  46. package/ui/content/invalid-hdocbook-json.md +7 -7
  47. package/ui/css/theme-default/styles/components/content.css +124 -124
  48. package/ui/css/theme-default/styles/components/sidebar.css +182 -182
  49. package/ui/css/theme-default/styles/htldoc.layouts.css +310 -310
  50. package/ui/index.html +419 -419
  51. package/ui/js/doc.hornbill.js +31 -44
  52. package/ui/js/mermaid-theme.json +27 -0
  53. package/hdoc-build-onyx.js +0 -134
  54. package/templates/mermaid-theme.yaml +0 -28
  55. package/templates/pdf/fonts/inter-cyrillic copy.woff2 +0 -0
@@ -33,6 +33,34 @@ function escape_html(str) {
33
33
  .replace(/>/g, ">");
34
34
  }
35
35
 
36
+ // Resolve a request URL path against a root directory, refusing anything that
37
+ // escapes the root (path traversal via ../, encoded %2e%2e%2f, backslashes on
38
+ // Windows, or NUL bytes). Returns the absolute resolved path, or null when the
39
+ // URL is malformed or lands outside the root. Both `hdoc serve` (which binds
40
+ // 0.0.0.0 and is LAN-reachable) and `hdoc edit` route raw request URLs through
41
+ // this before touching the filesystem.
42
+ function resolve_within_root(root, url_path) {
43
+ let decoded;
44
+ try {
45
+ decoded = decodeURIComponent(url_path);
46
+ } catch {
47
+ return null; // malformed percent-encoding
48
+ }
49
+ // Drop any query string and reject NUL bytes outright
50
+ decoded = decoded.split("?")[0];
51
+ if (decoded.includes("\0")) return null;
52
+ const resolved_root = path.resolve(root);
53
+ const resolved = path.resolve(resolved_root, `.${path.sep}${decoded.replace(/^[/\\]+/, "")}`);
54
+ if (
55
+ resolved !== resolved_root &&
56
+ !resolved.startsWith(resolved_root + path.sep)
57
+ ) {
58
+ return null;
59
+ }
60
+ return resolved;
61
+ }
62
+ exports.resolve_within_root = resolve_within_root;
63
+
36
64
  // Recursively walk <source_path>/<docId>/_inline building the nav fragment
37
65
  // consumed by the viewer's nav-section-component. Subfolders become
38
66
  // expandable container nodes (items, no link); files become leaf links.
@@ -94,6 +122,30 @@ exports.create_content_handler = (ctx) => {
94
122
  // process_includes resolves include paths relative to the source root.
95
123
  const global_source_path = source_path;
96
124
 
125
+ // Configs are re-read from disk per request so `hdoc serve` / `hdoc edit`
126
+ // pick up edits to hdocbook.json / hdocbook-project.json without a restart.
127
+ // Falls back to the startup copy when a mid-edit save is momentarily
128
+ // invalid JSON.
129
+ function load_json(file_path, fallback) {
130
+ try {
131
+ return JSON.parse(fs.readFileSync(file_path, "utf8"));
132
+ } catch {
133
+ return fallback;
134
+ }
135
+ }
136
+ function get_project_config() {
137
+ return load_json(
138
+ path.join(source_path, "hdocbook-project.json"),
139
+ hdocbook_project,
140
+ );
141
+ }
142
+ function get_book_config() {
143
+ return load_json(
144
+ path.join(source_path, docId, "hdocbook.json"),
145
+ hdocbook_config,
146
+ );
147
+ }
148
+
97
149
  // Render markdown SOURCE to HTML through the same pipeline as published output:
98
150
  // expand_variables -> process_includes -> markdown-it (+ mermaid, tips, frontmatter).
99
151
  //
@@ -236,45 +288,50 @@ exports.create_content_handler = (ctx) => {
236
288
  // we will transform to HTML and return that
237
289
  //
238
290
  // Anything else in this handler will return a 404 error
239
- function handle_books_request(req, res) {
240
- let url = req.url.replace("/_books/", "/");
241
-
242
- console.log("URL Requested:", url);
243
-
244
- // Process redirect
245
- if (
246
- hdocbook_project.redirects &&
247
- Array.isArray(hdocbook_project.redirects) &&
248
- hdocbook_project.redirects.length > 0
249
- ) {
250
- const source_url = url.indexOf("/") === 0 ? url : `/${url}`;
251
- for (const redir of hdocbook_project.redirects) {
252
- redir.url =
253
- redir.url.indexOf("/") === 0 ? redir.url : `/${redir.url}`;
254
- if (
255
- redir.url === source_url &&
256
- redir.location &&
257
- redir.location !== ""
258
- ) {
259
- url = `${redir.location}`;
260
- console.log(`Redirecting to ${url}`);
291
+ async function handle_books_request(req, res) {
292
+ try {
293
+ let url = req.url.replace("/_books/", "/");
294
+
295
+ console.log("URL Requested:", url);
296
+
297
+ // Process redirect (config read fresh so redirect edits apply live)
298
+ const project_config = get_project_config();
299
+ if (
300
+ project_config.redirects &&
301
+ Array.isArray(project_config.redirects) &&
302
+ project_config.redirects.length > 0
303
+ ) {
304
+ const source_url = url.indexOf("/") === 0 ? url : `/${url}`;
305
+ for (const redir of project_config.redirects) {
306
+ redir.url =
307
+ redir.url.indexOf("/") === 0 ? redir.url : `/${redir.url}`;
308
+ if (
309
+ redir.url === source_url &&
310
+ redir.location &&
311
+ redir.location !== ""
312
+ ) {
313
+ url = `${redir.location}`;
314
+ console.log(`Redirecting to ${url}`);
315
+ }
261
316
  }
262
317
  }
263
- }
264
318
 
265
- const file_path = path.join(source_path, url);
266
-
267
- if (path.extname(file_path) === ".html") {
268
- // 1a. check for html files, and send/transform as required
269
- if (fs.existsSync(file_path)) {
270
- // HTML file exists on disk, just return it verbatim
271
- res.setHeader("Content-Type", "text/html");
272
- send_file(req, res, file_path);
273
- return true;
319
+ const file_path = resolve_within_root(source_path, url);
320
+ if (file_path === null) {
321
+ send_content_resource_404(req, res);
322
+ return;
274
323
  }
275
- if (fs.existsSync(file_path.replace(".html", ".md"))) {
324
+
325
+ if (path.extname(file_path) === ".html") {
326
+ // 1a. check for html files, and send/transform as required
327
+ if (fs.existsSync(file_path)) {
328
+ // HTML file exists on disk, just return it verbatim
329
+ res.setHeader("Content-Type", "text/html");
330
+ send_file(req, res, file_path);
331
+ return;
332
+ }
276
333
  if (
277
- transform_markdown_and_send_html(
334
+ await transform_markdown_and_send_html(
278
335
  req,
279
336
  res,
280
337
  file_path.replace(".html", ".md"),
@@ -282,22 +339,19 @@ exports.create_content_handler = (ctx) => {
282
339
  ) {
283
340
  return;
284
341
  }
285
- }
286
- } else if (path.extname(file_path) === ".md") {
287
- // If the markdown file exists, just send to caller as is
288
- if (fs.existsSync(file_path)) {
289
- send_content_file(req, res, file_path);
290
- return true;
291
- }
292
- } else if (path.extname(file_path).length === 0) {
293
- // 2. If we request a file, without any file extension
294
- if (fs.existsSync(`${file_path}.md`)) {
295
- if (transform_markdown_and_send_html(req, res, `${file_path}.md`)) {
342
+ } else if (path.extname(file_path) === ".md") {
343
+ // If the markdown file exists, just send to caller as is
344
+ if (fs.existsSync(file_path)) {
345
+ send_content_file(req, res, file_path);
346
+ return;
347
+ }
348
+ } else if (path.extname(file_path).length === 0) {
349
+ // 2. If we request a file, without any file extension
350
+ if (await transform_markdown_and_send_html(req, res, `${file_path}.md`)) {
296
351
  return;
297
352
  }
298
- } else if (fs.existsSync(path.join(`${file_path}index.md`))) {
299
353
  if (
300
- transform_markdown_and_send_html(
354
+ await transform_markdown_and_send_html(
301
355
  req,
302
356
  res,
303
357
  path.join(file_path, "index.md"),
@@ -305,55 +359,61 @@ exports.create_content_handler = (ctx) => {
305
359
  ) {
306
360
  return;
307
361
  }
308
- } else if (fs.existsSync(path.join(`${file_path}index.html`))) {
309
- res.setHeader("Content-Type", "text/html");
310
- send_content_file(req, res, path.join(`${file_path}index.html`));
311
- return;
312
- } else if (fs.existsSync(`${file_path}/index.md`)) {
362
+ if (fs.existsSync(path.join(`${file_path}index.html`))) {
363
+ res.setHeader("Content-Type", "text/html");
364
+ send_content_file(req, res, path.join(`${file_path}index.html`));
365
+ return;
366
+ }
367
+ if (fs.existsSync(path.join(`${file_path}/index.html`))) {
368
+ res.setHeader("Content-Type", "text/html");
369
+ send_content_file(req, res, path.join(`${file_path}/index.html`));
370
+ return;
371
+ }
372
+ if (fs.existsSync(path.join(`${file_path}.html`))) {
373
+ res.setHeader("Content-Type", "text/html");
374
+ send_content_file(req, res, path.join(`${file_path}.html`));
375
+ return;
376
+ }
377
+ if (fs.existsSync(path.join(`${file_path}.htm`))) {
378
+ res.setHeader("Content-Type", "text/html");
379
+ send_content_file(req, res, path.join(`${file_path}.htm`));
380
+ return;
381
+ }
382
+ } else if (fs.existsSync(file_path)) {
313
383
  if (
314
- transform_markdown_and_send_html(req, res, `${file_path}/index.md`)
384
+ file_path.endsWith("hdocbook.json") ||
385
+ file_path.endsWith("hdocbook_project.json")
315
386
  ) {
316
- return;
387
+ try {
388
+ // Read & parse file
389
+ JSON.parse(fs.readFileSync(file_path));
390
+ } catch (e) {
391
+ console.error(`Error parsing hdocbook.json: ${e}`);
392
+ }
317
393
  }
318
- } else if (fs.existsSync(path.join(`${file_path}/index.html`))) {
319
- res.setHeader("Content-Type", "text/html");
320
- send_content_file(req, res, path.join(`${file_path}/index.html`));
321
- return;
322
- } else if (fs.existsSync(path.join(`${file_path}.html`))) {
323
- res.setHeader("Content-Type", "text/html");
324
- send_content_file(req, res, path.join(`${file_path}.html`));
325
- return;
326
- } else if (fs.existsSync(path.join(`${file_path}.htm`))) {
327
- res.setHeader("Content-Type", "text/html");
328
- send_content_file(req, res, path.join(`${file_path}.htm`));
394
+ send_file(req, res, file_path);
329
395
  return;
330
396
  }
331
- } else if (fs.existsSync(file_path)) {
332
- if (
333
- file_path.endsWith("hdocbook.json") ||
334
- file_path.endsWith("hdocbook_project.json")
335
- ) {
336
- try {
337
- // Read & parse file
338
- JSON.parse(fs.readFileSync(file_path));
339
- } catch (e) {
340
- console.error(`Error parsing hdocbook.json: ${e}`);
341
- }
397
+
398
+ // Return a 404 error here
399
+ send_content_resource_404(req, res);
400
+ } catch (e) {
401
+ // A markdown render failure (bad include, plugin throw, etc.) lands
402
+ // here rather than as an unhandled promise rejection.
403
+ console.error(`Error handling content request ${req.url}:`, e);
404
+ if (!res.headersSent) {
405
+ res.status(500).send("Error rendering content");
342
406
  }
343
- send_file(req, res, file_path);
344
- return;
345
407
  }
346
-
347
- // Return a 404 error here
348
- send_content_resource_404(req, res);
349
408
  }
350
409
 
351
410
  function handle_library_request(req, res) {
411
+ const book_config = get_book_config();
352
412
  const library = {
353
413
  books: [
354
414
  {
355
- docId: hdocbook_config.docId,
356
- title: hdocbook_config.title,
415
+ docId: book_config.docId,
416
+ title: book_config.title,
357
417
  nav_inline: nav_inline,
358
418
  },
359
419
  ],
package/hdoc-create.js CHANGED
@@ -1,108 +1,110 @@
1
- (() => {
2
- const fs = require("node:fs");
3
- const path = require("node:path");
4
- const hdoc = require(path.join(__dirname, "hdoc-module.js"));
5
-
6
- let doc_id;
7
- let file_count = 0;
8
- let folder_count = 0;
9
-
10
- const processed_links = {};
11
-
12
- exports.run = async (source_path) => {
13
- console.log("Hornbill HDocBook Create", "\n");
14
- console.log("Path:", source_path, "\n");
15
-
16
- // Load the hdocbook-project.json file to get the docId
17
- // use the docId to get the book config
18
- const hdocbook_project_config_path = path.join(
19
- source_path,
20
- "hdocbook-project.json",
21
- );
22
- let hdocbook_project = {};
23
- try {
24
- hdocbook_project = require(hdocbook_project_config_path);
25
- } catch (e) {
26
- console.error(`File not found: ${hdocbook_project_config_path}\n`);
27
- console.error(
28
- "hdoc create needs to be run in the root of a HDoc Book.\n",
29
- );
30
- process.exit(1);
31
- }
32
- doc_id = hdocbook_project.docId;
33
-
34
- const book_path = path.join(source_path, doc_id);
35
- const hdocbook_path = path.join(book_path, "hdocbook.json");
36
-
37
- let hdocbook = {};
38
- try {
39
- hdocbook = require(hdocbook_path);
40
- } catch (e) {
41
- console.error(`File not found: ${hdocbook_path}\n`);
42
- console.error(
43
- "hdoc create needs to be run in the root of a HDoc Book.\n",
44
- );
45
- process.exit(1);
46
- }
47
-
48
- // Get paths from breadcrumb builder
49
- const nav_paths = hdoc.build_breadcrumbs(hdocbook.navigation.items);
50
- for (const key in nav_paths) {
51
- if (Object.hasOwn(nav_paths, key)) {
52
- for (const navkey in nav_paths[key]) {
53
- if (Object.hasOwn(nav_paths[key], navkey)) {
54
- for (let i = 0; i < nav_paths[key][navkey].length; i++) {
55
- if (
56
- nav_paths[key][navkey][i].link &&
57
- !processed_links[nav_paths[key][navkey][i].link]
58
- ) {
59
- nav_paths[key][navkey][i].path = path.join(
60
- source_path,
61
- nav_paths[key][navkey][i].link,
62
- );
63
- await add_doc(nav_paths[key][navkey][i]);
64
- }
65
- }
66
- }
67
- }
68
- }
69
- }
70
- console.log("\n-----------------------");
71
- console.log(" Docs Creation Summary");
72
- console.log("-----------------------\n");
73
- console.log(` Files Created: ${file_count}`);
74
- console.log(`Folders Created: ${folder_count}\n`);
75
- };
76
-
77
- const add_doc = async (doc_info) => {
78
- // Does folder exist? Create if not
79
- const folder = path.dirname(doc_info.path);
80
- if (!fs.existsSync(folder)) {
81
- try {
82
- fs.mkdirSync(folder, { recursive: true });
83
- console.log("Folder created:", folder);
84
- folder_count++;
85
- } catch (e) {
86
- console.error("\nError creating folder", folder, ":", e);
87
- return;
88
- }
89
- }
90
-
91
- // Does file exist? Create if not
92
- if (
93
- !fs.existsSync(`${doc_info.path}.md`) &&
94
- !fs.existsSync(`${doc_info.path}.htm`) &&
95
- !fs.existsSync(`${doc_info.path}.html`)
96
- ) {
97
- try {
98
- const file_path = `${doc_info.path}.md`;
99
- fs.writeFileSync(file_path, `# ${doc_info.text}\n`);
100
- console.log(" File created:", file_path);
101
- processed_links[doc_info.link] = true;
102
- file_count++;
103
- } catch (e) {
104
- console.error("\nError creating file", doc_info.path, ":", e);
105
- }
106
- }
107
- };
108
- })();
1
+ (() => {
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const hdoc = require(path.join(__dirname, "hdoc-module.js"));
5
+
6
+ let doc_id;
7
+ let file_count = 0;
8
+ let folder_count = 0;
9
+
10
+ const processed_links = {};
11
+
12
+ exports.run = async (source_path) => {
13
+ console.log("Hornbill HDocBook Create", "\n");
14
+ console.log("Path:", source_path, "\n");
15
+
16
+ // Load the hdocbook-project.json file to get the docId
17
+ // use the docId to get the book config
18
+ const hdocbook_project_config_path = path.join(
19
+ source_path,
20
+ "hdocbook-project.json",
21
+ );
22
+ let hdocbook_project = {};
23
+ try {
24
+ hdocbook_project = JSON.parse(
25
+ fs.readFileSync(hdocbook_project_config_path, "utf8"),
26
+ );
27
+ } catch (e) {
28
+ console.error(`File not found: ${hdocbook_project_config_path}\n`);
29
+ console.error(
30
+ "hdoc create needs to be run in the root of a HDoc Book.\n",
31
+ );
32
+ process.exit(1);
33
+ }
34
+ doc_id = hdocbook_project.docId;
35
+
36
+ const book_path = path.join(source_path, doc_id);
37
+ const hdocbook_path = path.join(book_path, "hdocbook.json");
38
+
39
+ let hdocbook = {};
40
+ try {
41
+ hdocbook = JSON.parse(fs.readFileSync(hdocbook_path, "utf8"));
42
+ } catch (e) {
43
+ console.error(`File not found: ${hdocbook_path}\n`);
44
+ console.error(
45
+ "hdoc create needs to be run in the root of a HDoc Book.\n",
46
+ );
47
+ process.exit(1);
48
+ }
49
+
50
+ // Get paths from breadcrumb builder
51
+ const nav_paths = hdoc.build_breadcrumbs(hdocbook.navigation.items);
52
+ for (const key in nav_paths) {
53
+ if (Object.hasOwn(nav_paths, key)) {
54
+ for (const navkey in nav_paths[key]) {
55
+ if (Object.hasOwn(nav_paths[key], navkey)) {
56
+ for (let i = 0; i < nav_paths[key][navkey].length; i++) {
57
+ if (
58
+ nav_paths[key][navkey][i].link &&
59
+ !processed_links[nav_paths[key][navkey][i].link]
60
+ ) {
61
+ nav_paths[key][navkey][i].path = path.join(
62
+ source_path,
63
+ nav_paths[key][navkey][i].link,
64
+ );
65
+ await add_doc(nav_paths[key][navkey][i]);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ console.log("\n-----------------------");
73
+ console.log(" Docs Creation Summary");
74
+ console.log("-----------------------\n");
75
+ console.log(` Files Created: ${file_count}`);
76
+ console.log(`Folders Created: ${folder_count}\n`);
77
+ };
78
+
79
+ const add_doc = async (doc_info) => {
80
+ // Does folder exist? Create if not
81
+ const folder = path.dirname(doc_info.path);
82
+ if (!fs.existsSync(folder)) {
83
+ try {
84
+ fs.mkdirSync(folder, { recursive: true });
85
+ console.log("Folder created:", folder);
86
+ folder_count++;
87
+ } catch (e) {
88
+ console.error("\nError creating folder", folder, ":", e);
89
+ return;
90
+ }
91
+ }
92
+
93
+ // Does file exist? Create if not
94
+ if (
95
+ !fs.existsSync(`${doc_info.path}.md`) &&
96
+ !fs.existsSync(`${doc_info.path}.htm`) &&
97
+ !fs.existsSync(`${doc_info.path}.html`)
98
+ ) {
99
+ try {
100
+ const file_path = `${doc_info.path}.md`;
101
+ fs.writeFileSync(file_path, `# ${doc_info.text}\n`);
102
+ console.log(" File created:", file_path);
103
+ processed_links[doc_info.link] = true;
104
+ file_count++;
105
+ } catch (e) {
106
+ console.error("\nError creating file", doc_info.path, ":", e);
107
+ }
108
+ }
109
+ };
110
+ })();