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-validate-config.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const project_pdf_keys = Object.keys(project_schema.properties.pdfGeneration.properties);
|
|
14
14
|
const project_validation_keys = Object.keys(project_schema.properties.validation.properties);
|
|
15
15
|
const project_redirect_keys = Object.keys(project_schema.properties.redirects.items.properties);
|
|
16
|
+
const project_ai_keys = Object.keys(project_schema.properties.ai.properties);
|
|
16
17
|
const redirect_valid_codes = project_schema.properties.redirects.items.properties.code.enum;
|
|
17
18
|
|
|
18
19
|
// Reports any keys in obj that are not in the allowed set.
|
|
@@ -152,6 +153,31 @@
|
|
|
152
153
|
typeof config.validation.external_link_warnings !== 'boolean') {
|
|
153
154
|
errors.push(`${file}: "validation.external_link_warnings" must be a boolean`);
|
|
154
155
|
}
|
|
156
|
+
if (config.validation.spellcheckDictionary !== undefined) {
|
|
157
|
+
if (!Array.isArray(config.validation.spellcheckDictionary)) {
|
|
158
|
+
errors.push(`${file}: "validation.spellcheckDictionary" must be an array`);
|
|
159
|
+
} else {
|
|
160
|
+
for (let i = 0; i < config.validation.spellcheckDictionary.length; i++) {
|
|
161
|
+
if (typeof config.validation.spellcheckDictionary[i] !== 'string') {
|
|
162
|
+
errors.push(`${file}: "validation.spellcheckDictionary[${i}]" must be a string`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (config.ai !== undefined) {
|
|
171
|
+
if (typeof config.ai !== 'object' || Array.isArray(config.ai) || config.ai === null) {
|
|
172
|
+
errors.push(`${file}: "ai" must be an object`);
|
|
173
|
+
} else {
|
|
174
|
+
check_extra_keys(config.ai, project_ai_keys, 'ai', file, errors);
|
|
175
|
+
if (config.ai.model !== undefined && typeof config.ai.model !== 'string') {
|
|
176
|
+
errors.push(`${file}: "ai.model" must be a string`);
|
|
177
|
+
}
|
|
178
|
+
if (config.ai.maxTokens !== undefined && !Number.isInteger(config.ai.maxTokens)) {
|
|
179
|
+
errors.push(`${file}: "ai.maxTokens" must be an integer`);
|
|
180
|
+
}
|
|
155
181
|
}
|
|
156
182
|
}
|
|
157
183
|
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Inter-book link validation.
|
|
2
|
+
//
|
|
3
|
+
// When a GitHub token is supplied to build/validate, root-relative links that
|
|
4
|
+
// point at OTHER Hornbill Docs books (e.g. /esp-config/some/article#anchor) —
|
|
5
|
+
// and fully-qualified docs.hornbill.com links where those are permitted
|
|
6
|
+
// (_inline content) — are validated instead of being skipped:
|
|
7
|
+
//
|
|
8
|
+
// 1. The target book must exist — either listed in the published library
|
|
9
|
+
// (https://docs.hornbill.com/_books/library.json) or as a repo under
|
|
10
|
+
// github.com/Hornbill-Docs/<docId>.
|
|
11
|
+
// 2. The target article must exist in the book's GitHub repo (default
|
|
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.
|
|
14
|
+
// 3. If the source link carries a #hash-anchor, a matching heading anchor
|
|
15
|
+
// must exist in the target article. Anchor ids are derived from h2/h3
|
|
16
|
+
// headings with the same hdoc.makeAnchorIdFriendly slug used at build.
|
|
17
|
+
//
|
|
18
|
+
// All lookups are cached as promises (per book, per article, per repo config)
|
|
19
|
+
// so concurrent link checks for the same target collapse into one API call.
|
|
20
|
+
// GitHub API calls go through hdoc.fetchWithRetry which throttles to
|
|
21
|
+
// 1 request/800ms and honours rate-limit headers.
|
|
22
|
+
//
|
|
23
|
+
// Result levels:
|
|
24
|
+
// ok - link verified (caller appends to validated-links.txt)
|
|
25
|
+
// skip - could not verify by design (book has no public source) — logged
|
|
26
|
+
// warning - could not verify due to access/infra (401/403, library down)
|
|
27
|
+
// error - target book/article/anchor definitively missing
|
|
28
|
+
(() => {
|
|
29
|
+
const cheerio = require("cheerio");
|
|
30
|
+
const path = require("node:path");
|
|
31
|
+
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
32
|
+
|
|
33
|
+
const LIBRARY_URL = "https://docs.hornbill.com/_books/library.json";
|
|
34
|
+
const GITHUB_ORG_FALLBACK = "https://github.com/Hornbill-Docs";
|
|
35
|
+
|
|
36
|
+
let git_token = "";
|
|
37
|
+
let library_promise = null;
|
|
38
|
+
const book_cache = {}; // docId -> promise of book resolution
|
|
39
|
+
const article_cache = {}; // docId|path -> promise of article fetch
|
|
40
|
+
const redirects_cache = {}; // repo url -> promise of redirect url set
|
|
41
|
+
|
|
42
|
+
const gh_headers = () => {
|
|
43
|
+
const headers = {
|
|
44
|
+
"User-Agent": "HornbillDocsBuild",
|
|
45
|
+
"Cache-Control": "no-cache",
|
|
46
|
+
Accept: "application/vnd.github.raw+json",
|
|
47
|
+
};
|
|
48
|
+
if (git_token !== "") headers.authorization = `Bearer ${git_token}`;
|
|
49
|
+
return headers;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// github.com/<owner>/<repo> -> api.github.com/repos/<owner>/<repo>
|
|
53
|
+
const repo_api_base = (repo_url) => {
|
|
54
|
+
const clean = repo_url.endsWith("/") ? repo_url.slice(0, -1) : repo_url;
|
|
55
|
+
return clean.replace(
|
|
56
|
+
"https://github.com/",
|
|
57
|
+
"https://api.github.com/repos/",
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const get_library = () => {
|
|
62
|
+
if (library_promise === null) {
|
|
63
|
+
library_promise = (async () => {
|
|
64
|
+
const books = {};
|
|
65
|
+
const resp = await hdoc.fetchWithRetry(LIBRARY_URL, {
|
|
66
|
+
headers: { "User-Agent": "HornbillDocsBuild" },
|
|
67
|
+
timeoutMs: 10000,
|
|
68
|
+
});
|
|
69
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
70
|
+
const data = await resp.json();
|
|
71
|
+
for (const book of data.books || []) {
|
|
72
|
+
books[book.docId] = book;
|
|
73
|
+
}
|
|
74
|
+
return books;
|
|
75
|
+
})().catch((e) => {
|
|
76
|
+
// Cache the failure — one warning per run, not one per link
|
|
77
|
+
return { __error: `${e}` };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return library_promise;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Resolve a docId to { exists, repo|null, unverifiable|undefined }
|
|
84
|
+
const resolve_book = (doc_id) => {
|
|
85
|
+
if (!book_cache[doc_id]) {
|
|
86
|
+
book_cache[doc_id] = (async () => {
|
|
87
|
+
const library = await get_library();
|
|
88
|
+
if (!library.__error && library[doc_id]) {
|
|
89
|
+
return {
|
|
90
|
+
exists: true,
|
|
91
|
+
repo: library[doc_id].publicSource || null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Not in the public library (internal book?) or library down —
|
|
95
|
+
// fall back to the conventional repo location.
|
|
96
|
+
const fallback_repo = `${GITHUB_ORG_FALLBACK}/${doc_id}`;
|
|
97
|
+
const details = await hdoc.get_github_repo_details(
|
|
98
|
+
repo_api_base(fallback_repo),
|
|
99
|
+
git_token,
|
|
100
|
+
);
|
|
101
|
+
if (details.success) return { exists: true, repo: fallback_repo };
|
|
102
|
+
// get_github_repo_details reports 404 as "Error: HTTP 404" (early
|
|
103
|
+
// return) or "404 : Not Found" (json branch) depending on path
|
|
104
|
+
const err = details.error instanceof Error ? details.error.message : `${details.error}`;
|
|
105
|
+
if (err.includes("404")) {
|
|
106
|
+
if (library.__error) {
|
|
107
|
+
// Library unreachable AND no fallback repo — can't be sure
|
|
108
|
+
return {
|
|
109
|
+
exists: false,
|
|
110
|
+
repo: null,
|
|
111
|
+
unverifiable: `book library unreachable (${library.__error}) and no repo at ${fallback_repo}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { exists: false, repo: null };
|
|
115
|
+
}
|
|
116
|
+
// 401/403/network — repo may exist but we can't see it
|
|
117
|
+
return {
|
|
118
|
+
exists: false,
|
|
119
|
+
repo: null,
|
|
120
|
+
unverifiable: `GitHub lookup failed for ${fallback_repo}: ${err}`,
|
|
121
|
+
};
|
|
122
|
+
})();
|
|
123
|
+
}
|
|
124
|
+
return book_cache[doc_id];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Fetch a repo file's raw content via the contents API.
|
|
128
|
+
// Returns { status, content } — content null unless status 200.
|
|
129
|
+
const fetch_repo_file = async (repo, file_path) => {
|
|
130
|
+
const encoded = file_path.split("/").map(encodeURIComponent).join("/");
|
|
131
|
+
const resp = await hdoc.fetchWithRetry(
|
|
132
|
+
`${repo_api_base(repo)}/contents/${encoded}`,
|
|
133
|
+
{ headers: gh_headers(), timeoutMs: 10000 },
|
|
134
|
+
2,
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
status: resp.status,
|
|
138
|
+
content: resp.ok ? await resp.text() : null,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// The redirect urls declared in the target book's hdocbook-project.json —
|
|
143
|
+
// a link to a redirected path is valid (the published site serves 301/308).
|
|
144
|
+
const get_redirect_urls = (repo) => {
|
|
145
|
+
if (!redirects_cache[repo]) {
|
|
146
|
+
redirects_cache[repo] = (async () => {
|
|
147
|
+
const urls = {};
|
|
148
|
+
try {
|
|
149
|
+
const file = await fetch_repo_file(repo, "hdocbook-project.json");
|
|
150
|
+
if (file.status === 200) {
|
|
151
|
+
const project = JSON.parse(file.content);
|
|
152
|
+
for (const redirect of project.redirects || []) {
|
|
153
|
+
if (redirect.url) urls[redirect.url] = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Malformed/missing project file in target repo — no redirects
|
|
158
|
+
}
|
|
159
|
+
return urls;
|
|
160
|
+
})();
|
|
161
|
+
}
|
|
162
|
+
return redirects_cache[repo];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Derive the set of hb-doc-anchor-* ids the build would generate for a
|
|
166
|
+
// target article. Markdown: h2/h3 ATX headings outside code fences, with
|
|
167
|
+
// [text](url) collapsed to text before slugging (matching rendered text).
|
|
168
|
+
// HTML: real h2/h3 elements via cheerio.
|
|
169
|
+
const extract_anchor_ids = (content, is_html) => {
|
|
170
|
+
const anchors = {};
|
|
171
|
+
if (is_html) {
|
|
172
|
+
const $ = cheerio.load(content);
|
|
173
|
+
$("h2, h3").each(function () {
|
|
174
|
+
anchors[hdoc.makeAnchorIdFriendly($(this).text().trim())] = true;
|
|
175
|
+
});
|
|
176
|
+
return anchors;
|
|
177
|
+
}
|
|
178
|
+
let in_fence = false;
|
|
179
|
+
for (const raw_line of content.split("\n")) {
|
|
180
|
+
const line = raw_line.trimEnd();
|
|
181
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
182
|
+
in_fence = !in_fence;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (in_fence) continue;
|
|
186
|
+
const match = line.match(/^(##|###)\s+(.*)$/);
|
|
187
|
+
if (!match) continue;
|
|
188
|
+
const text = match[2]
|
|
189
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // md links -> link text
|
|
190
|
+
.replace(/[`*_]/g, "") // inline emphasis/code markers
|
|
191
|
+
.trim();
|
|
192
|
+
anchors[hdoc.makeAnchorIdFriendly(text)] = true;
|
|
193
|
+
}
|
|
194
|
+
return anchors;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Locate an article inside a book repo, trying the same resolution order
|
|
198
|
+
// the published site uses. Returns
|
|
199
|
+
// { found, redirected, unverifiable, anchors } — anchors null when the
|
|
200
|
+
// article was matched via redirect (content not fetched).
|
|
201
|
+
const resolve_article = (repo, doc_id, article_path) => {
|
|
202
|
+
const cache_key = `${doc_id}|${article_path}`;
|
|
203
|
+
if (!article_cache[cache_key]) {
|
|
204
|
+
article_cache[cache_key] = (async () => {
|
|
205
|
+
const base = `${doc_id}/${article_path}`;
|
|
206
|
+
const candidates = [
|
|
207
|
+
{ file: `${base}.md`, html: false },
|
|
208
|
+
{ file: `${base}/index.md`, html: false },
|
|
209
|
+
{ file: `${base}.html`, html: true },
|
|
210
|
+
{ file: `${base}.htm`, html: true },
|
|
211
|
+
];
|
|
212
|
+
// Book-root link (/other-book) → the book's index page
|
|
213
|
+
if (article_path === "index") candidates.splice(1, 1);
|
|
214
|
+
for (const candidate of candidates) {
|
|
215
|
+
const file = await fetch_repo_file(repo, candidate.file);
|
|
216
|
+
if (file.status === 200) {
|
|
217
|
+
return {
|
|
218
|
+
found: true,
|
|
219
|
+
anchors: extract_anchor_ids(file.content, candidate.html),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (file.status !== 404) {
|
|
223
|
+
return {
|
|
224
|
+
found: false,
|
|
225
|
+
unverifiable: `GitHub returned HTTP ${file.status} for ${candidate.file}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const redirect_urls = await get_redirect_urls(repo);
|
|
230
|
+
if (redirect_urls[`/${base}`]) {
|
|
231
|
+
return { found: true, redirected: true, anchors: null };
|
|
232
|
+
}
|
|
233
|
+
return { found: false };
|
|
234
|
+
})();
|
|
235
|
+
}
|
|
236
|
+
return article_cache[cache_key];
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
exports.init = (token) => {
|
|
240
|
+
git_token = token || "";
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
exports.enabled = () => git_token !== "";
|
|
244
|
+
|
|
245
|
+
// link: root-relative inter-book link, e.g. /esp-config/path/article#anchor
|
|
246
|
+
// Returns { level: 'ok'|'skip'|'warning'|'error', message }
|
|
247
|
+
exports.check_link = async (link) => {
|
|
248
|
+
const [link_path, hash_anchor] = link.split("#");
|
|
249
|
+
const segments = link_path.split("/").filter((s) => s !== "");
|
|
250
|
+
if (segments[0] === "_books") segments.shift();
|
|
251
|
+
const doc_id = segments.shift();
|
|
252
|
+
const article_path = segments.length > 0 ? segments.join("/") : "index";
|
|
253
|
+
|
|
254
|
+
let book;
|
|
255
|
+
try {
|
|
256
|
+
book = await resolve_book(doc_id);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
return {
|
|
259
|
+
level: "warning",
|
|
260
|
+
message: `Unable to verify inter-book link [${link}]: ${e}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (book.unverifiable) {
|
|
264
|
+
return {
|
|
265
|
+
level: "warning",
|
|
266
|
+
message: `Unable to verify inter-book link [${link}]: ${book.unverifiable}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (!book.exists) {
|
|
270
|
+
return {
|
|
271
|
+
level: "error",
|
|
272
|
+
message: `Inter-book link target book does not exist: ${doc_id} [${link}]`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (!book.repo) {
|
|
276
|
+
// Generated books (API references) have no public source repo
|
|
277
|
+
return {
|
|
278
|
+
level: "skip",
|
|
279
|
+
message: `Inter-book link target book [${doc_id}] exists but has no source repo - article not verified: ${link}`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let article;
|
|
284
|
+
try {
|
|
285
|
+
article = await resolve_article(book.repo, doc_id, article_path);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
return {
|
|
288
|
+
level: "warning",
|
|
289
|
+
message: `Unable to verify inter-book link [${link}]: ${e}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (article.unverifiable) {
|
|
293
|
+
return {
|
|
294
|
+
level: "warning",
|
|
295
|
+
message: `Unable to verify inter-book link [${link}]: ${article.unverifiable}`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (!article.found) {
|
|
299
|
+
return {
|
|
300
|
+
level: "error",
|
|
301
|
+
message: `Inter-book link target article does not exist in book [${doc_id}]: ${link}`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (hash_anchor) {
|
|
306
|
+
if (article.anchors === null) {
|
|
307
|
+
return {
|
|
308
|
+
level: "skip",
|
|
309
|
+
message: `Inter-book link resolves via redirect - hash anchor not verified: ${link}`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (!article.anchors[`hb-doc-anchor-${hash_anchor}`]) {
|
|
313
|
+
return {
|
|
314
|
+
level: "error",
|
|
315
|
+
message: `Inter-book link hash anchor not present in target article: ${link}`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { level: "ok", message: `Inter-book link verified: ${link}` };
|
|
320
|
+
};
|
|
321
|
+
})();
|
package/hdoc-validate.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
const e = require("express");
|
|
2
|
-
const { error } = require("node:console");
|
|
3
|
-
|
|
4
1
|
(() => {
|
|
5
2
|
const cheerio = require("cheerio");
|
|
6
3
|
const dns = require("node:dns");
|
|
7
4
|
const fs = require("node:fs");
|
|
8
5
|
const path = require("node:path");
|
|
9
6
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
7
|
+
const interbook = require(path.join(__dirname, "hdoc-validate-interbook.js"));
|
|
10
8
|
const translator = require("american-british-english-translator");
|
|
11
9
|
|
|
12
10
|
const spellcheck_options = {
|
|
@@ -500,6 +498,24 @@ const { error } = require("node:console");
|
|
|
500
498
|
return resp.status;
|
|
501
499
|
};
|
|
502
500
|
|
|
501
|
+
// Map an inter-book check result onto errors/warnings/messages and the
|
|
502
|
+
// validated-links cache. 'ok' and 'skip' outcomes are stable, so they are
|
|
503
|
+
// appended to validated-links.txt like any other passing link.
|
|
504
|
+
const handleInterbookResult = (result, link, htmlFile, markdown_paths, markdown_content) => {
|
|
505
|
+
if (result.level === "error") {
|
|
506
|
+
errors[htmlFile.relativePath].push(
|
|
507
|
+
processErrorMessage(result.message, markdown_paths.relativePath, markdown_content, link),
|
|
508
|
+
);
|
|
509
|
+
} else if (result.level === "warning") {
|
|
510
|
+
warnings[htmlFile.relativePath].push(
|
|
511
|
+
processErrorMessage(result.message, markdown_paths.relativePath, markdown_content, link),
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
messages[htmlFile.relativePath].push(result.message);
|
|
515
|
+
fs.appendFileSync(skip_link_file, `${link}\n`);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
503
519
|
const checkLinks = async (source_path, htmlFile, links, hdocbook_config, hdocbook_project, global_links_checked, output_links) => {
|
|
504
520
|
const markdown_paths = getMDPathFromHtmlPath(htmlFile);
|
|
505
521
|
const markdown_content = fs.readFileSync(markdown_paths.markdownPath, 'utf8');
|
|
@@ -545,9 +561,23 @@ const { error } = require("node:console");
|
|
|
545
561
|
errors[htmlFile.relativePath].push(error_message);
|
|
546
562
|
}
|
|
547
563
|
|
|
548
|
-
//
|
|
564
|
+
// Links into other books: verified against GitHub when a token
|
|
565
|
+
// was supplied, otherwise skipped (can't validate locally).
|
|
549
566
|
if ((link_segments.length > 1 && link_root !== hdocbook_config.docId) || (link_segments.length === 1 && link_root !== hdocbook_config.docId && link_root !== "index")) {
|
|
550
|
-
|
|
567
|
+
if (interbook.enabled() && path.extname(links[i].split("#")[0]) === "") {
|
|
568
|
+
const link = links[i];
|
|
569
|
+
externalChecks.push(async () =>
|
|
570
|
+
handleInterbookResult(
|
|
571
|
+
await interbook.check_link(link),
|
|
572
|
+
link,
|
|
573
|
+
htmlFile,
|
|
574
|
+
markdown_paths,
|
|
575
|
+
markdown_content,
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
} else {
|
|
579
|
+
fs.appendFileSync(skip_link_file, `${links[i]}\n`);
|
|
580
|
+
}
|
|
551
581
|
continue;
|
|
552
582
|
}
|
|
553
583
|
isRelativePath(source_path, htmlFile, links[i]);
|
|
@@ -611,6 +641,29 @@ const { error } = require("node:console");
|
|
|
611
641
|
continue;
|
|
612
642
|
}
|
|
613
643
|
|
|
644
|
+
// Fully-qualified Hornbill Docs links (only permitted in _inline
|
|
645
|
+
// content — anything else errored above): with a GitHub token,
|
|
646
|
+
// validate book/article/anchor instead of a plain HTTP check.
|
|
647
|
+
const link_host = valid_url.hostname ? valid_url.hostname.toLowerCase() : "";
|
|
648
|
+
if (
|
|
649
|
+
interbook.enabled() &&
|
|
650
|
+
(link_host === "docs.hornbill.com" || link_host === "docs-internal.hornbill.com") &&
|
|
651
|
+
path.extname(valid_url.pathname) === ""
|
|
652
|
+
) {
|
|
653
|
+
const link = links[i];
|
|
654
|
+
const book_link = valid_url.pathname + valid_url.hash;
|
|
655
|
+
externalChecks.push(async () =>
|
|
656
|
+
handleInterbookResult(
|
|
657
|
+
await interbook.check_link(book_link),
|
|
658
|
+
link,
|
|
659
|
+
htmlFile,
|
|
660
|
+
markdown_paths,
|
|
661
|
+
markdown_content,
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
614
667
|
// Capture url in closure for the async check below
|
|
615
668
|
const url = links[i];
|
|
616
669
|
const isInternal = url.toLowerCase().includes("internal.hornbill.com");
|
|
@@ -840,7 +893,9 @@ const { error } = require("node:console");
|
|
|
840
893
|
};
|
|
841
894
|
|
|
842
895
|
const processErrorMessage = (message, md_path, content, search) => {
|
|
843
|
-
|
|
896
|
+
// whole_link — a link that is a substring of a longer link elsewhere in
|
|
897
|
+
// the file must not be located at the longer link's position
|
|
898
|
+
const link_location = hdoc.find_string_in_string(content, search, true);
|
|
844
899
|
let error_message = message;
|
|
845
900
|
if (link_location !== null)
|
|
846
901
|
error_message = `${md_path}:${link_location.line}:${link_location.column} - ${error_message}`;
|
|
@@ -915,10 +970,16 @@ const { error } = require("node:console");
|
|
|
915
970
|
browser,
|
|
916
971
|
source_root_path,
|
|
917
972
|
output_links = true,
|
|
973
|
+
git_token = "",
|
|
918
974
|
) => {
|
|
919
975
|
console.log("Performing Validation and Building SEO Link List...");
|
|
920
976
|
redirects = gen_redirects;
|
|
921
977
|
private_repo = is_private;
|
|
978
|
+
interbook.init(git_token);
|
|
979
|
+
if (interbook.enabled())
|
|
980
|
+
console.log(
|
|
981
|
+
"GitHub token supplied - inter-book links will be validated against GitHub",
|
|
982
|
+
);
|
|
922
983
|
|
|
923
984
|
// Load the skip link validation file if it exists
|
|
924
985
|
loadSkipLinkValidation(source_root_path);
|
|
@@ -1132,6 +1193,18 @@ const { error } = require("node:console");
|
|
|
1132
1193
|
console.log("\r\n-----------------------");
|
|
1133
1194
|
console.log(" Validation Output ");
|
|
1134
1195
|
console.log("-----------------------");
|
|
1196
|
+
let warning_count = 0;
|
|
1197
|
+
for (const key in warnings) {
|
|
1198
|
+
if (Object.hasOwn(warnings, key) && warnings[key].length > 0) {
|
|
1199
|
+
for (let i = 0; i < warnings[key].length; i++) {
|
|
1200
|
+
console.info(`[WARNING] ${key} - ${warnings[key][i]}`);
|
|
1201
|
+
warning_count++;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (warning_count > 0) {
|
|
1206
|
+
console.info(`\r\n${warning_count} Validation Warnings Found (non-fatal)\r\n`);
|
|
1207
|
+
}
|
|
1135
1208
|
if (Object.keys(errors).length > 0) {
|
|
1136
1209
|
let error_count = 0;
|
|
1137
1210
|
for (const key in errors) {
|
package/hdoc-ver.js
CHANGED
|
@@ -1,43 +1,45 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
const fs = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
|
|
5
|
-
exports.run = (source_path) => {
|
|
6
|
-
console.log("Retrieving book version...\n");
|
|
7
|
-
|
|
8
|
-
// Get document ID
|
|
9
|
-
const hdocbook_project_config_path = path.join(
|
|
10
|
-
source_path,
|
|
11
|
-
"hdocbook-project.json",
|
|
12
|
-
);
|
|
13
|
-
let hdocbook_project;
|
|
14
|
-
try {
|
|
15
|
-
hdocbook_project =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
console.error("
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.error("
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
1
|
+
(() => {
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
exports.run = (source_path) => {
|
|
6
|
+
console.log("Retrieving book version...\n");
|
|
7
|
+
|
|
8
|
+
// Get document ID
|
|
9
|
+
const hdocbook_project_config_path = path.join(
|
|
10
|
+
source_path,
|
|
11
|
+
"hdocbook-project.json",
|
|
12
|
+
);
|
|
13
|
+
let hdocbook_project;
|
|
14
|
+
try {
|
|
15
|
+
hdocbook_project = JSON.parse(
|
|
16
|
+
fs.readFileSync(hdocbook_project_config_path, "utf8"),
|
|
17
|
+
);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error("File not found: hdocbook-project.json:");
|
|
20
|
+
console.log(e, "\n");
|
|
21
|
+
console.error("hdoc ver needs to be run in the root of a HDoc Book.\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const doc_id = hdocbook_project.docId;
|
|
25
|
+
|
|
26
|
+
const book_path = path.join(source_path, doc_id);
|
|
27
|
+
const hdocbook_path = path.join(book_path, "hdocbook.json");
|
|
28
|
+
|
|
29
|
+
let hdocbook_config;
|
|
30
|
+
try {
|
|
31
|
+
hdocbook_config = JSON.parse(fs.readFileSync(hdocbook_path, "utf8"));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error("File not found: hdocbook.json");
|
|
34
|
+
console.log(e, "\n");
|
|
35
|
+
console.error("hdoc ver needs to be run in the root of a HDoc Book.\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (hdocbook_config.version && hdocbook_config.version !== "") {
|
|
39
|
+
console.log(`Book version: ${hdocbook_config.version}\n`);
|
|
40
|
+
} else {
|
|
41
|
+
console.error("Error - this book has no version defined.\n");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
})();
|
package/hdoc.js
CHANGED
|
@@ -162,7 +162,6 @@
|
|
|
162
162
|
let gen_exclude = false;
|
|
163
163
|
let bump_type = "patch"; // To generate spellcheck exclusions for all files
|
|
164
164
|
let output_links = true;
|
|
165
|
-
let onyx_index = false;
|
|
166
165
|
let build_embeddings = true;
|
|
167
166
|
|
|
168
167
|
// Get options from command args
|
|
@@ -211,8 +210,6 @@
|
|
|
211
210
|
console_color = false;
|
|
212
211
|
} else if (process.argv[x].toLowerCase() === "--no-links") {
|
|
213
212
|
output_links = false;
|
|
214
|
-
} else if (process.argv[x].toLowerCase() === "--onyx") {
|
|
215
|
-
onyx_index = true;
|
|
216
213
|
} else if (process.argv[x].toLowerCase() === "--no-embeddings") {
|
|
217
214
|
build_embeddings = false;
|
|
218
215
|
} else if (process.argv[x].toLowerCase() === "--quiet") {
|
|
@@ -247,15 +244,19 @@
|
|
|
247
244
|
}
|
|
248
245
|
|
|
249
246
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
247
|
+
// build/validate write a validated-links.txt link cache into the book repo;
|
|
248
|
+
// make sure it's gitignored. Other commands don't touch the file, so they
|
|
249
|
+
// leave .gitignore alone.
|
|
250
|
+
if (["build", "validate"].includes(command.toLowerCase())) {
|
|
251
|
+
const gitignorePath = path.join(source_path, ".gitignore");
|
|
252
|
+
if (fs.existsSync(gitignorePath)) {
|
|
253
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
|
|
254
|
+
if (!gitignoreContent.includes("validated-links.txt")) {
|
|
255
|
+
fs.appendFileSync(gitignorePath, "\nvalidated-links.txt\n");
|
|
256
|
+
console.info("Added validated-links.txt to .gitignore");
|
|
257
|
+
}
|
|
257
258
|
}
|
|
258
|
-
}
|
|
259
|
+
}
|
|
259
260
|
|
|
260
261
|
if (command.toLowerCase() === "serve") {
|
|
261
262
|
const server = require(path.join(__dirname, "hdoc-serve.js"));
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hdoc-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hdoc-tools",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.61.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hdoc-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Hornbill HDocBook Development Support Tool",
|
|
5
5
|
"main": "hdoc.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"hdoc-build.js",
|
|
13
13
|
"hdoc-build-db.js",
|
|
14
14
|
"hdoc-build-embeddings.js",
|
|
15
|
-
"hdoc-build-onyx.js",
|
|
16
15
|
"hdoc-build-pdf.js",
|
|
17
16
|
"hdoc-bump.js",
|
|
18
17
|
"hdoc-content-routes.js",
|
|
@@ -29,6 +28,7 @@
|
|
|
29
28
|
"hdoc-toc.js",
|
|
30
29
|
"hdoc-validate.js",
|
|
31
30
|
"hdoc-validate-config.js",
|
|
31
|
+
"hdoc-validate-interbook.js",
|
|
32
32
|
"hdoc-ver.js",
|
|
33
33
|
"validateNodeVer.js",
|
|
34
34
|
".puppeteerrc.cjs",
|