hdoc-tools 0.60.1 → 0.61.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.
@@ -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
@@ -21,7 +21,9 @@
21
21
  );
22
22
  let hdocbook_project = {};
23
23
  try {
24
- hdocbook_project = require(hdocbook_project_config_path);
24
+ hdocbook_project = JSON.parse(
25
+ fs.readFileSync(hdocbook_project_config_path, "utf8"),
26
+ );
25
27
  } catch (e) {
26
28
  console.error(`File not found: ${hdocbook_project_config_path}\n`);
27
29
  console.error(
@@ -36,7 +38,7 @@
36
38
 
37
39
  let hdocbook = {};
38
40
  try {
39
- hdocbook = require(hdocbook_path);
41
+ hdocbook = JSON.parse(fs.readFileSync(hdocbook_path, "utf8"));
40
42
  } catch (e) {
41
43
  console.error(`File not found: ${hdocbook_path}\n`);
42
44
  console.error(
package/hdoc-init.js CHANGED
@@ -4,6 +4,15 @@
4
4
  const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
 
7
+ // Valid values come from the same schema the build validator harvests, so
8
+ // init can never scaffold a book that validate would reject.
9
+ const hdocbook_schema = require(
10
+ path.join(__dirname, "schemas", "hdocbook.schema.json"),
11
+ );
12
+ const valid_product_families =
13
+ hdocbook_schema.properties.productFamily.enum;
14
+ const valid_audience = hdocbook_schema.properties.audience.items.enum;
15
+
7
16
  const promptProps = [
8
17
  {
9
18
  name: "id",
@@ -36,6 +45,29 @@
36
45
  description: "Package Author",
37
46
  required: true,
38
47
  },
48
+ {
49
+ name: "productFamily",
50
+ description: `Product Family [${valid_product_families.join(", ")}]`,
51
+ default: "hdocs",
52
+ options: valid_product_families,
53
+ required: true,
54
+ },
55
+ {
56
+ name: "audience",
57
+ description: `Audience [${valid_audience.join(", ")}]`,
58
+ default: "public",
59
+ options: valid_audience,
60
+ required: true,
61
+ },
62
+ {
63
+ name: "bookType",
64
+ description:
65
+ "Book Type (0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref)",
66
+ default: "0",
67
+ validator: /^[0-4]$/,
68
+ warning: "Book Type must be a number between 0 and 4.",
69
+ required: true,
70
+ },
39
71
  ];
40
72
 
41
73
  // Asks a single question, re-prompting if the field is required and empty or if
@@ -55,6 +87,11 @@
55
87
  ask();
56
88
  return;
57
89
  }
90
+ if (value && field.options && !field.options.includes(value)) {
91
+ console.error(`Value must be one of: ${field.options.join(", ")}`);
92
+ ask();
93
+ return;
94
+ }
58
95
  resolve(value);
59
96
  });
60
97
  };
@@ -62,11 +99,38 @@
62
99
  });
63
100
 
64
101
  const createBook = (server_path, source_path, docProps) => {
102
+ // Refuse to scaffold over an existing book — re-running init in a
103
+ // populated folder used to silently overwrite template-named files.
104
+ const conflict_candidates = [
105
+ "hdocbook-project.json",
106
+ "package.json",
107
+ "gitignore",
108
+ ".gitignore",
109
+ "_hdocbook",
110
+ docProps.id,
111
+ ];
112
+ const conflicts = conflict_candidates.filter((f) =>
113
+ fs.existsSync(path.join(source_path, f)),
114
+ );
115
+ if (conflicts.length > 0) {
116
+ console.error(
117
+ "\r\nThe target folder already contains files that init would overwrite:\r\n",
118
+ );
119
+ for (const f of conflicts) console.error(` ${f}`);
120
+ console.error(
121
+ "\r\nRun hdoc init in an empty folder, or remove these first.\r\n",
122
+ );
123
+ process.exit(1);
124
+ }
125
+
65
126
  console.log("\r\nCreating book with the following properties:\r\n");
66
127
  console.log(" Doc ID:", docProps.id);
67
128
  console.log(" Title:", docProps.title);
68
129
  console.log(" Description:", docProps.description);
69
130
  console.log(" Author:", docProps.author);
131
+ console.log(" Product Family:", docProps.productFamily);
132
+ console.log(" Audience:", docProps.audience);
133
+ console.log(" Book Type:", docProps.bookType);
70
134
  console.log(" Initial Version:", docProps.version, "\r\n");
71
135
 
72
136
  // Now copy files over
@@ -92,79 +156,50 @@
92
156
  process.exit(1);
93
157
  }
94
158
 
159
+ // Synchronous read/modify/write of the three scaffolded JSON files —
160
+ // exits non-zero on failure instead of racing the process exit.
161
+ const update_json = (file_path, mutate) => {
162
+ try {
163
+ const obj = JSON.parse(fs.readFileSync(file_path, "utf8"));
164
+ mutate(obj);
165
+ fs.writeFileSync(file_path, JSON.stringify(obj, null, 2));
166
+ console.log("Updated:", file_path);
167
+ } catch (err) {
168
+ console.error("Error updating:", file_path, "\r\n", err);
169
+ process.exit(1);
170
+ }
171
+ };
172
+
95
173
  // Update hdocbook-project.json
96
- const hdocBookProjectFilePath = path.join(
97
- source_path,
98
- "hdocbook-project.json",
99
- );
100
- const hdocBookProjectFile = require(hdocBookProjectFilePath);
101
- hdocBookProjectFile.docId = docProps.id;
102
- fs.writeFile(
103
- hdocBookProjectFilePath,
104
- JSON.stringify(hdocBookProjectFile, null, 2),
105
- function writeJSON(err) {
106
- if (err)
107
- return console.error(
108
- "Error updating:",
109
- hdocBookProjectFilePath,
110
- "\r\n",
111
- err,
112
- );
113
- console.log("Updated:", hdocBookProjectFilePath);
114
- },
115
- );
174
+ update_json(path.join(source_path, "hdocbook-project.json"), (obj) => {
175
+ obj.docId = docProps.id;
176
+ });
116
177
 
117
178
  // Update root/hdocbook.json
118
- const hdocBookFilePath = path.join(bookContentRoot, "hdocbook.json");
119
- const hdocbookFile = require(hdocBookFilePath);
120
- hdocbookFile.docId = docProps.id;
121
- hdocbookFile.title = docProps.title;
122
- hdocbookFile.description = docProps.description;
123
- hdocbookFile.version = docProps.version;
124
- hdocbookFile.publicSource = `https://github.com/Hornbill-Docs/${docProps.id}`;
125
- hdocbookFile.navigation.items[0].items = [
126
- {
127
- text: "Welcome",
128
- link: `${docProps.id}/index`,
129
- },
130
- ];
131
- fs.writeFile(
132
- hdocBookFilePath,
133
- JSON.stringify(hdocbookFile, null, 2),
134
- function writeJSON(err) {
135
- if (err)
136
- return console.error(
137
- "Error updating:",
138
- hdocBookFilePath,
139
- "\r\n",
140
- err,
141
- );
142
- console.log("Updated:", hdocBookFilePath);
143
- },
144
- );
179
+ update_json(path.join(bookContentRoot, "hdocbook.json"), (obj) => {
180
+ obj.docId = docProps.id;
181
+ obj.title = docProps.title;
182
+ obj.description = docProps.description;
183
+ obj.version = docProps.version;
184
+ obj.publicSource = `https://github.com/Hornbill-Docs/${docProps.id}`;
185
+ obj.productFamily = docProps.productFamily;
186
+ obj.audience = [docProps.audience];
187
+ obj.bookType = Number(docProps.bookType);
188
+ obj.navigation.items[0].items = [
189
+ {
190
+ text: "Welcome",
191
+ link: `${docProps.id}/index`,
192
+ },
193
+ ];
194
+ });
145
195
 
146
196
  // Update package.json
147
- const packageFilePath = path.join(source_path, "package.json");
148
- const packageFile = require(packageFilePath);
149
- packageFile.name = docProps.id;
150
- packageFile.version = docProps.version;
151
- hdocbookFile.description = docProps.description;
152
- hdocbookFile.version = docProps.version;
153
- hdocbookFile.author = docProps.author;
154
- fs.writeFile(
155
- packageFilePath,
156
- JSON.stringify(packageFile, null, 2),
157
- function writeJSON(err) {
158
- if (err)
159
- return console.error(
160
- "Error updating:",
161
- packageFilePath,
162
- "\r\n",
163
- err,
164
- );
165
- console.log("Updated:", packageFilePath);
166
- },
167
- );
197
+ update_json(path.join(source_path, "package.json"), (obj) => {
198
+ obj.name = docProps.id;
199
+ obj.version = docProps.version;
200
+ obj.description = docProps.description;
201
+ obj.author = docProps.author;
202
+ });
168
203
 
169
204
  // Rename gitignore to .gitignore
170
205
  const gitignorePath = path.join(source_path, "gitignore");
package/hdoc-module.js CHANGED
@@ -290,6 +290,10 @@
290
290
  return false;
291
291
  };
292
292
 
293
+ // Exported for inter-book anchor validation — the anchor id derivation MUST
294
+ // stay identical between build-time wrapping and cross-book link checks.
295
+ exports.makeAnchorIdFriendly = (str) => makeAnchorIdFriendly(str);
296
+
293
297
  const makeAnchorIdFriendly = (str) => {
294
298
  return `hb-doc-anchor-${str // Add prefix
295
299
  .toLowerCase() // Convert to lowercase
@@ -1061,19 +1065,38 @@
1061
1065
  return out.trimEnd();
1062
1066
  };
1063
1067
 
1064
- exports.find_string_in_string = (fileContent, searchString) => {
1068
+ // Locates searchString in fileContent, returning 1-based {line, column}.
1069
+ // With whole_link=true, occurrences that are substrings of a longer
1070
+ // URL/path are skipped (e.g. searching for /book/page#anchor must not match
1071
+ // inside /book/page#anchor-more on an earlier line) — the match must not be
1072
+ // immediately preceded or followed by a link-continuation character.
1073
+ exports.find_string_in_string = (fileContent, searchString, whole_link = false) => {
1065
1074
  const lines = fileContent.split('\n');
1066
-
1075
+ const link_char = /[a-zA-Z0-9\-_/#?&=.%~]/;
1076
+ let loose_match = null; // first substring hit — fallback when no exact-boundary match exists (e.g. bare URL followed by punctuation)
1077
+
1067
1078
  for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
1068
- const columnNumber = lines[lineNumber].indexOf(searchString);
1069
-
1070
- if (columnNumber !== -1) {
1071
- // Return 1-based line and column numbers
1072
- return { line: lineNumber + 1, column: columnNumber + 1 };
1073
- }
1079
+ let from = 0;
1080
+ while (true) {
1081
+ const columnNumber = lines[lineNumber].indexOf(searchString, from);
1082
+ if (columnNumber === -1) break;
1083
+ if (whole_link) {
1084
+ const prev = lines[lineNumber][columnNumber - 1];
1085
+ const next = lines[lineNumber][columnNumber + searchString.length];
1086
+ if ((prev !== undefined && link_char.test(prev)) ||
1087
+ (next !== undefined && link_char.test(next))) {
1088
+ if (loose_match === null)
1089
+ loose_match = { line: lineNumber + 1, column: columnNumber + 1 };
1090
+ from = columnNumber + 1;
1091
+ continue;
1092
+ }
1093
+ }
1094
+ // Return 1-based line and column numbers
1095
+ return { line: lineNumber + 1, column: columnNumber + 1 };
1096
+ }
1074
1097
  }
1075
-
1076
- // If not found, return null
1077
- return null;
1078
- }
1098
+
1099
+ // No exact-boundary match — fall back to the first substring hit, or null
1100
+ return loose_match;
1101
+ }
1079
1102
  })();
package/hdoc-serve.js CHANGED
@@ -4,9 +4,8 @@
4
4
  const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const hdoc = require(path.join(__dirname, "hdoc-module.js"));
7
- const { create_content_handler, build_nav_inline } = require(
8
- path.join(__dirname, "hdoc-content-routes.js"),
9
- );
7
+ const { create_content_handler, build_nav_inline, resolve_within_root } =
8
+ require(path.join(__dirname, "hdoc-content-routes.js"));
10
9
 
11
10
  let port = 3000;
12
11
  let docId;
@@ -47,9 +46,12 @@
47
46
  "hdocbook-project.json",
48
47
  );
49
48
 
50
- // Load the hdocbook config file
49
+ // Load the hdocbook config file (readFileSync, not require — no module
50
+ // cache, and the content routes re-read it per request anyway)
51
51
  try {
52
- hdocbook_project = require(hdocbook_project_config_path);
52
+ hdocbook_project = JSON.parse(
53
+ fs.readFileSync(hdocbook_project_config_path, "utf8"),
54
+ );
53
55
  } catch (e) {
54
56
  console.error(`\nFailed to load hdocbook-project.json:\n${e}\n`);
55
57
  process.exit(1);
@@ -72,7 +74,7 @@
72
74
 
73
75
  // Pull in the book config file
74
76
  try {
75
- hdocbook_config = require(hdocbook_path);
77
+ hdocbook_config = JSON.parse(fs.readFileSync(hdocbook_path, "utf8"));
76
78
  } catch (e) {
77
79
  console.error(`\nFailed to load hdocbook.json:${e}\n`);
78
80
  process.exit(1);
@@ -93,7 +95,11 @@
93
95
  // Catch all
94
96
  app.get("/{*splat}", (req, res) => {
95
97
 
96
- const ui_file_path = path.join(ui_path, req.url);
98
+ const ui_file_path = resolve_within_root(ui_path, req.url);
99
+ if (ui_file_path === null) {
100
+ content.send_content_resource_404(req, res);
101
+ return;
102
+ }
97
103
 
98
104
  // To support the SPA application behavior, if there is no file extension present, then
99
105
  // we simply return the /index.html file content to the client
package/hdoc-stats.js CHANGED
@@ -5,9 +5,10 @@
5
5
  const hdoc = require(path.join(__dirname, "hdoc-module.js"));
6
6
 
7
7
 
8
- // Regex to remove Hornbill-specific tags
8
+ // Regex to remove Hornbill admonition markers (::: note etc — see
9
+ // custom_modules/tips.js) so the marker keywords don't inflate word counts.
9
10
  const hbMDTagRegex =
10
- /(:{3}[ ]note)|(:{3}[ ]tip)|(:{3}[ ]important)|(:{3}[ ]caution)|(:{3}[ ]warning)|(:{3})/g;
11
+ /(:{3}[ ]note)|(:{3}[ ]tip)|(:{3}[ ]important)|(:{3}[ ]info)|(:{3}[ ]caution)|(:{3}[ ]warning)|(:{3})/g;
11
12
 
12
13
  const stats = {
13
14
  totalMDFiles: 0,
@@ -42,15 +43,12 @@
42
43
  }
43
44
  };
44
45
 
45
- const dreeOptions = {
46
- descendants: true,
46
+ // Options for hdoc.scan_dir (see hdoc-module.js for the supported keys)
47
+ const scanOptions = {
47
48
  depth: 10,
48
49
  extensions: ["md", "html", "htm"],
49
- hash: false,
50
50
  normalize: true,
51
- size: true,
52
51
  sizeInBytes: true,
53
- stat: false,
54
52
  symbolicLinks: false,
55
53
  };
56
54
 
@@ -105,12 +103,14 @@
105
103
  });
106
104
 
107
105
  // Scan content path directory, send file info to callback for processing
108
- hdoc.scan_dir(bookPath, dreeOptions, fileCallback);
106
+ hdoc.scan_dir(bookPath, scanOptions, fileCallback);
109
107
  for (const element of markdownFiles) {
110
108
  // Load markdown file
111
109
  const md_txt = fs.readFileSync(element.path, "utf8");
112
110
 
113
- const html_txt = md.render(md_txt.toString());
111
+ const html_txt = md.render(
112
+ md_txt.toString().replace(hbMDTagRegex, ""),
113
+ );
114
114
  const text = hdoc.html_to_text(html_txt);
115
115
 
116
116
  // Do the wordcount and add to status