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.
@@ -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
- // Checking for internal links in other books - can't easily validate those here, returning
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
- fs.appendFileSync(skip_link_file, `${links[i]}\n`);
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 !== undefined &&
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
- const link_location = hdoc.find_string_in_string(content, search);
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 = require(hdocbook_project_config_path);
16
- } catch (e) {
17
- console.error("File not found: hdocbook-project.json:");
18
- console.log(e, "\n");
19
- console.error("hdoc ver needs to be run in the root of a HDoc Book.\n");
20
- process.exit(1);
21
- }
22
- const doc_id = hdocbook_project.docId;
23
-
24
- const book_path = path.join(source_path, doc_id);
25
- const hdocbook_path = path.join(book_path, "hdocbook.json");
26
-
27
- let hdocbook_config;
28
- try {
29
- hdocbook_config = require(hdocbook_path);
30
- } catch (e) {
31
- console.error("File not found: hdocbook.json");
32
- console.log(e, "\n");
33
- console.error("hdoc ver needs to be run in the root of a HDoc Book.\n");
34
- process.exit(1);
35
- }
36
- if (hdocbook_config.version && hdocbook_config.version !== "") {
37
- console.log(`Book version: ${hdocbook_config.version}\n`);
38
- } else {
39
- console.error("Error - this book has no version defined.\n");
40
- process.exit(1);
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
- // Add validated-links.txt to .gitignore if it doesn't exist
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");
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"));
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hdoc-tools",
3
- "version": "0.59.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.59.0",
9
+ "version": "0.61.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "ISC",
12
12
  "dependencies": {