hdoc-tools 0.62.1 → 0.62.3
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/hdoc-init.js
CHANGED
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
{
|
|
63
63
|
name: "bookType",
|
|
64
64
|
description:
|
|
65
|
-
"Book Type (0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref)",
|
|
65
|
+
"Book Type (0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref, 5=content_pack)",
|
|
66
66
|
default: "0",
|
|
67
|
-
validator: /^[0-
|
|
68
|
-
warning: "Book Type must be a number between 0 and
|
|
67
|
+
validator: /^[0-5]$/,
|
|
68
|
+
warning: "Book Type must be a number between 0 and 5.",
|
|
69
69
|
required: true,
|
|
70
70
|
},
|
|
71
71
|
];
|
package/hdoc-validate-config.js
CHANGED
|
@@ -295,7 +295,7 @@
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
if (config.bookType !== undefined && !valid_book_types.includes(config.bookType)) {
|
|
298
|
-
errors.push(`${file}: "bookType" must be an integer, one of: ${valid_book_types.join(', ')} (0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref)`);
|
|
298
|
+
errors.push(`${file}: "bookType" must be an integer, one of: ${valid_book_types.join(', ')} (0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref, 5=content_pack)`);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
if (config.coverImage !== undefined && typeof config.coverImage !== 'string') {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// github.com/Hornbill-Docs/<docId>.
|
|
11
11
|
// 2. The target article must exist in the book's GitHub repo (default
|
|
12
12
|
// branch): <docId>/<path>.md, <path>/index.md, <path>.html or <path>.htm.
|
|
13
|
-
// A path matched by the target book's redirects[] also passes
|
|
13
|
+
// A path matched by the target book's redirects[] also passes, unless
|
|
14
|
+
// the redirect code is 410 (content deliberately removed) — error.
|
|
14
15
|
// 3. If the source link carries a #hash-anchor, a matching heading anchor
|
|
15
16
|
// must exist in the target article. Anchor ids are derived from h2/h3
|
|
16
17
|
// headings with the same hdoc.makeAnchorIdFriendly slug used at build.
|
|
@@ -32,12 +33,22 @@
|
|
|
32
33
|
|
|
33
34
|
const LIBRARY_URL = "https://docs.hornbill.com/_books/library.json";
|
|
34
35
|
const GITHUB_ORG_FALLBACK = "https://github.com/Hornbill-Docs";
|
|
36
|
+
const PUBLISHED_SITE = "https://docs.hornbill.com";
|
|
37
|
+
|
|
38
|
+
// Books generated outside GitHub (built in Hornbill Projects, published
|
|
39
|
+
// straight to docs.hornbill.com) have no repo to inspect, so they are keyed
|
|
40
|
+
// off the docId suffix instead:
|
|
41
|
+
// -api / -db → not verifiable at all, skip
|
|
42
|
+
// -packages → verify the page exists on the published site
|
|
43
|
+
const UNVERIFIABLE_SUFFIXES = ["-api", "-db"];
|
|
44
|
+
const PUBLISHED_ONLY_SUFFIXES = ["-packages"];
|
|
35
45
|
|
|
36
46
|
let git_token = "";
|
|
37
47
|
let library_promise = null;
|
|
38
48
|
const book_cache = {}; // docId -> promise of book resolution
|
|
39
49
|
const article_cache = {}; // docId|path -> promise of article fetch
|
|
40
50
|
const redirects_cache = {}; // repo url -> promise of redirect url set
|
|
51
|
+
const published_cache = {}; // published url -> promise of { ok, status }
|
|
41
52
|
|
|
42
53
|
const gh_headers = () => {
|
|
43
54
|
const headers = {
|
|
@@ -139,8 +150,10 @@
|
|
|
139
150
|
};
|
|
140
151
|
};
|
|
141
152
|
|
|
142
|
-
// The redirect urls declared in the target book's hdocbook-project.json
|
|
143
|
-
//
|
|
153
|
+
// The redirect urls declared in the target book's hdocbook-project.json,
|
|
154
|
+
// mapped to their redirect code. A link to a 301/308-redirected path is
|
|
155
|
+
// valid (the published site forwards it); a 410 means the content was
|
|
156
|
+
// deliberately removed, so a link to it is broken.
|
|
144
157
|
const get_redirect_urls = (repo) => {
|
|
145
158
|
if (!redirects_cache[repo]) {
|
|
146
159
|
redirects_cache[repo] = (async () => {
|
|
@@ -150,7 +163,7 @@
|
|
|
150
163
|
if (file.status === 200) {
|
|
151
164
|
const project = JSON.parse(file.content);
|
|
152
165
|
for (const redirect of project.redirects || []) {
|
|
153
|
-
if (redirect.url) urls[redirect.url] =
|
|
166
|
+
if (redirect.url) urls[redirect.url] = redirect.code || 301;
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
} catch {
|
|
@@ -196,8 +209,9 @@
|
|
|
196
209
|
|
|
197
210
|
// Locate an article inside a book repo, trying the same resolution order
|
|
198
211
|
// the published site uses. Returns
|
|
199
|
-
// { found, redirected, unverifiable, anchors } — anchors null when
|
|
200
|
-
// article was matched via redirect (content not fetched)
|
|
212
|
+
// { found, redirected, gone, unverifiable, anchors } — anchors null when
|
|
213
|
+
// the article was matched via redirect (content not fetched); gone true
|
|
214
|
+
// when the path matched a 410 redirect (content removed).
|
|
201
215
|
const resolve_article = (repo, doc_id, article_path) => {
|
|
202
216
|
const cache_key = `${doc_id}|${article_path}`;
|
|
203
217
|
if (!article_cache[cache_key]) {
|
|
@@ -227,7 +241,9 @@
|
|
|
227
241
|
}
|
|
228
242
|
}
|
|
229
243
|
const redirect_urls = await get_redirect_urls(repo);
|
|
230
|
-
|
|
244
|
+
const redirect_code = redirect_urls[`/${base}`];
|
|
245
|
+
if (redirect_code) {
|
|
246
|
+
if (redirect_code === 410) return { found: false, gone: true };
|
|
231
247
|
return { found: true, redirected: true, anchors: null };
|
|
232
248
|
}
|
|
233
249
|
return { found: false };
|
|
@@ -236,6 +252,29 @@
|
|
|
236
252
|
return article_cache[cache_key];
|
|
237
253
|
};
|
|
238
254
|
|
|
255
|
+
const has_suffix = (doc_id, suffixes) =>
|
|
256
|
+
suffixes.some((suffix) => doc_id.endsWith(suffix));
|
|
257
|
+
|
|
258
|
+
// GET the published site to confirm a generated book page exists. HEAD is
|
|
259
|
+
// NOT usable here — docs.hornbill.com answers 404 to HEAD on pages that
|
|
260
|
+
// GET serves 200.
|
|
261
|
+
const check_published = (url) => {
|
|
262
|
+
if (!published_cache[url]) {
|
|
263
|
+
published_cache[url] = (async () => {
|
|
264
|
+
const resp = await hdoc.fetchWithRetry(
|
|
265
|
+
url,
|
|
266
|
+
{
|
|
267
|
+
headers: { "User-Agent": "HornbillDocsBuild" },
|
|
268
|
+
timeoutMs: 10000,
|
|
269
|
+
},
|
|
270
|
+
2,
|
|
271
|
+
);
|
|
272
|
+
return { ok: resp.ok, status: resp.status };
|
|
273
|
+
})();
|
|
274
|
+
}
|
|
275
|
+
return published_cache[url];
|
|
276
|
+
};
|
|
277
|
+
|
|
239
278
|
exports.init = (token) => {
|
|
240
279
|
git_token = token || "";
|
|
241
280
|
};
|
|
@@ -251,6 +290,48 @@
|
|
|
251
290
|
const doc_id = segments.shift();
|
|
252
291
|
const article_path = segments.length > 0 ? segments.join("/") : "index";
|
|
253
292
|
|
|
293
|
+
// Books not sourced from GitHub — resolved by docId suffix, no repo
|
|
294
|
+
if (has_suffix(doc_id, UNVERIFIABLE_SUFFIXES)) {
|
|
295
|
+
return {
|
|
296
|
+
level: "skip",
|
|
297
|
+
message: `Inter-book link target book [${doc_id}] is generated - link not verified: ${link}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (has_suffix(doc_id, PUBLISHED_ONLY_SUFFIXES)) {
|
|
301
|
+
const url = `${PUBLISHED_SITE}${link_path}`;
|
|
302
|
+
let published;
|
|
303
|
+
try {
|
|
304
|
+
published = await check_published(url);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return {
|
|
307
|
+
level: "warning",
|
|
308
|
+
message: `Unable to verify inter-book link [${link}]: ${e}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (published.status === 404) {
|
|
312
|
+
return {
|
|
313
|
+
level: "error",
|
|
314
|
+
message: `Inter-book link target page does not exist on ${PUBLISHED_SITE} [${doc_id}]: ${link}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (!published.ok) {
|
|
318
|
+
return {
|
|
319
|
+
level: "warning",
|
|
320
|
+
message: `Unable to verify inter-book link [${link}]: ${url} returned HTTP ${published.status}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (hash_anchor) {
|
|
324
|
+
return {
|
|
325
|
+
level: "skip",
|
|
326
|
+
message: `Inter-book link page verified on ${PUBLISHED_SITE} - hash anchor not verified: ${link}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
level: "ok",
|
|
331
|
+
message: `Inter-book link verified on ${PUBLISHED_SITE}: ${link}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
254
335
|
let book;
|
|
255
336
|
try {
|
|
256
337
|
book = await resolve_book(doc_id);
|
|
@@ -296,6 +377,12 @@
|
|
|
296
377
|
};
|
|
297
378
|
}
|
|
298
379
|
if (!article.found) {
|
|
380
|
+
if (article.gone) {
|
|
381
|
+
return {
|
|
382
|
+
level: "error",
|
|
383
|
+
message: `Inter-book link target article was removed (410 gone) from book [${doc_id}]: ${link}`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
299
386
|
return {
|
|
300
387
|
level: "error",
|
|
301
388
|
message: `Inter-book link target article does not exist in book [${doc_id}]: ${link}`,
|
package/package.json
CHANGED
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"bookType": {
|
|
54
54
|
"type": "integer",
|
|
55
|
-
"description": "The type of content this book contains: 0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref. Defaults to 0 when omitted.",
|
|
56
|
-
"enum": [0, 1, 2, 3, 4]
|
|
55
|
+
"description": "The type of content this book contains: 0=document, 1=api_ref, 2=db_ref, 3=etl_ref, 4=mcp_ref, 5=content_pack. Defaults to 0 when omitted.",
|
|
56
|
+
"enum": [0, 1, 2, 3, 4, 5]
|
|
57
57
|
},
|
|
58
58
|
"coverImage": {
|
|
59
59
|
"type": "string",
|