hdoc-tools 0.48.0 → 0.50.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/hdoc-build.js +28 -1
- package/hdoc-help.js +1 -0
- package/hdoc-validate-config.js +321 -0
- package/hdoc.js +84 -53
- package/package.json +3 -1
- package/schemas/hdocbook-project.schema.json +102 -0
- package/schemas/hdocbook.schema.json +146 -0
package/hdoc-build.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const path = require("node:path");
|
|
6
6
|
const puppeteer = require("puppeteer");
|
|
7
7
|
const hdoc_validate = require(path.join(__dirname, "hdoc-validate.js"));
|
|
8
|
+
const hdoc_validate_config = require(path.join(__dirname, "hdoc-validate-config.js"));
|
|
8
9
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
9
10
|
const hdoc_build_db = require(path.join(__dirname, "hdoc-build-db.js"));
|
|
10
11
|
const hdoc_build_onyx = require(path.join(__dirname, "hdoc-build-onyx.js"));
|
|
@@ -977,6 +978,17 @@
|
|
|
977
978
|
);
|
|
978
979
|
process.exit(1);
|
|
979
980
|
}
|
|
981
|
+
|
|
982
|
+
const project_config_errors = hdoc_validate_config.validate_project(hdocbook_project);
|
|
983
|
+
if (project_config_errors.length > 0) {
|
|
984
|
+
console.log("\r\n-----------------------");
|
|
985
|
+
console.log(" Validation Output ");
|
|
986
|
+
console.log("-----------------------");
|
|
987
|
+
for (const err of project_config_errors) console.error(`- ${err}`);
|
|
988
|
+
console.error(`\r\n${project_config_errors.length} Configuration Error${project_config_errors.length !== 1 ? 's' : ''} Found in hdocbook-project.json`);
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
|
|
980
992
|
doc_id = hdocbook_project.docId;
|
|
981
993
|
|
|
982
994
|
if (
|
|
@@ -1007,7 +1019,12 @@
|
|
|
1007
1019
|
const work_path = path.join(source_path, "_work");
|
|
1008
1020
|
const work_hdocbook_path = path.join(work_path, doc_id, "hdocbook.json");
|
|
1009
1021
|
|
|
1010
|
-
|
|
1022
|
+
try {
|
|
1023
|
+
hdocbook_config = require(hdocbook_path);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
console.error(`File not found: ${doc_id}/hdocbook.json\n`);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1011
1028
|
if (build_version !== "") {
|
|
1012
1029
|
if (build_version.match(regex_version)) {
|
|
1013
1030
|
hdocbook_config.version = build_version;
|
|
@@ -1018,6 +1035,16 @@
|
|
|
1018
1035
|
}
|
|
1019
1036
|
}
|
|
1020
1037
|
|
|
1038
|
+
const book_config_errors = hdoc_validate_config.validate_book(hdocbook_config, doc_id);
|
|
1039
|
+
if (book_config_errors.length > 0) {
|
|
1040
|
+
console.log("\r\n-----------------------");
|
|
1041
|
+
console.log(" Validation Output ");
|
|
1042
|
+
console.log("-----------------------");
|
|
1043
|
+
for (const err of book_config_errors) console.error(`- ${err}`);
|
|
1044
|
+
console.error(`\r\n${book_config_errors.length} Configuration Error${book_config_errors.length !== 1 ? 's' : ''} Found in ${doc_id}/hdocbook.json`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1021
1048
|
if (!hdocbook_config.version.match(regex_version)) {
|
|
1022
1049
|
console.error(
|
|
1023
1050
|
`ERROR: Version number does not match required format - ${hdocbook_config.version}\n`,
|
package/hdoc-help.js
CHANGED
|
@@ -35,6 +35,7 @@ Commands
|
|
|
35
35
|
- Use the '--set-version 1.2.3' argument to set the version number of the built book.
|
|
36
36
|
- Use the '--no-color' argument to remove any color control characters from the output.
|
|
37
37
|
- Use the '--no-links' argument to skip link output to CLI during validation.
|
|
38
|
+
- Use the '--quiet' argument to suppress most console output, and only output validation errors if they are found.
|
|
38
39
|
|
|
39
40
|
- bump
|
|
40
41
|
Updates the semantic version number of the current book. If no options are specified, then the default of patch is applied:
|
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
|
|
10
|
+
const project_schema = require(path.join(__dirname, "schemas", "hdocbook-project.schema.json"));
|
|
11
|
+
const project_top_keys = Object.keys(project_schema.properties);
|
|
12
|
+
const project_pdf_keys = Object.keys(project_schema.properties.pdfGeneration.properties);
|
|
13
|
+
const project_validation_keys = Object.keys(project_schema.properties.validation.properties);
|
|
14
|
+
const project_redirect_keys = Object.keys(project_schema.properties.redirects.items.properties);
|
|
15
|
+
const redirect_valid_codes = project_schema.properties.redirects.items.properties.code.enum;
|
|
16
|
+
|
|
17
|
+
// Reports any keys in obj that are not in the allowed set.
|
|
18
|
+
const check_extra_keys = (obj, allowed, path, file, errors) => {
|
|
19
|
+
for (const key of Object.keys(obj)) {
|
|
20
|
+
if (!allowed.includes(key)) {
|
|
21
|
+
errors.push(`${file}: "${path ? path + '.' : ''}${key}" is not a recognised property`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const validate_nav_item = (item, path, errors, depth) => {
|
|
27
|
+
if (typeof item !== 'object' || Array.isArray(item) || item === null) {
|
|
28
|
+
errors.push(`hdocbook.json: "${path}" must be an object`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
check_extra_keys(item, ['text', 'link', 'expand', 'draft', 'items'], path, 'hdocbook.json', errors);
|
|
32
|
+
if (!item.text || typeof item.text !== 'string') {
|
|
33
|
+
errors.push(`hdocbook.json: "${path}.text" is required and must be a non-empty string`);
|
|
34
|
+
}
|
|
35
|
+
if (item.link !== undefined && typeof item.link !== 'string') {
|
|
36
|
+
errors.push(`hdocbook.json: "${path}.link" must be a string`);
|
|
37
|
+
}
|
|
38
|
+
if (item.expand !== undefined && typeof item.expand !== 'boolean') {
|
|
39
|
+
errors.push(`hdocbook.json: "${path}.expand" must be a boolean`);
|
|
40
|
+
}
|
|
41
|
+
if (item.draft !== undefined && typeof item.draft !== 'boolean') {
|
|
42
|
+
errors.push(`hdocbook.json: "${path}.draft" must be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
if (item.items !== undefined) {
|
|
45
|
+
if (!Array.isArray(item.items)) {
|
|
46
|
+
errors.push(`hdocbook.json: "${path}.items" must be an array`);
|
|
47
|
+
} else if (depth >= 3) {
|
|
48
|
+
errors.push(`hdocbook.json: "${path}.items" exceeds the maximum navigation nesting depth of 3 levels`);
|
|
49
|
+
} else {
|
|
50
|
+
for (let i = 0; i < item.items.length; i++) {
|
|
51
|
+
validate_nav_item(item.items[i], `${path}.items[${i}]`, errors, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Validates the structure of hdocbook-project.json
|
|
58
|
+
const validate_project = (config) => {
|
|
59
|
+
const errors = [];
|
|
60
|
+
const file = 'hdocbook-project.json';
|
|
61
|
+
|
|
62
|
+
check_extra_keys(config, project_top_keys, '', file, errors);
|
|
63
|
+
|
|
64
|
+
if (!config.docId || typeof config.docId !== 'string') {
|
|
65
|
+
errors.push(`${file}: "docId" is required and must be a string`);
|
|
66
|
+
} else if (!regex_doc_id.test(config.docId)) {
|
|
67
|
+
errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (config.pdfGeneration !== undefined) {
|
|
71
|
+
if (typeof config.pdfGeneration !== 'object' || Array.isArray(config.pdfGeneration)) {
|
|
72
|
+
errors.push(`${file}: "pdfGeneration" must be an object`);
|
|
73
|
+
} else {
|
|
74
|
+
check_extra_keys(config.pdfGeneration, project_pdf_keys, 'pdfGeneration', file, errors);
|
|
75
|
+
if (config.pdfGeneration.enable === undefined) {
|
|
76
|
+
errors.push(`${file}: "pdfGeneration.enable" is required`);
|
|
77
|
+
} else if (typeof config.pdfGeneration.enable !== 'boolean') {
|
|
78
|
+
errors.push(`${file}: "pdfGeneration.enable" must be a boolean`);
|
|
79
|
+
}
|
|
80
|
+
if (config.pdfGeneration.exclude_paths !== undefined) {
|
|
81
|
+
if (!Array.isArray(config.pdfGeneration.exclude_paths)) {
|
|
82
|
+
errors.push(`${file}: "pdfGeneration.exclude_paths" must be an array`);
|
|
83
|
+
} else {
|
|
84
|
+
for (let i = 0; i < config.pdfGeneration.exclude_paths.length; i++) {
|
|
85
|
+
if (typeof config.pdfGeneration.exclude_paths[i] !== 'string') {
|
|
86
|
+
errors.push(`${file}: "pdfGeneration.exclude_paths[${i}]" must be a string`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (config.validation !== undefined) {
|
|
95
|
+
if (typeof config.validation !== 'object' || Array.isArray(config.validation)) {
|
|
96
|
+
errors.push(`${file}: "validation" must be an object`);
|
|
97
|
+
} else {
|
|
98
|
+
check_extra_keys(config.validation, project_validation_keys, 'validation', file, errors);
|
|
99
|
+
if (config.validation.exclude_links !== undefined) {
|
|
100
|
+
if (!Array.isArray(config.validation.exclude_links)) {
|
|
101
|
+
errors.push(`${file}: "validation.exclude_links" must be an array`);
|
|
102
|
+
} else {
|
|
103
|
+
for (let i = 0; i < config.validation.exclude_links.length; i++) {
|
|
104
|
+
if (typeof config.validation.exclude_links[i] !== 'string') {
|
|
105
|
+
errors.push(`${file}: "validation.exclude_links[${i}]" must be a string`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (config.validation.exclude_spellcheck !== undefined) {
|
|
111
|
+
if (!Array.isArray(config.validation.exclude_spellcheck)) {
|
|
112
|
+
errors.push(`${file}: "validation.exclude_spellcheck" must be an array`);
|
|
113
|
+
} else {
|
|
114
|
+
for (let i = 0; i < config.validation.exclude_spellcheck.length; i++) {
|
|
115
|
+
const item = config.validation.exclude_spellcheck[i];
|
|
116
|
+
if (typeof item !== 'object' || Array.isArray(item) || item === null) {
|
|
117
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}]" must be an object`);
|
|
118
|
+
} else {
|
|
119
|
+
check_extra_keys(item, ['document_path', 'words'], `validation.exclude_spellcheck[${i}]`, file, errors);
|
|
120
|
+
if (!item.document_path || typeof item.document_path !== 'string') {
|
|
121
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}].document_path" is required and must be a non-empty string`);
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(item.words)) {
|
|
124
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}].words" is required and must be an array`);
|
|
125
|
+
} else {
|
|
126
|
+
for (let j = 0; j < item.words.length; j++) {
|
|
127
|
+
if (typeof item.words[j] !== 'string') {
|
|
128
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}].words[${j}]" must be a string`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (config.validation.exclude_h1_count !== undefined) {
|
|
137
|
+
if (!Array.isArray(config.validation.exclude_h1_count)) {
|
|
138
|
+
errors.push(`${file}: "validation.exclude_h1_count" must be an array`);
|
|
139
|
+
} else {
|
|
140
|
+
for (let i = 0; i < config.validation.exclude_h1_count.length; i++) {
|
|
141
|
+
if (typeof config.validation.exclude_h1_count[i] !== 'string') {
|
|
142
|
+
errors.push(`${file}: "validation.exclude_h1_count[${i}]" must be a string`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (config.validation.external_link_warnings !== undefined &&
|
|
148
|
+
typeof config.validation.external_link_warnings !== 'boolean') {
|
|
149
|
+
errors.push(`${file}: "validation.external_link_warnings" must be a boolean`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.redirects !== undefined) {
|
|
155
|
+
if (!Array.isArray(config.redirects)) {
|
|
156
|
+
errors.push(`${file}: "redirects" must be an array`);
|
|
157
|
+
} else {
|
|
158
|
+
for (let i = 0; i < config.redirects.length; i++) {
|
|
159
|
+
const r = config.redirects[i];
|
|
160
|
+
if (typeof r !== 'object' || Array.isArray(r) || r === null) {
|
|
161
|
+
errors.push(`${file}: "redirects[${i}]" must be an object`);
|
|
162
|
+
} else {
|
|
163
|
+
check_extra_keys(r, project_redirect_keys, `redirects[${i}]`, file, errors);
|
|
164
|
+
if (!r.url || typeof r.url !== 'string') {
|
|
165
|
+
errors.push(`${file}: "redirects[${i}].url" is required and must be a non-empty string`);
|
|
166
|
+
}
|
|
167
|
+
if (r.code === undefined) {
|
|
168
|
+
errors.push(`${file}: "redirects[${i}].code" is required`);
|
|
169
|
+
} else if (!redirect_valid_codes.includes(r.code)) {
|
|
170
|
+
errors.push(`${file}: "redirects[${i}].code" must be ${redirect_valid_codes.join(', ')} (got ${r.code})`);
|
|
171
|
+
}
|
|
172
|
+
if (r.location !== undefined && typeof r.location !== 'string') {
|
|
173
|
+
errors.push(`${file}: "redirects[${i}].location" must be a string`);
|
|
174
|
+
}
|
|
175
|
+
if (r.skip_location_validation !== undefined && typeof r.skip_location_validation !== 'boolean') {
|
|
176
|
+
errors.push(`${file}: "redirects[${i}].skip_location_validation" must be a boolean`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return errors;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Validates the structure of hdocbook.json
|
|
187
|
+
const validate_book = (config, expected_doc_id) => {
|
|
188
|
+
const errors = [];
|
|
189
|
+
const file = 'hdocbook.json';
|
|
190
|
+
|
|
191
|
+
check_extra_keys(config,
|
|
192
|
+
['docId', 'title', 'description', 'publicSource', 'version', 'productFamily',
|
|
193
|
+
'coverImage', 'tags', 'audience', 'languages', 'readingTime', 'navigation', 'inline'],
|
|
194
|
+
'', file, errors);
|
|
195
|
+
|
|
196
|
+
if (!config.docId || typeof config.docId !== 'string') {
|
|
197
|
+
errors.push(`${file}: "docId" is required and must be a string`);
|
|
198
|
+
} else {
|
|
199
|
+
if (!regex_doc_id.test(config.docId)) {
|
|
200
|
+
errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
|
|
201
|
+
}
|
|
202
|
+
if (expected_doc_id && config.docId !== expected_doc_id) {
|
|
203
|
+
errors.push(`${file}: "docId" value "${config.docId}" does not match the folder name and the docId in hdocbook-project.json ("${expected_doc_id}")`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!config.title || typeof config.title !== 'string') {
|
|
208
|
+
errors.push(`${file}: "title" is required and must be a non-empty string`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!config.version || typeof config.version !== 'string') {
|
|
212
|
+
errors.push(`${file}: "version" is required and must be a string`);
|
|
213
|
+
} else if (!regex_version.test(config.version)) {
|
|
214
|
+
errors.push(`${file}: "version" must be in X.Y.Z format (got "${config.version}")`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!config.productFamily || typeof config.productFamily !== 'string') {
|
|
218
|
+
errors.push(`${file}: "productFamily" is required and must be a string`);
|
|
219
|
+
} else if (!valid_product_families.includes(config.productFamily)) {
|
|
220
|
+
errors.push(`${file}: "productFamily" value "${config.productFamily}" is not recognised. Valid values: ${valid_product_families.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!config.audience || !Array.isArray(config.audience) || config.audience.length === 0) {
|
|
224
|
+
errors.push(`${file}: "audience" is required and must be a non-empty array`);
|
|
225
|
+
} else {
|
|
226
|
+
for (let i = 0; i < config.audience.length; i++) {
|
|
227
|
+
if (typeof config.audience[i] !== 'string') {
|
|
228
|
+
errors.push(`${file}: "audience[${i}]" must be a string`);
|
|
229
|
+
} else if (!valid_audience.includes(config.audience[i])) {
|
|
230
|
+
errors.push(`${file}: "audience[${i}]" value "${config.audience[i]}" is not recognised. Valid values: ${valid_audience.join(', ')}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!config.navigation || typeof config.navigation !== 'object' || Array.isArray(config.navigation)) {
|
|
236
|
+
errors.push(`${file}: "navigation" is required and must be an object`);
|
|
237
|
+
} else {
|
|
238
|
+
check_extra_keys(config.navigation, ['items'], 'navigation', file, errors);
|
|
239
|
+
if (!Array.isArray(config.navigation.items)) {
|
|
240
|
+
errors.push(`${file}: "navigation.items" is required and must be an array`);
|
|
241
|
+
} else if (config.navigation.items.length === 0) {
|
|
242
|
+
errors.push(`${file}: "navigation.items" must not be empty`);
|
|
243
|
+
} else {
|
|
244
|
+
for (let i = 0; i < config.navigation.items.length; i++) {
|
|
245
|
+
validate_nav_item(config.navigation.items[i], `navigation.items[${i}]`, errors, 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (config.description !== undefined && typeof config.description !== 'string') {
|
|
251
|
+
errors.push(`${file}: "description" must be a string`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (config.publicSource !== undefined && config.publicSource !== '') {
|
|
255
|
+
if (typeof config.publicSource !== 'string') {
|
|
256
|
+
errors.push(`${file}: "publicSource" must be a string`);
|
|
257
|
+
} else if (config.publicSource.toLowerCase() === '--publicsource--') {
|
|
258
|
+
errors.push(`${file}: "publicSource" is still set to its default template value`);
|
|
259
|
+
} else if (
|
|
260
|
+
!config.publicSource.startsWith('https://github.com') &&
|
|
261
|
+
!config.publicSource.startsWith('https://api.github.com')
|
|
262
|
+
) {
|
|
263
|
+
errors.push(`${file}: "publicSource" must be a GitHub URL starting with https://github.com or https://api.github.com (got "${config.publicSource}")`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (config.coverImage !== undefined && typeof config.coverImage !== 'string') {
|
|
268
|
+
errors.push(`${file}: "coverImage" must be a string`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (config.tags !== undefined) {
|
|
272
|
+
if (!Array.isArray(config.tags)) {
|
|
273
|
+
errors.push(`${file}: "tags" must be an array`);
|
|
274
|
+
} else {
|
|
275
|
+
for (let i = 0; i < config.tags.length; i++) {
|
|
276
|
+
if (typeof config.tags[i] !== 'string') {
|
|
277
|
+
errors.push(`${file}: "tags[${i}]" must be a string`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (config.languages !== undefined) {
|
|
284
|
+
if (!Array.isArray(config.languages)) {
|
|
285
|
+
errors.push(`${file}: "languages" must be an array`);
|
|
286
|
+
} else {
|
|
287
|
+
for (let i = 0; i < config.languages.length; i++) {
|
|
288
|
+
if (typeof config.languages[i] !== 'string') {
|
|
289
|
+
errors.push(`${file}: "languages[${i}]" must be a string`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (config.inline !== undefined) {
|
|
296
|
+
if (!Array.isArray(config.inline)) {
|
|
297
|
+
errors.push(`${file}: "inline" must be an array`);
|
|
298
|
+
} else {
|
|
299
|
+
for (let i = 0; i < config.inline.length; i++) {
|
|
300
|
+
const item = config.inline[i];
|
|
301
|
+
if (typeof item !== 'object' || Array.isArray(item) || item === null) {
|
|
302
|
+
errors.push(`${file}: "inline[${i}]" must be an object`);
|
|
303
|
+
} else {
|
|
304
|
+
check_extra_keys(item, ['title', 'link'], `inline[${i}]`, file, errors);
|
|
305
|
+
if (!item.title || typeof item.title !== 'string') {
|
|
306
|
+
errors.push(`${file}: "inline[${i}].title" is required and must be a non-empty string`);
|
|
307
|
+
}
|
|
308
|
+
if (!item.link || typeof item.link !== 'string') {
|
|
309
|
+
errors.push(`${file}: "inline[${i}].link" is required and must be a non-empty string`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return errors;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
exports.validate_project = validate_project;
|
|
320
|
+
exports.validate_book = validate_book;
|
|
321
|
+
})();
|
package/hdoc.js
CHANGED
|
@@ -13,69 +13,98 @@
|
|
|
13
13
|
|
|
14
14
|
let console_color = true;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return arg;
|
|
28
|
-
});
|
|
29
|
-
// Use the original console.log with escaped arguments
|
|
30
|
-
originalConsoleLog(...escapedArgs);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
16
|
+
// Quiet mode: suppress all output, show a spinner, print only errors (or success) at exit
|
|
17
|
+
const _quietMode = process.argv.includes('--quiet') && process.argv[2] === 'validate';
|
|
18
|
+
const _quietErrors = [];
|
|
19
|
+
|
|
20
|
+
if (_quietMode) {
|
|
21
|
+
const _spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
22
|
+
let _spinnerIdx = 0;
|
|
23
|
+
const _spinnerInterval = setInterval(() => {
|
|
24
|
+
process.stderr.write(`\r${_spinnerFrames[_spinnerIdx++ % _spinnerFrames.length]} Validating...`);
|
|
25
|
+
}, 100);
|
|
26
|
+
_spinnerInterval.unref(); // Don't prevent natural process exit
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
console.
|
|
28
|
+
console.log = () => {};
|
|
29
|
+
console.info = () => {};
|
|
30
|
+
console.error = (...args) => {
|
|
31
|
+
_quietErrors.push(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
process.on('exit', (code) => {
|
|
35
|
+
clearInterval(_spinnerInterval);
|
|
36
|
+
process.stderr.write('\r\x1b[K');
|
|
37
|
+
if (_quietErrors.length > 0) {
|
|
38
|
+
for (const e of _quietErrors) originalConsoleError(`${e}\n`);
|
|
39
|
+
} else if (code === 0) {
|
|
40
|
+
originalConsoleLog('No validation errors found');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
36
43
|
} else {
|
|
37
|
-
console.log
|
|
38
|
-
|
|
44
|
+
console.log = (...args) => {
|
|
45
|
+
if (process.env.GITHUB_ACTIONS !== 'true') {
|
|
46
|
+
// If not running in GitHub Actions, send args to the original console.log
|
|
47
|
+
originalConsoleLog(...args);
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
} else {
|
|
50
|
+
// If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
|
|
51
|
+
const escapedArgs = args.map(arg => {
|
|
52
|
+
if (typeof arg === 'string') {
|
|
53
|
+
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
54
|
+
}
|
|
55
|
+
return arg;
|
|
56
|
+
});
|
|
57
|
+
// Use the original console.log with escaped arguments
|
|
58
|
+
originalConsoleLog(...escapedArgs);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
41
61
|
|
|
42
|
-
if (process.env.GITHUB_ACTIONS
|
|
43
|
-
|
|
44
|
-
if (console_color) originalConsoleInfo(`\x1b[33m${args}\x1b[0m`);
|
|
45
|
-
else originalConsoleInfo(...args);
|
|
62
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
63
|
+
console.log("\nRunning in GitHub Actions environment\n");
|
|
46
64
|
} else {
|
|
47
|
-
|
|
48
|
-
const escapedArgs = args.map(arg => {
|
|
49
|
-
if (typeof arg === 'string') {
|
|
50
|
-
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
51
|
-
}
|
|
52
|
-
return arg;
|
|
53
|
-
});
|
|
54
|
-
// Use the original console.log with escaped arguments
|
|
55
|
-
originalConsoleInfo(...escapedArgs);
|
|
65
|
+
console.log("\nRunning in non-GitHub Actions environment\n");
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
console.info = (...args) => {
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
if (process.env.GITHUB_ACTIONS !== 'true') {
|
|
71
|
+
// If not running in GitHub Actions, send args to the original console.log
|
|
72
|
+
if (console_color) originalConsoleInfo(`\x1b[33m${args}\x1b[0m`);
|
|
73
|
+
else originalConsoleInfo(...args);
|
|
74
|
+
} else {
|
|
75
|
+
// If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
|
|
76
|
+
const escapedArgs = args.map(arg => {
|
|
77
|
+
if (typeof arg === 'string') {
|
|
78
|
+
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
79
|
+
}
|
|
80
|
+
return arg;
|
|
81
|
+
});
|
|
82
|
+
// Use the original console.log with escaped arguments
|
|
83
|
+
originalConsoleInfo(...escapedArgs);
|
|
84
|
+
}
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
// If not running in GitHub Actions, send args to the original console.log
|
|
64
|
-
if (console_color) originalConsoleError(`\x1b[33m${args}\x1b[0m`);
|
|
65
|
-
else originalConsoleError(...args);
|
|
66
|
-
} else {
|
|
86
|
+
};
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
console.error = (...args) => {
|
|
89
|
+
|
|
90
|
+
if (process.env.GITHUB_ACTIONS !== 'true') {
|
|
91
|
+
// If not running in GitHub Actions, send args to the original console.log
|
|
92
|
+
if (console_color) originalConsoleError(`\x1b[33m${args}\x1b[0m`);
|
|
93
|
+
else originalConsoleError(...args);
|
|
94
|
+
} else {
|
|
95
|
+
|
|
96
|
+
// If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
|
|
97
|
+
const escapedArgs = args.map(arg => {
|
|
98
|
+
if (typeof arg === 'string') {
|
|
99
|
+
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
100
|
+
}
|
|
101
|
+
return arg;
|
|
102
|
+
});
|
|
103
|
+
// Use the original console.log with escaped arguments
|
|
104
|
+
originalConsoleError(...escapedArgs);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
79
108
|
|
|
80
109
|
const getHdocPackageVersion = (packagePath) => {
|
|
81
110
|
if (fs.existsSync(packagePath)) {
|
|
@@ -154,6 +183,8 @@
|
|
|
154
183
|
output_links = false;
|
|
155
184
|
} else if (process.argv[x].toLowerCase() === "--onyx") {
|
|
156
185
|
onyx_index = true;
|
|
186
|
+
} else if (process.argv[x].toLowerCase() === "--quiet") {
|
|
187
|
+
// handled at startup via _quietMode
|
|
157
188
|
}
|
|
158
189
|
}
|
|
159
190
|
source_path = hdoc.true_case_path_sync(source_path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hdoc-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Hornbill HDocBook Development Support Tool",
|
|
5
5
|
"main": "hdoc.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
"hdoc-serve.js",
|
|
22
22
|
"hdoc-stats.js",
|
|
23
23
|
"hdoc-validate.js",
|
|
24
|
+
"hdoc-validate-config.js",
|
|
24
25
|
"hdoc-ver.js",
|
|
25
26
|
"validateNodeVer.js",
|
|
26
27
|
"ui",
|
|
27
28
|
"custom_modules",
|
|
29
|
+
"schemas",
|
|
28
30
|
"templates",
|
|
29
31
|
"templates/init/.npmignore",
|
|
30
32
|
"templates/init/gitignore",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "hdocbook-project.schema.json",
|
|
4
|
+
"title": "HDocBook Project Configuration",
|
|
5
|
+
"description": "Schema for hdocbook-project.json - project-level build and validation configuration",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["docId"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"docId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The document ID. Must match the book folder name and use kebab-case.",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]+$"
|
|
14
|
+
},
|
|
15
|
+
"pdfGeneration": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"description": "Controls PDF generation during the book build.",
|
|
18
|
+
"required": ["enable"],
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"properties": {
|
|
21
|
+
"enable": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"description": "Whether to generate PDFs for page content during the build."
|
|
24
|
+
},
|
|
25
|
+
"exclude_paths": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"description": "Paths to exclude from PDF generation. Supports a trailing * wildcard.",
|
|
28
|
+
"items": { "type": "string" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"validation": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "Controls book validation behaviour.",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"properties": {
|
|
37
|
+
"exclude_links": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"description": "Links to skip during link validation. Supports a trailing * wildcard.",
|
|
40
|
+
"items": { "type": "string" }
|
|
41
|
+
},
|
|
42
|
+
"exclude_spellcheck": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"description": "Per-document spellcheck word exclusions.",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["document_path", "words"],
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"properties": {
|
|
50
|
+
"document_path": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Root-relative path to the document, without file extension."
|
|
53
|
+
},
|
|
54
|
+
"words": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"description": "Words to exclude from the British English spellcheck for this document.",
|
|
57
|
+
"items": { "type": "string" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"exclude_h1_count": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"description": "Root-relative paths of documents allowed to have more than one H1 tag.",
|
|
65
|
+
"items": { "type": "string" }
|
|
66
|
+
},
|
|
67
|
+
"external_link_warnings": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"description": "When true, external link failures are reported as warnings rather than errors."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"redirects": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "Permanent redirects for pages that have moved or been deleted.",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"required": ["url", "code"],
|
|
79
|
+
"additionalProperties": false,
|
|
80
|
+
"properties": {
|
|
81
|
+
"url": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "The path being redirected from."
|
|
84
|
+
},
|
|
85
|
+
"code": {
|
|
86
|
+
"type": "integer",
|
|
87
|
+
"description": "HTTP redirect code. 301 = moved permanently, 308 = permanent redirect, 410 = gone.",
|
|
88
|
+
"enum": [301, 308, 410]
|
|
89
|
+
},
|
|
90
|
+
"location": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Destination path for 301/308 redirects. Not applicable for 410."
|
|
93
|
+
},
|
|
94
|
+
"skip_location_validation": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"description": "Skip validation of the redirect destination path."
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "hdocbook.schema.json",
|
|
4
|
+
"title": "HDocBook Configuration",
|
|
5
|
+
"description": "Schema for hdocbook.json - book metadata, navigation, and publishing configuration",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["docId", "title", "description", "version", "productFamily", "audience", "navigation"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"docId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The unique document ID. Must match the folder name that contains this file, using kebab-case.",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]+$"
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "The full title of the book as presented without other context."
|
|
18
|
+
},
|
|
19
|
+
"description": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "A short description of the book's purpose and contents."
|
|
22
|
+
},
|
|
23
|
+
"publicSource": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "URL to the public GitHub repository for this book. Omit or leave empty for private books.",
|
|
26
|
+
"pattern": "^https://(github\\.com|api\\.github\\.com)/"
|
|
27
|
+
},
|
|
28
|
+
"version": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Book version in X.Y.Z format. Controls automated publishing — increment to trigger a new publish.",
|
|
31
|
+
"pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,6}$"
|
|
32
|
+
},
|
|
33
|
+
"productFamily": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "The ID of the product family this book belongs to.",
|
|
36
|
+
"enum": [
|
|
37
|
+
"esp",
|
|
38
|
+
"com.hornbill.docmanager",
|
|
39
|
+
"com.hornbill.boardmanager",
|
|
40
|
+
"com.hornbill.customermanager",
|
|
41
|
+
"com.hornbill.grc",
|
|
42
|
+
"com.hornbill.itom",
|
|
43
|
+
"com.hornbill.livechat",
|
|
44
|
+
"com.hornbill.projectmanager",
|
|
45
|
+
"com.hornbill.servicemanager",
|
|
46
|
+
"com.hornbill.suppliermanager",
|
|
47
|
+
"com.hornbill.timesheetmanager",
|
|
48
|
+
"com.hornbill.collaboration",
|
|
49
|
+
"appdev",
|
|
50
|
+
"hdocs"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"coverImage": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Root-relative path to a cover image used for gallery and social sharing. Optional."
|
|
56
|
+
},
|
|
57
|
+
"tags": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"description": "Tags associated with this book.",
|
|
60
|
+
"items": { "type": "string" }
|
|
61
|
+
},
|
|
62
|
+
"audience": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"description": "Target audiences for this book. At least one value is required.",
|
|
65
|
+
"minItems": 1,
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": [
|
|
69
|
+
"public",
|
|
70
|
+
"private",
|
|
71
|
+
"private.cloud",
|
|
72
|
+
"private.dev.platform",
|
|
73
|
+
"private.dev.apps",
|
|
74
|
+
"private.hdocs",
|
|
75
|
+
"private.elearning"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"languages": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"description": "Language codes for the languages available in this book (e.g. 'en').",
|
|
82
|
+
"items": { "type": "string" }
|
|
83
|
+
},
|
|
84
|
+
"readingTime": {
|
|
85
|
+
"type": "number",
|
|
86
|
+
"description": "Total estimated reading time in minutes. Calculated and written automatically during build — do not set manually."
|
|
87
|
+
},
|
|
88
|
+
"navigation": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"description": "Defines the navigation tree displayed alongside book content.",
|
|
91
|
+
"required": ["items"],
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"items": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"description": "Top-level navigation items. Maximum three levels of nesting are supported.",
|
|
97
|
+
"minItems": 1,
|
|
98
|
+
"items": { "$ref": "#/$defs/navigationItem" }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"inline": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"description": "Inline content sources — content embedded from other books.",
|
|
105
|
+
"items": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["title", "link"],
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"title": { "type": "string" },
|
|
111
|
+
"link": { "type": "string" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"$defs": {
|
|
117
|
+
"navigationItem": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"required": ["text"],
|
|
120
|
+
"additionalProperties": false,
|
|
121
|
+
"properties": {
|
|
122
|
+
"text": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Display text for the navigation item."
|
|
125
|
+
},
|
|
126
|
+
"link": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Path to the content page. Ignored when 'items' is present."
|
|
129
|
+
},
|
|
130
|
+
"expand": {
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"description": "Whether this item is expanded by default."
|
|
133
|
+
},
|
|
134
|
+
"draft": {
|
|
135
|
+
"type": "boolean",
|
|
136
|
+
"description": "When true, this item is excluded from published output."
|
|
137
|
+
},
|
|
138
|
+
"items": {
|
|
139
|
+
"type": "array",
|
|
140
|
+
"description": "Child navigation items.",
|
|
141
|
+
"items": { "$ref": "#/$defs/navigationItem" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|