hdoc-tools 0.23.0 → 0.24.1
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-module.js +16 -0
- package/hdoc-validate.js +86 -54
- package/package.json +1 -1
package/hdoc-module.js
CHANGED
@@ -725,4 +725,20 @@
|
|
725
725
|
}
|
726
726
|
return response;
|
727
727
|
};
|
728
|
+
|
729
|
+
exports.find_string_in_string = (fileContent, searchString) => {
|
730
|
+
const lines = fileContent.split('\n');
|
731
|
+
|
732
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
733
|
+
const columnNumber = lines[lineNumber].indexOf(searchString);
|
734
|
+
|
735
|
+
if (columnNumber !== -1) {
|
736
|
+
// Return 1-based line and column numbers
|
737
|
+
return { line: lineNumber + 1, column: columnNumber + 1 };
|
738
|
+
}
|
739
|
+
}
|
740
|
+
|
741
|
+
// If not found, return null
|
742
|
+
return null;
|
743
|
+
}
|
728
744
|
})();
|
package/hdoc-validate.js
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
const e = require("express");
|
2
|
+
|
1
3
|
(() => {
|
2
4
|
const axios = require("axios");
|
3
5
|
const cheerio = require("cheerio");
|
@@ -31,14 +33,14 @@
|
|
31
33
|
|
32
34
|
const excludeLink = async (url) => {
|
33
35
|
if (exclude_links[url]) return true;
|
34
|
-
|
35
36
|
for (let key in exclude_links) {
|
36
|
-
if (
|
37
|
-
|
38
|
-
|
37
|
+
if (Object.hasOwn(exclude_links, key)) {
|
38
|
+
if (key.endsWith("*")) {
|
39
|
+
key = key.substring(0, key.length - 1);
|
40
|
+
if (url.startsWith(key)) return true;
|
41
|
+
}
|
39
42
|
}
|
40
43
|
}
|
41
|
-
|
42
44
|
return false;
|
43
45
|
};
|
44
46
|
|
@@ -50,17 +52,25 @@
|
|
50
52
|
`.${sourceFile.extension}`,
|
51
53
|
"",
|
52
54
|
);
|
55
|
+
|
56
|
+
const markdown_path = getMDPathFromHtmlPath(sourceFile);
|
53
57
|
const translate_output = translator.translate(text, spellcheck_options);
|
54
58
|
if (Object.keys(translate_output).length) {
|
55
59
|
for (const key in translate_output) {
|
56
60
|
if (Object.hasOwn(translate_output, key)) {
|
57
|
-
|
61
|
+
// key is the line of text
|
62
|
+
let error_message = `British spelling:`;
|
58
63
|
for (let i = 0; i < translate_output[key].length; i++) {
|
59
64
|
for (const spelling in translate_output[key][i]) {
|
60
65
|
if (
|
61
66
|
Object.hasOwn(translate_output[key][i], spelling) &&
|
62
67
|
typeof translate_output[key][i][spelling].details === "string"
|
63
68
|
) {
|
69
|
+
const link_location = hdoc.find_string_in_string(text.split('\n')[key - 1], spelling);
|
70
|
+
if (link_location !== null)
|
71
|
+
error_message = `${markdown_path}:${key}:${link_location.column} - ${error_message}`;
|
72
|
+
else
|
73
|
+
error_message = `${markdown_path}:${key} - ${error_message}`;
|
64
74
|
if (!excludes[source_path]) {
|
65
75
|
errors[sourceFile.relativePath].push(
|
66
76
|
`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`,
|
@@ -325,7 +335,7 @@
|
|
325
335
|
};
|
326
336
|
|
327
337
|
const checkRedirects = async (source_path) => {
|
328
|
-
const
|
338
|
+
const redir_errors = [];
|
329
339
|
for (const key in redirects) {
|
330
340
|
if (Object.hasOwn(redirects, key)) {
|
331
341
|
if (
|
@@ -333,7 +343,7 @@
|
|
333
343
|
redirects[key].code !== 308 &&
|
334
344
|
redirects[key].code !== 410
|
335
345
|
)
|
336
|
-
|
346
|
+
redir_errors.push(`Invalid redirect code: ${redirects[key].code}`);
|
337
347
|
|
338
348
|
if (redirects[key].location) {
|
339
349
|
const redir_locations = [
|
@@ -352,13 +362,13 @@
|
|
352
362
|
}
|
353
363
|
}
|
354
364
|
if (!redir_location_ok)
|
355
|
-
|
365
|
+
redir_errors.push(
|
356
366
|
`Redirect location does not exist: ${redirects[key].location}`,
|
357
367
|
);
|
358
368
|
}
|
359
369
|
}
|
360
370
|
}
|
361
|
-
return
|
371
|
+
return redir_errors;
|
362
372
|
};
|
363
373
|
|
364
374
|
const checkRedirect = (source_path, nav_path) => {
|
@@ -422,7 +432,19 @@
|
|
422
432
|
}
|
423
433
|
};
|
424
434
|
|
435
|
+
const getMDPathFromHtmlPath = (htmlFile) => {
|
436
|
+
const markdownPath = htmlFile.relativePath.replace(`.${htmlFile.extension}`, '.md');
|
437
|
+
if (!fs.existsSync(markdownPath)) {
|
438
|
+
// No matching markdown
|
439
|
+
return htmlFile.relativePath;
|
440
|
+
}
|
441
|
+
return markdownPath;
|
442
|
+
}
|
443
|
+
|
425
444
|
const checkLinks = async (source_path, htmlFile, links, hdocbook_config) => {
|
445
|
+
const markdown_path = getMDPathFromHtmlPath(htmlFile);
|
446
|
+
const markdown_content = fs.readFileSync(markdown_path, 'utf8');
|
447
|
+
|
426
448
|
for (let i = 0; i < links.length; i++) {
|
427
449
|
// Validate that link is a valid URL first
|
428
450
|
const valid_url = hdoc.valid_url(links[i]);
|
@@ -443,95 +465,93 @@
|
|
443
465
|
//Flat Anchor - validate we have a same-file hit
|
444
466
|
isHashAnchor(htmlFile, links[i]);
|
445
467
|
} else {
|
446
|
-
|
447
|
-
|
448
|
-
);
|
468
|
+
const error_message = processErrorMessage(`Root relative links should start with a forward-slash: ${links[i]}`, markdown_path, markdown_content, links[i]);
|
469
|
+
errors[htmlFile.relativePath].push(error_message);
|
449
470
|
}
|
450
471
|
} else {
|
451
472
|
messages[htmlFile.relativePath].push(
|
452
473
|
`Link is a properly formatted external URL: ${links[i]}`,
|
453
474
|
);
|
454
|
-
|
475
|
+
|
455
476
|
// Skip if it's the auto-generated edit url, as these could be part of a private repo which would return a 404
|
456
477
|
if (
|
457
478
|
hdocbook_config.publicSource !== undefined &&
|
458
479
|
links[i] ===
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
480
|
+
hdoc
|
481
|
+
.get_github_api_path(
|
482
|
+
hdocbook_config.publicSource,
|
483
|
+
htmlFile.relativePath,
|
484
|
+
)
|
485
|
+
.edit_path.replace(path.extname(htmlFile.relativePath), ".md")
|
465
486
|
) {
|
466
487
|
continue;
|
467
488
|
}
|
468
|
-
|
489
|
+
|
469
490
|
if (valid_url.protocol === "mailto:") {
|
470
491
|
continue;
|
471
492
|
}
|
472
|
-
|
493
|
+
|
473
494
|
// Skip if the link is excluded in the project config
|
474
|
-
|
495
|
+
const skip_link = await excludeLink(links[i]);
|
496
|
+
if (skip_link) {
|
475
497
|
messages[htmlFile.relativePath].push(
|
476
498
|
`Skipping link validation for: ${links[i]}`,
|
477
499
|
);
|
478
500
|
continue;
|
479
501
|
}
|
480
502
|
|
503
|
+
|
481
504
|
if (
|
482
505
|
links[i].toLowerCase().includes("docs.hornbill.com") ||
|
483
506
|
links[i].toLowerCase().includes("docs-internal.hornbill.com")
|
484
507
|
) {
|
485
|
-
|
486
|
-
|
487
|
-
);
|
508
|
+
const error_message = processErrorMessage(`Hornbill Docs links should not be fully-qualified: ${links[i]}`, markdown_path, markdown_content, links[i]);
|
509
|
+
errors[htmlFile.relativePath].push( error_message );
|
488
510
|
continue;
|
489
511
|
}
|
490
512
|
|
491
513
|
try {
|
492
|
-
await axios.get(links[i]
|
493
|
-
httpsAgent: agent,
|
494
|
-
});
|
514
|
+
await axios.get(links[i]);
|
495
515
|
messages[htmlFile.relativePath].push(
|
496
516
|
`Link is a valid external URL: ${links[i]}`,
|
497
517
|
);
|
498
518
|
} catch (e) {
|
499
|
-
|
500
|
-
errors[htmlFile.relativePath].push(
|
501
|
-
`Link is not responding: ${links[i]} - [${e.message}]`,
|
502
|
-
);
|
519
|
+
const error_message = processErrorMessage(`External link is not responding: ${links[i]}`, markdown_path, markdown_content, links[i]);
|
520
|
+
errors[htmlFile.relativePath].push(error_message);
|
503
521
|
}
|
504
522
|
}
|
505
523
|
}
|
506
524
|
};
|
507
525
|
|
508
526
|
const checkImages = async (source_path, htmlFile, links) => {
|
527
|
+
const markdown_path = getMDPathFromHtmlPath(htmlFile);
|
528
|
+
const markdown_content = fs.readFileSync(markdown_path, 'utf8');
|
509
529
|
for (let i = 0; i < links.length; i++) {
|
510
530
|
// Validate that image is a valid URL first
|
511
531
|
if (!hdoc.valid_url(links[i])) {
|
512
532
|
// Could be a relative path, check image exists
|
513
|
-
doesFileExist(source_path, htmlFile, links[i]);
|
533
|
+
doesFileExist(source_path, htmlFile, links[i], markdown_path, markdown_content);
|
514
534
|
} else {
|
515
535
|
messages[htmlFile.relativePath].push(
|
516
536
|
`Image link is a properly formatted external URL: ${links[i]}`,
|
517
537
|
);
|
518
538
|
// Do a Get to the URL to see if it exists
|
519
539
|
try {
|
520
|
-
|
540
|
+
await axios.get(links[i]);
|
521
541
|
messages[htmlFile.relativePath].push(
|
522
542
|
`Image link is a valid external URL: ${links[i]}`,
|
523
543
|
);
|
524
544
|
} catch (e) {
|
525
545
|
// Handle errors
|
526
|
-
|
527
|
-
|
528
|
-
);
|
546
|
+
const error_message = processErrorMessage(`External image link error: ${links[i]} - ${e.message}`, markdown_path, markdown_content, links[i]);
|
547
|
+
errors[htmlFile.relativePath].push(error_message);
|
529
548
|
}
|
530
549
|
}
|
531
550
|
}
|
532
551
|
};
|
533
552
|
|
534
553
|
const checkTags = async (htmlFile) => {
|
554
|
+
const markdown_path = getMDPathFromHtmlPath(htmlFile);
|
535
555
|
// Check if file is excluded from tag check
|
536
556
|
const file_no_ext = htmlFile.relativePath.replace(
|
537
557
|
path.extname(htmlFile.relativePath),
|
@@ -554,7 +574,7 @@
|
|
554
574
|
error_msg += h1_tags[i].text();
|
555
575
|
if (i < h1_tags.length - 1) error_msg += "; ";
|
556
576
|
}
|
557
|
-
errors[htmlFile.relativePath].push(error_msg);
|
577
|
+
errors[htmlFile.relativePath].push(`${markdown_path} - ${error_msg}`);
|
558
578
|
}
|
559
579
|
};
|
560
580
|
|
@@ -580,10 +600,14 @@
|
|
580
600
|
};
|
581
601
|
|
582
602
|
const isRelativePath = (source_path, html_path, relative_path) => {
|
603
|
+
const markdown_path = getMDPathFromHtmlPath(html_path);
|
604
|
+
const markdown_content = fs.readFileSync(markdown_path, 'utf8');
|
605
|
+
|
583
606
|
const rel_path_ext = path.extname(relative_path);
|
584
607
|
const response = {
|
585
608
|
is_rel_path: false,
|
586
609
|
has_md_extension: rel_path_ext === ".md",
|
610
|
+
has_html_extension: rel_path_ext === ".htm" || rel_path_ext === ".html",
|
587
611
|
};
|
588
612
|
const supported_relpaths = [
|
589
613
|
`${path.sep}index.htm`,
|
@@ -627,10 +651,9 @@
|
|
627
651
|
}
|
628
652
|
}
|
629
653
|
}
|
630
|
-
if (response.has_md_extension) {
|
631
|
-
|
632
|
-
|
633
|
-
);
|
654
|
+
if (response.has_md_extension || response.has_html_extension) {
|
655
|
+
const error_message = processErrorMessage(`Relative link has ${rel_path_ext} extension, but should not: ${relative_path}`, markdown_path, markdown_content, relative_path);
|
656
|
+
errors[html_path.relativePath].push(error_message);
|
634
657
|
return;
|
635
658
|
}
|
636
659
|
if (response.is_rel_path) {
|
@@ -647,16 +670,27 @@
|
|
647
670
|
: clean_relative_path;
|
648
671
|
const redir = checkRedirect(source_path, relpath);
|
649
672
|
if (redir.exists) {
|
650
|
-
if (redir.error !== null)
|
651
|
-
|
673
|
+
if (redir.error !== null) {
|
674
|
+
const error_message = processErrorMessage(`${redir.error}: ${relative_path}`, markdown_path, markdown_content, clean_relative_path);
|
675
|
+
errors[html_path.relativePath].push(error_message);
|
676
|
+
}
|
652
677
|
} else {
|
653
|
-
|
654
|
-
|
655
|
-
);
|
678
|
+
const error_message = processErrorMessage(`Link path does not exist: ${relative_path}`, markdown_path, markdown_content, clean_relative_path);
|
679
|
+
errors[html_path.relativePath].push(error_message);
|
656
680
|
}
|
657
681
|
};
|
658
682
|
|
659
|
-
const
|
683
|
+
const processErrorMessage = (message, md_path, content, search) => {
|
684
|
+
const link_location = hdoc.find_string_in_string(content, search);
|
685
|
+
let error_message = message;
|
686
|
+
if (link_location !== null)
|
687
|
+
error_message = `${md_path}:${link_location.line}:${link_location.column} - ${error_message}`;
|
688
|
+
else
|
689
|
+
error_message = `${md_path} - ${error_message}`;
|
690
|
+
return error_message;
|
691
|
+
};
|
692
|
+
|
693
|
+
const doesFileExist = (source_path, html_path, relative_path, markdown_path, markdown_content) => {
|
660
694
|
// Remove explicit anchor links and _books prefix
|
661
695
|
const clean_relative_path = relative_path
|
662
696
|
.split("#")[0]
|
@@ -669,9 +703,8 @@
|
|
669
703
|
!fs.existsSync(`${file_path}.htm`) &&
|
670
704
|
!fs.existsSync(`${file_path}.html`)
|
671
705
|
) {
|
672
|
-
|
673
|
-
|
674
|
-
);
|
706
|
+
const error_message = processErrorMessage(`Book resource does not exist: ${clean_relative_path}`, markdown_path, markdown_content, relative_path);
|
707
|
+
errors[html_path.relativePath].push(error_message);
|
675
708
|
return false;
|
676
709
|
}
|
677
710
|
messages[html_path.relativePath].push(
|
@@ -935,9 +968,8 @@
|
|
935
968
|
let error_count = 0;
|
936
969
|
for (const key in errors) {
|
937
970
|
if (Object.hasOwn(errors, key) && errors[key].length > 0) {
|
938
|
-
console.error(`\r\n${errors[key].length} error(s) in ${key}`);
|
939
971
|
for (let i = 0; i < errors[key].length; i++) {
|
940
|
-
console.error(
|
972
|
+
console.error(`${errors[key][i]}`);
|
941
973
|
error_count++;
|
942
974
|
}
|
943
975
|
}
|