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.
- package/editor/dist/assets/index-Blewr90z.css +1 -1
- package/editor/dist/assets/index-BtxvGZHW.js +111 -111
- package/editor/dist/assets/spell.worker-sryEKmOj.js +13 -13
- package/editor/dist/index.html +14 -14
- package/hdoc-build.js +14 -6
- package/hdoc-bump.js +125 -123
- package/hdoc-content-routes.js +143 -83
- package/hdoc-create.js +4 -2
- package/hdoc-init.js +103 -68
- package/hdoc-module.js +35 -12
- package/hdoc-serve.js +13 -7
- package/hdoc-stats.js +9 -9
- package/hdoc-validate-config.js +26 -0
- package/hdoc-validate-interbook.js +321 -0
- package/hdoc-validate.js +79 -6
- package/hdoc-ver.js +45 -43
- package/hdoc.js +12 -11
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/schemas/hdocbook-project.schema.json +20 -0
- package/schemas/hdocbook.schema.json +6 -2
- package/ui/js/doc.hornbill.js +31 -44
- package/ui/js/mermaid-theme.json +27 -0
- package/hdoc-build-onyx.js +0 -134
- package/templates/mermaid-theme.yaml +0 -28
- package/templates/pdf/fonts/inter-cyrillic copy.woff2 +0 -0
package/hdoc-content-routes.js
CHANGED
|
@@ -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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
384
|
+
file_path.endsWith("hdocbook.json") ||
|
|
385
|
+
file_path.endsWith("hdocbook_project.json")
|
|
315
386
|
) {
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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:
|
|
356
|
-
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 =
|
|
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 =
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
//
|
|
1077
|
-
return
|
|
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 } =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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,
|
|
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(
|
|
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
|