hdoc-tools 0.62.4 → 0.63.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-BtxvGZHW.js → index-Dknn5g5A.js} +5 -4
- package/editor/dist/index.html +13 -13
- package/hdoc-build.js +5 -3
- package/hdoc-content-routes.js +136 -4
- package/hdoc-serve.js +26 -3
- package/hdoc-validate-interbook.js +8 -0
- package/hdoc-validate.js +92 -35
- package/package.json +1 -1
- package/ui/css/theme-default/styles/base.css +86 -21
- package/ui/css/theme-default/styles/components/api-doc.css +6 -3
- package/ui/css/theme-default/styles/components/content.css +140 -39
- package/ui/css/theme-default/styles/components/custom-block.css +1 -1
- package/ui/css/theme-default/styles/components/htl-doc.css +243 -64
- package/ui/css/theme-default/styles/components/htl-library.css +152 -0
- package/ui/css/theme-default/styles/components/htl-search.css +267 -0
- package/ui/css/theme-default/styles/components/sidebar.css +59 -16
- package/ui/css/theme-default/styles/fonts.css +17 -3
- package/ui/css/theme-default/styles/htldoc.layouts.css +756 -87
- package/ui/css/theme-default/styles/vars.css +40 -35
- package/ui/favicon.svg +64 -0
- package/ui/images/hornbill-logo-full-reversed.svg +92 -0
- package/ui/images/hornbill-logo-full.svg +92 -0
- package/ui/images/hug_library.jpg +0 -0
- package/ui/images/mcp-catalog.svg +9 -0
- package/ui/images/products/hornbill-square.svg +1 -0
- package/ui/index.html +1106 -342
- package/ui/js/bootstrap.js +89 -0
- package/ui/js/doc.hornbill.js +3156 -751
- package/ui/js/hb.vue.js +121 -0
- package/ui/js/highlightjs/highlight.pack.js +1513 -2
- package/ui/js/highlightjs/styles/vs2015-accessible.css +141 -0
- package/ui/js/highlightjs-badge.js +41 -58
- package/ui/js/mermaid.min.js +1447 -1295
- package/ui/js/webcomponents/hdocApprove.js +115 -0
- package/ui/js/highlightjs/styles/brown-paper.css +0 -64
- package/ui/js/highlightjs/styles/brown-papersq.png +0 -0
- package/ui/js/highlightjs/styles/codepen-embed.css +0 -60
- package/ui/js/highlightjs/styles/color-brewer.css +0 -71
- package/ui/js/highlightjs/styles/darcula.css +0 -77
- package/ui/js/highlightjs/styles/dark.css +0 -63
- package/ui/js/highlightjs/styles/darkula.css +0 -6
- package/ui/js/highlightjs/styles/default.css +0 -99
- package/ui/js/highlightjs/styles/dracula.css +0 -76
- package/ui/js/highlightjs/styles/far.css +0 -71
- package/ui/js/highlightjs/styles/foundation.css +0 -88
- package/ui/js/highlightjs/styles/github-gist.css +0 -71
- package/ui/js/highlightjs/styles/github-mm.css +0 -71
- package/ui/js/highlightjs/styles/github.css +0 -99
- package/ui/js/highlightjs/styles/googlecode.css +0 -89
- package/ui/js/highlightjs/styles/grayscale.css +0 -101
- package/ui/js/highlightjs/styles/idea.css +0 -97
- package/ui/js/highlightjs/styles/ir-black.css +0 -73
- package/ui/js/highlightjs/styles/kavadocs.css +0 -71
- package/ui/js/highlightjs/styles/kavadocsdark.css +0 -120
- package/ui/js/highlightjs/styles/kimbie.dark.css +0 -74
- package/ui/js/highlightjs/styles/kimbie.light.css +0 -74
- package/ui/js/highlightjs/styles/magula.css +0 -70
- package/ui/js/highlightjs/styles/mono-blue.css +0 -59
- package/ui/js/highlightjs/styles/monokai-sublime.css +0 -83
- package/ui/js/highlightjs/styles/monokai.css +0 -70
- package/ui/js/highlightjs/styles/obsidian.css +0 -88
- package/ui/js/highlightjs/styles/paraiso-dark.css +0 -72
- package/ui/js/highlightjs/styles/paraiso-light.css +0 -72
- package/ui/js/highlightjs/styles/railscasts.css +0 -106
- package/ui/js/highlightjs/styles/rainbow.css +0 -85
- package/ui/js/highlightjs/styles/solarized-dark.css +0 -84
- package/ui/js/highlightjs/styles/solarized-light.css +0 -84
- package/ui/js/highlightjs/styles/sunburst.css +0 -102
- package/ui/js/highlightjs/styles/twilight.css +0 -97
- package/ui/js/highlightjs/styles/vs.css +0 -68
- package/ui/js/highlightjs/styles/vs2015.css +0 -117
- package/ui/js/highlightjs/styles/xcode.css +0 -104
- package/ui/js/highlightjs/styles/zenburn.css +0 -80
|
@@ -63,8 +63,8 @@ Error generating stack: `+e.message+`
|
|
|
63
63
|
}
|
|
64
64
|
} catch (e) {}
|
|
65
65
|
<\/script>
|
|
66
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.
|
|
67
|
-
<link
|
|
66
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" integrity="sha384-Bk5cbLkZQ5raZ0+H2/+VbfYx3WpvxvQK4zqXZr7sYODuaX7bKXoSOnipQxkaS8sv" crossorigin="anonymous">
|
|
67
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
|
68
68
|
<link rel="stylesheet" href="/_ui/css/theme-default/styles/fonts.css">
|
|
69
69
|
<link rel="stylesheet" href="/_ui/css/theme-default/styles/vars.css">
|
|
70
70
|
<link rel="stylesheet" href="/_ui/css/theme-default/styles/base.css">
|
|
@@ -74,7 +74,7 @@ Error generating stack: `+e.message+`
|
|
|
74
74
|
<link rel="stylesheet" href="/_ui/css/theme-default/styles/components/content.css">
|
|
75
75
|
<link rel="stylesheet" href="/_ui/css/theme-default/styles/components/custom-block.css">
|
|
76
76
|
<link rel="stylesheet" href="/_ui/css/theme-default/styles/components/api-doc.css">
|
|
77
|
-
<link rel="stylesheet" href="/_ui/js/highlightjs/styles/vs2015.css">
|
|
77
|
+
<link rel="stylesheet" href="/_ui/js/highlightjs/styles/vs2015-accessible.css">
|
|
78
78
|
<style>html,body{margin:0;}</style>
|
|
79
79
|
</head>
|
|
80
80
|
<body>
|
|
@@ -98,7 +98,8 @@ Error generating stack: `+e.message+`
|
|
|
98
98
|
try {
|
|
99
99
|
if (window.hljs) {
|
|
100
100
|
var blocks = root.querySelectorAll('pre>code');
|
|
101
|
-
|
|
101
|
+
// highlight.js v11 API (highlightBlock was removed in v11)
|
|
102
|
+
for (var i = 0; i < blocks.length; i++) window.hljs.highlightElement(blocks[i]);
|
|
102
103
|
}
|
|
103
104
|
if (window.highlightJsBadge) {
|
|
104
105
|
window.highlightJsBadge({ contentSelector: '#hdoc-preview-root' });
|
package/editor/dist/index.html
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>hdoc edit</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>hdoc edit</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Dknn5g5A.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-Blewr90z.css">
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
<div id="root"></div>
|
|
13
|
-
</body>
|
|
14
|
-
</html>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/hdoc-build.js
CHANGED
|
@@ -48,9 +48,11 @@
|
|
|
48
48
|
"pdf-header-non-git.html",
|
|
49
49
|
);
|
|
50
50
|
|
|
51
|
-
// Mermaid theme
|
|
52
|
-
//
|
|
53
|
-
//
|
|
51
|
+
// Mermaid theme for the PDF SVG bake. Emitted into each queued diagram as
|
|
52
|
+
// YAML frontmatter; inline JSON is valid YAML, so no conversion. NOTE: the
|
|
53
|
+
// viewer no longer fetches this file — since the ui/ sync with the live
|
|
54
|
+
// htdocs viewer it embeds the same values as HB_MERMAID_CONFIG in
|
|
55
|
+
// ui/js/doc.hornbill.js. Keep the two in step when changing the theme.
|
|
54
56
|
const mermaid_theme_path = path.resolve(
|
|
55
57
|
__dirname,
|
|
56
58
|
"ui",
|
package/hdoc-content-routes.js
CHANGED
|
@@ -21,6 +21,14 @@ const stream = require("node:stream");
|
|
|
21
21
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
22
22
|
const mdfm = require("markdown-it-front-matter");
|
|
23
23
|
|
|
24
|
+
// Document header template baked around rendered markdown, so the local
|
|
25
|
+
// preview shows the same header (breadcrumbs/title/type/reading time) as
|
|
26
|
+
// published output. Package-static, so read once.
|
|
27
|
+
const doc_header_template = fs.readFileSync(
|
|
28
|
+
path.join(__dirname, "templates", "doc-header-non-git.html"),
|
|
29
|
+
"utf8",
|
|
30
|
+
);
|
|
31
|
+
|
|
24
32
|
// Escape a Mermaid definition for safe embedding as the text content of a
|
|
25
33
|
// <pre> element. The viewer's client-side Mermaid plugin reads the element's
|
|
26
34
|
// textContent and renders it in the browser, so we only need HTML-text safety
|
|
@@ -192,18 +200,81 @@ exports.create_content_handler = (ctx) => {
|
|
|
192
200
|
const tips = require(`${__dirname}/custom_modules/tips.js`);
|
|
193
201
|
md.use(tips, { links: true });
|
|
194
202
|
|
|
195
|
-
|
|
203
|
+
// Wrap h2/h3 sections in anchor-id <div>s exactly as the build pipeline
|
|
204
|
+
// does (hdoc.wrapAndExtract) — the theme CSS targets that structure, and
|
|
205
|
+
// it also yields the reading time for the document header.
|
|
206
|
+
const rendered = md.render(md_txt.toString());
|
|
207
|
+
const extracted = hdoc.wrapAndExtract(rendered, ["h1"]);
|
|
208
|
+
const html = extracted.html;
|
|
196
209
|
|
|
197
210
|
const frontmatter = frontmatter_content.length
|
|
198
211
|
? hdoc.parse_yaml(frontmatter_content)
|
|
199
212
|
: null;
|
|
200
|
-
return { html, frontmatter };
|
|
213
|
+
return { html, frontmatter, read_time: extracted.readTimeMins };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Wrap rendered markdown with the same document header the build pipeline
|
|
217
|
+
// bakes into published pages (templates/doc-header-non-git.html): breadcrumb
|
|
218
|
+
// trail, first-<h1> title, doc type and reading time. Without this the viewer
|
|
219
|
+
// shows the raw body with the <h1> inline, which is not what publishing
|
|
220
|
+
// produces. logical_path is the extensionless book-relative request path
|
|
221
|
+
// (e.g. "my-book/some/page") used for the breadcrumb lookup.
|
|
222
|
+
function wrap_with_document_header(html, frontmatter, logical_path, read_time) {
|
|
223
|
+
let body = html;
|
|
224
|
+
|
|
225
|
+
// Pull the first <h1> out of the body — the header owns the title, exactly
|
|
226
|
+
// as hdoc-build does for published output.
|
|
227
|
+
let title = "";
|
|
228
|
+
const h1_match = body.match(/<h1[^>]*>([\s\S]*?)<\/h1>/);
|
|
229
|
+
if (h1_match) {
|
|
230
|
+
title = h1_match[1].trim();
|
|
231
|
+
body = body.replace(h1_match[0], "");
|
|
232
|
+
} else if (frontmatter?.title) {
|
|
233
|
+
title = frontmatter.title;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const reading_time = frontmatter?.["reading-time"] ?? read_time ?? 0;
|
|
237
|
+
|
|
238
|
+
// Breadcrumbs from the live nav (config re-read per request), emitted with
|
|
239
|
+
// the same <li> shape hdoc-build writes. Last crumb (the page itself) is
|
|
240
|
+
// dropped, matching the build pipeline.
|
|
241
|
+
let bc_tags = "\n";
|
|
242
|
+
try {
|
|
243
|
+
const bc = hdoc.build_breadcrumbs(
|
|
244
|
+
get_book_config().navigation.items,
|
|
245
|
+
).bc;
|
|
246
|
+
const crumbs = bc[logical_path] || bc[`/${logical_path}`];
|
|
247
|
+
if (crumbs) {
|
|
248
|
+
for (let i = 0; i < crumbs.length - 1; i++) {
|
|
249
|
+
if (crumbs[i].link) {
|
|
250
|
+
const bc_link = crumbs[i].link.startsWith("/")
|
|
251
|
+
? crumbs[i].link
|
|
252
|
+
: `/${crumbs[i].link}`;
|
|
253
|
+
bc_tags += `\t\t\t\t<li class="mt-0 nav-bar-item"><a href="${bc_link}" class="ps-0 pe-0 text-decoration-none">${crumbs[i].text}</a></li>\n`;
|
|
254
|
+
} else {
|
|
255
|
+
bc_tags += `\t\t\t\t<li class="mt-0 nav-bar-item">${crumbs[i].text}</li>\n`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// Bad/missing navigation is validate's problem to report, not the
|
|
261
|
+
// preview's — render the page with an empty trail.
|
|
262
|
+
}
|
|
263
|
+
bc_tags += "\t\t\t";
|
|
264
|
+
|
|
265
|
+
const header = doc_header_template
|
|
266
|
+
.replaceAll("{{title}}", title)
|
|
267
|
+
.replaceAll("{{doc-type}}", frontmatter?.type || "Article")
|
|
268
|
+
.replaceAll("{{reading-time}}", String(reading_time))
|
|
269
|
+
.replaceAll("{{breadcrumbs}}", bc_tags);
|
|
270
|
+
|
|
271
|
+
return `${header}\n${body}`;
|
|
201
272
|
}
|
|
202
273
|
|
|
203
274
|
async function transform_markdown_and_send_html(req, res, file_path) {
|
|
204
275
|
if (!fs.existsSync(file_path)) return false;
|
|
205
276
|
|
|
206
|
-
const { html, frontmatter } = await render_markdown(
|
|
277
|
+
const { html, frontmatter, read_time } = await render_markdown(
|
|
207
278
|
file_path,
|
|
208
279
|
fs.readFileSync(file_path).toString(),
|
|
209
280
|
);
|
|
@@ -215,8 +286,20 @@ exports.create_content_handler = (ctx) => {
|
|
|
215
286
|
res.setHeader("X-frontmatter", base64);
|
|
216
287
|
}
|
|
217
288
|
|
|
289
|
+
// Inline (_inline/) fragments are embedded elsewhere and get no header,
|
|
290
|
+
// same as the build pipeline.
|
|
291
|
+
const logical_path = req.url
|
|
292
|
+
.replace(/^\/_books\//, "")
|
|
293
|
+
.split("?")[0]
|
|
294
|
+
.split("#")[0]
|
|
295
|
+
.replace(/\.(html|htm|md)$/i, "")
|
|
296
|
+
.replace(/\/+$/, "");
|
|
297
|
+
const out = logical_path.includes("/_inline/")
|
|
298
|
+
? html
|
|
299
|
+
: wrap_with_document_header(html, frontmatter, logical_path, read_time);
|
|
300
|
+
|
|
218
301
|
res.setHeader("Content-Type", "text/html");
|
|
219
|
-
res.send(
|
|
302
|
+
res.send(out);
|
|
220
303
|
return true;
|
|
221
304
|
}
|
|
222
305
|
|
|
@@ -409,11 +492,22 @@ exports.create_content_handler = (ctx) => {
|
|
|
409
492
|
|
|
410
493
|
function handle_library_request(req, res) {
|
|
411
494
|
const book_config = get_book_config();
|
|
495
|
+
// Mirror the fields the production library.json carries so the viewer's
|
|
496
|
+
// library home can group/describe the book. audience is an array in
|
|
497
|
+
// hdocbook.json but a plain string in the production library feed.
|
|
412
498
|
const library = {
|
|
413
499
|
books: [
|
|
414
500
|
{
|
|
415
501
|
docId: book_config.docId,
|
|
502
|
+
version: book_config.version,
|
|
503
|
+
audience: Array.isArray(book_config.audience)
|
|
504
|
+
? book_config.audience[0]
|
|
505
|
+
: book_config.audience,
|
|
416
506
|
title: book_config.title,
|
|
507
|
+
description: book_config.description || "",
|
|
508
|
+
productFamilyId: book_config.productFamily,
|
|
509
|
+
tags: book_config.tags || [],
|
|
510
|
+
publicSource: book_config.publicSource,
|
|
417
511
|
nav_inline: nav_inline,
|
|
418
512
|
},
|
|
419
513
|
],
|
|
@@ -422,9 +516,47 @@ exports.create_content_handler = (ctx) => {
|
|
|
422
516
|
res.send(JSON.stringify(library, null, 3));
|
|
423
517
|
}
|
|
424
518
|
|
|
519
|
+
// The production viewer needs a valid products.json (its init dereferences
|
|
520
|
+
// data.products). Fetch the real product family list once in the background
|
|
521
|
+
// (same helper the build pipeline uses) and serve it; if docs.hornbill.com
|
|
522
|
+
// is unreachable, serve an empty list — the library page then groups the
|
|
523
|
+
// book under "Other" instead of failing to initialise.
|
|
524
|
+
let products_cache = null;
|
|
525
|
+
const products_fetch = hdoc
|
|
526
|
+
.load_product_families()
|
|
527
|
+
.then((p) => {
|
|
528
|
+
if (p.success) products_cache = p.prod_families;
|
|
529
|
+
else
|
|
530
|
+
console.log(
|
|
531
|
+
"[WARNING] Could not fetch product families from docs.hornbill.com — serving an empty product list to the viewer",
|
|
532
|
+
);
|
|
533
|
+
})
|
|
534
|
+
.catch(() => {});
|
|
535
|
+
|
|
536
|
+
async function handle_products_request(req, res) {
|
|
537
|
+
await products_fetch;
|
|
538
|
+
res.setHeader("Content-Type", "application/json");
|
|
539
|
+
res.send(JSON.stringify(products_cache || { products: [] }));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Endpoints the production viewer probes but that have no local backing
|
|
543
|
+
// service (docs sign-in session, MCP catalog, full-text search). A JSON 404
|
|
544
|
+
// makes the viewer's fetchJsonFile resolve null and degrade gracefully;
|
|
545
|
+
// without these the SPA catch-all would answer with index.html instead.
|
|
546
|
+
function handle_unavailable_api(req, res) {
|
|
547
|
+
res.status(404);
|
|
548
|
+
res.setHeader("Content-Type", "application/json");
|
|
549
|
+
res.send(
|
|
550
|
+
JSON.stringify({ error: "Not available in the hdoc local preview" }),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
425
554
|
function register(app) {
|
|
555
|
+
app.get("/_books/products.json", handle_products_request);
|
|
426
556
|
app.get("/_books/library.json", handle_library_request);
|
|
427
557
|
app.get("/_books/*splat", handle_books_request);
|
|
558
|
+
app.get("/_api/{*splat}", handle_unavailable_api);
|
|
559
|
+
app.get("/_search", handle_unavailable_api);
|
|
428
560
|
}
|
|
429
561
|
|
|
430
562
|
return {
|
package/hdoc-serve.js
CHANGED
|
@@ -92,6 +92,12 @@
|
|
|
92
92
|
});
|
|
93
93
|
content.register(app);
|
|
94
94
|
|
|
95
|
+
// Local preview serves exactly one book — skip the viewer's library home
|
|
96
|
+
// and land straight in the book.
|
|
97
|
+
app.get("/", (req, res) => {
|
|
98
|
+
res.redirect(`/${docId}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
95
101
|
// Catch all
|
|
96
102
|
app.get("/{*splat}", (req, res) => {
|
|
97
103
|
|
|
@@ -110,9 +116,26 @@
|
|
|
110
116
|
|
|
111
117
|
// If the file exists, send it.
|
|
112
118
|
if (fs.existsSync(ui_file_path)) {
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
119
|
+
// Only text assets go through send_content_file (which reads the
|
|
120
|
+
// file as a string for variable expansion) — binary assets (fonts,
|
|
121
|
+
// images) are corrupted by that path, so stream them verbatim.
|
|
122
|
+
// mermaid.min.js is also streamed (skips per-request variable
|
|
123
|
+
// expansion over ~4.5MB, and send_file sets its cache header).
|
|
124
|
+
const text_exts = new Set([
|
|
125
|
+
".html",
|
|
126
|
+
".htm",
|
|
127
|
+
".css",
|
|
128
|
+
".js",
|
|
129
|
+
".json",
|
|
130
|
+
".svg",
|
|
131
|
+
".txt",
|
|
132
|
+
".md",
|
|
133
|
+
".map",
|
|
134
|
+
]);
|
|
135
|
+
if (
|
|
136
|
+
path.basename(ui_file_path) === "mermaid.min.js" ||
|
|
137
|
+
!text_exts.has(path.extname(ui_file_path).toLowerCase())
|
|
138
|
+
) {
|
|
116
139
|
content.send_file(req, res, ui_file_path);
|
|
117
140
|
return;
|
|
118
141
|
}
|
|
@@ -290,6 +290,14 @@
|
|
|
290
290
|
const doc_id = segments.shift();
|
|
291
291
|
const article_path = segments.length > 0 ? segments.join("/") : "index";
|
|
292
292
|
|
|
293
|
+
// No book in the path (e.g. a bare "/") - nothing to resolve
|
|
294
|
+
if (!doc_id) {
|
|
295
|
+
return {
|
|
296
|
+
level: "skip",
|
|
297
|
+
message: `Inter-book link has no target book - link not verified: ${link}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
293
301
|
// Books not sourced from GitHub — resolved by docId suffix, no repo
|
|
294
302
|
if (has_suffix(doc_id, UNVERIFIABLE_SUFFIXES)) {
|
|
295
303
|
return {
|
package/hdoc-validate.js
CHANGED
|
@@ -498,10 +498,22 @@
|
|
|
498
498
|
return resp.status;
|
|
499
499
|
};
|
|
500
500
|
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
const
|
|
501
|
+
// Links already written to validated-links.txt this run. A link can appear
|
|
502
|
+
// on many pages, but only needs writing to the cache file once.
|
|
503
|
+
const skip_links_written = new Set();
|
|
504
|
+
const appendSkipLink = (link) => {
|
|
505
|
+
if (skip_links_written.has(link)) return;
|
|
506
|
+
skip_links_written.add(link);
|
|
507
|
+
fs.appendFileSync(skip_link_file, `${link}\n`);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Map a network check result (inter-book or external URL) onto
|
|
511
|
+
// errors/warnings/messages and the validated-links cache. 'ok' and 'skip'
|
|
512
|
+
// outcomes are stable, so they are appended to validated-links.txt like any
|
|
513
|
+
// other passing link. Called once per PAGE carrying the link, so the
|
|
514
|
+
// message is positioned against the page being reported on.
|
|
515
|
+
const emitLinkResult = (result, link, htmlFile, markdown_paths, markdown_content) => {
|
|
516
|
+
if (!result) return;
|
|
505
517
|
if (result.level === "error") {
|
|
506
518
|
errors[htmlFile.relativePath].push(
|
|
507
519
|
processErrorMessage(result.message, markdown_paths.relativePath, markdown_content, link),
|
|
@@ -512,10 +524,25 @@
|
|
|
512
524
|
);
|
|
513
525
|
} else {
|
|
514
526
|
messages[htmlFile.relativePath].push(result.message);
|
|
515
|
-
|
|
527
|
+
appendSkipLink(link);
|
|
516
528
|
}
|
|
517
529
|
};
|
|
518
530
|
|
|
531
|
+
// Run a network check for a link at most once per run, but hand the SAME
|
|
532
|
+
// result back to every page that carries the link. Deduplicating the fetch
|
|
533
|
+
// is a performance concern; deduplicating the reporting is not - a broken
|
|
534
|
+
// link on ten pages has to be flagged on all ten, or fixing the first page
|
|
535
|
+
// just surfaces the second on the next build.
|
|
536
|
+
// global_links_checked: Map link -> Promise<{ level, message } | null>
|
|
537
|
+
const checkLinkOnce = (global_links_checked, link, check) => {
|
|
538
|
+
let pending = global_links_checked.get(link);
|
|
539
|
+
if (!pending) {
|
|
540
|
+
pending = check();
|
|
541
|
+
global_links_checked.set(link, pending);
|
|
542
|
+
}
|
|
543
|
+
return pending;
|
|
544
|
+
};
|
|
545
|
+
|
|
519
546
|
const checkLinks = async (source_path, htmlFile, links, hdocbook_config, hdocbook_project, global_links_checked, output_links) => {
|
|
520
547
|
const markdown_paths = getMDPathFromHtmlPath(htmlFile);
|
|
521
548
|
const markdown_content = fs.readFileSync(markdown_paths.markdownPath, 'utf8');
|
|
@@ -537,11 +564,15 @@
|
|
|
537
564
|
// concurrently rather than one-at-a-time.
|
|
538
565
|
const externalChecks = [];
|
|
539
566
|
|
|
567
|
+
// Same link twice on the same page is reported once. Across pages it is
|
|
568
|
+
// reported every time - see checkLinkOnce.
|
|
569
|
+
const page_links_checked = new Set();
|
|
570
|
+
|
|
540
571
|
for (let i = 0; i < links.length; i++) {
|
|
541
572
|
if (output_links) console.log(` - ${links[i]}`);
|
|
542
573
|
if (exclude_links[links[i]]) continue;
|
|
543
|
-
if (
|
|
544
|
-
|
|
574
|
+
if (page_links_checked.has(links[i])) continue;
|
|
575
|
+
page_links_checked.add(links[i]);
|
|
545
576
|
|
|
546
577
|
const valid_url = hdoc.valid_url(links[i]);
|
|
547
578
|
if (!valid_url) {
|
|
@@ -554,6 +585,13 @@
|
|
|
554
585
|
if (link_segments[0] === "") link_segments.shift();
|
|
555
586
|
const link_root = link_segments[0] === "_books" ? link_segments[1] : link_segments[0];
|
|
556
587
|
|
|
588
|
+
// A bare "/" is the docs site home page - no book, nothing to
|
|
589
|
+
// resolve locally or against GitHub.
|
|
590
|
+
if (link_root === undefined || link_root === "") {
|
|
591
|
+
appendSkipLink(links[i]);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
557
595
|
// Check for links with a _books path that have no specific file target
|
|
558
596
|
// We do need to exclude those with an extension though, for pages that link downloadable resources
|
|
559
597
|
if (link_segments[0] === "_books" && path.extname(links[i]) === '') {
|
|
@@ -567,8 +605,10 @@
|
|
|
567
605
|
if (interbook.enabled() && path.extname(links[i].split("#")[0]) === "") {
|
|
568
606
|
const link = links[i];
|
|
569
607
|
externalChecks.push(async () =>
|
|
570
|
-
|
|
571
|
-
await
|
|
608
|
+
emitLinkResult(
|
|
609
|
+
await checkLinkOnce(global_links_checked, link, () =>
|
|
610
|
+
interbook.check_link(link),
|
|
611
|
+
),
|
|
572
612
|
link,
|
|
573
613
|
htmlFile,
|
|
574
614
|
markdown_paths,
|
|
@@ -576,7 +616,7 @@
|
|
|
576
616
|
),
|
|
577
617
|
);
|
|
578
618
|
} else {
|
|
579
|
-
|
|
619
|
+
appendSkipLink(links[i]);
|
|
580
620
|
}
|
|
581
621
|
continue;
|
|
582
622
|
}
|
|
@@ -603,12 +643,12 @@
|
|
|
603
643
|
)
|
|
604
644
|
.edit_path.replace(path.extname(htmlFile.relativePath), ".md")
|
|
605
645
|
) {
|
|
606
|
-
|
|
646
|
+
appendSkipLink(links[i]);
|
|
607
647
|
continue;
|
|
608
648
|
}
|
|
609
649
|
|
|
610
650
|
if (valid_url.protocol === "mailto:") {
|
|
611
|
-
|
|
651
|
+
appendSkipLink(links[i]);
|
|
612
652
|
continue;
|
|
613
653
|
}
|
|
614
654
|
|
|
@@ -653,8 +693,10 @@
|
|
|
653
693
|
const link = links[i];
|
|
654
694
|
const book_link = valid_url.pathname + valid_url.hash;
|
|
655
695
|
externalChecks.push(async () =>
|
|
656
|
-
|
|
657
|
-
await
|
|
696
|
+
emitLinkResult(
|
|
697
|
+
await checkLinkOnce(global_links_checked, link, () =>
|
|
698
|
+
interbook.check_link(book_link),
|
|
699
|
+
),
|
|
658
700
|
link,
|
|
659
701
|
htmlFile,
|
|
660
702
|
markdown_paths,
|
|
@@ -668,20 +710,19 @@
|
|
|
668
710
|
const url = links[i];
|
|
669
711
|
const isInternal = url.toLowerCase().includes("internal.hornbill.com");
|
|
670
712
|
|
|
671
|
-
|
|
713
|
+
// Returns a page-independent { level, message } (or null for "say
|
|
714
|
+
// nothing") so the outcome can be cached per URL and replayed
|
|
715
|
+
// against every page that links to it.
|
|
716
|
+
const checkExternalUrl = async () => {
|
|
672
717
|
// For internal.hornbill.com links, check network reachability first (result cached)
|
|
673
718
|
if (isInternal) {
|
|
674
719
|
const on_int_net = await ensureIntNetCached();
|
|
675
720
|
if (!on_int_net) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return;
|
|
721
|
+
return {
|
|
722
|
+
level: "skip",
|
|
723
|
+
message: `Outside of Hornbill network - skipping internal link validation for: ${url}`,
|
|
724
|
+
};
|
|
681
725
|
}
|
|
682
|
-
messages[htmlFile.relativePath].push(
|
|
683
|
-
`Inside of Hornbill network - performing internal link validation for: ${url}`,
|
|
684
|
-
);
|
|
685
726
|
}
|
|
686
727
|
|
|
687
728
|
try {
|
|
@@ -689,25 +730,39 @@
|
|
|
689
730
|
if ((status < 200 || status > 299) && status !== 304) {
|
|
690
731
|
if (process.env.GITHUB_ACTIONS === 'true' && status === 403 && url.includes(".hornbill.com")) {
|
|
691
732
|
// Always returns 403 for Hornbill sites through GitHub Actions — not a real error
|
|
692
|
-
|
|
693
|
-
throw `Unexpected Status Returned: ${status}`;
|
|
733
|
+
return null;
|
|
694
734
|
}
|
|
695
|
-
|
|
696
|
-
fs.appendFileSync(skip_link_file, `${url}\n`);
|
|
735
|
+
throw `Unexpected Status Returned: ${status}`;
|
|
697
736
|
}
|
|
737
|
+
return {
|
|
738
|
+
level: "ok",
|
|
739
|
+
message: `External link is valid: ${url}`,
|
|
740
|
+
};
|
|
698
741
|
} catch (e) {
|
|
699
742
|
let error_message;
|
|
700
743
|
if (e instanceof AggregateError) {
|
|
701
|
-
error_message =
|
|
744
|
+
error_message = `Issue with external link [${url}]: ${e.message} - ${JSON.stringify(e.errors)}`;
|
|
702
745
|
} else {
|
|
703
|
-
error_message =
|
|
746
|
+
error_message = `Issue with external link [${url}]: ${e}`;
|
|
704
747
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
748
|
+
const level =
|
|
749
|
+
hdocbook_project.validation.external_link_warnings ||
|
|
750
|
+
process.env.GITHUB_ACTIONS === 'true'
|
|
751
|
+
? "warning"
|
|
752
|
+
: "error";
|
|
753
|
+
return { level, message: error_message };
|
|
709
754
|
}
|
|
710
|
-
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
externalChecks.push(async () =>
|
|
758
|
+
emitLinkResult(
|
|
759
|
+
await checkLinkOnce(global_links_checked, url, checkExternalUrl),
|
|
760
|
+
url,
|
|
761
|
+
htmlFile,
|
|
762
|
+
markdown_paths,
|
|
763
|
+
markdown_content,
|
|
764
|
+
),
|
|
765
|
+
);
|
|
711
766
|
}
|
|
712
767
|
}
|
|
713
768
|
|
|
@@ -1136,7 +1191,9 @@
|
|
|
1136
1191
|
}
|
|
1137
1192
|
|
|
1138
1193
|
|
|
1139
|
-
|
|
1194
|
+
// link -> Promise<{ level, message } | null> for network checks: fetched
|
|
1195
|
+
// once per run, but reported on every page that carries the link.
|
|
1196
|
+
const global_links_checked = new Map();
|
|
1140
1197
|
|
|
1141
1198
|
for (const key in html_to_validate) {
|
|
1142
1199
|
const file = html_to_validate[key];
|