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
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { Link } from "./links.js";
|
|
2
|
+
import { sharedData } from "./shared_data.js";
|
|
3
|
+
|
|
4
|
+
// Returns slug for a string (markdown heading) using Vuepress algorithm.
|
|
5
|
+
// Algorithm from chatgpt - needs testing.
|
|
6
|
+
const processMarkdown = (contents, page) => {
|
|
7
|
+
sharedData.options.log.includes("functions")
|
|
8
|
+
? console.log(`Function: processMarkdown(): page: ${page}`)
|
|
9
|
+
: null;
|
|
10
|
+
const headings = [];
|
|
11
|
+
//const anchors = [];
|
|
12
|
+
const htmlAnchors = []; //{};
|
|
13
|
+
const relativeLinks = [];
|
|
14
|
+
const urlLinks = [];
|
|
15
|
+
const urlLocalLinks = [];
|
|
16
|
+
const urlImageLinks = [];
|
|
17
|
+
const relativeImageLinks = [];
|
|
18
|
+
const unHandledLinkTypes = [];
|
|
19
|
+
let redirectTo; //Pages that contain <Redirect to="string"/> links
|
|
20
|
+
|
|
21
|
+
//console.log("SHARED_DATA");
|
|
22
|
+
//console.log(sharedData);
|
|
23
|
+
// Check if page is a redirect.
|
|
24
|
+
// If it is, add to list then return.
|
|
25
|
+
// Otherwise do other file processing.
|
|
26
|
+
const regex = /<Redirect to="(.+?)" \/>/;
|
|
27
|
+
const matches = contents.match(regex);
|
|
28
|
+
matches ? (redirectTo = matches[1]) : (redirectTo = null);
|
|
29
|
+
if (redirectTo) {
|
|
30
|
+
//console.log(`REDIRECT: ${file}`)
|
|
31
|
+
} else {
|
|
32
|
+
// Don't do anything else for redirects pages
|
|
33
|
+
|
|
34
|
+
const lines = contents.split(/\r?\n/);
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
const line = lines[i];
|
|
38
|
+
|
|
39
|
+
// match headings
|
|
40
|
+
const matches = line.match(/^#+\s+(.+)$/);
|
|
41
|
+
if (matches) {
|
|
42
|
+
headings.push(matches[1]);
|
|
43
|
+
}
|
|
44
|
+
// TODO - have to slugify later.
|
|
45
|
+
|
|
46
|
+
const links = processLineMarkdownLinks(
|
|
47
|
+
line,
|
|
48
|
+
relativeLinks,
|
|
49
|
+
relativeImageLinks,
|
|
50
|
+
urlLinks,
|
|
51
|
+
urlLocalLinks,
|
|
52
|
+
urlImageLinks,
|
|
53
|
+
unHandledLinkTypes,
|
|
54
|
+
page
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Match html tags that have an id element
|
|
59
|
+
// (another way an anchor can be created)
|
|
60
|
+
const htmlTagsWithIdsMatches = contents.match(
|
|
61
|
+
/<([a-z]+)(?:\s+[^>]*?\bid=(["'])(.*?)\2[^>]*?)?>/gi
|
|
62
|
+
);
|
|
63
|
+
if (htmlTagsWithIdsMatches) {
|
|
64
|
+
htmlTagsWithIdsMatches.forEach((match) => {
|
|
65
|
+
const tagMatches = match.match(/^<([a-z]+)/i);
|
|
66
|
+
const idMatches = match.match(/id=(["'])(.*?)\1/);
|
|
67
|
+
if (tagMatches && idMatches) {
|
|
68
|
+
const tag = tagMatches[1].toLowerCase();
|
|
69
|
+
const id = idMatches[2];
|
|
70
|
+
if (tag && id) {
|
|
71
|
+
htmlAnchors.push(id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
//page_file: file,
|
|
79
|
+
headings: headings,
|
|
80
|
+
//anchors_auto_headings: anchors,
|
|
81
|
+
anchors_tag_ids: htmlAnchors,
|
|
82
|
+
relativeLinks,
|
|
83
|
+
urlLinks,
|
|
84
|
+
urlLocalLinks,
|
|
85
|
+
urlImageLinks,
|
|
86
|
+
relativeImageLinks,
|
|
87
|
+
unHandledLinkTypes,
|
|
88
|
+
redirectTo,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Processes line, taking arrays of different link types.
|
|
93
|
+
// Update the incoming values and return
|
|
94
|
+
// Note, assumption is all links are on one line, not split across lines.
|
|
95
|
+
// This is generally true, but does not have to be.
|
|
96
|
+
const processLineMarkdownLinks = (
|
|
97
|
+
line,
|
|
98
|
+
relativeLinks,
|
|
99
|
+
relativeImageLinks,
|
|
100
|
+
urlLinks,
|
|
101
|
+
urlLocalLinks,
|
|
102
|
+
urlImageLinks,
|
|
103
|
+
unHandledLinkTypes,
|
|
104
|
+
page
|
|
105
|
+
) => {
|
|
106
|
+
sharedData.options.log.includes("functions")
|
|
107
|
+
? console.log(`Function: processLineMarkdownLinks(): page: ${page}`)
|
|
108
|
+
: null;
|
|
109
|
+
//const regex = /(?<prefix>[!@]?)\[(?<text>[^\]]+)\]\((?<url>\S+?)(?:\s+"(?<title>[^"]+)")?\)/g;
|
|
110
|
+
const regex =
|
|
111
|
+
/(?<prefix>[!@]?)\[(?<text>[^\]]*)\]\((?<url>\S+?)(?:\s+"(?<title>[^"]+)")?\)/g;
|
|
112
|
+
const matches = line.matchAll(regex);
|
|
113
|
+
|
|
114
|
+
// 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
|
|
115
|
+
// Maybe a backwards lookup on @
|
|
116
|
+
// Not sure if we can generalize
|
|
117
|
+
|
|
118
|
+
for (const match of matches) {
|
|
119
|
+
const { prefix, text, url, title } = match.groups;
|
|
120
|
+
const isMarkdownImageLink = prefix == "!" ? true : false;
|
|
121
|
+
const isVuepressYouTubeLink = prefix == "@" ? true : false;
|
|
122
|
+
|
|
123
|
+
const linkText = text;
|
|
124
|
+
const linkUrl = url;
|
|
125
|
+
const linkTitle = title ? title : "";
|
|
126
|
+
|
|
127
|
+
// Work out Link type
|
|
128
|
+
let linkType = "";
|
|
129
|
+
|
|
130
|
+
if (isVuepressYouTubeLink) {
|
|
131
|
+
if (linkUrl.startsWith("http")) {
|
|
132
|
+
linkType = "urlLink";
|
|
133
|
+
} else {
|
|
134
|
+
// Not going to handle this (yet)
|
|
135
|
+
// TODO - prepend the standard URL
|
|
136
|
+
}
|
|
137
|
+
} else if (
|
|
138
|
+
sharedData.options.site_url &&
|
|
139
|
+
(linkUrl.startsWith(`http://${sharedData.options.site_url}`) ||
|
|
140
|
+
linkUrl.startsWith(`https://${sharedData.options.site_url}`))
|
|
141
|
+
) {
|
|
142
|
+
//console.log(link);
|
|
143
|
+
linkType = "urlLocalLink";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!linkUrl) {
|
|
147
|
+
// We should never get to this logging
|
|
148
|
+
console.log(
|
|
149
|
+
`WWregexMarkdownLinkAndImage: page: ${page}, linkUrl: ${linkUrl}, linkText: ${linkText}, linkTitle: ${linkTitle}, linkType: ${linkType}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//Create link
|
|
154
|
+
const link = new Link({
|
|
155
|
+
page: page,
|
|
156
|
+
url: linkUrl,
|
|
157
|
+
text: linkText,
|
|
158
|
+
title: linkTitle,
|
|
159
|
+
type: linkType,
|
|
160
|
+
});
|
|
161
|
+
//console.log(`XXLINKTESTnewLink: ${JSON.stringify(link, null, 2)}`);
|
|
162
|
+
|
|
163
|
+
// For now, dump in different arrays. Might just add to one array eventually
|
|
164
|
+
switch (link.type) {
|
|
165
|
+
case "urlLink": {
|
|
166
|
+
urlLinks.push(link);
|
|
167
|
+
//console.log("This is a URL link");
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "urlLocalLink": {
|
|
171
|
+
urlLocalLinks.push(link);
|
|
172
|
+
//console.log("This is a URL local link");
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "urlImageLink": {
|
|
176
|
+
urlImageLinks.push(link);
|
|
177
|
+
//console.log("This is a URL image link");
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "relativeImageLink": {
|
|
181
|
+
relativeImageLinks.push(link);
|
|
182
|
+
//console.log("This is a relative image link");
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "relativeLink": {
|
|
186
|
+
relativeLinks.push(link);
|
|
187
|
+
//console.log("This is a relative link");
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "relativeAnchorLink": {
|
|
191
|
+
relativeLinks.push(link); // This is an anchor link - but currently handled in the same code.
|
|
192
|
+
//console.log("This is a relative link");
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "relativeHTMLLink": {
|
|
196
|
+
relativeLinks.push(link); // This is HTML link handled in same code.
|
|
197
|
+
//console.log("This is a relative link");
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
default: {
|
|
201
|
+
unHandledLinkTypes.push(link);
|
|
202
|
+
sharedData.options.log.includes("todo") ? console.log(`TODO: 3Unhandled link.type: ${link.type}`) : null;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//Match for html a - append to the lists
|
|
209
|
+
const regexHTMLLinkTotal = /<a\s+(?<attributes>.*?)>(?<linktext>.*?)<\/a>/gi;
|
|
210
|
+
const regexHTMLTitle =
|
|
211
|
+
/title\s*[=]\s*(?<quote>['"])(?<title>.*?)(?<!\\)\k<quote>/i;
|
|
212
|
+
//title\s*[=]\s*(?<title>['"]?)([^'"\s>]+)\k<title>/i;
|
|
213
|
+
const regexHTMLhref =
|
|
214
|
+
/href\s*[=]\s*(?<quote>['"])(?<href>.*?)(?<!\\)\k<quote>/i;
|
|
215
|
+
const regexHTMLid = /id\s*[=]\s*(?<quote>['"])(?<id>.*?)(?<!\\)\k<quote>/i;
|
|
216
|
+
for (const match of line.matchAll(regexHTMLLinkTotal)) {
|
|
217
|
+
const attributes = match.groups.attributes;
|
|
218
|
+
//console.log(`XXXXXattributes_s: ${attributes}`)
|
|
219
|
+
const linkText =
|
|
220
|
+
match && match.groups.linktext ? match.groups.linktext : "";
|
|
221
|
+
//console.log(`XXXXXlinktext: ${linktext}`)
|
|
222
|
+
let linkTitle = "";
|
|
223
|
+
let linkUrl = "";
|
|
224
|
+
let linkId = "";
|
|
225
|
+
if (attributes) {
|
|
226
|
+
const titlematch = attributes.match(regexHTMLTitle);
|
|
227
|
+
linkTitle = titlematch && titlematch.groups.title ? titlematch.groups.title : "";
|
|
228
|
+
const hrefmatch = attributes.match(regexHTMLhref);
|
|
229
|
+
linkUrl = hrefmatch && hrefmatch.groups.href ? hrefmatch.groups.href : "";
|
|
230
|
+
const idMatch = attributes.match(regexHTMLid);
|
|
231
|
+
linkId = idMatch && idMatch.groups.id ? idMatch.groups.id : "";
|
|
232
|
+
}
|
|
233
|
+
// If not linkUrl then this is probably and anchor link.
|
|
234
|
+
//
|
|
235
|
+
if (!linkUrl && linkId) {
|
|
236
|
+
// This is an anchor-only link. Skip to next found link
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let linkType = "";
|
|
241
|
+
if (
|
|
242
|
+
sharedData.options.site_url &&
|
|
243
|
+
(linkUrl.startsWith(`http://${sharedData.options.site_url}`) ||
|
|
244
|
+
linkUrl.startsWith(`https://${sharedData.options.site_url}`))
|
|
245
|
+
) {
|
|
246
|
+
//console.log(link);
|
|
247
|
+
linkType = "urlLocalLink";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
//const link = new Link(linkUrl, linkText, linkTitle);
|
|
251
|
+
if (!linkUrl) {
|
|
252
|
+
//We should only get here for empty links.
|
|
253
|
+
console.log( `WWregexHTMLmatchAtag: page: ${page}, linkUrl: ${linkUrl}, linkText: ${linkText}, linkTitle: ${linkTitle}, linkType: ${linkType}` );
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const link = new Link({
|
|
257
|
+
page: page,
|
|
258
|
+
url: linkUrl,
|
|
259
|
+
type: linkType,
|
|
260
|
+
text: linkText,
|
|
261
|
+
title: linkTitle /* type: linkType */,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// For now, dump in different arrays. Might just add to one array eventually
|
|
265
|
+
switch (link.type) {
|
|
266
|
+
case "urlLink": {
|
|
267
|
+
urlLinks.push(link);
|
|
268
|
+
//console.log("This is a URL link");
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "urlLocalLink": {
|
|
272
|
+
urlLocalLinks.push(link);
|
|
273
|
+
//console.log("This is a URL local link");
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "urlImageLink": {
|
|
277
|
+
urlImageLinks.push(link);
|
|
278
|
+
//console.log("This is a URL image link");
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "relativeImageLink": {
|
|
282
|
+
relativeImageLinks.push(link);
|
|
283
|
+
//console.log("This is a relative image link");
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case "relativeLink": {
|
|
287
|
+
relativeLinks.push(link);
|
|
288
|
+
//console.log("This is a relative link");
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "relativeAnchorLink": {
|
|
292
|
+
relativeLinks.push(link); // This is an anchor link - but currently handled in the same code.
|
|
293
|
+
//console.log("This is a relative link");
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "relativeHTMLLink": {
|
|
297
|
+
relativeLinks.push(link); // This is an anchor link - but currently handled in the same code.
|
|
298
|
+
//console.log("This is a relative link");
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
default: {
|
|
303
|
+
unHandledLinkTypes.push(link);
|
|
304
|
+
sharedData.options.log.includes("todo") ? console.log(`TODO: 2Unhandled link.type: ${link.type}`) : null;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//Might further parse this to catch img in anchor.
|
|
311
|
+
|
|
312
|
+
//Match for html img - append to the lists
|
|
313
|
+
const regexHTMLImgTotal = /<img\s+(?<attributes>.*?)\/>/gi;
|
|
314
|
+
const regex_htmlattr_src =
|
|
315
|
+
/src\s*[=]\s*(?<quote>['"])(?<src>.*?)(?<!\\)\k<quote>/i;
|
|
316
|
+
for (const match of line.matchAll(regexHTMLImgTotal)) {
|
|
317
|
+
const attributes = match.groups.attributes;
|
|
318
|
+
//console.log(`XXXXXImageattributes_s: ${attributes}`)
|
|
319
|
+
const linkText = "";
|
|
320
|
+
let linkTitle = "";
|
|
321
|
+
let linkUrl = "";
|
|
322
|
+
if (attributes) {
|
|
323
|
+
const titlematch = attributes.match(regexHTMLTitle);
|
|
324
|
+
linkTitle =
|
|
325
|
+
titlematch && titlematch.groups.title ? titlematch.groups.title : "";
|
|
326
|
+
const srcmatch = attributes.match(regex_htmlattr_src);
|
|
327
|
+
linkUrl = srcmatch && srcmatch.groups.src ? srcmatch.groups.src : "";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//const link = new Link(linkUrl, linkText, linkTitle);
|
|
331
|
+
//console.log(`WWregexHTML_matchImage: page: ${page}, linkUrl: ${linkUrl}, linkText: ${linkText}, linkTitle: ${linkTitle},`);
|
|
332
|
+
const link = new Link({
|
|
333
|
+
page: page,
|
|
334
|
+
url: linkUrl,
|
|
335
|
+
text: linkText,
|
|
336
|
+
title: linkTitle /* type: linkType */,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/*
|
|
340
|
+
if (linkUrl) {
|
|
341
|
+
linkUrl.startsWith("http")
|
|
342
|
+
? urlImageLinks.push(link)
|
|
343
|
+
: relativeImageLinks.push(link);
|
|
344
|
+
}
|
|
345
|
+
*/
|
|
346
|
+
// For now, dump in different arrays. Might just add to one array eventually
|
|
347
|
+
switch (link.type) {
|
|
348
|
+
case "urlLink": {
|
|
349
|
+
urlLinks.push(link);
|
|
350
|
+
//console.log("This is a URL link");
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "urlLocalLink": {
|
|
354
|
+
urlLocalLinks.push(link);
|
|
355
|
+
//console.log("This is a URL local link");
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "urlImageLink": {
|
|
359
|
+
urlImageLinks.push(link);
|
|
360
|
+
//console.log("This is a URL image link");
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case "relativeImageLink": {
|
|
364
|
+
relativeImageLinks.push(link);
|
|
365
|
+
//console.log("This is a relative image link");
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case "relativeLink": {
|
|
369
|
+
relativeLinks.push(link);
|
|
370
|
+
//console.log("This is a relative link");
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "relativeAnchorLink": {
|
|
374
|
+
relativeLinks.push(link); // This is an anchor link - but currently handled in the same code.
|
|
375
|
+
//console.log("This is a relative link");
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "relativeHTMLLink": {
|
|
379
|
+
relativeLinks.push(link); // This is an HTML link.
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
default: {
|
|
384
|
+
unHandledLinkTypes.push(link);
|
|
385
|
+
sharedData.options.log.includes("todo") ? console.log(`TODO: 1Unhandled link.type: ${link.type}`) : null;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
//console.log(link);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
relativeLinks,
|
|
394
|
+
urlLinks,
|
|
395
|
+
urlImageLinks,
|
|
396
|
+
relativeImageLinks,
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export { processMarkdown };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { logToFile } from "./helpers.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { sharedData } from "./shared_data.js";
|
|
4
|
+
import { PageNotInTOCError, PageNotLinkedInternallyError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Gets page with most links. Supposed to be used on the allResults object that is an array of objects about each page.
|
|
8
|
+
// Will use to get the summary.
|
|
9
|
+
function getPageWithMostLinks(pages) {
|
|
10
|
+
if (sharedData.options.log.includes("functions")) {
|
|
11
|
+
console.log("Function: getPageWithMostLinks");
|
|
12
|
+
}
|
|
13
|
+
return pages.reduce(
|
|
14
|
+
(maxLinksPage, currentPage) => {
|
|
15
|
+
if (
|
|
16
|
+
currentPage.relativeLinks.length > maxLinksPage.relativeLinks.length
|
|
17
|
+
) {
|
|
18
|
+
return currentPage;
|
|
19
|
+
} else {
|
|
20
|
+
return maxLinksPage;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{ relativeLinks: [] }
|
|
24
|
+
).page_file;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get any orphans (no links from summary and no links at all)
|
|
28
|
+
//
|
|
29
|
+
function checkPageOrphans(results) {
|
|
30
|
+
const resultObj = {};
|
|
31
|
+
const allInternalAbsLinks = [];
|
|
32
|
+
|
|
33
|
+
//Create result object that has page as property
|
|
34
|
+
// And value is an array of links in/from that page converted to absolute.
|
|
35
|
+
results.forEach((obj) => {
|
|
36
|
+
const filePath = obj.page_file;
|
|
37
|
+
const relativeLinks = obj.relativeLinks;
|
|
38
|
+
const absLinks = [];
|
|
39
|
+
|
|
40
|
+
relativeLinks.forEach((linkObj) => {
|
|
41
|
+
const linkUrl = linkObj.url;
|
|
42
|
+
const absLink = path.resolve(path.dirname(filePath), linkUrl);
|
|
43
|
+
absLinks.push(absLink);
|
|
44
|
+
allInternalAbsLinks.push(absLink);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
resultObj[filePath] = absLinks;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Invert resultObj to get all objects to link to page.
|
|
51
|
+
// Add the links to to the big results object we process later.
|
|
52
|
+
const pagesObj = {};
|
|
53
|
+
for (const [page, links] of Object.entries(resultObj)) {
|
|
54
|
+
for (const link of links) {
|
|
55
|
+
if (!pagesObj[link]) {
|
|
56
|
+
pagesObj[link] = [];
|
|
57
|
+
}
|
|
58
|
+
pagesObj[link].push(page);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
results.forEach((obj) => {
|
|
62
|
+
obj["linkedFrom"] = pagesObj[obj.page_file];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Check that every filepath has at least one object in some absLink that matches it
|
|
66
|
+
let allFilesReferenced = true;
|
|
67
|
+
let allFilesSummaryReferenced = true;
|
|
68
|
+
const allFilesNoReference = [];
|
|
69
|
+
const allFilesNoSummaryReference = [];
|
|
70
|
+
results.forEach((obj) => {
|
|
71
|
+
const filePath = obj.page_file;
|
|
72
|
+
if (!allInternalAbsLinks.some((absLink) => absLink === filePath)) {
|
|
73
|
+
if (obj.redirectTo) {
|
|
74
|
+
//do nothing
|
|
75
|
+
} else if (obj.page_file === sharedData.options.toc) {
|
|
76
|
+
//do nothing
|
|
77
|
+
} else {
|
|
78
|
+
//if it a redirect file then it shouldn't be linked.
|
|
79
|
+
allFilesNoReference.push(filePath);
|
|
80
|
+
//console.log(`File "${filePath}" not referenced by any absolute link`);
|
|
81
|
+
|
|
82
|
+
const error = new PageNotLinkedInternallyError({file: obj.page_file});
|
|
83
|
+
results.allErrors.push(error);
|
|
84
|
+
allFilesReferenced = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const summaryFileLinks = resultObj[sharedData.options.toc];
|
|
89
|
+
|
|
90
|
+
if (summaryFileLinks && !summaryFileLinks.some((absLink) => absLink === filePath)) {
|
|
91
|
+
if (obj.redirectTo) {
|
|
92
|
+
// do nothing /-if it a redirect file then it shouldn't be linked.
|
|
93
|
+
//console.log(`EXECUTED: ${obj.page_file} in redirect`)
|
|
94
|
+
} else if (obj.page_file === sharedData.options.toc) {
|
|
95
|
+
//do nothing - summary shouldt be error for summary.
|
|
96
|
+
} else {
|
|
97
|
+
|
|
98
|
+
allFilesNoSummaryReference.push(filePath);
|
|
99
|
+
const error = new PageNotInTOCError({file: obj.page_file});
|
|
100
|
+
|
|
101
|
+
if (!results.allErrors) {
|
|
102
|
+
results["allErrors"] = [];
|
|
103
|
+
}
|
|
104
|
+
results.allErrors.push(error);
|
|
105
|
+
allFilesSummaryReferenced = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!allFilesReferenced) {
|
|
111
|
+
const jsonAllFilesNotReferenced = JSON.stringify(
|
|
112
|
+
allFilesNoReference,
|
|
113
|
+
null,
|
|
114
|
+
2
|
|
115
|
+
);
|
|
116
|
+
logToFile("./logs/allFilesNoReference.json", jsonAllFilesNotReferenced);
|
|
117
|
+
} else {
|
|
118
|
+
//console.log("All files referenced at least once");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!allFilesSummaryReferenced) {
|
|
122
|
+
const jsonAllFilesNotSummaryReferenced = JSON.stringify(
|
|
123
|
+
allFilesNoSummaryReference,
|
|
124
|
+
null,
|
|
125
|
+
2
|
|
126
|
+
);
|
|
127
|
+
logToFile(
|
|
128
|
+
"./logs/allFilesNoSummaryReference.json",
|
|
129
|
+
jsonAllFilesNotSummaryReferenced
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
//console.log("All files referenced at least once");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (sharedData.options.log.includes("quick")) {
|
|
136
|
+
//console.log(resultObj);
|
|
137
|
+
const jsonFilesWithAbsoluteLinks = JSON.stringify(resultObj, null, 2);
|
|
138
|
+
logToFile(
|
|
139
|
+
"./logs/pagesResolvedAbsoluteLinks.json",
|
|
140
|
+
jsonFilesWithAbsoluteLinks
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { checkPageOrphans, getPageWithMostLinks };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import {
|
|
3
|
+
/*LinkError,*/ CurrentFileMissingAnchorError,
|
|
4
|
+
LinkedFileMissingAnchorError,
|
|
5
|
+
LinkedInternalPageMissingError,
|
|
6
|
+
InternalLinkToHTMLError,
|
|
7
|
+
UrlToLocalSiteError,
|
|
8
|
+
} from "./errors.js";
|
|
9
|
+
import { sharedData } from "./shared_data.js";
|
|
10
|
+
|
|
11
|
+
// An array of errors given a results object that contains our array of objects containing relativeLinks (and other information).
|
|
12
|
+
function processRelativeLinks(results) {
|
|
13
|
+
sharedData.options.log.includes("functions")
|
|
14
|
+
? console.log("Function: processRelativeLinks")
|
|
15
|
+
: null;
|
|
16
|
+
const errors = [];
|
|
17
|
+
|
|
18
|
+
//console.log(sharedData);
|
|
19
|
+
|
|
20
|
+
results.forEach((page, index, array) => {
|
|
21
|
+
//console.log(`PAGE:${JSON.stringify(page, null, 2)}`);
|
|
22
|
+
|
|
23
|
+
page.relativeLinks.forEach((link, index, array) => {
|
|
24
|
+
//console.log(`LINK: ${JSON.stringify(link, null, 2)}`);
|
|
25
|
+
if (link.address === "") {
|
|
26
|
+
// This is a page-local link
|
|
27
|
+
// Verify the link goes to either heading or id defined in page.
|
|
28
|
+
if (
|
|
29
|
+
!(
|
|
30
|
+
page.anchors_auto_headings.includes(link.anchor) ||
|
|
31
|
+
page.anchors_tag_ids.includes(link.anchor)
|
|
32
|
+
)
|
|
33
|
+
) {
|
|
34
|
+
// There is no heading link to specified anchor in current page
|
|
35
|
+
const error = new CurrentFileMissingAnchorError({ link: link });
|
|
36
|
+
//console.log(`XXX_LMA_Error: ${JSON.stringify(error, null, 2)}`);
|
|
37
|
+
errors.push(error);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
// This is a link to another page
|
|
41
|
+
// See if that page is in our results
|
|
42
|
+
// Report error if not. Otherwise check if anchor is in page.
|
|
43
|
+
|
|
44
|
+
//find the path of the linked page.
|
|
45
|
+
//console.log(`LINK: ${JSON.stringify(link, null, 2)}`);
|
|
46
|
+
//console.log(`LINKADDRESS: ${link.address}`);
|
|
47
|
+
|
|
48
|
+
const linkAbsoluteFilePath = link.getAbsolutePath();
|
|
49
|
+
|
|
50
|
+
//console.log(link);
|
|
51
|
+
|
|
52
|
+
// Get the matching file matching our link, if it exists
|
|
53
|
+
let linkedFile =
|
|
54
|
+
results.find(
|
|
55
|
+
(linkedFile) =>
|
|
56
|
+
linkedFile.hasOwnProperty("page_file") &&
|
|
57
|
+
path.normalize(linkedFile.page_file) === linkAbsoluteFilePath
|
|
58
|
+
) || null;
|
|
59
|
+
|
|
60
|
+
if (!linkedFile) {
|
|
61
|
+
if (sharedData.options.tryMarkdownforHTML && link.isHTML) {
|
|
62
|
+
// The file was HTML so it might be a file extension mistake (linking to html instead of md)
|
|
63
|
+
// In this case we'll try find it.
|
|
64
|
+
|
|
65
|
+
const markdownAbsoluteFilePath = `${
|
|
66
|
+
linkAbsoluteFilePath.split(".html")[0]
|
|
67
|
+
}.md`;
|
|
68
|
+
|
|
69
|
+
const linkedHTMLFile =
|
|
70
|
+
results.find(
|
|
71
|
+
(linkedHTMLFile) =>
|
|
72
|
+
linkedHTMLFile.hasOwnProperty("page_file") &&
|
|
73
|
+
path.normalize(linkedHTMLFile.page_file) ===
|
|
74
|
+
markdownAbsoluteFilePath
|
|
75
|
+
) || null;
|
|
76
|
+
|
|
77
|
+
if (linkedHTMLFile) {
|
|
78
|
+
const error = new InternalLinkToHTMLError({ link: link });
|
|
79
|
+
//console.log(error);
|
|
80
|
+
errors.push(error);
|
|
81
|
+
linkedFile = linkedHTMLFile;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!linkedFile) {
|
|
87
|
+
//File not found as .html or md
|
|
88
|
+
const error = new LinkedInternalPageMissingError({ link: link });
|
|
89
|
+
//console.log(error);
|
|
90
|
+
errors.push(error);
|
|
91
|
+
} else {
|
|
92
|
+
// There is a linked file, so now see if there are anchors, and whether they work
|
|
93
|
+
|
|
94
|
+
if (!link.anchor) {
|
|
95
|
+
// No anchors, so go to next step
|
|
96
|
+
//null
|
|
97
|
+
} else if (
|
|
98
|
+
//List of anchors in linked file includes the anchor
|
|
99
|
+
linkedFile.anchors_auto_headings.includes(link.anchor) ||
|
|
100
|
+
linkedFile.anchors_tag_ids.includes(link.anchor)
|
|
101
|
+
) {
|
|
102
|
+
//
|
|
103
|
+
//do nothing - the linked page includes the anchor from this link
|
|
104
|
+
} else {
|
|
105
|
+
// File exists but does not contain matching anchor
|
|
106
|
+
const error = new LinkedFileMissingAnchorError({ link: link });
|
|
107
|
+
errors.push(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return errors;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { processRelativeLinks };
|
package/src/slugify.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Returns slug for a string (markdown heading) using Vuepress algorithm.
|
|
2
|
+
// Algorithm from chatgpt - needs testing.
|
|
3
|
+
function slugifyVuepress(str) {
|
|
4
|
+
const slug = str
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/\/+/g, "-") // replace / with hyphens
|
|
7
|
+
.replace(/[^A-Za-z0-9/]+/g, "-") // replace non-word characters except / with hyphens
|
|
8
|
+
.replace(/[\s_-]+/g, "-") // Replace spaces and underscores with hyphens
|
|
9
|
+
.replace(/^-+|-+$/g, ""); // Remove extra hyphens from the beginning or end of the string
|
|
10
|
+
|
|
11
|
+
if (str.includes("/")) {
|
|
12
|
+
//console.log(`DEBUG: SLUG: str: ${str} slug: ${slug}`);
|
|
13
|
+
}
|
|
14
|
+
return `${slug}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { slugifyVuepress };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Tests if a heading present for anchor link
|
|
2
|
+
|
|
3
|
+
Run like: `node .\index.js -d tests/errortype/current_file_missing_anchor`
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
This is URL to anchor that should be present: [Url to anchor with matching heading - show no eror](#heading-to-match) yeah!
|
|
7
|
+
|
|
8
|
+
No error should show up
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Heading to Match
|
|
12
|
+
|
|
13
|
+
Yeah baby!
|