hdoc-tools 0.60.0 → 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 +82 -7
- 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]);
|
|
@@ -561,8 +591,10 @@ const { error } = require("node:console");
|
|
|
561
591
|
);
|
|
562
592
|
|
|
563
593
|
// Skip if it's the auto-generated edit url, as these could be part of a private repo which would return a 404
|
|
594
|
+
// publicSource must be truthy, not just defined - an empty string makes
|
|
595
|
+
// get_github_api_path return "" which has no edit_path
|
|
564
596
|
if (
|
|
565
|
-
hdocbook_config.publicSource
|
|
597
|
+
hdocbook_config.publicSource &&
|
|
566
598
|
links[i] ===
|
|
567
599
|
hdoc
|
|
568
600
|
.get_github_api_path(
|
|
@@ -609,6 +641,29 @@ const { error } = require("node:console");
|
|
|
609
641
|
continue;
|
|
610
642
|
}
|
|
611
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
|
+
|
|
612
667
|
// Capture url in closure for the async check below
|
|
613
668
|
const url = links[i];
|
|
614
669
|
const isInternal = url.toLowerCase().includes("internal.hornbill.com");
|
|
@@ -838,7 +893,9 @@ const { error } = require("node:console");
|
|
|
838
893
|
};
|
|
839
894
|
|
|
840
895
|
const processErrorMessage = (message, md_path, content, search) => {
|
|
841
|
-
|
|
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);
|
|
842
899
|
let error_message = message;
|
|
843
900
|
if (link_location !== null)
|
|
844
901
|
error_message = `${md_path}:${link_location.line}:${link_location.column} - ${error_message}`;
|
|
@@ -913,10 +970,16 @@ const { error } = require("node:console");
|
|
|
913
970
|
browser,
|
|
914
971
|
source_root_path,
|
|
915
972
|
output_links = true,
|
|
973
|
+
git_token = "",
|
|
916
974
|
) => {
|
|
917
975
|
console.log("Performing Validation and Building SEO Link List...");
|
|
918
976
|
redirects = gen_redirects;
|
|
919
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
|
+
);
|
|
920
983
|
|
|
921
984
|
// Load the skip link validation file if it exists
|
|
922
985
|
loadSkipLinkValidation(source_root_path);
|
|
@@ -1130,6 +1193,18 @@ const { error } = require("node:console");
|
|
|
1130
1193
|
console.log("\r\n-----------------------");
|
|
1131
1194
|
console.log(" Validation Output ");
|
|
1132
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
|
+
}
|
|
1133
1208
|
if (Object.keys(errors).length > 0) {
|
|
1134
1209
|
let error_count = 0;
|
|
1135
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": {
|