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.
- package/LICENSE +21 -21
- package/README.md +89 -75
- 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-db.js +275 -275
- package/hdoc-build-embeddings.js +202 -202
- package/hdoc-build-pdf.js +232 -232
- package/hdoc-bump.js +125 -125
- package/hdoc-create.js +110 -110
- package/hdoc-db.js +114 -114
- package/hdoc-help.js +60 -60
- package/hdoc-install-browser.js +145 -145
- package/hdoc-mermaid.js +204 -204
- package/hdoc-module.js +1102 -1102
- package/hdoc-validate-config.js +355 -355
- package/hdoc-validate-interbook.js +321 -321
- package/hdoc-validate.js +1231 -1231
- package/hdoc-ver.js +45 -45
- package/package.json +12 -1
- package/templates/doc-header-non-git.html +19 -19
- package/templates/doc-header.html +26 -26
- package/templates/init/.github/workflows/hdocbuild_onpull.yml +16 -16
- package/templates/init/.github/workflows/hdocbuild_onpush.yml +15 -15
- package/templates/init/LICENSE +21 -21
- package/templates/init/README.md +9 -9
- package/templates/init/_hdocbook/index.md +4 -4
- package/templates/init/gitignore +8 -8
- package/templates/init/resources/README.md +2 -2
- package/templates/pdf/css/custom-block.css +90 -90
- package/templates/pdf/css/fonts.css +221 -221
- package/templates/pdf/css/hdocs-pdf.css +495 -495
- package/templates/pdf/css/vars.css +404 -404
- package/templates/pdf/template-footer.html +19 -19
- package/templates/pdf/template-header.html +37 -37
- package/templates/pdf/template.html +20 -20
- package/templates/pdf-header-non-git.html +12 -12
- package/templates/pdf-header.html +16 -16
- package/ui/content/invalid-hdocbook-json.html +6 -6
- package/ui/content/invalid-hdocbook-json.md +7 -7
- package/ui/css/theme-default/styles/components/content.css +124 -124
- package/ui/css/theme-default/styles/components/sidebar.css +182 -182
- package/ui/css/theme-default/styles/htldoc.layouts.css +310 -310
- package/ui/index.html +419 -419
package/hdoc-validate-config.js
CHANGED
|
@@ -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
|
+
})();
|