markdown_link_checker_sc 0.0.13 → 0.0.116
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/README.md +66 -12
- package/biglog.txt +0 -0
- package/index.js +181 -435
- package/package.json +6 -1
- package/src/errors.js +148 -0
- package/src/helpers.js +41 -0
- package/src/links.js +194 -0
- package/src/output_errors.js +49 -0
- package/src/process_image_orphans.js +97 -0
- package/src/process_internal_url_links.js +20 -0
- package/src/process_local_image_links.js +57 -0
- package/src/process_markdown.js +400 -0
- package/src/process_orphans.js +145 -0
- package/src/process_relative_links.js +116 -0
- package/src/shared_data.js +2 -0
- package/src/slugify.js +17 -0
- package/tests/errortype/current_file_missing_anchor/heading_present_for_anchor.md +13 -0
- package/tests/errortype/current_file_missing_anchor/missing_heading.md +5 -0
- package/tests/errortype/linked_file_missing_anchor/file_with_broken_heading_link.md +10 -0
- package/tests/errortype/linked_file_missing_anchor/file_without_heading.md +10 -0
- package/tests/errortype/linked_internal_file_html/file_exists.html +5 -0
- package/tests/errortype/linked_internal_file_html/file_exists_as_markdown.md +6 -0
- package/tests/errortype/linked_internal_file_html/links_to_file_that_is_html.md +10 -0
- package/tests/errortype/linked_internal_file_missing/file_present_relative_link_no_error.md +5 -0
- package/tests/errortype/linked_internal_file_missing/file_present_should_be_no_error.md +5 -0
- package/tests/errortype/linked_internal_file_missing/links_to_file_that_is_not_present.md +8 -0
- package/tests/errortype/local_image_not_found/page_with_missing_image.md +9 -0
- package/tests/errortype/local_image_not_found/test.png +0 -0
- package/tests/errortype/orphan_images/assets/image1_not_linked.png +0 -0
- package/tests/errortype/orphan_images/assets/image2_not_linked.png +0 -0
- package/tests/errortype/orphan_images/test/image3_not_linked.png +0 -0
- package/tests/errortype/orphan_images/test/image4_linked.png +0 -0
- package/tests/errortype/orphan_images/test/intro.md +11 -0
- package/tests/errortype/page_not_in_toc/page1intoc_should_not_error.md +7 -0
- package/tests/errortype/page_not_in_toc/page2intoc_should_not_error.md +7 -0
- package/tests/errortype/page_not_in_toc/page3NOTinTOC.md +7 -0
- package/tests/errortype/page_not_in_toc/toc.md +9 -0
- package/tests/errortype/url_to_local_site/mylocalsite_dot_com.md +8 -0
- package/tests/links/tests1.md +93 -0
- package/tests/links/tests2.md +3 -0
- package/tests/tests1.md +0 -42
package/index.js
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { sharedData } from "./src/shared_data.js";
|
|
6
|
+
//const path = require("path");
|
|
7
|
+
import { program } from "commander";
|
|
8
|
+
//const { program } = require("commander");
|
|
9
|
+
import { logToFile, isMarkdown, isHTML, isImage } from "./src/helpers.js";
|
|
10
|
+
import { outputErrors } from "./src/output_errors.js";
|
|
11
|
+
|
|
12
|
+
import { slugifyVuepress } from "./src/slugify.js";
|
|
13
|
+
import { processMarkdown } from "./src/process_markdown.js";
|
|
14
|
+
import { processRelativeLinks } from "./src/process_relative_links.js";
|
|
15
|
+
import { checkLocalImageLinks } from "./src/process_local_image_links.js";
|
|
16
|
+
import { processUrlsToLocalSource } from "./src/process_internal_url_links.js";
|
|
17
|
+
import {
|
|
18
|
+
checkPageOrphans,
|
|
19
|
+
getPageWithMostLinks,
|
|
20
|
+
} from "./src/process_orphans.js";
|
|
21
|
+
import { checkImageOrphansGlobal } from "./src/process_image_orphans.js";
|
|
6
22
|
|
|
7
23
|
program
|
|
8
24
|
.option(
|
|
9
|
-
"-
|
|
10
|
-
"
|
|
25
|
+
"-r, --root <path>",
|
|
26
|
+
"Root directory of your source (i.e. root of github repo). Use -d as well to specify a folder if docs are not in the root, or to just run on particular subfolder. Defaults to current directory.",
|
|
11
27
|
process.cwd()
|
|
12
28
|
)
|
|
13
29
|
.option(
|
|
14
|
-
"-
|
|
30
|
+
"-d, --directory [directory]",
|
|
31
|
+
"The directory to search for markdown and html files, relative to root - such as: `en` for an English subfolder. Default empty (same as -r directory)",
|
|
32
|
+
""
|
|
33
|
+
)
|
|
34
|
+
.option(
|
|
35
|
+
"-i, --imagedir [directory]",
|
|
36
|
+
"The directory to search for all image files for global orphan checking, relative to root - such as: `assets` or `en`. Default empty if not explicitly set, and global orphan checking will not be done",
|
|
37
|
+
""
|
|
38
|
+
)
|
|
39
|
+
.option(
|
|
40
|
+
"-c, --headingAnchorSlugify [value]",
|
|
15
41
|
"Slugify approach for turning markdown headings into heading anchors. Currently support vuepress only and always",
|
|
16
42
|
"vuepress"
|
|
17
43
|
)
|
|
@@ -20,36 +46,66 @@ program
|
|
|
20
46
|
"Try a markdown file extension check if a link to HTML fails.",
|
|
21
47
|
true
|
|
22
48
|
)
|
|
23
|
-
.option(
|
|
49
|
+
.option(
|
|
50
|
+
"-l, --log <types...>",
|
|
51
|
+
"Export logs for debugging. Types: allerrors, filterederrors, allresults etc."
|
|
52
|
+
)
|
|
24
53
|
.option(
|
|
25
54
|
"-f, --files <path>",
|
|
26
55
|
"JSON file with array of files to report on (default is all files). Paths are relative relative to -d by default, but -r can be used to set a different root.",
|
|
27
56
|
""
|
|
28
57
|
)
|
|
58
|
+
|
|
29
59
|
.option(
|
|
30
|
-
"-
|
|
31
|
-
"
|
|
32
|
-
""
|
|
60
|
+
"-s, --toc [value]",
|
|
61
|
+
"full filename of TOC/Summary file in file system. If not specified, inferred from file with most links to other files"
|
|
33
62
|
)
|
|
63
|
+
.option(
|
|
64
|
+
"-u, --site_url [value]",
|
|
65
|
+
"Site base url in form dev.example.com (used to catch absolute urls to local files)"
|
|
66
|
+
)
|
|
67
|
+
|
|
34
68
|
.parse(process.argv);
|
|
35
69
|
|
|
36
70
|
// TODO PX4 special parsing - errors or pages we exclude by default.
|
|
37
71
|
// Particular error types on particular pages?
|
|
38
72
|
|
|
39
|
-
const options = program.opts();
|
|
73
|
+
//const options = program.opts();
|
|
74
|
+
sharedData.options = program.opts();
|
|
75
|
+
sharedData.options.log ? null : (sharedData.options.log = []);
|
|
76
|
+
sharedData.allMarkdownFiles = new Set([]);
|
|
77
|
+
sharedData.allHTMLFiles = new Set([]);
|
|
78
|
+
sharedData.allImageFiles = new Set([]);
|
|
79
|
+
sharedData.allOtherFiles = new Set([]);
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
const markdownDirectory = path.join(sharedData.options.root, sharedData.options.directory);
|
|
83
|
+
if (sharedData.options.log == "fast") {
|
|
84
|
+
console.log(`MARKDOWN DIR ${markdownDirectory}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async () => {
|
|
88
|
+
// Load JSON file containing file paths and reassign as array to the JSON path
|
|
89
|
+
sharedData.options.files
|
|
90
|
+
? (sharedData.options.files = await loadJSONFileToReportOn(sharedData.options.files))
|
|
91
|
+
: (sharedData.options.files = []);
|
|
92
|
+
if (sharedData.options.log == "quick") {
|
|
93
|
+
for (const file of sharedData.options.files) {
|
|
94
|
+
console.log(file);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
40
98
|
|
|
41
99
|
// Function for loading JSON file that contains files to report on
|
|
42
100
|
async function loadJSONFileToReportOn(filePath) {
|
|
101
|
+
sharedData.options.log.includes("functions")
|
|
102
|
+
? console.log(`Function: loadJSONFileToReportOn(): filePath: ${filePath}`)
|
|
103
|
+
: null;
|
|
43
104
|
try {
|
|
44
105
|
const fileContent = await fs.promises.readFile(filePath, "utf8");
|
|
45
106
|
let filesArray = JSON.parse(fileContent);
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
filesArray = filesArray.map((str) => path.join(options.root, str));
|
|
49
|
-
} else {
|
|
50
|
-
//
|
|
51
|
-
filesArray = filesArray.map((str) => path.join(options.directory, str));
|
|
52
|
-
}
|
|
107
|
+
// Array relative to root, so update to have full path
|
|
108
|
+
filesArray = filesArray.map((str) => path.join(sharedData.options.root, str));
|
|
53
109
|
|
|
54
110
|
//console.log(filesArray);
|
|
55
111
|
return filesArray;
|
|
@@ -59,193 +115,30 @@ async function loadJSONFileToReportOn(filePath) {
|
|
|
59
115
|
}
|
|
60
116
|
}
|
|
61
117
|
|
|
62
|
-
|
|
63
|
-
const isHtml = (file) => path.extname(file).toLowerCase() === ".html";
|
|
118
|
+
|
|
64
119
|
const replaceDelimiter = (str, underscore) =>
|
|
65
120
|
underscore ? str.replace(/\s+/g, "_") : str.replace(/\s+/g, "-");
|
|
66
121
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
.replace(/[^A-Za-z0-9/]+/g, "-") // replace non-word characters except / with hyphens
|
|
72
|
-
.replace(/[\s_-]+/g, "-") // Replace spaces and underscores with hyphens
|
|
73
|
-
.replace(/^-+|-+$/g, ""); // Remove extra hyphens from the beginning or end of the string
|
|
74
|
-
|
|
75
|
-
if (str.includes("/")) {
|
|
76
|
-
//console.log(`DEBUG: SLUG: str: ${str} slug: ${slug}`);
|
|
77
|
-
}
|
|
78
|
-
return `${slug}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const processHeading = (line, slugifyApproach) => {
|
|
82
|
-
const matches = line.match(/^#+\s+(.+)$/);
|
|
83
|
-
if (matches) {
|
|
84
|
-
//slugifyApproach is currently only slugifyVuepress so we do no test.
|
|
85
|
-
return slugifyVuepress(matches[1]);
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const processMarkdownLink = (
|
|
91
|
-
line,
|
|
92
|
-
relativeLinks,
|
|
93
|
-
relativeImageLinks,
|
|
94
|
-
absoluteLinks,
|
|
95
|
-
absoluteImageLinks,
|
|
96
|
-
unHandledLinkTypes
|
|
97
|
-
) => {
|
|
98
|
-
const matches = line.matchAll(/([!@]?)\[([^\]]+)\]\((\S+?)\)/g);
|
|
99
|
-
|
|
100
|
-
// TODO - THIS matches @[youtube](gjHj6YsxcZk) valid link which is used for vuepress plugin URLs. We probably want to exclude it and deal with it separately
|
|
101
|
-
// Maybe a backwards lookup on @
|
|
102
|
-
// Not sure if we can generalize
|
|
103
|
-
|
|
104
|
-
for (const match of matches) {
|
|
105
|
-
const isMarkdownImageLink = match[1] == "!" ? true : false;
|
|
106
|
-
const isVuepressYouTubeLink = match[1] == "@" ? true : false;
|
|
107
|
-
|
|
108
|
-
const linkText = match[2];
|
|
109
|
-
let linkUrl = match[3];
|
|
110
|
-
const linkAnchorSplit = linkUrl.split("#");
|
|
111
|
-
linkUrl = linkAnchorSplit[0].trim();
|
|
112
|
-
const linkAnchor = linkAnchorSplit[1] ? linkAnchorSplit[1] : null;
|
|
113
|
-
|
|
114
|
-
const link = { linkText, linkUrl, linkAnchor };
|
|
115
|
-
|
|
116
|
-
if (isVuepressYouTubeLink) {
|
|
117
|
-
if (linkUrl.startsWith("http")) {
|
|
118
|
-
absoluteLinks.push(link);
|
|
119
|
-
} else {
|
|
120
|
-
unHandledLinkTypes.push(link); // Not going to handle this (yet)
|
|
121
|
-
// TODO - prepend the standard URL
|
|
122
|
-
}
|
|
123
|
-
} else if (linkUrl.startsWith("http")) {
|
|
124
|
-
isMarkdownImageLink
|
|
125
|
-
? absoluteImageLinks.push(link)
|
|
126
|
-
: absoluteLinks.push(link);
|
|
127
|
-
} else if (
|
|
128
|
-
linkUrl.startsWith("ftp:") ||
|
|
129
|
-
linkUrl.startsWith("ftps") ||
|
|
130
|
-
linkUrl.startsWith("mailto")
|
|
131
|
-
) {
|
|
132
|
-
// One of the types we specifically do not handle
|
|
133
|
-
unHandledLinkTypes.push(link);
|
|
134
|
-
} else if (
|
|
135
|
-
linkUrl.endsWith(".png") ||
|
|
136
|
-
linkUrl.endsWith(".jpg") ||
|
|
137
|
-
linkUrl.endsWith(".jpeg") ||
|
|
138
|
-
linkUrl.endsWith(".gif") ||
|
|
139
|
-
linkUrl.endsWith(".webp")
|
|
140
|
-
) {
|
|
141
|
-
//console.log("???Markdown");
|
|
142
|
-
//Catch case where image link is inside
|
|
143
|
-
relativeImageLinks.push(link);
|
|
144
|
-
} else {
|
|
145
|
-
isMarkdownImageLink
|
|
146
|
-
? relativeImageLinks.push(link)
|
|
147
|
-
: relativeLinks.push(link);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
//Match for html img and a - append to the lists
|
|
152
|
-
const regexHTMLLinks =
|
|
153
|
-
/<(a|img)[^>]*(href|src)="([^"]+)"[^>]*(?:title="([^"]+)"|>([^<]+)<\/\1>)/gi;
|
|
154
|
-
|
|
155
|
-
for (const match of line.matchAll(regexHTMLLinks)) {
|
|
156
|
-
const isMarkdownImageLink = match[1] == "img" ? true : false;
|
|
157
|
-
//const tagType = match[1];
|
|
158
|
-
//const hrefOrSrc = match[2];
|
|
159
|
-
let linkUrl = match[3];
|
|
160
|
-
const linkText = match[4] || match[5] || "";
|
|
161
|
-
const linkAnchorSplit = linkUrl.split("#");
|
|
162
|
-
linkUrl = linkAnchorSplit[0];
|
|
163
|
-
const linkAnchor = linkAnchorSplit[1] ? linkAnchorSplit[1] : null;
|
|
164
|
-
const link = { linkText, linkUrl, linkAnchor };
|
|
165
|
-
|
|
166
|
-
if (linkUrl.startsWith("http")) {
|
|
167
|
-
isMarkdownImageLink
|
|
168
|
-
? absoluteImageLinks.push(link)
|
|
169
|
-
: absoluteLinks.push(link);
|
|
170
|
-
} else {
|
|
171
|
-
isMarkdownImageLink
|
|
172
|
-
? relativeImageLinks.push(link)
|
|
173
|
-
: relativeLinks.push(link);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
relativeLinks,
|
|
179
|
-
absoluteLinks,
|
|
180
|
-
absoluteImageLinks,
|
|
181
|
-
relativeImageLinks,
|
|
182
|
-
};
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const processFile = async (file, slugifyApproach) => {
|
|
122
|
+
const processFile = async (file) => {
|
|
123
|
+
sharedData.options.log.includes("functions")
|
|
124
|
+
? console.log(`Function: processFile(): file: ${file}`)
|
|
125
|
+
: null;
|
|
186
126
|
try {
|
|
187
127
|
const contents = await fs.promises.readFile(file, "utf8");
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (heading) {
|
|
201
|
-
anchors.push(heading);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const links = processMarkdownLink(
|
|
205
|
-
line,
|
|
206
|
-
relativeLinks,
|
|
207
|
-
relativeImageLinks,
|
|
208
|
-
absoluteLinks,
|
|
209
|
-
absoluteImageLinks,
|
|
210
|
-
unHandledLinkTypes
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const htmlTagsWithIdsMatches = contents.match(
|
|
215
|
-
/<([a-z]+)(?:\s+[^>]*?\bid=(["'])(.*?)\2[^>]*?)?>/gi
|
|
216
|
-
);
|
|
217
|
-
if (htmlTagsWithIdsMatches) {
|
|
218
|
-
htmlTagsWithIdsMatches.forEach((match) => {
|
|
219
|
-
const tagMatches = match.match(/^<([a-z]+)/i);
|
|
220
|
-
const idMatches = match.match(/id=(["'])(.*?)\1/);
|
|
221
|
-
if (tagMatches && idMatches) {
|
|
222
|
-
const tag = tagMatches[1].toLowerCase();
|
|
223
|
-
const id = idMatches[2];
|
|
224
|
-
|
|
225
|
-
if (tag && id) {
|
|
226
|
-
/*
|
|
227
|
-
if (!htmlAnchors[tag]) {
|
|
228
|
-
htmlAnchors[tag] = [];
|
|
229
|
-
}
|
|
230
|
-
htmlAnchors[tag].push(id);
|
|
231
|
-
*/
|
|
232
|
-
htmlAnchors.push(id);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
}
|
|
128
|
+
const resultsForFile = processMarkdown(contents, file);
|
|
129
|
+
resultsForFile["page_file"] = file;
|
|
130
|
+
|
|
131
|
+
// Call slugify slugifyVuepress() on each of the headings
|
|
132
|
+
// Update resultsForFile[''] with values
|
|
133
|
+
// return slugifyVuepress(matches[1]);
|
|
134
|
+
const anchorArray = [];
|
|
135
|
+
resultsForFile.headings.forEach((item) => {
|
|
136
|
+
anchorArray.push(slugifyVuepress(item));
|
|
137
|
+
});
|
|
138
|
+
resultsForFile["anchors_auto_headings"] = anchorArray;
|
|
139
|
+
//console.log(resultsForFile);
|
|
237
140
|
|
|
238
|
-
return
|
|
239
|
-
page_file: file,
|
|
240
|
-
anchors_auto_headings: anchors,
|
|
241
|
-
anchors_tag_ids: htmlAnchors,
|
|
242
|
-
relativeLinks,
|
|
243
|
-
absoluteLinks,
|
|
244
|
-
absoluteImageLinks,
|
|
245
|
-
relativeImageLinks,
|
|
246
|
-
unHandledLinkTypes,
|
|
247
|
-
allErrors,
|
|
248
|
-
};
|
|
141
|
+
return resultsForFile;
|
|
249
142
|
} catch (err) {
|
|
250
143
|
console.error(`Error processing file ${file}: ${err.message}`);
|
|
251
144
|
console.error(err);
|
|
@@ -253,165 +146,60 @@ const processFile = async (file, slugifyApproach) => {
|
|
|
253
146
|
}
|
|
254
147
|
};
|
|
255
148
|
|
|
256
|
-
const processDirectory = async (dir
|
|
149
|
+
const processDirectory = async (dir) => {
|
|
150
|
+
sharedData.options.log.includes("functions")
|
|
151
|
+
? console.log(`Function: processDirectory(): dir: ${dir}`)
|
|
152
|
+
: null;
|
|
257
153
|
const files = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
258
154
|
const results = [];
|
|
259
155
|
for (let i = 0; i < files.length; i++) {
|
|
260
156
|
const file = path.join(dir, files[i].name);
|
|
157
|
+
//console.log(`XxxxXprocessDirectory: file: ${file}`);
|
|
261
158
|
if (files[i].isDirectory()) {
|
|
262
|
-
const subResults = await processDirectory(file
|
|
159
|
+
const subResults = await processDirectory(file);
|
|
263
160
|
results.push(...subResults);
|
|
264
|
-
} else if (isMarkdown(file)
|
|
265
|
-
|
|
161
|
+
} else if (isMarkdown(file)) {
|
|
162
|
+
sharedData.allMarkdownFiles.add(file);
|
|
163
|
+
const result = await processFile(file);
|
|
266
164
|
if (result) {
|
|
267
165
|
results.push(result);
|
|
268
166
|
}
|
|
269
167
|
}
|
|
168
|
+
|
|
169
|
+
else if (isHTML(file)) {
|
|
170
|
+
sharedData.allHTMLFiles.add(file);
|
|
171
|
+
const result = await processFile(file);
|
|
172
|
+
if (result) {
|
|
173
|
+
results.push(result);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
else if (isImage(file)) {
|
|
178
|
+
sharedData.allImageFiles.add(file);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
sharedData.allOtherFiles.add(file);
|
|
182
|
+
}
|
|
270
183
|
}
|
|
271
184
|
return results;
|
|
272
185
|
};
|
|
273
186
|
|
|
274
|
-
function processRelativeLinks(results) {
|
|
275
|
-
if (!results.allErrors) {
|
|
276
|
-
results["allErrors"] = [];
|
|
277
|
-
}
|
|
278
|
-
results.forEach((page, index, array) => {
|
|
279
|
-
//console.log(page);
|
|
280
|
-
|
|
281
|
-
page.relativeLinks.forEach((link, index, array) => {
|
|
282
|
-
//console.log(link);
|
|
283
|
-
//resolve the path for the link
|
|
284
|
-
const page_rel_path = page.page_file.split(options.directory)[1];
|
|
285
|
-
if (link.linkUrl === "") {
|
|
286
|
-
//page local link - check current page for headings
|
|
287
|
-
//console.log(link);
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
page.anchors_auto_headings.includes(link.linkAnchor) ||
|
|
291
|
-
page.anchors_tag_ids.includes(link.linkAnchor)
|
|
292
|
-
) {
|
|
293
|
-
//do nothing - we're good
|
|
294
|
-
} else {
|
|
295
|
-
const error = {
|
|
296
|
-
type: "InternalLocalMissingAnchor",
|
|
297
|
-
page: `${page.page_file}`,
|
|
298
|
-
linkAnchor: `${link.linkAnchor}`,
|
|
299
|
-
linkText: `${link.linkText}`,
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
results.allErrors.push(error);
|
|
303
|
-
//console.log(error);
|
|
304
|
-
//console.log( `ERROR: ${page_rel_path}: Missing local anchor [${link.linkText}](#${link.linkAnchor})` );
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
// relative link on another page.
|
|
308
|
-
|
|
309
|
-
//find the path of the linked page.
|
|
310
|
-
const linkAbsoluteFilePath = path.resolve(
|
|
311
|
-
path.dirname(page.page_file),
|
|
312
|
-
link.linkUrl
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
// Get the matching file matching our link, if it exists
|
|
316
|
-
let linkedFile =
|
|
317
|
-
results.find(
|
|
318
|
-
(linkedFile) =>
|
|
319
|
-
linkedFile.hasOwnProperty("page_file") &&
|
|
320
|
-
path.normalize(linkedFile.page_file) === linkAbsoluteFilePath
|
|
321
|
-
) || null;
|
|
322
|
-
|
|
323
|
-
if (!linkedFile) {
|
|
324
|
-
if (
|
|
325
|
-
options.tryMarkdownforHTML &&
|
|
326
|
-
linkAbsoluteFilePath.endsWith(".html")
|
|
327
|
-
) {
|
|
328
|
-
// The file was HTML so it might be a file extension mistake (linking to html instead of md)
|
|
329
|
-
// In this case we'll try find it.
|
|
330
|
-
|
|
331
|
-
const markdownAbsoluteFilePath = `${
|
|
332
|
-
linkAbsoluteFilePath.split(".html")[0]
|
|
333
|
-
}.md`;
|
|
334
|
-
const linkedHTMLFile =
|
|
335
|
-
results.find(
|
|
336
|
-
(linkedHTMLFile) =>
|
|
337
|
-
linkedHTMLFile.hasOwnProperty("page_file") &&
|
|
338
|
-
path.normalize(linkedHTMLFile.page_file) ===
|
|
339
|
-
markdownAbsoluteFilePath
|
|
340
|
-
) || null;
|
|
341
|
-
|
|
342
|
-
if (linkedHTMLFile) {
|
|
343
|
-
const error = {
|
|
344
|
-
type: "InternalLinkToHTML",
|
|
345
|
-
page: `${page.page_file}`,
|
|
346
|
-
linkUrl: `${link.linkUrl}`,
|
|
347
|
-
linkText: `${link.linkText}`,
|
|
348
|
-
linkUrlFilePath: `${linkAbsoluteFilePath}`,
|
|
349
|
-
};
|
|
350
|
-
results.allErrors.push(error);
|
|
351
|
-
// console.log(`: ${page_rel_path}: WARN: Link to .html not .md '${link.linkUrl}' with text '${link.linkText}' (${linkAbsoluteFilePath} )` );
|
|
352
|
-
linkedFile = linkedHTMLFile;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (!linkedFile) {
|
|
358
|
-
//File not found as .html or md
|
|
359
|
-
const error = {
|
|
360
|
-
type: "InternalLinkMissingFile",
|
|
361
|
-
page: `${page.page_file}`,
|
|
362
|
-
linkUrl: `${link.linkUrl}`,
|
|
363
|
-
linkText: `${link.linkText}`,
|
|
364
|
-
linkUrlFilePath: `${linkAbsoluteFilePath}`,
|
|
365
|
-
};
|
|
366
|
-
results.allErrors.push(error);
|
|
367
|
-
// console.log(`ERROR: ${page_rel_path}: ERROR Broken rel. link '${link.linkUrl}' with text '${link.linkText}' (${linkAbsoluteFilePath} )` );
|
|
368
|
-
} else {
|
|
369
|
-
// There is a link, so now see if there are anchors, and whether they work
|
|
370
|
-
if (!link.linkAnchor) {
|
|
371
|
-
//null
|
|
372
|
-
return;
|
|
373
|
-
} else if (
|
|
374
|
-
linkedFile.anchors_auto_headings.includes(link.linkAnchor) ||
|
|
375
|
-
linkedFile.anchors_tag_ids.includes(link.linkAnchor)
|
|
376
|
-
) {
|
|
377
|
-
//
|
|
378
|
-
//do nothing - we're good
|
|
379
|
-
} else {
|
|
380
|
-
// Link exists, but anchor broken
|
|
381
|
-
|
|
382
|
-
const link_rel_path = linkedFile.page_file.split(
|
|
383
|
-
options.directory
|
|
384
|
-
)[1];
|
|
385
|
-
const error = {
|
|
386
|
-
type: "InternalMissingAnchor",
|
|
387
|
-
page: `${page.page_file}`,
|
|
388
|
-
linkAnchor: `${link.linkAnchor}`,
|
|
389
|
-
linkUrl: `${link.linkUrl}`,
|
|
390
|
-
linkText: `${link.linkText}`,
|
|
391
|
-
linkUrlFilePath: `${linkAbsoluteFilePath}`,
|
|
392
|
-
};
|
|
393
|
-
results.allErrors.push(error);
|
|
394
|
-
//console.log( `WARN: ${page_rel_path}: Missing anchor \`${link.linkAnchor}\` linked in '${link_rel_path}' (linkText '${link.linkText}')` );
|
|
395
|
-
//console.log(`ERRORS CAUSED BY INCORRECT GUESS ABOUT FORMAT OF / in the new URL - e.g. mounting/orientation`)
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
187
|
function filterErrors(errors) {
|
|
188
|
+
sharedData.options.log.includes("functions")
|
|
189
|
+
? console.log(`Function: filterErrors()`)
|
|
190
|
+
: null;
|
|
404
191
|
// This method filters all errors against settings in the command line - such as pages to output.
|
|
405
192
|
let filteredErrors = errors;
|
|
406
193
|
// Filter results on specified file names (if any specified)
|
|
407
|
-
//console.log(`Number pages to filter: ${options.files.length}`);
|
|
408
|
-
if (options.files.length > 0) {
|
|
194
|
+
//console.log(`Number pages to filter: ${sharedData.options.files.length}`);
|
|
195
|
+
if (sharedData.options.files.length > 0) {
|
|
409
196
|
filteredErrors = errors.filter((error) => {
|
|
410
|
-
//console.log(`
|
|
197
|
+
//console.log(`UError: ${error}`);
|
|
411
198
|
//console.log(JSON.stringify(error, null, 2));
|
|
412
|
-
//console.log(`
|
|
413
|
-
|
|
414
|
-
|
|
199
|
+
//console.log(`UError page: ${error.page}`);
|
|
200
|
+
const filterResult = sharedData.options.files.includes(error.page);
|
|
201
|
+
//console.log(`filterResult: ${filterResult}`);
|
|
202
|
+
return filterResult;
|
|
415
203
|
});
|
|
416
204
|
}
|
|
417
205
|
// Filter on other things - such as errors.
|
|
@@ -420,115 +208,73 @@ function filterErrors(errors) {
|
|
|
420
208
|
return filteredErrors;
|
|
421
209
|
}
|
|
422
210
|
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// if this is empty skip step
|
|
428
|
-
// These are path relative, so we will need to
|
|
429
|
-
//console.log(`File options: ${options.files}`);
|
|
430
|
-
|
|
431
|
-
//Sort results by page and type.
|
|
432
|
-
// Perhaps next step is to create only get info for particular pages.
|
|
433
|
-
const sortedByPageErrors = {};
|
|
434
|
-
|
|
435
|
-
//for (const error of results.allErrors) {
|
|
436
|
-
for (const error of results) {
|
|
437
|
-
//Report errors for listed pages or all
|
|
438
|
-
//console.log("error:");
|
|
439
|
-
//console.log(error);
|
|
440
|
-
//console.log(error.page);
|
|
441
|
-
if (!sortedByPageErrors[error.page]) {
|
|
442
|
-
sortedByPageErrors[error.page] = [];
|
|
443
|
-
}
|
|
444
|
-
sortedByPageErrors[error.page].push(error);
|
|
211
|
+
//main function, after options et have been set up.
|
|
212
|
+
(async () => {
|
|
213
|
+
// process containing markdown, return results which includes links, headings, id anchors
|
|
214
|
+
const results = await processDirectory(markdownDirectory);
|
|
445
215
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
216
|
+
// Process just the relative links to find errors like missing files, anchors
|
|
217
|
+
const errorsFromRelativeLinks = processRelativeLinks(results);
|
|
218
|
+
if (!results.allErrors) {
|
|
219
|
+
results.allErrors = [];
|
|
450
220
|
}
|
|
221
|
+
results["allErrors"].push(...errorsFromRelativeLinks);
|
|
451
222
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
pageFromRoot = page.split(options.directory)[1];
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
console.log(`\n${pageFromRoot}`);
|
|
462
|
-
for (const error of sortedByPageErrors[page]) {
|
|
463
|
-
if (error.type == "InternalLinkMissingFile") {
|
|
464
|
-
console.log(`- ${error.type}: ${error.linkUrl}`);
|
|
465
|
-
//console.log(` ${error.type}: ${error.linkAnchor}, linkURL: ${error.linkUrl}`);
|
|
466
|
-
// { "type": "InternalLinkMissingFile", "page": `${page.page_file}`, "linkUrl": `${link.linkUrl}`, "linkText": `${link.linkText}`, "linkUrlFilePath": `${linkAbsoluteFilePath}` };
|
|
467
|
-
} else if (error.type == "InternalLocalMissingAnchor") {
|
|
468
|
-
// missing anchor in linked file that exists.
|
|
469
|
-
//console.log(error);
|
|
470
|
-
console.log(
|
|
471
|
-
`- ${error.type}: ` +
|
|
472
|
-
"`[" +
|
|
473
|
-
`${error.linkText}](#${error.linkAnchor})` +
|
|
474
|
-
"` (Internal link without matching heading name or element id)"
|
|
475
|
-
);
|
|
476
|
-
//console.log( `- ${error.type}: #${error.linkAnchor} (Internal link without matching heading name or element id)` );
|
|
477
|
-
//console.log(` ${error.type}: #${error.linkAnchor} (heading/anchor missing?)`);
|
|
478
|
-
//console.log(` #${error.linkAnchor} - Internal anchor not found`);
|
|
479
|
-
//console.log(` [${error.linkText}](#${error.linkAnchor}) - Anchor not found`);
|
|
480
|
-
//console.log(` Internal anchor not found: #${error.linkAnchor} `);
|
|
481
|
-
// `{ "type": "InternalLocalMissingAnchor", "page": "${page.page_file}", "anchor": "${link.linkAnchor}", "linktext", "${link.linkText}" }`;
|
|
482
|
-
} else if (error.type == "InternalMissingAnchor") {
|
|
483
|
-
// missing anchor in linked file that exists.
|
|
484
|
-
//console.log(error);
|
|
485
|
-
console.log(
|
|
486
|
-
`- ${error.type}: #${error.linkAnchor} not found in ${error.linkUrlFilePath}`
|
|
487
|
-
);
|
|
488
|
-
//console.log(` ${error.type}: #${error.linkAnchor} (heading/anchor missing?)`);
|
|
489
|
-
//console.log(` #${error.linkAnchor} - Internal anchor not found`);
|
|
490
|
-
//console.log(` [${error.linkText}](#${error.linkAnchor}) - Anchor not found`);
|
|
491
|
-
//console.log(` Internal anchor not found: #${error.linkAnchor} `);
|
|
492
|
-
// { "type": "InternalMissingAnchor", "page": `${page.page_file}`, "linkAnchor": `${link.linkAnchor}`, "linkUrl": `${link.linkUrl}`, "linktext": `${link.linkText}`, "linkUrlFilePath": `${linkAbsoluteFilePath}` };
|
|
493
|
-
} else if (error.type == "InternalLinkToHTML") {
|
|
494
|
-
console.log(`- ${error.type}: ${error.linkUrl} (should be ".md"?)`);
|
|
495
|
-
//console.log(` ${error.type}: linkURL: ${error.linkUrl} ends in ".html"`);
|
|
496
|
-
// { "type": "InternalLinkToHTML", "page": `${page.page_file}`, "linkUrl": `${link.linkUrl}`, "linkText": `${link.linkText}`, "linkUrlFilePath": `${linkAbsoluteFilePath}` };
|
|
497
|
-
} else {
|
|
498
|
-
console.log(error);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
//console.log(page)
|
|
502
|
-
//console.log(page.errors);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
223
|
+
// Process just images linked in local file system - find errors like missing images.
|
|
224
|
+
const errorsFromLocalImageLinks = await checkLocalImageLinks(
|
|
225
|
+
results
|
|
226
|
+
);
|
|
227
|
+
//console.log(errorsFromLocalImageLinks)
|
|
228
|
+
results["allErrors"].push(...errorsFromLocalImageLinks);
|
|
505
229
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
console.log(file);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
230
|
+
// Process links to current site URL - should be relative links normally.
|
|
231
|
+
const errorsFromUrlsToLocalSite = await processUrlsToLocalSource(
|
|
232
|
+
results
|
|
233
|
+
);
|
|
234
|
+
//console.log(errorsFromUrlsToLocalSite)
|
|
235
|
+
results["allErrors"].push(...errorsFromUrlsToLocalSite);
|
|
515
236
|
|
|
516
|
-
//
|
|
237
|
+
// Check for page orphans - markdown files not linked anywhere and not in summary.
|
|
238
|
+
// Guesses the table of contents file if not specified in options.toc
|
|
239
|
+
sharedData.options.toc ? null : (sharedData.options.toc = getPageWithMostLinks(results));
|
|
240
|
+
checkPageOrphans(results); // Perhaps should follow pattern of returning errors - currently updates results
|
|
517
241
|
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
options.headingAnchorSlugify
|
|
242
|
+
const errorsGlobalImageOrphanCheck = await checkImageOrphansGlobal(
|
|
243
|
+
results
|
|
521
244
|
);
|
|
245
|
+
results["allErrors"].push(...errorsGlobalImageOrphanCheck);
|
|
522
246
|
|
|
523
|
-
|
|
247
|
+
// Filter the errors based on the settings in options.
|
|
248
|
+
// At time of writing just filters on specific set of pages.
|
|
524
249
|
const filteredResults = filterErrors(results.allErrors);
|
|
250
|
+
|
|
251
|
+
// Output the errors as console.logs
|
|
525
252
|
outputErrors(filteredResults);
|
|
526
253
|
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
254
|
+
//make array and document options? ie. if includes ...
|
|
255
|
+
const jsonFilteredErrors = JSON.stringify(filteredResults, null, 2);
|
|
256
|
+
logToFile("./logs/filteredErrors.json", jsonFilteredErrors);
|
|
257
|
+
|
|
258
|
+
// Log filtered errors to standard out
|
|
259
|
+
if (sharedData.options.log.includes("filterederrors")) {
|
|
260
|
+
console.log(jsonFilteredErrors);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//make array and document options? ie. if includes ...
|
|
264
|
+
const jsonAllResults = JSON.stringify(results, null, 2);
|
|
265
|
+
logToFile("./logs/allResults.json", jsonAllResults);
|
|
266
|
+
if (sharedData.options.log.includes("allresults")) {
|
|
267
|
+
console.log(jsonAllResults);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//make array and document options? ie. if includes ...
|
|
271
|
+
const jsonAllErrors = JSON.stringify(results.allErrors, null, 2);
|
|
272
|
+
logToFile("./logs/allErrors.json", jsonAllErrors);
|
|
273
|
+
|
|
274
|
+
if (sharedData.options.log.includes("allerrors")) {
|
|
275
|
+
console.log(jsonAllErrors);
|
|
531
276
|
}
|
|
277
|
+
//console.log(`OPTIONS.LOG ${options.log}`);
|
|
532
278
|
})();
|
|
533
279
|
|
|
534
280
|
//OpenQuestions
|