hdoc-tools 0.9.49 → 0.9.50

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-validate.js CHANGED
@@ -1,496 +1,496 @@
1
- (function () {
2
- 'use strict';
3
-
4
- const axios = require('axios'),
5
- cheerio = require('cheerio'),
6
- dree = require('dree'),
7
- fs = require('fs'),
8
- path = require('path'),
9
- https = require('https'),
10
- hdoc = require(path.join(__dirname, 'hdoc-module.js')),
11
- translator = require('american-british-english-translator'),
12
- { trueCasePathSync } = require('true-case-path');
13
-
14
- const spellcheck_options = {
15
- british: true,
16
- spelling: true
17
- },
18
- regex_nav_paths = /[a-z0-9-\/]+[a-z0-9]+#{0,1}[a-z0-9-\/]+/,
19
- agent = new https.Agent({
20
- rejectUnauthorized: false
21
- });
22
-
23
- let errors = {},
24
- messages = {},
25
- warnings = {},
26
- html_to_validate = [],
27
- md_to_validate = [],
28
- exclude_links = {},
29
- exclude_spellcheck = {},
30
- exclude_h1_count = {};
31
-
32
- const spellcheckContent = async function (sourceFile, excludes) {
33
- let spelling_errors = {};
34
- let words = [];
35
- const text = fs.readFileSync(sourceFile.path, 'utf8');
36
- const source_path = sourceFile.relativePath.replace('.' + sourceFile.extension, '');
37
- const translate_output = translator.translate(text, spellcheck_options);
38
- if (Object.keys(translate_output).length) {
39
- for (const key in translate_output) {
40
- if (translate_output.hasOwnProperty(key)) {
41
- let error_message = `Line ${key} - British spelling:`;
42
- for (let i = 0; i < translate_output[key].length; i++) {
43
- for (const spelling in translate_output[key][i]) {
44
- if (translate_output[key][i].hasOwnProperty(spelling) && (typeof translate_output[key][i][spelling].details === 'string')) {
45
- if (!excludes[source_path]) {
46
- errors[sourceFile.relativePath].push(`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`);
47
- spelling_errors[spelling] = true;
48
- } else if (!excludes[source_path].includes(spelling.toLowerCase())) {
49
- errors[sourceFile.relativePath].push(`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`);
50
- spelling_errors[spelling] = true;
51
- }
52
- }
53
- }
54
- }
55
- }
56
- }
57
- }
58
- if (Object.keys(spelling_errors).length) {
59
- for (const word in spelling_errors) {
60
- if (spelling_errors.hasOwnProperty(word)) {
61
- words.push(word);
62
- }
63
- }
64
- }
65
- return words;
66
- };
67
-
68
- const checkNavigation = async function (source_path, flat_nav) {
69
- let nav_errors = [];
70
- for (let key in flat_nav) {
71
- if (flat_nav.hasOwnProperty(key)) {
72
- // doc paths should only contain a-z - characters
73
- const invalid_chars = key.replace(regex_nav_paths, '');
74
- if (invalid_chars !== '') {
75
- nav_errors.push(`Navigation path [${key}] contains the following invalid characters: [${[...invalid_chars].join('] [')}]`);
76
- }
77
- const key_split = key.split('#');
78
- const key_no_hash = key_split[0];
79
-
80
- // Validate path exists - key should be a html file at this point
81
- let file_exists = true;
82
- let file_name = path.join(source_path, key_no_hash + '.html');
83
- if (!fs.existsSync(file_name)) {
84
- file_name = path.join(source_path, key_no_hash + '.htm');
85
- if (!fs.existsSync(file_name)) {
86
- file_name = path.join(source_path, key_no_hash, 'index.html');
87
- if (!fs.existsSync(file_name)) {
88
- file_name = path.join(source_path, key_no_hash, 'index.htm');
89
- if (!fs.existsSync(file_name)) {
90
- file_exists = false;
91
- nav_errors.push(`Navigation path [${key_no_hash}] file does not exist.`);
92
- }
93
- }
94
- }
95
- }
96
-
97
- if (file_exists) {
98
- const true_file = trueCasePathSync(file_name).replace(source_path, '').replaceAll('\\', '/');
99
- const relative_file = file_name.replace(source_path, '').replaceAll('\\', '/');
100
- if (true_file !== relative_file) {
101
- nav_errors.push(`Navigation path [${key}] for filename [${relative_file}] does not match filename case [${true_file}].`);
102
- }
103
- }
104
-
105
- // Validate path spellings
106
- const paths = key.split('/');
107
- for (let i = 0; i < paths.length; i++) {
108
- const path_words = paths[i].split('-');
109
- for (let j = 0; j < path_words.length; j++) {
110
- const translate_output = translator.translate(path_words[j], spellcheck_options);
111
- if (Object.keys(translate_output).length) {
112
- for (const spell_val in translate_output) {
113
- if (translate_output.hasOwnProperty(spell_val)) {
114
- for (const spelling in translate_output[spell_val][0]) {
115
- if (translate_output[spell_val][0].hasOwnProperty(spelling)) {
116
- nav_errors.push(`Navigation path [${key}] key contains a British English spelling: ${spelling} should be ${translate_output[spell_val][0][spelling].details}`);
117
- }
118
- }
119
- }
120
- }
121
- }
122
- }
123
- }
124
-
125
- // Validate display names/bookmarks
126
- console.log(key)
127
- for (let i = 0; i < flat_nav[key].length; i++) {
128
- if (flat_nav[key][i].link === key) {
129
- const translate_output = translator.translate(flat_nav[key][i].text, spellcheck_options);
130
- if (Object.keys(translate_output).length) {
131
- for (const spell_val in translate_output) {
132
- if (translate_output.hasOwnProperty(spell_val)) {
133
- for (let j = 0; j < translate_output[spell_val].length; j++) {
134
- for (const spelling in translate_output[spell_val][j]) {
135
- if (translate_output[spell_val][j].hasOwnProperty(spelling)) {
136
- nav_errors.push(`Navigation path [${key}] display text contains a British English spelling: ${spelling} should be ${translate_output[spell_val][j][spelling].details}`);
137
- }
138
- }
139
- }
140
- }
141
- }
142
- }
143
- }
144
- }
145
-
146
- }
147
- }
148
- return nav_errors;
149
- };
150
-
151
- const checkLinks = async function (source_path, htmlFile, links, hdocbook_config) {
152
- for (let i = 0; i < links.length; i++) {
153
-
154
- // Validate that link is a valid URL first
155
- const valid_url = hdoc.valid_url(links[i]);
156
- if (!valid_url) {
157
- // Could be a relative path, check
158
- if (links[i].startsWith('/')) {
159
- const link_root = links[i].split('/')[0];
160
- if (link_root !== hdocbook_config.docId) continue;
161
- }
162
- isRelativePath(source_path, htmlFile, links[i]);
163
- } else {
164
- messages[htmlFile.relativePath].push(`Link is a properly formatted external URL: ${links[i]}`);
165
-
166
- // Skip if it's the auto-generated edit url, as these could be part of a private repo which would return a 404
167
- if (links[i] === hdoc.get_github_api_path(hdocbook_config.publicSource, htmlFile.relativePath).edit_path.replace(path.extname(htmlFile.relativePath), '.md')) {
168
- continue;
169
- }
170
-
171
- if (valid_url.protocol === 'mailto:') {
172
- continue;
173
- }
174
-
175
- // Skip if the link is excluded in the project config
176
- if (exclude_links[links[i]]) {
177
- continue;
178
- }
179
- if (links[i].toLowerCase().includes('docs.hornbill.com') || links[i].toLowerCase().includes('docs-internal.hornbill.com')) {
180
- errors[htmlFile.relativePath].push(`Links to Hornbill Docs should rooted and not fully-qualified: ${links[i]}`);
181
- continue;
182
- }
183
-
184
- try {
185
- await axios.get(links[i], {
186
- httpsAgent: agent
187
- });
188
- messages[htmlFile.relativePath].push(`Link is a valid external URL: ${links[i]}`);
189
- } catch (e) {
190
- // Handle errors
191
- errors[htmlFile.relativePath].push(`Link is not responding: ${links[i]} - [${e.message}]`);
192
- }
193
- }
194
- }
195
- };
196
-
197
- const checkImages = async function (source_path, htmlFile, links) {
198
- for (let i = 0; i < links.length; i++) {
199
-
200
- // Validate that image is a valid URL first
201
- if (!hdoc.valid_url(links[i])) {
202
- // Could be a relative path, check image exists
203
- doesFileExist(source_path, htmlFile, links[i]);
204
- } else {
205
- messages[htmlFile.relativePath].push(`Image link is a properly formatted external URL: ${links[i]}`);
206
- // Do a Get to the URL to see if it exists
207
- try {
208
- const res = await axios.get(links[i]);
209
- messages[htmlFile.relativePath].push(`Image link is a valid external URL: ${links[i]}`);
210
- } catch (e) {
211
- // Handle errors
212
- errors[htmlFile.relativePath].push(`Unexpected Error from external image link: ${links[i]} - ${e.message}`);
213
- }
214
- }
215
- }
216
- };
217
-
218
- const checkTags = async function (htmlFile) {
219
- // Check if file is excluded from tag check
220
- const file_no_ext = htmlFile.relativePath.replace(path.extname(htmlFile.relativePath), '');
221
- if (exclude_h1_count[file_no_ext]) return;
222
-
223
- // Check tags
224
- const htmlBody = fs.readFileSync(htmlFile.path, 'utf8');
225
- const $ = cheerio.load(htmlBody);
226
-
227
- const h1_tags = $('h1').map(function () {
228
- return $(this);
229
- }).get();
230
- if (h1_tags.length && h1_tags.length > 1) {
231
- let error_msg = `${h1_tags.length} <h1> tags found in content: `;
232
- for (let i = 0; i < h1_tags.length; i++) {
233
- error_msg += h1_tags[i].text();
234
- if (i < h1_tags.length - 1) error_msg += '; ';
235
- }
236
- errors[htmlFile.relativePath].push(error_msg);
237
- }
238
- };
239
-
240
- const dreeOptions = {
241
- descendants: true,
242
- depth: 10,
243
- extensions: ['htm', 'html', 'md'],
244
- hash: false,
245
- normalize: true,
246
- size: false,
247
- sizeInBytes: false,
248
- stat: false,
249
- symbolicLinks: false
250
- };
251
-
252
- // File scan callback
253
- const fileCallback = function (element) {
254
- if (element.extension.toLowerCase() === 'md') {
255
- md_to_validate.push(element);
256
- } else {
257
- html_to_validate.push(element);
258
- }
259
- };
260
-
261
- const isRelativePath = function (source_path, html_path, relative_path) {
262
- const rel_path_ext = path.extname(relative_path);
263
- let response = {
264
- is_rel_path: false,
265
- has_md_extension: rel_path_ext === '.md'
266
- };
267
-
268
- const supported_relpaths = [
269
- path.sep + 'index.htm',
270
- path.sep + 'index.html',
271
- '.htm',
272
- '.html',
273
- '.md'
274
- ];
275
-
276
- // Remove explicit anchor links and _books prefix
277
- relative_path = relative_path.split('#')[0].replace('_books/', '');
278
-
279
- // Make full file path
280
- const file_path = path.join(source_path, relative_path);
281
-
282
- // Does path exist?
283
- if (fs.existsSync(file_path)) {
284
- response.is_rel_path = true;
285
- } else {
286
- // Path
287
- for (let i = 0; i < supported_relpaths.length; i++) {
288
- if (fs.existsSync(`${file_path}${supported_relpaths[i]}`)) {
289
- response.is_rel_path = true;
290
- break;
291
- }
292
- }
293
- }
294
- if (response.has_md_extension) {
295
- errors[html_path.relativePath].push(`Relative link contains MD extension, but should not: ${relative_path}`);
296
- } else {
297
- if (response.is_rel_path) {
298
- messages[html_path.relativePath].push(`Relative path exists: ${relative_path}`);
299
- } else {
300
- errors[html_path.relativePath].push(`Link path does not exist: ${relative_path}`);
301
- }
302
- }
303
- }
304
-
305
- const doesFileExist = function (source_path, html_path, relative_path) {
306
- // Remove explicit anchor links and _books prefix
307
- relative_path = relative_path.split('#')[0].replace('_books/', '');
308
- const file_path = path.join(source_path, relative_path);
309
- if (!fs.existsSync(file_path) && !fs.existsSync(file_path + path.sep + 'index.htm') && !fs.existsSync(file_path + 'index.html') && !fs.existsSync(file_path + '.htm') && !fs.existsSync(file_path + '.html')) {
310
- errors[html_path.relativePath].push(`Book resource does not exist: ${relative_path}`);
311
- return false;
312
- } else {
313
- messages[html_path.relativePath].push(`Book resource exists: ${relative_path}`);
314
- }
315
- return true;
316
- };
317
-
318
- // Takes a dree element, returns an object with a pair of arrays
319
- const getLinks = function (file) {
320
- messages[file.relativePath].push('Parsing HTML file');
321
- const htmlBody = fs.readFileSync(file.path, 'utf8');
322
- let links = {
323
- href: [],
324
- img: []
325
- };
326
- const $ = cheerio.load(htmlBody);
327
- const hrefs = $('a').map(function (i) {
328
- return $(this).attr('href');
329
- }).get();
330
- const srcs = $('img').map(function (i) {
331
- return $(this).attr('src');
332
- }).get();
333
- links.href.push(...hrefs);
334
- links.img.push(...srcs);
335
- return links;
336
- };
337
-
338
- exports.run = async function (source_path, doc_id, verbose, hdocbook_config, hdocbook_project, nav_items, prod_families, prods_supported, gen_exclude) {
339
- console.log(`Performing Validation and Building SEO Link List...`);
340
-
341
- // Get a list of HTML files in source_path
342
- dree.scan(source_path, dreeOptions, fileCallback);
343
-
344
- // Check product family
345
- let valid_product = false;
346
- let meta_errors = [];
347
- for (let i = 0; i < prod_families.products.length; i++) {
348
- if (prod_families.products[i].id === hdocbook_config.productFamily) {
349
- valid_product = true;
350
- }
351
- }
352
- if (!valid_product) {
353
- let val_prod_error = `Incorrect productFamily: ${hdocbook_config.productFamily}. Supported values:`;
354
- for (let i = 0; i < prods_supported.length; i++) {
355
- val_prod_error += `\n - ${prods_supported[i]}`
356
- }
357
- meta_errors.push(val_prod_error)
358
-
359
- }
360
-
361
- // Check navigation spellings
362
- const nav_errors = await checkNavigation(source_path, nav_items);
363
- if (nav_errors.length > 0) meta_errors.push(...nav_errors);
364
-
365
- if (meta_errors.length > 0) {
366
- console.log('\r\n-----------------------');
367
- console.log(' Validation Output ');
368
- console.log('-----------------------');
369
- for (let i = 0; i < meta_errors.length; i++) {
370
- console.log(`- ${meta_errors[i]}`);
371
- }
372
- console.log(`\r\n${meta_errors.length} Validation Errors Found`);
373
- return false;
374
- }
375
-
376
- if (hdocbook_project.validation) {
377
- if (hdocbook_project.validation.exclude_links && hdocbook_project.validation.exclude_links instanceof Array) {
378
- hdocbook_project.validation.exclude_links.forEach(function (excl_link) {
379
- exclude_links[excl_link] = true;
380
- });
381
- }
382
- if (hdocbook_project.validation.exclude_spellcheck && hdocbook_project.validation.exclude_spellcheck instanceof Array) {
383
- hdocbook_project.validation.exclude_spellcheck.forEach(function (excl_sc) {
384
- exclude_spellcheck[excl_sc.document_path] = excl_sc.words;
385
- });
386
- }
387
- if (hdocbook_project.validation.exclude_h1_count && hdocbook_project.validation.exclude_h1_count instanceof Array) {
388
- hdocbook_project.validation.exclude_h1_count.forEach(function (excl_h1) {
389
- exclude_h1_count[excl_h1] = true;
390
- });
391
- }
392
- }
393
-
394
- let excl_output = [];
395
-
396
- // Do spellchecking on markdown files
397
- let md_files_spellchecked = {};
398
- let mdPromiseArray = [];
399
- for (let i = 0; i < md_to_validate.length; i++) {
400
- errors[md_to_validate[i].relativePath] = [];
401
- messages[md_to_validate[i].relativePath] = [];
402
- warnings[md_to_validate[i].relativePath] = [];
403
- mdPromiseArray.push(md_to_validate[i]);
404
- }
405
- await Promise.all(mdPromiseArray.map(async (file) => {
406
- // Initiate maps for errors and verbose messages for markdown file
407
- const exclusions = await spellcheckContent(file, exclude_spellcheck);
408
- if (gen_exclude && exclusions.length > 0) excl_output.push({ document_path: file.relativePath.replace('.' + file.extension, ''), words: exclusions });
409
- md_files_spellchecked[file.relativePath.replace('.' + file.extension, '')] = true;
410
- }));
411
-
412
- // Perform rest of validation against HTML files
413
- let listContent = '';
414
- let htmlPromiseArray = [];
415
- for (let i = 0; i < html_to_validate.length; i++) {
416
- errors[html_to_validate[i].relativePath] = [];
417
- messages[html_to_validate[i].relativePath] = [];
418
- warnings[html_to_validate[i].relativePath] = [];
419
- htmlPromiseArray.push(html_to_validate[i]);
420
- }
421
- await Promise.all(mdPromiseArray.map(async (file) => {
422
- // Check for British spellings in static HTML content
423
- if (!md_files_spellchecked[file.relativePath.replace('.' + file.extension, '')]) {
424
- const exclusions = await spellcheckContent(file, exclude_spellcheck);
425
- if (gen_exclude && exclusions.length > 0) excl_output.push({ document_path: file.relativePath.replace('.' + file.extension, ''), words: exclusions });
426
- }
427
-
428
- const links = getLinks(file);
429
- if (links.href.length === 0) {
430
- messages[file.relativePath].push('No links found in file');
431
- } else {
432
- await checkLinks(source_path, file, links.href, hdocbook_config);
433
- }
434
- if (links.img.length === 0) {
435
- messages[file.relativePath].push('No images found in file');
436
- } else {
437
- await checkImages(source_path, file, links.img);
438
- }
439
-
440
- // Check for multiple H1 tags
441
- await checkTags(file);
442
-
443
- // Build list content for Google
444
- listContent += `/${file.relativePath.replace(path.extname(file.relativePath), '')}`;
445
- listContent += '\r\n';
446
- }));
447
-
448
- if (gen_exclude) console.log(JSON.stringify(excl_output, null, 2));
449
-
450
- try {
451
- // Write list
452
- const listFile = path.join(source_path, doc_id, 'links.txt');
453
- fs.writeFileSync(listFile, listContent);
454
- console.log(`\r\nLink list text file created successfully: ${listFile}`);
455
- } catch (err) {
456
- console.error(err);
457
- }
458
-
459
- if (verbose) {
460
- console.log('\r\n-------------');
461
- console.log(' Verbose ');
462
- console.log('-------------');
463
- for (const key in messages) {
464
- if (messages.hasOwnProperty(key) && messages[key].length > 0) {
465
- console.log(`\r\nMessage output for ${key}`);
466
- for (let i = 0; i < messages[key].length; i++) {
467
- console.log(` - ${messages[key][i]}`);
468
- }
469
- }
470
- }
471
- }
472
-
473
- console.log('\r\n-----------------------');
474
- console.log(' Validation Output ');
475
- console.log('-----------------------');
476
- if (Object.keys(errors).length > 0) {
477
- let error_count = 0;
478
- for (const key in errors) {
479
- if (errors.hasOwnProperty(key) && errors[key].length > 0) {
480
- console.log(`\r\n${errors[key].length} error(s) in ${key}`);
481
- for (let i = 0; i < errors[key].length; i++) {
482
- console.log(` - ${errors[key][i]}`);
483
- error_count++
484
- }
485
- }
486
- }
487
- if (error_count > 0) {
488
- console.log(`\r\n${error_count} Validation Errors Found`);
489
- return false;
490
- }
491
- }
492
-
493
- console.log(`\r\nNo Validation Errors Found!\n`);
494
- return true;
495
- };
1
+ (function () {
2
+ 'use strict';
3
+
4
+ const axios = require('axios'),
5
+ cheerio = require('cheerio'),
6
+ dree = require('dree'),
7
+ fs = require('fs'),
8
+ path = require('path'),
9
+ https = require('https'),
10
+ hdoc = require(path.join(__dirname, 'hdoc-module.js')),
11
+ translator = require('american-british-english-translator'),
12
+ { trueCasePathSync } = require('true-case-path');
13
+
14
+ const spellcheck_options = {
15
+ british: true,
16
+ spelling: true
17
+ },
18
+ regex_nav_paths = /[a-z0-9-\/]+[a-z0-9]+#{0,1}[a-z0-9-\/]+/,
19
+ agent = new https.Agent({
20
+ rejectUnauthorized: false
21
+ });
22
+
23
+ let errors = {},
24
+ messages = {},
25
+ warnings = {},
26
+ html_to_validate = [],
27
+ md_to_validate = [],
28
+ exclude_links = {},
29
+ exclude_spellcheck = {},
30
+ exclude_h1_count = {};
31
+
32
+ const spellcheckContent = async function (sourceFile, excludes) {
33
+ let spelling_errors = {};
34
+ let words = [];
35
+ const text = fs.readFileSync(sourceFile.path, 'utf8');
36
+ const source_path = sourceFile.relativePath.replace('.' + sourceFile.extension, '');
37
+ const translate_output = translator.translate(text, spellcheck_options);
38
+ if (Object.keys(translate_output).length) {
39
+ for (const key in translate_output) {
40
+ if (translate_output.hasOwnProperty(key)) {
41
+ let error_message = `Line ${key} - British spelling:`;
42
+ for (let i = 0; i < translate_output[key].length; i++) {
43
+ for (const spelling in translate_output[key][i]) {
44
+ if (translate_output[key][i].hasOwnProperty(spelling) && (typeof translate_output[key][i][spelling].details === 'string')) {
45
+ if (!excludes[source_path]) {
46
+ errors[sourceFile.relativePath].push(`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`);
47
+ spelling_errors[spelling] = true;
48
+ } else if (!excludes[source_path].includes(spelling.toLowerCase())) {
49
+ errors[sourceFile.relativePath].push(`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`);
50
+ spelling_errors[spelling] = true;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if (Object.keys(spelling_errors).length) {
59
+ for (const word in spelling_errors) {
60
+ if (spelling_errors.hasOwnProperty(word)) {
61
+ words.push(word);
62
+ }
63
+ }
64
+ }
65
+ return words;
66
+ };
67
+
68
+ const checkNavigation = async function (source_path, flat_nav) {
69
+ let nav_errors = [];
70
+ for (let key in flat_nav) {
71
+ if (flat_nav.hasOwnProperty(key)) {
72
+ // doc paths should only contain a-z - characters
73
+ const invalid_chars = key.replace(regex_nav_paths, '');
74
+ if (invalid_chars !== '') {
75
+ nav_errors.push(`Navigation path [${key}] contains the following invalid characters: [${[...invalid_chars].join('] [')}]`);
76
+ }
77
+ const key_split = key.split('#');
78
+ const key_no_hash = key_split[0];
79
+
80
+ // Validate path exists - key should be a html file at this point
81
+ let file_exists = true;
82
+ let file_name = path.join(source_path, key_no_hash + '.html');
83
+ if (!fs.existsSync(file_name)) {
84
+ file_name = path.join(source_path, key_no_hash + '.htm');
85
+ if (!fs.existsSync(file_name)) {
86
+ file_name = path.join(source_path, key_no_hash, 'index.html');
87
+ if (!fs.existsSync(file_name)) {
88
+ file_name = path.join(source_path, key_no_hash, 'index.htm');
89
+ if (!fs.existsSync(file_name)) {
90
+ file_exists = false;
91
+ nav_errors.push(`Navigation path [${key_no_hash}] file does not exist.`);
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ if (file_exists) {
98
+ const true_file = trueCasePathSync(file_name).replace(source_path, '').replaceAll('\\', '/');
99
+ const relative_file = file_name.replace(source_path, '').replaceAll('\\', '/');
100
+ if (true_file !== relative_file) {
101
+ nav_errors.push(`Navigation path [${key}] for filename [${relative_file}] does not match filename case [${true_file}].`);
102
+ }
103
+ }
104
+
105
+ // Validate path spellings
106
+ const paths = key.split('/');
107
+ for (let i = 0; i < paths.length; i++) {
108
+ const path_words = paths[i].split('-');
109
+ for (let j = 0; j < path_words.length; j++) {
110
+ const translate_output = translator.translate(path_words[j], spellcheck_options);
111
+ if (Object.keys(translate_output).length) {
112
+ for (const spell_val in translate_output) {
113
+ if (translate_output.hasOwnProperty(spell_val)) {
114
+ for (const spelling in translate_output[spell_val][0]) {
115
+ if (translate_output[spell_val][0].hasOwnProperty(spelling)) {
116
+ nav_errors.push(`Navigation path [${key}] key contains a British English spelling: ${spelling} should be ${translate_output[spell_val][0][spelling].details}`);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // Validate display names/bookmarks
126
+ console.log(key)
127
+ for (let i = 0; i < flat_nav[key].length; i++) {
128
+ if (flat_nav[key][i].link === key) {
129
+ const translate_output = translator.translate(flat_nav[key][i].text, spellcheck_options);
130
+ if (Object.keys(translate_output).length) {
131
+ for (const spell_val in translate_output) {
132
+ if (translate_output.hasOwnProperty(spell_val)) {
133
+ for (let j = 0; j < translate_output[spell_val].length; j++) {
134
+ for (const spelling in translate_output[spell_val][j]) {
135
+ if (translate_output[spell_val][j].hasOwnProperty(spelling)) {
136
+ nav_errors.push(`Navigation path [${key}] display text contains a British English spelling: ${spelling} should be ${translate_output[spell_val][j][spelling].details}`);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ }
147
+ }
148
+ return nav_errors;
149
+ };
150
+
151
+ const checkLinks = async function (source_path, htmlFile, links, hdocbook_config) {
152
+ for (let i = 0; i < links.length; i++) {
153
+
154
+ // Validate that link is a valid URL first
155
+ const valid_url = hdoc.valid_url(links[i]);
156
+ if (!valid_url) {
157
+ // Could be a relative path, check
158
+ if (links[i].startsWith('/')) {
159
+ const link_root = links[i].split('/')[0];
160
+ if (link_root !== hdocbook_config.docId) continue;
161
+ }
162
+ isRelativePath(source_path, htmlFile, links[i]);
163
+ } else {
164
+ messages[htmlFile.relativePath].push(`Link is a properly formatted external URL: ${links[i]}`);
165
+
166
+ // Skip if it's the auto-generated edit url, as these could be part of a private repo which would return a 404
167
+ if (links[i] === hdoc.get_github_api_path(hdocbook_config.publicSource, htmlFile.relativePath).edit_path.replace(path.extname(htmlFile.relativePath), '.md')) {
168
+ continue;
169
+ }
170
+
171
+ if (valid_url.protocol === 'mailto:') {
172
+ continue;
173
+ }
174
+
175
+ // Skip if the link is excluded in the project config
176
+ if (exclude_links[links[i]]) {
177
+ continue;
178
+ }
179
+ if (links[i].toLowerCase().includes('docs.hornbill.com') || links[i].toLowerCase().includes('docs-internal.hornbill.com')) {
180
+ errors[htmlFile.relativePath].push(`Links to Hornbill Docs should rooted and not fully-qualified: ${links[i]}`);
181
+ continue;
182
+ }
183
+
184
+ try {
185
+ await axios.get(links[i], {
186
+ httpsAgent: agent
187
+ });
188
+ messages[htmlFile.relativePath].push(`Link is a valid external URL: ${links[i]}`);
189
+ } catch (e) {
190
+ // Handle errors
191
+ errors[htmlFile.relativePath].push(`Link is not responding: ${links[i]} - [${e.message}]`);
192
+ }
193
+ }
194
+ }
195
+ };
196
+
197
+ const checkImages = async function (source_path, htmlFile, links) {
198
+ for (let i = 0; i < links.length; i++) {
199
+
200
+ // Validate that image is a valid URL first
201
+ if (!hdoc.valid_url(links[i])) {
202
+ // Could be a relative path, check image exists
203
+ doesFileExist(source_path, htmlFile, links[i]);
204
+ } else {
205
+ messages[htmlFile.relativePath].push(`Image link is a properly formatted external URL: ${links[i]}`);
206
+ // Do a Get to the URL to see if it exists
207
+ try {
208
+ const res = await axios.get(links[i]);
209
+ messages[htmlFile.relativePath].push(`Image link is a valid external URL: ${links[i]}`);
210
+ } catch (e) {
211
+ // Handle errors
212
+ errors[htmlFile.relativePath].push(`Unexpected Error from external image link: ${links[i]} - ${e.message}`);
213
+ }
214
+ }
215
+ }
216
+ };
217
+
218
+ const checkTags = async function (htmlFile) {
219
+ // Check if file is excluded from tag check
220
+ const file_no_ext = htmlFile.relativePath.replace(path.extname(htmlFile.relativePath), '');
221
+ if (exclude_h1_count[file_no_ext]) return;
222
+
223
+ // Check tags
224
+ const htmlBody = fs.readFileSync(htmlFile.path, 'utf8');
225
+ const $ = cheerio.load(htmlBody);
226
+
227
+ const h1_tags = $('h1').map(function () {
228
+ return $(this);
229
+ }).get();
230
+ if (h1_tags.length && h1_tags.length > 1) {
231
+ let error_msg = `${h1_tags.length} <h1> tags found in content: `;
232
+ for (let i = 0; i < h1_tags.length; i++) {
233
+ error_msg += h1_tags[i].text();
234
+ if (i < h1_tags.length - 1) error_msg += '; ';
235
+ }
236
+ errors[htmlFile.relativePath].push(error_msg);
237
+ }
238
+ };
239
+
240
+ const dreeOptions = {
241
+ descendants: true,
242
+ depth: 10,
243
+ extensions: ['htm', 'html', 'md'],
244
+ hash: false,
245
+ normalize: true,
246
+ size: false,
247
+ sizeInBytes: false,
248
+ stat: false,
249
+ symbolicLinks: false
250
+ };
251
+
252
+ // File scan callback
253
+ const fileCallback = function (element) {
254
+ if (element.extension.toLowerCase() === 'md') {
255
+ md_to_validate.push(element);
256
+ } else {
257
+ html_to_validate.push(element);
258
+ }
259
+ };
260
+
261
+ const isRelativePath = function (source_path, html_path, relative_path) {
262
+ const rel_path_ext = path.extname(relative_path);
263
+ let response = {
264
+ is_rel_path: false,
265
+ has_md_extension: rel_path_ext === '.md'
266
+ };
267
+
268
+ const supported_relpaths = [
269
+ path.sep + 'index.htm',
270
+ path.sep + 'index.html',
271
+ '.htm',
272
+ '.html',
273
+ '.md'
274
+ ];
275
+
276
+ // Remove explicit anchor links and _books prefix
277
+ relative_path = relative_path.split('#')[0].replace('_books/', '');
278
+
279
+ // Make full file path
280
+ const file_path = path.join(source_path, relative_path);
281
+
282
+ // Does path exist?
283
+ if (fs.existsSync(file_path)) {
284
+ response.is_rel_path = true;
285
+ } else {
286
+ // Path
287
+ for (let i = 0; i < supported_relpaths.length; i++) {
288
+ if (fs.existsSync(`${file_path}${supported_relpaths[i]}`)) {
289
+ response.is_rel_path = true;
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ if (response.has_md_extension) {
295
+ errors[html_path.relativePath].push(`Relative link contains MD extension, but should not: ${relative_path}`);
296
+ } else {
297
+ if (response.is_rel_path) {
298
+ messages[html_path.relativePath].push(`Relative path exists: ${relative_path}`);
299
+ } else {
300
+ errors[html_path.relativePath].push(`Link path does not exist: ${relative_path}`);
301
+ }
302
+ }
303
+ }
304
+
305
+ const doesFileExist = function (source_path, html_path, relative_path) {
306
+ // Remove explicit anchor links and _books prefix
307
+ relative_path = relative_path.split('#')[0].replace('_books/', '');
308
+ const file_path = path.join(source_path, relative_path);
309
+ if (!fs.existsSync(file_path) && !fs.existsSync(file_path + path.sep + 'index.htm') && !fs.existsSync(file_path + 'index.html') && !fs.existsSync(file_path + '.htm') && !fs.existsSync(file_path + '.html')) {
310
+ errors[html_path.relativePath].push(`Book resource does not exist: ${relative_path}`);
311
+ return false;
312
+ } else {
313
+ messages[html_path.relativePath].push(`Book resource exists: ${relative_path}`);
314
+ }
315
+ return true;
316
+ };
317
+
318
+ // Takes a dree element, returns an object with a pair of arrays
319
+ const getLinks = function (file) {
320
+ messages[file.relativePath].push('Parsing HTML file');
321
+ const htmlBody = fs.readFileSync(file.path, 'utf8');
322
+ let links = {
323
+ href: [],
324
+ img: []
325
+ };
326
+ const $ = cheerio.load(htmlBody);
327
+ const hrefs = $('a').map(function (i) {
328
+ return $(this).attr('href');
329
+ }).get();
330
+ const srcs = $('img').map(function (i) {
331
+ return $(this).attr('src');
332
+ }).get();
333
+ links.href.push(...hrefs);
334
+ links.img.push(...srcs);
335
+ return links;
336
+ };
337
+
338
+ exports.run = async function (source_path, doc_id, verbose, hdocbook_config, hdocbook_project, nav_items, prod_families, prods_supported, gen_exclude) {
339
+ console.log(`Performing Validation and Building SEO Link List...`);
340
+
341
+ // Get a list of HTML files in source_path
342
+ dree.scan(source_path, dreeOptions, fileCallback);
343
+
344
+ // Check product family
345
+ let valid_product = false;
346
+ let meta_errors = [];
347
+ for (let i = 0; i < prod_families.products.length; i++) {
348
+ if (prod_families.products[i].id === hdocbook_config.productFamily) {
349
+ valid_product = true;
350
+ }
351
+ }
352
+ if (!valid_product) {
353
+ let val_prod_error = `Incorrect productFamily: ${hdocbook_config.productFamily}. Supported values:`;
354
+ for (let i = 0; i < prods_supported.length; i++) {
355
+ val_prod_error += `\n - ${prods_supported[i]}`
356
+ }
357
+ meta_errors.push(val_prod_error)
358
+
359
+ }
360
+
361
+ // Check navigation spellings
362
+ const nav_errors = await checkNavigation(source_path, nav_items);
363
+ if (nav_errors.length > 0) meta_errors.push(...nav_errors);
364
+
365
+ if (meta_errors.length > 0) {
366
+ console.log('\r\n-----------------------');
367
+ console.log(' Validation Output ');
368
+ console.log('-----------------------');
369
+ for (let i = 0; i < meta_errors.length; i++) {
370
+ console.log(`- ${meta_errors[i]}`);
371
+ }
372
+ console.log(`\r\n${meta_errors.length} Validation Errors Found`);
373
+ return false;
374
+ }
375
+
376
+ if (hdocbook_project.validation) {
377
+ if (hdocbook_project.validation.exclude_links && hdocbook_project.validation.exclude_links instanceof Array) {
378
+ hdocbook_project.validation.exclude_links.forEach(function (excl_link) {
379
+ exclude_links[excl_link] = true;
380
+ });
381
+ }
382
+ if (hdocbook_project.validation.exclude_spellcheck && hdocbook_project.validation.exclude_spellcheck instanceof Array) {
383
+ hdocbook_project.validation.exclude_spellcheck.forEach(function (excl_sc) {
384
+ exclude_spellcheck[excl_sc.document_path] = excl_sc.words;
385
+ });
386
+ }
387
+ if (hdocbook_project.validation.exclude_h1_count && hdocbook_project.validation.exclude_h1_count instanceof Array) {
388
+ hdocbook_project.validation.exclude_h1_count.forEach(function (excl_h1) {
389
+ exclude_h1_count[excl_h1] = true;
390
+ });
391
+ }
392
+ }
393
+
394
+ let excl_output = [];
395
+
396
+ // Do spellchecking on markdown files
397
+ let md_files_spellchecked = {};
398
+ let mdPromiseArray = [];
399
+ for (let i = 0; i < md_to_validate.length; i++) {
400
+ errors[md_to_validate[i].relativePath] = [];
401
+ messages[md_to_validate[i].relativePath] = [];
402
+ warnings[md_to_validate[i].relativePath] = [];
403
+ mdPromiseArray.push(md_to_validate[i]);
404
+ }
405
+ await Promise.all(mdPromiseArray.map(async (file) => {
406
+ // Initiate maps for errors and verbose messages for markdown file
407
+ const exclusions = await spellcheckContent(file, exclude_spellcheck);
408
+ if (gen_exclude && exclusions.length > 0) excl_output.push({ document_path: file.relativePath.replace('.' + file.extension, ''), words: exclusions });
409
+ md_files_spellchecked[file.relativePath.replace('.' + file.extension, '')] = true;
410
+ }));
411
+
412
+ // Perform rest of validation against HTML files
413
+ let listContent = '';
414
+ let htmlPromiseArray = [];
415
+ for (let i = 0; i < html_to_validate.length; i++) {
416
+ errors[html_to_validate[i].relativePath] = [];
417
+ messages[html_to_validate[i].relativePath] = [];
418
+ warnings[html_to_validate[i].relativePath] = [];
419
+ htmlPromiseArray.push(html_to_validate[i]);
420
+ }
421
+ await Promise.all(mdPromiseArray.map(async (file) => {
422
+ // Check for British spellings in static HTML content
423
+ if (!md_files_spellchecked[file.relativePath.replace('.' + file.extension, '')]) {
424
+ const exclusions = await spellcheckContent(file, exclude_spellcheck);
425
+ if (gen_exclude && exclusions.length > 0) excl_output.push({ document_path: file.relativePath.replace('.' + file.extension, ''), words: exclusions });
426
+ }
427
+
428
+ const links = getLinks(file);
429
+ if (links.href.length === 0) {
430
+ messages[file.relativePath].push('No links found in file');
431
+ } else {
432
+ await checkLinks(source_path, file, links.href, hdocbook_config);
433
+ }
434
+ if (links.img.length === 0) {
435
+ messages[file.relativePath].push('No images found in file');
436
+ } else {
437
+ await checkImages(source_path, file, links.img);
438
+ }
439
+
440
+ // Check for multiple H1 tags
441
+ await checkTags(file);
442
+
443
+ // Build list content for Google
444
+ listContent += `/${file.relativePath.replace(path.extname(file.relativePath), '')}`;
445
+ listContent += '\r\n';
446
+ }));
447
+
448
+ if (gen_exclude) console.log(JSON.stringify(excl_output, null, 2));
449
+
450
+ try {
451
+ // Write list
452
+ const listFile = path.join(source_path, doc_id, 'links.txt');
453
+ fs.writeFileSync(listFile, listContent);
454
+ console.log(`\r\nLink list text file created successfully: ${listFile}`);
455
+ } catch (err) {
456
+ console.error(err);
457
+ }
458
+
459
+ if (verbose) {
460
+ console.log('\r\n-------------');
461
+ console.log(' Verbose ');
462
+ console.log('-------------');
463
+ for (const key in messages) {
464
+ if (messages.hasOwnProperty(key) && messages[key].length > 0) {
465
+ console.log(`\r\nMessage output for ${key}`);
466
+ for (let i = 0; i < messages[key].length; i++) {
467
+ console.log(` - ${messages[key][i]}`);
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ console.log('\r\n-----------------------');
474
+ console.log(' Validation Output ');
475
+ console.log('-----------------------');
476
+ if (Object.keys(errors).length > 0) {
477
+ let error_count = 0;
478
+ for (const key in errors) {
479
+ if (errors.hasOwnProperty(key) && errors[key].length > 0) {
480
+ console.log(`\r\n${errors[key].length} error(s) in ${key}`);
481
+ for (let i = 0; i < errors[key].length; i++) {
482
+ console.log(` - ${errors[key][i]}`);
483
+ error_count++
484
+ }
485
+ }
486
+ }
487
+ if (error_count > 0) {
488
+ console.log(`\r\n${error_count} Validation Errors Found`);
489
+ return false;
490
+ }
491
+ }
492
+
493
+ console.log(`\r\nNo Validation Errors Found!\n`);
494
+ return true;
495
+ };
496
496
  })();