hdoc-tools 0.61.0 → 0.62.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.
Files changed (45) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +89 -75
  3. package/editor/dist/assets/index-Blewr90z.css +1 -1
  4. package/editor/dist/assets/index-BtxvGZHW.js +111 -111
  5. package/editor/dist/assets/spell.worker-sryEKmOj.js +13 -13
  6. package/editor/dist/index.html +14 -14
  7. package/hdoc-build-db.js +275 -275
  8. package/hdoc-build-embeddings.js +202 -202
  9. package/hdoc-build-pdf.js +232 -232
  10. package/hdoc-bump.js +125 -125
  11. package/hdoc-create.js +110 -110
  12. package/hdoc-db.js +114 -114
  13. package/hdoc-help.js +60 -60
  14. package/hdoc-install-browser.js +145 -145
  15. package/hdoc-mermaid.js +204 -204
  16. package/hdoc-module.js +1102 -1102
  17. package/hdoc-validate-config.js +355 -355
  18. package/hdoc-validate-interbook.js +321 -321
  19. package/hdoc-validate.js +1231 -1231
  20. package/hdoc-ver.js +45 -45
  21. package/package.json +12 -1
  22. package/templates/doc-header-non-git.html +19 -19
  23. package/templates/doc-header.html +26 -26
  24. package/templates/init/.github/workflows/hdocbuild_onpull.yml +16 -16
  25. package/templates/init/.github/workflows/hdocbuild_onpush.yml +15 -15
  26. package/templates/init/LICENSE +21 -21
  27. package/templates/init/README.md +9 -9
  28. package/templates/init/_hdocbook/index.md +4 -4
  29. package/templates/init/gitignore +8 -8
  30. package/templates/init/resources/README.md +2 -2
  31. package/templates/pdf/css/custom-block.css +90 -90
  32. package/templates/pdf/css/fonts.css +221 -221
  33. package/templates/pdf/css/hdocs-pdf.css +495 -495
  34. package/templates/pdf/css/vars.css +404 -404
  35. package/templates/pdf/template-footer.html +19 -19
  36. package/templates/pdf/template-header.html +37 -37
  37. package/templates/pdf/template.html +20 -20
  38. package/templates/pdf-header-non-git.html +12 -12
  39. package/templates/pdf-header.html +16 -16
  40. package/ui/content/invalid-hdocbook-json.html +6 -6
  41. package/ui/content/invalid-hdocbook-json.md +7 -7
  42. package/ui/css/theme-default/styles/components/content.css +124 -124
  43. package/ui/css/theme-default/styles/components/sidebar.css +182 -182
  44. package/ui/css/theme-default/styles/htldoc.layouts.css +310 -310
  45. package/ui/index.html +419 -419
@@ -1,355 +1,355 @@
1
- (() => {
2
- const path = require("node:path");
3
- const regex_doc_id = /^[a-z][a-z0-9-]+$/;
4
- const regex_version = /^[0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,6}$/;
5
-
6
- const hdocbook_schema = require(path.join(__dirname, "schemas", "hdocbook.schema.json"));
7
- const valid_audience = hdocbook_schema.properties.audience.items.enum;
8
- const valid_product_families = hdocbook_schema.properties.productFamily.enum;
9
- const valid_book_types = hdocbook_schema.properties.bookType.enum;
10
-
11
- const project_schema = require(path.join(__dirname, "schemas", "hdocbook-project.schema.json"));
12
- const project_top_keys = Object.keys(project_schema.properties);
13
- const project_pdf_keys = Object.keys(project_schema.properties.pdfGeneration.properties);
14
- const project_validation_keys = Object.keys(project_schema.properties.validation.properties);
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);
17
- const redirect_valid_codes = project_schema.properties.redirects.items.properties.code.enum;
18
-
19
- // Reports any keys in obj that are not in the allowed set.
20
- const check_extra_keys = (obj, allowed, path, file, errors) => {
21
- for (const key of Object.keys(obj)) {
22
- if (!allowed.includes(key)) {
23
- errors.push(`${file}: "${path ? path + '.' : ''}${key}" is not a recognised property`);
24
- }
25
- }
26
- };
27
-
28
- const validate_nav_item = (item, path, errors, depth) => {
29
- if (typeof item !== 'object' || Array.isArray(item) || item === null) {
30
- errors.push(`hdocbook.json: "${path}" must be an object`);
31
- return;
32
- }
33
- check_extra_keys(item, ['text', 'link', 'expand', 'draft', 'items', 'id'], path, 'hdocbook.json', errors);
34
- if (!item.text || typeof item.text !== 'string') {
35
- errors.push(`hdocbook.json: "${path}.text" is required and must be a non-empty string`);
36
- }
37
- if (item.id !== undefined && typeof item.id !== 'string') {
38
- errors.push(`hdocbook.json: "${path}.id" must be a string`);
39
- }
40
- if (item.link !== undefined && typeof item.link !== 'string') {
41
- errors.push(`hdocbook.json: "${path}.link" must be a string`);
42
- }
43
- if (item.expand !== undefined && typeof item.expand !== 'boolean') {
44
- errors.push(`hdocbook.json: "${path}.expand" must be a boolean`);
45
- }
46
- if (item.draft !== undefined && typeof item.draft !== 'boolean') {
47
- errors.push(`hdocbook.json: "${path}.draft" must be a boolean`);
48
- }
49
- if (item.items !== undefined) {
50
- if (!Array.isArray(item.items)) {
51
- errors.push(`hdocbook.json: "${path}.items" must be an array`);
52
- } else if (depth >= 5) {
53
- errors.push(`hdocbook.json: "${path}.items" exceeds the maximum navigation nesting depth of 5 levels`);
54
- } else {
55
- for (let i = 0; i < item.items.length; i++) {
56
- validate_nav_item(item.items[i], `${path}.items[${i}]`, errors, depth + 1);
57
- }
58
- }
59
- }
60
- };
61
-
62
- // Validates the structure of hdocbook-project.json
63
- const validate_project = (config) => {
64
- const errors = [];
65
- const file = 'hdocbook-project.json';
66
-
67
- check_extra_keys(config, project_top_keys, '', file, errors);
68
-
69
- if (!config.docId || typeof config.docId !== 'string') {
70
- errors.push(`${file}: "docId" is required and must be a string`);
71
- } else if (!regex_doc_id.test(config.docId)) {
72
- errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
73
- }
74
-
75
- if (config.pdfGeneration !== undefined) {
76
- if (typeof config.pdfGeneration !== 'object' || Array.isArray(config.pdfGeneration)) {
77
- errors.push(`${file}: "pdfGeneration" must be an object`);
78
- } else {
79
- check_extra_keys(config.pdfGeneration, project_pdf_keys, 'pdfGeneration', file, errors);
80
- if (config.pdfGeneration.enable === undefined) {
81
- errors.push(`${file}: "pdfGeneration.enable" is required`);
82
- } else if (typeof config.pdfGeneration.enable !== 'boolean') {
83
- errors.push(`${file}: "pdfGeneration.enable" must be a boolean`);
84
- }
85
- if (config.pdfGeneration.exclude_paths !== undefined) {
86
- if (!Array.isArray(config.pdfGeneration.exclude_paths)) {
87
- errors.push(`${file}: "pdfGeneration.exclude_paths" must be an array`);
88
- } else {
89
- for (let i = 0; i < config.pdfGeneration.exclude_paths.length; i++) {
90
- if (typeof config.pdfGeneration.exclude_paths[i] !== 'string') {
91
- errors.push(`${file}: "pdfGeneration.exclude_paths[${i}]" must be a string`);
92
- }
93
- }
94
- }
95
- }
96
- }
97
- }
98
-
99
- if (config.validation !== undefined) {
100
- if (typeof config.validation !== 'object' || Array.isArray(config.validation)) {
101
- errors.push(`${file}: "validation" must be an object`);
102
- } else {
103
- check_extra_keys(config.validation, project_validation_keys, 'validation', file, errors);
104
- if (config.validation.exclude_links !== undefined) {
105
- if (!Array.isArray(config.validation.exclude_links)) {
106
- errors.push(`${file}: "validation.exclude_links" must be an array`);
107
- } else {
108
- for (let i = 0; i < config.validation.exclude_links.length; i++) {
109
- if (typeof config.validation.exclude_links[i] !== 'string') {
110
- errors.push(`${file}: "validation.exclude_links[${i}]" must be a string`);
111
- }
112
- }
113
- }
114
- }
115
- if (config.validation.exclude_spellcheck !== undefined) {
116
- if (!Array.isArray(config.validation.exclude_spellcheck)) {
117
- errors.push(`${file}: "validation.exclude_spellcheck" must be an array`);
118
- } else {
119
- for (let i = 0; i < config.validation.exclude_spellcheck.length; i++) {
120
- const item = config.validation.exclude_spellcheck[i];
121
- if (typeof item !== 'object' || Array.isArray(item) || item === null) {
122
- errors.push(`${file}: "validation.exclude_spellcheck[${i}]" must be an object`);
123
- } else {
124
- check_extra_keys(item, ['document_path', 'words'], `validation.exclude_spellcheck[${i}]`, file, errors);
125
- if (!item.document_path || typeof item.document_path !== 'string') {
126
- errors.push(`${file}: "validation.exclude_spellcheck[${i}].document_path" is required and must be a non-empty string`);
127
- }
128
- if (!Array.isArray(item.words)) {
129
- errors.push(`${file}: "validation.exclude_spellcheck[${i}].words" is required and must be an array`);
130
- } else {
131
- for (let j = 0; j < item.words.length; j++) {
132
- if (typeof item.words[j] !== 'string') {
133
- errors.push(`${file}: "validation.exclude_spellcheck[${i}].words[${j}]" must be a string`);
134
- }
135
- }
136
- }
137
- }
138
- }
139
- }
140
- }
141
- if (config.validation.exclude_h1_count !== undefined) {
142
- if (!Array.isArray(config.validation.exclude_h1_count)) {
143
- errors.push(`${file}: "validation.exclude_h1_count" must be an array`);
144
- } else {
145
- for (let i = 0; i < config.validation.exclude_h1_count.length; i++) {
146
- if (typeof config.validation.exclude_h1_count[i] !== 'string') {
147
- errors.push(`${file}: "validation.exclude_h1_count[${i}]" must be a string`);
148
- }
149
- }
150
- }
151
- }
152
- if (config.validation.external_link_warnings !== undefined &&
153
- typeof config.validation.external_link_warnings !== 'boolean') {
154
- errors.push(`${file}: "validation.external_link_warnings" must be a boolean`);
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
- }
181
- }
182
- }
183
-
184
- if (config.redirects !== undefined) {
185
- if (!Array.isArray(config.redirects)) {
186
- errors.push(`${file}: "redirects" must be an array`);
187
- } else {
188
- for (let i = 0; i < config.redirects.length; i++) {
189
- const r = config.redirects[i];
190
- if (typeof r !== 'object' || Array.isArray(r) || r === null) {
191
- errors.push(`${file}: "redirects[${i}]" must be an object`);
192
- } else {
193
- check_extra_keys(r, project_redirect_keys, `redirects[${i}]`, file, errors);
194
- if (!r.url || typeof r.url !== 'string') {
195
- errors.push(`${file}: "redirects[${i}].url" is required and must be a non-empty string`);
196
- }
197
- if (r.code === undefined) {
198
- errors.push(`${file}: "redirects[${i}].code" is required`);
199
- } else if (!redirect_valid_codes.includes(r.code)) {
200
- errors.push(`${file}: "redirects[${i}].code" must be ${redirect_valid_codes.join(', ')} (got ${r.code})`);
201
- }
202
- if (r.location !== undefined && typeof r.location !== 'string') {
203
- errors.push(`${file}: "redirects[${i}].location" must be a string`);
204
- }
205
- if (r.skip_location_validation !== undefined && typeof r.skip_location_validation !== 'boolean') {
206
- errors.push(`${file}: "redirects[${i}].skip_location_validation" must be a boolean`);
207
- }
208
- }
209
- }
210
- }
211
- }
212
-
213
- return errors;
214
- };
215
-
216
- // Validates the structure of hdocbook.json
217
- const validate_book = (config, expected_doc_id) => {
218
- const errors = [];
219
- const file = 'hdocbook.json';
220
-
221
- check_extra_keys(config,
222
- ['docId', 'title', 'description', 'publicSource', 'version', 'productFamily', 'bookType',
223
- 'coverImage', 'tags', 'audience', 'languages', 'readingTime', 'navigation', 'inline'],
224
- '', file, errors);
225
-
226
- if (!config.docId || typeof config.docId !== 'string') {
227
- errors.push(`${file}: "docId" is required and must be a string`);
228
- } else {
229
- if (!regex_doc_id.test(config.docId)) {
230
- errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
231
- }
232
- if (expected_doc_id && config.docId !== expected_doc_id) {
233
- errors.push(`${file}: "docId" value "${config.docId}" does not match the folder name and the docId in hdocbook-project.json ("${expected_doc_id}")`);
234
- }
235
- }
236
-
237
- if (!config.title || typeof config.title !== 'string') {
238
- errors.push(`${file}: "title" is required and must be a non-empty string`);
239
- }
240
-
241
- if (!config.version || typeof config.version !== 'string') {
242
- errors.push(`${file}: "version" is required and must be a string`);
243
- } else if (!regex_version.test(config.version)) {
244
- errors.push(`${file}: "version" must be in X.Y.Z format (got "${config.version}")`);
245
- }
246
-
247
- if (!config.productFamily || typeof config.productFamily !== 'string') {
248
- errors.push(`${file}: "productFamily" is required and must be a string`);
249
- } else if (!valid_product_families.includes(config.productFamily)) {
250
- errors.push(`${file}: "productFamily" value "${config.productFamily}" is not recognised. Valid values: ${valid_product_families.join(', ')}`);
251
- }
252
-
253
- if (!config.audience || !Array.isArray(config.audience) || config.audience.length === 0) {
254
- errors.push(`${file}: "audience" is required and must be a non-empty array`);
255
- } else {
256
- for (let i = 0; i < config.audience.length; i++) {
257
- if (typeof config.audience[i] !== 'string') {
258
- errors.push(`${file}: "audience[${i}]" must be a string`);
259
- } else if (!valid_audience.includes(config.audience[i])) {
260
- errors.push(`${file}: "audience[${i}]" value "${config.audience[i]}" is not recognised. Valid values: ${valid_audience.join(', ')}`);
261
- }
262
- }
263
- }
264
-
265
- if (!config.navigation || typeof config.navigation !== 'object' || Array.isArray(config.navigation)) {
266
- errors.push(`${file}: "navigation" is required and must be an object`);
267
- } else {
268
- check_extra_keys(config.navigation, ['items'], 'navigation', file, errors);
269
- if (!Array.isArray(config.navigation.items)) {
270
- errors.push(`${file}: "navigation.items" is required and must be an array`);
271
- } else if (config.navigation.items.length === 0) {
272
- errors.push(`${file}: "navigation.items" must not be empty`);
273
- } else {
274
- for (let i = 0; i < config.navigation.items.length; i++) {
275
- validate_nav_item(config.navigation.items[i], `navigation.items[${i}]`, errors, 1);
276
- }
277
- }
278
- }
279
-
280
- if (config.description !== undefined && typeof config.description !== 'string') {
281
- errors.push(`${file}: "description" must be a string`);
282
- }
283
-
284
- if (config.publicSource !== undefined && config.publicSource !== '') {
285
- if (typeof config.publicSource !== 'string') {
286
- errors.push(`${file}: "publicSource" must be a string`);
287
- } else if (config.publicSource.toLowerCase() === '--publicsource--') {
288
- errors.push(`${file}: "publicSource" is still set to its default template value`);
289
- } else if (
290
- !config.publicSource.startsWith('https://github.com') &&
291
- !config.publicSource.startsWith('https://api.github.com')
292
- ) {
293
- errors.push(`${file}: "publicSource" must be a GitHub URL starting with https://github.com or https://api.github.com (got "${config.publicSource}")`);
294
- }
295
- }
296
-
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)`);
299
- }
300
-
301
- if (config.coverImage !== undefined && typeof config.coverImage !== 'string') {
302
- errors.push(`${file}: "coverImage" must be a string`);
303
- }
304
-
305
- if (config.tags !== undefined) {
306
- if (!Array.isArray(config.tags)) {
307
- errors.push(`${file}: "tags" must be an array`);
308
- } else {
309
- for (let i = 0; i < config.tags.length; i++) {
310
- if (typeof config.tags[i] !== 'string') {
311
- errors.push(`${file}: "tags[${i}]" must be a string`);
312
- }
313
- }
314
- }
315
- }
316
-
317
- if (config.languages !== undefined) {
318
- if (!Array.isArray(config.languages)) {
319
- errors.push(`${file}: "languages" must be an array`);
320
- } else {
321
- for (let i = 0; i < config.languages.length; i++) {
322
- if (typeof config.languages[i] !== 'string') {
323
- errors.push(`${file}: "languages[${i}]" must be a string`);
324
- }
325
- }
326
- }
327
- }
328
-
329
- if (config.inline !== undefined) {
330
- if (!Array.isArray(config.inline)) {
331
- errors.push(`${file}: "inline" must be an array`);
332
- } else {
333
- for (let i = 0; i < config.inline.length; i++) {
334
- const item = config.inline[i];
335
- if (typeof item !== 'object' || Array.isArray(item) || item === null) {
336
- errors.push(`${file}: "inline[${i}]" must be an object`);
337
- } else {
338
- check_extra_keys(item, ['title', 'link'], `inline[${i}]`, file, errors);
339
- if (!item.title || typeof item.title !== 'string') {
340
- errors.push(`${file}: "inline[${i}].title" is required and must be a non-empty string`);
341
- }
342
- if (!item.link || typeof item.link !== 'string') {
343
- errors.push(`${file}: "inline[${i}].link" is required and must be a non-empty string`);
344
- }
345
- }
346
- }
347
- }
348
- }
349
-
350
- return errors;
351
- };
352
-
353
- exports.validate_project = validate_project;
354
- exports.validate_book = validate_book;
355
- })();
1
+ (() => {
2
+ const path = require("node:path");
3
+ const regex_doc_id = /^[a-z][a-z0-9-]+$/;
4
+ const regex_version = /^[0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,6}$/;
5
+
6
+ const hdocbook_schema = require(path.join(__dirname, "schemas", "hdocbook.schema.json"));
7
+ const valid_audience = hdocbook_schema.properties.audience.items.enum;
8
+ const valid_product_families = hdocbook_schema.properties.productFamily.enum;
9
+ const valid_book_types = hdocbook_schema.properties.bookType.enum;
10
+
11
+ const project_schema = require(path.join(__dirname, "schemas", "hdocbook-project.schema.json"));
12
+ const project_top_keys = Object.keys(project_schema.properties);
13
+ const project_pdf_keys = Object.keys(project_schema.properties.pdfGeneration.properties);
14
+ const project_validation_keys = Object.keys(project_schema.properties.validation.properties);
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);
17
+ const redirect_valid_codes = project_schema.properties.redirects.items.properties.code.enum;
18
+
19
+ // Reports any keys in obj that are not in the allowed set.
20
+ const check_extra_keys = (obj, allowed, path, file, errors) => {
21
+ for (const key of Object.keys(obj)) {
22
+ if (!allowed.includes(key)) {
23
+ errors.push(`${file}: "${path ? path + '.' : ''}${key}" is not a recognised property`);
24
+ }
25
+ }
26
+ };
27
+
28
+ const validate_nav_item = (item, path, errors, depth) => {
29
+ if (typeof item !== 'object' || Array.isArray(item) || item === null) {
30
+ errors.push(`hdocbook.json: "${path}" must be an object`);
31
+ return;
32
+ }
33
+ check_extra_keys(item, ['text', 'link', 'expand', 'draft', 'items', 'id'], path, 'hdocbook.json', errors);
34
+ if (!item.text || typeof item.text !== 'string') {
35
+ errors.push(`hdocbook.json: "${path}.text" is required and must be a non-empty string`);
36
+ }
37
+ if (item.id !== undefined && typeof item.id !== 'string') {
38
+ errors.push(`hdocbook.json: "${path}.id" must be a string`);
39
+ }
40
+ if (item.link !== undefined && typeof item.link !== 'string') {
41
+ errors.push(`hdocbook.json: "${path}.link" must be a string`);
42
+ }
43
+ if (item.expand !== undefined && typeof item.expand !== 'boolean') {
44
+ errors.push(`hdocbook.json: "${path}.expand" must be a boolean`);
45
+ }
46
+ if (item.draft !== undefined && typeof item.draft !== 'boolean') {
47
+ errors.push(`hdocbook.json: "${path}.draft" must be a boolean`);
48
+ }
49
+ if (item.items !== undefined) {
50
+ if (!Array.isArray(item.items)) {
51
+ errors.push(`hdocbook.json: "${path}.items" must be an array`);
52
+ } else if (depth >= 5) {
53
+ errors.push(`hdocbook.json: "${path}.items" exceeds the maximum navigation nesting depth of 5 levels`);
54
+ } else {
55
+ for (let i = 0; i < item.items.length; i++) {
56
+ validate_nav_item(item.items[i], `${path}.items[${i}]`, errors, depth + 1);
57
+ }
58
+ }
59
+ }
60
+ };
61
+
62
+ // Validates the structure of hdocbook-project.json
63
+ const validate_project = (config) => {
64
+ const errors = [];
65
+ const file = 'hdocbook-project.json';
66
+
67
+ check_extra_keys(config, project_top_keys, '', file, errors);
68
+
69
+ if (!config.docId || typeof config.docId !== 'string') {
70
+ errors.push(`${file}: "docId" is required and must be a string`);
71
+ } else if (!regex_doc_id.test(config.docId)) {
72
+ errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
73
+ }
74
+
75
+ if (config.pdfGeneration !== undefined) {
76
+ if (typeof config.pdfGeneration !== 'object' || Array.isArray(config.pdfGeneration)) {
77
+ errors.push(`${file}: "pdfGeneration" must be an object`);
78
+ } else {
79
+ check_extra_keys(config.pdfGeneration, project_pdf_keys, 'pdfGeneration', file, errors);
80
+ if (config.pdfGeneration.enable === undefined) {
81
+ errors.push(`${file}: "pdfGeneration.enable" is required`);
82
+ } else if (typeof config.pdfGeneration.enable !== 'boolean') {
83
+ errors.push(`${file}: "pdfGeneration.enable" must be a boolean`);
84
+ }
85
+ if (config.pdfGeneration.exclude_paths !== undefined) {
86
+ if (!Array.isArray(config.pdfGeneration.exclude_paths)) {
87
+ errors.push(`${file}: "pdfGeneration.exclude_paths" must be an array`);
88
+ } else {
89
+ for (let i = 0; i < config.pdfGeneration.exclude_paths.length; i++) {
90
+ if (typeof config.pdfGeneration.exclude_paths[i] !== 'string') {
91
+ errors.push(`${file}: "pdfGeneration.exclude_paths[${i}]" must be a string`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ if (config.validation !== undefined) {
100
+ if (typeof config.validation !== 'object' || Array.isArray(config.validation)) {
101
+ errors.push(`${file}: "validation" must be an object`);
102
+ } else {
103
+ check_extra_keys(config.validation, project_validation_keys, 'validation', file, errors);
104
+ if (config.validation.exclude_links !== undefined) {
105
+ if (!Array.isArray(config.validation.exclude_links)) {
106
+ errors.push(`${file}: "validation.exclude_links" must be an array`);
107
+ } else {
108
+ for (let i = 0; i < config.validation.exclude_links.length; i++) {
109
+ if (typeof config.validation.exclude_links[i] !== 'string') {
110
+ errors.push(`${file}: "validation.exclude_links[${i}]" must be a string`);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ if (config.validation.exclude_spellcheck !== undefined) {
116
+ if (!Array.isArray(config.validation.exclude_spellcheck)) {
117
+ errors.push(`${file}: "validation.exclude_spellcheck" must be an array`);
118
+ } else {
119
+ for (let i = 0; i < config.validation.exclude_spellcheck.length; i++) {
120
+ const item = config.validation.exclude_spellcheck[i];
121
+ if (typeof item !== 'object' || Array.isArray(item) || item === null) {
122
+ errors.push(`${file}: "validation.exclude_spellcheck[${i}]" must be an object`);
123
+ } else {
124
+ check_extra_keys(item, ['document_path', 'words'], `validation.exclude_spellcheck[${i}]`, file, errors);
125
+ if (!item.document_path || typeof item.document_path !== 'string') {
126
+ errors.push(`${file}: "validation.exclude_spellcheck[${i}].document_path" is required and must be a non-empty string`);
127
+ }
128
+ if (!Array.isArray(item.words)) {
129
+ errors.push(`${file}: "validation.exclude_spellcheck[${i}].words" is required and must be an array`);
130
+ } else {
131
+ for (let j = 0; j < item.words.length; j++) {
132
+ if (typeof item.words[j] !== 'string') {
133
+ errors.push(`${file}: "validation.exclude_spellcheck[${i}].words[${j}]" must be a string`);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ if (config.validation.exclude_h1_count !== undefined) {
142
+ if (!Array.isArray(config.validation.exclude_h1_count)) {
143
+ errors.push(`${file}: "validation.exclude_h1_count" must be an array`);
144
+ } else {
145
+ for (let i = 0; i < config.validation.exclude_h1_count.length; i++) {
146
+ if (typeof config.validation.exclude_h1_count[i] !== 'string') {
147
+ errors.push(`${file}: "validation.exclude_h1_count[${i}]" must be a string`);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ if (config.validation.external_link_warnings !== undefined &&
153
+ typeof config.validation.external_link_warnings !== 'boolean') {
154
+ errors.push(`${file}: "validation.external_link_warnings" must be a boolean`);
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
+ }
181
+ }
182
+ }
183
+
184
+ if (config.redirects !== undefined) {
185
+ if (!Array.isArray(config.redirects)) {
186
+ errors.push(`${file}: "redirects" must be an array`);
187
+ } else {
188
+ for (let i = 0; i < config.redirects.length; i++) {
189
+ const r = config.redirects[i];
190
+ if (typeof r !== 'object' || Array.isArray(r) || r === null) {
191
+ errors.push(`${file}: "redirects[${i}]" must be an object`);
192
+ } else {
193
+ check_extra_keys(r, project_redirect_keys, `redirects[${i}]`, file, errors);
194
+ if (!r.url || typeof r.url !== 'string') {
195
+ errors.push(`${file}: "redirects[${i}].url" is required and must be a non-empty string`);
196
+ }
197
+ if (r.code === undefined) {
198
+ errors.push(`${file}: "redirects[${i}].code" is required`);
199
+ } else if (!redirect_valid_codes.includes(r.code)) {
200
+ errors.push(`${file}: "redirects[${i}].code" must be ${redirect_valid_codes.join(', ')} (got ${r.code})`);
201
+ }
202
+ if (r.location !== undefined && typeof r.location !== 'string') {
203
+ errors.push(`${file}: "redirects[${i}].location" must be a string`);
204
+ }
205
+ if (r.skip_location_validation !== undefined && typeof r.skip_location_validation !== 'boolean') {
206
+ errors.push(`${file}: "redirects[${i}].skip_location_validation" must be a boolean`);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ return errors;
214
+ };
215
+
216
+ // Validates the structure of hdocbook.json
217
+ const validate_book = (config, expected_doc_id) => {
218
+ const errors = [];
219
+ const file = 'hdocbook.json';
220
+
221
+ check_extra_keys(config,
222
+ ['docId', 'title', 'description', 'publicSource', 'version', 'productFamily', 'bookType',
223
+ 'coverImage', 'tags', 'audience', 'languages', 'readingTime', 'navigation', 'inline'],
224
+ '', file, errors);
225
+
226
+ if (!config.docId || typeof config.docId !== 'string') {
227
+ errors.push(`${file}: "docId" is required and must be a string`);
228
+ } else {
229
+ if (!regex_doc_id.test(config.docId)) {
230
+ errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
231
+ }
232
+ if (expected_doc_id && config.docId !== expected_doc_id) {
233
+ errors.push(`${file}: "docId" value "${config.docId}" does not match the folder name and the docId in hdocbook-project.json ("${expected_doc_id}")`);
234
+ }
235
+ }
236
+
237
+ if (!config.title || typeof config.title !== 'string') {
238
+ errors.push(`${file}: "title" is required and must be a non-empty string`);
239
+ }
240
+
241
+ if (!config.version || typeof config.version !== 'string') {
242
+ errors.push(`${file}: "version" is required and must be a string`);
243
+ } else if (!regex_version.test(config.version)) {
244
+ errors.push(`${file}: "version" must be in X.Y.Z format (got "${config.version}")`);
245
+ }
246
+
247
+ if (!config.productFamily || typeof config.productFamily !== 'string') {
248
+ errors.push(`${file}: "productFamily" is required and must be a string`);
249
+ } else if (!valid_product_families.includes(config.productFamily)) {
250
+ errors.push(`${file}: "productFamily" value "${config.productFamily}" is not recognised. Valid values: ${valid_product_families.join(', ')}`);
251
+ }
252
+
253
+ if (!config.audience || !Array.isArray(config.audience) || config.audience.length === 0) {
254
+ errors.push(`${file}: "audience" is required and must be a non-empty array`);
255
+ } else {
256
+ for (let i = 0; i < config.audience.length; i++) {
257
+ if (typeof config.audience[i] !== 'string') {
258
+ errors.push(`${file}: "audience[${i}]" must be a string`);
259
+ } else if (!valid_audience.includes(config.audience[i])) {
260
+ errors.push(`${file}: "audience[${i}]" value "${config.audience[i]}" is not recognised. Valid values: ${valid_audience.join(', ')}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ if (!config.navigation || typeof config.navigation !== 'object' || Array.isArray(config.navigation)) {
266
+ errors.push(`${file}: "navigation" is required and must be an object`);
267
+ } else {
268
+ check_extra_keys(config.navigation, ['items'], 'navigation', file, errors);
269
+ if (!Array.isArray(config.navigation.items)) {
270
+ errors.push(`${file}: "navigation.items" is required and must be an array`);
271
+ } else if (config.navigation.items.length === 0) {
272
+ errors.push(`${file}: "navigation.items" must not be empty`);
273
+ } else {
274
+ for (let i = 0; i < config.navigation.items.length; i++) {
275
+ validate_nav_item(config.navigation.items[i], `navigation.items[${i}]`, errors, 1);
276
+ }
277
+ }
278
+ }
279
+
280
+ if (config.description !== undefined && typeof config.description !== 'string') {
281
+ errors.push(`${file}: "description" must be a string`);
282
+ }
283
+
284
+ if (config.publicSource !== undefined && config.publicSource !== '') {
285
+ if (typeof config.publicSource !== 'string') {
286
+ errors.push(`${file}: "publicSource" must be a string`);
287
+ } else if (config.publicSource.toLowerCase() === '--publicsource--') {
288
+ errors.push(`${file}: "publicSource" is still set to its default template value`);
289
+ } else if (
290
+ !config.publicSource.startsWith('https://github.com') &&
291
+ !config.publicSource.startsWith('https://api.github.com')
292
+ ) {
293
+ errors.push(`${file}: "publicSource" must be a GitHub URL starting with https://github.com or https://api.github.com (got "${config.publicSource}")`);
294
+ }
295
+ }
296
+
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)`);
299
+ }
300
+
301
+ if (config.coverImage !== undefined && typeof config.coverImage !== 'string') {
302
+ errors.push(`${file}: "coverImage" must be a string`);
303
+ }
304
+
305
+ if (config.tags !== undefined) {
306
+ if (!Array.isArray(config.tags)) {
307
+ errors.push(`${file}: "tags" must be an array`);
308
+ } else {
309
+ for (let i = 0; i < config.tags.length; i++) {
310
+ if (typeof config.tags[i] !== 'string') {
311
+ errors.push(`${file}: "tags[${i}]" must be a string`);
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ if (config.languages !== undefined) {
318
+ if (!Array.isArray(config.languages)) {
319
+ errors.push(`${file}: "languages" must be an array`);
320
+ } else {
321
+ for (let i = 0; i < config.languages.length; i++) {
322
+ if (typeof config.languages[i] !== 'string') {
323
+ errors.push(`${file}: "languages[${i}]" must be a string`);
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ if (config.inline !== undefined) {
330
+ if (!Array.isArray(config.inline)) {
331
+ errors.push(`${file}: "inline" must be an array`);
332
+ } else {
333
+ for (let i = 0; i < config.inline.length; i++) {
334
+ const item = config.inline[i];
335
+ if (typeof item !== 'object' || Array.isArray(item) || item === null) {
336
+ errors.push(`${file}: "inline[${i}]" must be an object`);
337
+ } else {
338
+ check_extra_keys(item, ['title', 'link'], `inline[${i}]`, file, errors);
339
+ if (!item.title || typeof item.title !== 'string') {
340
+ errors.push(`${file}: "inline[${i}].title" is required and must be a non-empty string`);
341
+ }
342
+ if (!item.link || typeof item.link !== 'string') {
343
+ errors.push(`${file}: "inline[${i}].link" is required and must be a non-empty string`);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ return errors;
351
+ };
352
+
353
+ exports.validate_project = validate_project;
354
+ exports.validate_book = validate_book;
355
+ })();