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.
Files changed (41) hide show
  1. package/README.md +66 -12
  2. package/biglog.txt +0 -0
  3. package/index.js +181 -435
  4. package/package.json +6 -1
  5. package/src/errors.js +148 -0
  6. package/src/helpers.js +41 -0
  7. package/src/links.js +194 -0
  8. package/src/output_errors.js +49 -0
  9. package/src/process_image_orphans.js +97 -0
  10. package/src/process_internal_url_links.js +20 -0
  11. package/src/process_local_image_links.js +57 -0
  12. package/src/process_markdown.js +400 -0
  13. package/src/process_orphans.js +145 -0
  14. package/src/process_relative_links.js +116 -0
  15. package/src/shared_data.js +2 -0
  16. package/src/slugify.js +17 -0
  17. package/tests/errortype/current_file_missing_anchor/heading_present_for_anchor.md +13 -0
  18. package/tests/errortype/current_file_missing_anchor/missing_heading.md +5 -0
  19. package/tests/errortype/linked_file_missing_anchor/file_with_broken_heading_link.md +10 -0
  20. package/tests/errortype/linked_file_missing_anchor/file_without_heading.md +10 -0
  21. package/tests/errortype/linked_internal_file_html/file_exists.html +5 -0
  22. package/tests/errortype/linked_internal_file_html/file_exists_as_markdown.md +6 -0
  23. package/tests/errortype/linked_internal_file_html/links_to_file_that_is_html.md +10 -0
  24. package/tests/errortype/linked_internal_file_missing/file_present_relative_link_no_error.md +5 -0
  25. package/tests/errortype/linked_internal_file_missing/file_present_should_be_no_error.md +5 -0
  26. package/tests/errortype/linked_internal_file_missing/links_to_file_that_is_not_present.md +8 -0
  27. package/tests/errortype/local_image_not_found/page_with_missing_image.md +9 -0
  28. package/tests/errortype/local_image_not_found/test.png +0 -0
  29. package/tests/errortype/orphan_images/assets/image1_not_linked.png +0 -0
  30. package/tests/errortype/orphan_images/assets/image2_not_linked.png +0 -0
  31. package/tests/errortype/orphan_images/test/image3_not_linked.png +0 -0
  32. package/tests/errortype/orphan_images/test/image4_linked.png +0 -0
  33. package/tests/errortype/orphan_images/test/intro.md +11 -0
  34. package/tests/errortype/page_not_in_toc/page1intoc_should_not_error.md +7 -0
  35. package/tests/errortype/page_not_in_toc/page2intoc_should_not_error.md +7 -0
  36. package/tests/errortype/page_not_in_toc/page3NOTinTOC.md +7 -0
  37. package/tests/errortype/page_not_in_toc/toc.md +9 -0
  38. package/tests/errortype/url_to_local_site/mylocalsite_dot_com.md +8 -0
  39. package/tests/links/tests1.md +93 -0
  40. package/tests/links/tests2.md +3 -0
  41. 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 };
@@ -0,0 +1,2 @@
1
+ export const sharedData = {
2
+ };
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!
@@ -0,0 +1,5 @@
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
+ This is URL to anchor that should NOT be present: [Url to anchor no matching heading or id - show Error](#there_should_be_no_match) yeah!