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/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markdown_link_checker_sc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.116",
|
|
4
4
|
"description": "Markdown Link Checker",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "test"
|
|
8
8
|
},
|
|
9
|
+
"type": "module",
|
|
9
10
|
"keywords": [
|
|
10
11
|
"link",
|
|
11
12
|
"checker",
|
|
12
13
|
"markdown"
|
|
13
14
|
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/hamishwillee/markdown_link_checker_sc.git"
|
|
18
|
+
},
|
|
14
19
|
"bin": {
|
|
15
20
|
"markdown_link_checker_sc": "./index.js"
|
|
16
21
|
},
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { sharedData } from "./shared_data.js";
|
|
2
|
+
|
|
3
|
+
class LinkError {
|
|
4
|
+
constructor({ type, file, link = null }) {
|
|
5
|
+
if (!type) {
|
|
6
|
+
throw new Error("LinkError: Type is required!");
|
|
7
|
+
} else {
|
|
8
|
+
this.type = type;
|
|
9
|
+
}
|
|
10
|
+
if (link) {
|
|
11
|
+
this.link = link;
|
|
12
|
+
this.file = this.link.page;
|
|
13
|
+
} else {
|
|
14
|
+
this.file = file; // i.e. infer file from link, but if link not specified then can take passed value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
output() {
|
|
19
|
+
console.log(`UNKNOWN ERROR:`);
|
|
20
|
+
console.log(this);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Anchor link in current file does not exist
|
|
25
|
+
class CurrentFileMissingAnchorError extends LinkError {
|
|
26
|
+
constructor({ file, link }) {
|
|
27
|
+
super({ file: file, link: link, type: "CurrentFileMissingAnchor" }); // call the super class constructor and pass in the param object
|
|
28
|
+
}
|
|
29
|
+
output() {
|
|
30
|
+
console.log(
|
|
31
|
+
`- ${this.type}: ` +
|
|
32
|
+
"`[" +
|
|
33
|
+
`${this.link.text}](#${this.link.anchor})` +
|
|
34
|
+
"`: anchor doesn't match any heading id or element id"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Linked file (relative) exists but anchor in it does not
|
|
40
|
+
class LinkedFileMissingAnchorError extends LinkError {
|
|
41
|
+
constructor({ file, link }) {
|
|
42
|
+
super({ file: file, link: link, type: "LinkedFileMissingAnchor" });
|
|
43
|
+
}
|
|
44
|
+
output() {
|
|
45
|
+
console.log(
|
|
46
|
+
`- ${this.type}: #${this.link.anchor} not found in ${
|
|
47
|
+
this.link.address
|
|
48
|
+
} (${this.link.getAbsolutePath()})`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A link to a page (markdown, and maybe HTML) that does not exist.
|
|
54
|
+
class LinkedInternalPageMissingError extends LinkError {
|
|
55
|
+
constructor({ file, link }) {
|
|
56
|
+
super({ file: file, link: link, type: "LinkedInternalPageMissing" });
|
|
57
|
+
}
|
|
58
|
+
output() {
|
|
59
|
+
console.log(
|
|
60
|
+
`- ${this.type}: This linked file is missing: ${this.link.address}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A link to an HTML file probably should be markdown
|
|
66
|
+
class InternalLinkToHTMLError extends LinkError {
|
|
67
|
+
constructor({ file, link }) {
|
|
68
|
+
super({ file: file, link: link, type: "InternalLinkToHTML" });
|
|
69
|
+
}
|
|
70
|
+
output() {
|
|
71
|
+
console.log(`- ${this.type}: ${this.link.url} (should be ".md"?)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A link to a URL that is this site, and should probably be an internal/relative link
|
|
76
|
+
class UrlToLocalSiteError extends LinkError {
|
|
77
|
+
constructor({ file, link }) {
|
|
78
|
+
super({ file: file, link: link, type: "UrlToLocalSite" });
|
|
79
|
+
}
|
|
80
|
+
output() {
|
|
81
|
+
console.log(
|
|
82
|
+
`- ${this.type}: Link is URL to this site. Should it be relative link?: \\[${this.link.text}](${this.link.url}))`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Page is not linked from TOC page - here TOC is the page with most links, or may be explicitly defined.
|
|
88
|
+
class PageNotInTOCError extends LinkError {
|
|
89
|
+
constructor({ file, link }) {
|
|
90
|
+
super({ file: file, link: link, type: "PageNotInTOC" });
|
|
91
|
+
}
|
|
92
|
+
output() {
|
|
93
|
+
console.log(
|
|
94
|
+
`- ${this.type}: Page not in Table of Contents (${sharedData.options.toc})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Page is not linked from any other page
|
|
100
|
+
class PageNotLinkedInternallyError extends LinkError {
|
|
101
|
+
constructor({ file, link }) {
|
|
102
|
+
super({ file: file, link: link, type: "PageNotLinkedInternally" });
|
|
103
|
+
}
|
|
104
|
+
output() {
|
|
105
|
+
console.log(
|
|
106
|
+
`- ${this.type}: Page is orphan (not linked by any other page)`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Image is linked from page but not found
|
|
112
|
+
class LocalImageNotFoundError extends LinkError {
|
|
113
|
+
constructor({ file, link }) {
|
|
114
|
+
super({ file: file, link: link, type: "LocalImageNotFound" });
|
|
115
|
+
}
|
|
116
|
+
output() {
|
|
117
|
+
console.log(
|
|
118
|
+
`- ${this.type}: Linked image not found in file system: ${this.link.url}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Image is linked from page but not found
|
|
124
|
+
class OrphanedImageError extends LinkError {
|
|
125
|
+
constructor({ file, link }) {
|
|
126
|
+
super({ file: file, link: link, type: "OrphanedImage" });
|
|
127
|
+
}
|
|
128
|
+
output() {
|
|
129
|
+
console.log(
|
|
130
|
+
`- ${this.type}: Image not linked from docs: ${this.file}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
LinkError,
|
|
139
|
+
CurrentFileMissingAnchorError,
|
|
140
|
+
LinkedFileMissingAnchorError,
|
|
141
|
+
LinkedInternalPageMissingError,
|
|
142
|
+
InternalLinkToHTMLError,
|
|
143
|
+
UrlToLocalSiteError,
|
|
144
|
+
PageNotInTOCError,
|
|
145
|
+
PageNotLinkedInternallyError,
|
|
146
|
+
LocalImageNotFoundError,
|
|
147
|
+
OrphanedImageError
|
|
148
|
+
};
|
package/src/helpers.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
// Log data to specified file path, replacing file.
|
|
5
|
+
async function logToFile(filePath, dataString) {
|
|
6
|
+
try {
|
|
7
|
+
await fs.writeFile(filePath, dataString);
|
|
8
|
+
//console.log("Data written to file");
|
|
9
|
+
} catch (err) {
|
|
10
|
+
console.error(err);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const imageExtensions = new Set([
|
|
15
|
+
".jpg",
|
|
16
|
+
".jpeg",
|
|
17
|
+
".png",
|
|
18
|
+
".svg",
|
|
19
|
+
".gif",
|
|
20
|
+
".webm",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Return true if file is an image.
|
|
24
|
+
//Just looks at file extension.
|
|
25
|
+
function isImage(file) {
|
|
26
|
+
const fileExtension = path.extname(file).toLowerCase();
|
|
27
|
+
return imageExtensions.has(fileExtension) ? true : false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isMarkdown(file) {
|
|
31
|
+
const fileExtension = path.extname(file).toLowerCase();
|
|
32
|
+
return fileExtension === ".md" ? true : false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isHTML(file) {
|
|
36
|
+
const fileExtension = path.extname(file).toLowerCase();
|
|
37
|
+
//console.log(`ext: ${fileExtension}`);
|
|
38
|
+
return fileExtension === ".html" || fileExtension === ".htm" ? true : false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { logToFile, isImage, isMarkdown, isHTML };
|
package/src/links.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { isImage, isMarkdown, isHTML } from "./helpers.js";
|
|
3
|
+
import { sharedData } from "./shared_data.js";
|
|
4
|
+
|
|
5
|
+
class Link {
|
|
6
|
+
address = "";
|
|
7
|
+
anchor = "";
|
|
8
|
+
params = "";
|
|
9
|
+
type = "unHandledLinkType";
|
|
10
|
+
goat = "This is a 2goat";
|
|
11
|
+
isImage = false;
|
|
12
|
+
isMarkdown = false;
|
|
13
|
+
isHTML = false;
|
|
14
|
+
isRelative = false;
|
|
15
|
+
|
|
16
|
+
//isImage = false;
|
|
17
|
+
static linkTypes;
|
|
18
|
+
static {
|
|
19
|
+
this.linkTypes = new Set([
|
|
20
|
+
"unHandledLinkType",
|
|
21
|
+
"urlLink", // http(s) link
|
|
22
|
+
"urlLocalLink", // URL (http) pointing to current site
|
|
23
|
+
"urlImageLink", // https(s) link to image
|
|
24
|
+
"relativeImageLink", // relative link to image
|
|
25
|
+
"relativeLink", // relative link to another page/file
|
|
26
|
+
"relativeHTMLLink", // relative link to an HTML file.
|
|
27
|
+
"relativeAnchorLink", // link to anchor in current page
|
|
28
|
+
//"relativeParamLink", // link that only has params - probably a bug
|
|
29
|
+
"ftpLink", // FTP URL (i.e. ftp://)
|
|
30
|
+
"ftpsLink", // FTPS URL (i.e. ftps://)
|
|
31
|
+
"mailtoLink", // Mailto link (ie mailto whatever)
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor({ page, url, type, text, title }) {
|
|
36
|
+
sharedData.options.log.includes("functions")
|
|
37
|
+
? console.log("Link:constructor")
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
if (page) {
|
|
41
|
+
this.page = page;
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error("Link: page argument is required.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (url) {
|
|
47
|
+
this.url = url;
|
|
48
|
+
this.splitURL(this.url);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error("Link: url argument is required.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const linkTypeGuess = this.findType(); // Do to populate the isXxxx values
|
|
54
|
+
if (type) {
|
|
55
|
+
if (!Link.linkTypes.has(type)) {
|
|
56
|
+
console.log("Supported Link Types:");
|
|
57
|
+
console.log(Link.linkTypes); //This is because having trouble getting the set to print
|
|
58
|
+
throw new Error(`Link: type ${type} must be in supported link types`);
|
|
59
|
+
} else {
|
|
60
|
+
this.type = type;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
//No type specified - use type inferred from extension etc.
|
|
64
|
+
this.type = linkTypeGuess;
|
|
65
|
+
}
|
|
66
|
+
text ? (this.text = text) : (this.text = "");
|
|
67
|
+
title ? (this.title = title) : (this.title = "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Take a URL and split to address, anchor, params
|
|
71
|
+
splitURL(url) {
|
|
72
|
+
//console.log(`Link:SplitUrl(${url})`);
|
|
73
|
+
const hashIndex = url.indexOf("#");
|
|
74
|
+
const queryIndex = url.indexOf("?");
|
|
75
|
+
|
|
76
|
+
let address = "";
|
|
77
|
+
let anchor = "";
|
|
78
|
+
let params = "";
|
|
79
|
+
|
|
80
|
+
if (hashIndex >= 0 && queryIndex >= 0) {
|
|
81
|
+
const splitIndex = hashIndex < queryIndex ? hashIndex : queryIndex;
|
|
82
|
+
address = url.substring(0, splitIndex);
|
|
83
|
+
if (hashIndex < queryIndex) {
|
|
84
|
+
anchor = url.substring(hashIndex + 1, queryIndex);
|
|
85
|
+
params = url.substring(queryIndex + 1);
|
|
86
|
+
} else {
|
|
87
|
+
params = url.substring(queryIndex + 1, hashIndex);
|
|
88
|
+
anchor = url.substring(hashIndex + 1);
|
|
89
|
+
}
|
|
90
|
+
} else if (hashIndex >= 0) {
|
|
91
|
+
// no queryIndex
|
|
92
|
+
address = url.substring(0, hashIndex);
|
|
93
|
+
anchor = url.substring(hashIndex + 1);
|
|
94
|
+
} else if (queryIndex >= 0) {
|
|
95
|
+
address = url.substring(0, queryIndex);
|
|
96
|
+
params = url.substring(queryIndex + 1);
|
|
97
|
+
} else {
|
|
98
|
+
address = url;
|
|
99
|
+
}
|
|
100
|
+
this.address = address;
|
|
101
|
+
this.params = params;
|
|
102
|
+
this.anchor = anchor;
|
|
103
|
+
//console.log(`url: ${this.url}`);
|
|
104
|
+
//console.log(`Address: ${this.address}`); //XXX
|
|
105
|
+
//console.log(`anchor: ${this.anchor}`);
|
|
106
|
+
//console.log(`param: ${this.params}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Assign a guessed type based on other information.
|
|
110
|
+
// This is only used if the type is not specified as an argument.
|
|
111
|
+
// Uses file extension etc, so should be run after SplitUrl() which finds the address
|
|
112
|
+
findType() {
|
|
113
|
+
sharedData.options.log.includes("functions")
|
|
114
|
+
? console.log("Link:findType()")
|
|
115
|
+
: null;
|
|
116
|
+
let linkType = "unHandledLinkType";
|
|
117
|
+
|
|
118
|
+
this.isImage = this.address && isImage(this.address) ? true : false; //only if address is true.
|
|
119
|
+
this.isMarkdown =
|
|
120
|
+
this.address && isMarkdown(this.address) ? true : false; //only if address is true.
|
|
121
|
+
this.isHTML = this.address && isHTML(this.address) ? true : false; //only if address is true.
|
|
122
|
+
const regexpTestProtocol = /^[a-z]+:/i;
|
|
123
|
+
|
|
124
|
+
//console.log(`Linkcheck1: ${this.address} `);
|
|
125
|
+
if (!this.address) {
|
|
126
|
+
//console.log(`Linkcheck2: ${this.address} `);
|
|
127
|
+
// local/relative link
|
|
128
|
+
if (this.anchor) {
|
|
129
|
+
//console.log(`Linkcheck3: ${this.anchor} `);
|
|
130
|
+
linkType = "relativeAnchorLink";
|
|
131
|
+
} else if (this.params) {
|
|
132
|
+
//console.log(`Linkcheck3: ${this.params} `);
|
|
133
|
+
// no anchor, no address, and has params
|
|
134
|
+
throw Error("Link: Invalid - only params, no address or anchor");
|
|
135
|
+
} else {
|
|
136
|
+
//console.log(`Linkcheck4: should be nothing url: ${this.url} `);
|
|
137
|
+
// This is the no url case - we already throw on this.
|
|
138
|
+
//Here we go again
|
|
139
|
+
throw Error("Link: Invalid - no address, params, anchor");
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// We have an address - so it is either a URL or relative of some kind
|
|
143
|
+
|
|
144
|
+
if (this.address.startsWith("ftp://")) {
|
|
145
|
+
linkType = "ftpLink";
|
|
146
|
+
} else if (this.address.startsWith("ftps://")) {
|
|
147
|
+
linkType = "ftpsLink";
|
|
148
|
+
} else if (this.address.startsWith("mailto:")) {
|
|
149
|
+
linkType = "mailtoLink";
|
|
150
|
+
} else if (
|
|
151
|
+
this.address.startsWith("http:") ||
|
|
152
|
+
this.address.startsWith("https:")
|
|
153
|
+
) {
|
|
154
|
+
linkType = this.isImage ? "urlImageLink" : "urlLink";
|
|
155
|
+
} else if (regexpTestProtocol.test(this.address)) {
|
|
156
|
+
// This is a protocol, but not one we handle.
|
|
157
|
+
// Leave type as unhandled. Should perhaps have a log type for unhandled stuff
|
|
158
|
+
//console.log("NN The string starts with an unhandled protocol - remove at some point");
|
|
159
|
+
} else {
|
|
160
|
+
this.isRelative = true;
|
|
161
|
+
// Must be a relative link of some kind.
|
|
162
|
+
//this.absolutePath = this.getAbsolutePath();
|
|
163
|
+
if (this.isImage) {
|
|
164
|
+
//console.log(`Linkcheck11 link is relative image : ${isImage} `);
|
|
165
|
+
linkType = "relativeImageLink";
|
|
166
|
+
} else if (this.isMarkdown) {
|
|
167
|
+
//console.log(`Linkcheck12 link is relative image : ${isMarkdown} `);
|
|
168
|
+
linkType = "relativeLink";
|
|
169
|
+
} else if (this.isHTML) {
|
|
170
|
+
//console.log(`Linkcheck13 link is relative image : ${isHTML} `);
|
|
171
|
+
linkType = "relativeHTMLLink"; //
|
|
172
|
+
} else {
|
|
173
|
+
//console.log(`Odd URLS : ${this.url} `);
|
|
174
|
+
// Its an unhandled relative link that isn't an image or markdown.
|
|
175
|
+
// Generally these links don't work in markdown. But for now let's leave it as unhandled.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return linkType; //Return whatever we guessed
|
|
181
|
+
} // end of file(type)
|
|
182
|
+
|
|
183
|
+
//get absolute path to link, if this is a relative URL link.
|
|
184
|
+
getAbsolutePath() {
|
|
185
|
+
sharedData.options.log.includes("functions")
|
|
186
|
+
? console.log(`Link:getAbsolutePath: page: ${this.page}, address: ${this.address} `)
|
|
187
|
+
: null;
|
|
188
|
+
if (!this.isRelative) throw new Error("Link:getAbsolutePath() called on non-relative path");
|
|
189
|
+
return path.resolve(path.dirname(this.page), this.address);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { Link };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//import { /*LinkError,*/ CurrentFileMissingAnchorError, LinkedFileMissingAnchorError, LinkedInternalPageMissingError, InternalLinkToHTMLError, UrlToLocalSiteError} from "./errors.js"
|
|
2
|
+
|
|
3
|
+
import { sharedData } from "./shared_data.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
//Function that generates console and/or log output from an array of error objects.
|
|
7
|
+
// - `results` is an array of error objects. These will have a `type` and a `page`. They may also have other values, depending on type of error - such as linkurl
|
|
8
|
+
function outputErrors(results) {
|
|
9
|
+
sharedData.options.log.includes("functions")
|
|
10
|
+
? console.log("Function: outputErrors()")
|
|
11
|
+
: null;
|
|
12
|
+
|
|
13
|
+
//Sort results by page and type.
|
|
14
|
+
// Perhaps next step is to create only get info for particular pages.
|
|
15
|
+
const sortedByPageErrors = {};
|
|
16
|
+
|
|
17
|
+
for (const error of results) {
|
|
18
|
+
//Report errors for listed pages or all
|
|
19
|
+
//console.log(error.page);
|
|
20
|
+
if (!sortedByPageErrors[error.file]) {
|
|
21
|
+
sortedByPageErrors[error.file] = [];
|
|
22
|
+
}
|
|
23
|
+
sortedByPageErrors[error.file].push(error);
|
|
24
|
+
|
|
25
|
+
// Sort by type as well.
|
|
26
|
+
for (const page in sortedByPageErrors) {
|
|
27
|
+
sortedByPageErrors[page].sort((a, b) => a.type.localeCompare(b.type));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//console.log(sortedByPageErrors);
|
|
32
|
+
for (const page in sortedByPageErrors) {
|
|
33
|
+
let pageFromRoot;
|
|
34
|
+
if (sharedData.options.root) {
|
|
35
|
+
pageFromRoot = page.split(sharedData.options.root)[1];
|
|
36
|
+
} else {
|
|
37
|
+
pageFromRoot = page.split(sharedData.options.directory)[1];
|
|
38
|
+
}
|
|
39
|
+
//console.log(`\nXX${page}`); //Root needs to full path - not '.' or whatever
|
|
40
|
+
console.log(`\n${pageFromRoot}`); //Root needs to full path - not '.' or whatever
|
|
41
|
+
for (const error of sortedByPageErrors[page]) {
|
|
42
|
+
if (error.output) {
|
|
43
|
+
error.output();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { outputErrors };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { logToFile } from "./helpers.js";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { sharedData } from "./shared_data.js";
|
|
5
|
+
import { OrphanedImageError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
function isImage(file) {
|
|
8
|
+
const imageExtensions = [".jpg", ".jpeg", ".png", ".svg", ".gif", ".webm"];
|
|
9
|
+
const fileExtension = path.extname(file).toLowerCase();
|
|
10
|
+
return imageExtensions.includes(fileExtension) ? true : false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var otherFileTypes = []; // Just used for logging in function below.
|
|
14
|
+
|
|
15
|
+
// Gets all image files in a directory.
|
|
16
|
+
async function getAllImageFilesInDirectory(dir) {
|
|
17
|
+
sharedData.options.log.includes("functions")
|
|
18
|
+
? console.log(`Function: getAllImageFilesInDirectory(${dir})`)
|
|
19
|
+
: null;
|
|
20
|
+
|
|
21
|
+
// TODO put this all in a try catch and return a better error.
|
|
22
|
+
// Or perhaps put around parent.
|
|
23
|
+
|
|
24
|
+
const files = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
25
|
+
const images = [];
|
|
26
|
+
for (let i = 0; i < files.length; i++) {
|
|
27
|
+
const file = path.join(dir, files[i].name);
|
|
28
|
+
if (files[i].isDirectory()) {
|
|
29
|
+
const subImages = await getAllImageFilesInDirectory(file);
|
|
30
|
+
images.push(...subImages);
|
|
31
|
+
} else if (isImage(file)) {
|
|
32
|
+
images.push(file);
|
|
33
|
+
} else {
|
|
34
|
+
const fileExtension = path.extname(file).toLowerCase();
|
|
35
|
+
otherFileTypes.includes(fileExtension)
|
|
36
|
+
? null
|
|
37
|
+
: otherFileTypes.push(path.extname(file).toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//console.log( `XXXX Other file types ${JSON.stringify(otherFileTypes, null, 2)}` );
|
|
41
|
+
return images;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Checks if any images in the options.directory
|
|
45
|
+
async function checkImageOrphansGlobal(results) {
|
|
46
|
+
sharedData.options.log.includes("functions")
|
|
47
|
+
? console.log("Function: checkImageOrphansGlobal")
|
|
48
|
+
: null;
|
|
49
|
+
const errors = [];
|
|
50
|
+
let allImagesFound = [];
|
|
51
|
+
|
|
52
|
+
if (sharedData.options.imagedir !== "") {
|
|
53
|
+
const imagePath = path.resolve(
|
|
54
|
+
sharedData.options.root,
|
|
55
|
+
sharedData.options.imagedir
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
allImagesFound = await getAllImageFilesInDirectory(imagePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sharedData.allImageFiles.forEach((value, valueAgain, set) => {
|
|
62
|
+
const imagePath = path.resolve(sharedData.options.root, value);
|
|
63
|
+
//console.log('val: ' + value);
|
|
64
|
+
allImagesFound.push(imagePath);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Check all image files listed are in the array of local images we have (this is from the options.images directory)
|
|
68
|
+
const allImagesLinked = [];
|
|
69
|
+
results.forEach((page) => {
|
|
70
|
+
page.relativeImageLinks.forEach((link) => {
|
|
71
|
+
const fullImagePath = link.getAbsolutePath();
|
|
72
|
+
allImagesLinked.push(fullImagePath);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
//console.log(`XXXXallImagesFound ${JSON.stringify(allImagesFound, null, 2)}`);
|
|
77
|
+
//console.log( `XXXXallImagesLinked ${JSON.stringify(allImagesLinked, null, 2)}` );
|
|
78
|
+
|
|
79
|
+
// Add the image/assets directory to all the files found in the markdown directory.
|
|
80
|
+
//
|
|
81
|
+
|
|
82
|
+
allImagesFound.forEach((image) => {
|
|
83
|
+
if (!allImagesLinked.includes(image)) {
|
|
84
|
+
const error = new OrphanedImageError({ file: image });
|
|
85
|
+
errors.push(error);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
//console.log(`XXXXOrphanedImageErrors ${JSON.stringify(errors, null, 2)}`);
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
Add function to check if supplied images are linked or orphans - i.e. PR that adds an image that isn't used.
|
|
94
|
+
Might not be needed - ie the above should make a filtered image on page.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
export { checkImageOrphansGlobal };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { UrlToLocalSiteError} from "./errors.js"
|
|
3
|
+
|
|
4
|
+
// An array of errors given a results object that contains our array of objects containing urls that link to our current site.
|
|
5
|
+
function processUrlsToLocalSource(results) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
results.forEach((page, index, array) => {
|
|
8
|
+
//console.log(`PAGE: ${page}`);
|
|
9
|
+
|
|
10
|
+
page.urlLocalLinks.forEach((link, index, array) => {
|
|
11
|
+
//console.log(`LINK: ${link}`);
|
|
12
|
+
|
|
13
|
+
const error = new UrlToLocalSiteError({link: link})
|
|
14
|
+
errors.push(error);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
return errors;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { processUrlsToLocalSource };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { sharedData } from "./shared_data.js";
|
|
4
|
+
import { LocalImageNotFoundError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
// Checks if every image in every markdown page (results.page.relativeImageLinks) is present on the file system.
|
|
7
|
+
// - results is the array of information coming out of markdown parsing.
|
|
8
|
+
async function checkLocalImageLinks(results) {
|
|
9
|
+
sharedData.options.log.includes("functions") ? console.log(`Function: checkLocalImageLinks()`) : null;
|
|
10
|
+
const errors = [];
|
|
11
|
+
const promises = [];
|
|
12
|
+
|
|
13
|
+
results.forEach((page /*, index, array*/) => {
|
|
14
|
+
//console.log(`PAGE: ${page}`);
|
|
15
|
+
|
|
16
|
+
page.relativeImageLinks.forEach((link, index, array) => {
|
|
17
|
+
//console.log(`XYYXLINK: ${JSON.stringify(link, null, 2)}`);
|
|
18
|
+
//console.log(`sharedData.options.root: ${sharedData.options.root}`);
|
|
19
|
+
//console.log(`sharedData.options.directory: ${sharedData.options.directory}`);
|
|
20
|
+
//console.log(`link.linkUrlt: ${link.url}`);
|
|
21
|
+
//console.log(`dirname: ${path.dirname(page.page_file)}`);
|
|
22
|
+
|
|
23
|
+
const fullImagePath = path.join(
|
|
24
|
+
path.dirname(page.page_file),
|
|
25
|
+
link.url
|
|
26
|
+
);
|
|
27
|
+
//console.log(`fullImagePath: ${fullImagePath}`);
|
|
28
|
+
const promise = new Promise((resolve) => {
|
|
29
|
+
fs.access(fullImagePath, fs.constants.F_OK, (err) => {
|
|
30
|
+
if (err) {
|
|
31
|
+
//console.log("Error");
|
|
32
|
+
const error = new LocalImageNotFoundError({link: link});
|
|
33
|
+
/*
|
|
34
|
+
const error = {
|
|
35
|
+
type: "LocalImageNotFound",
|
|
36
|
+
page: `${page.page_file}`,
|
|
37
|
+
linkUrl: `${link.url}`,
|
|
38
|
+
linkText: `${link.text}`,
|
|
39
|
+
linkFullPath: `${fullImagePath}`,
|
|
40
|
+
};
|
|
41
|
+
*/
|
|
42
|
+
errors.push(error);
|
|
43
|
+
resolve(false);
|
|
44
|
+
} else {
|
|
45
|
+
//console.log("SUCCESS");
|
|
46
|
+
resolve(true);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
promises.push(promise);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
await Promise.all(promises); //Wait for all files to be checked
|
|
54
|
+
return errors;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { checkLocalImageLinks };
|