3.41 kBJavaScriptView Raw
1"use strict";
2
3const fs = require('fs');
4const path = require('path');
5const fsWalk = require('fs-walk');
6const mdx = require('@mdx-js/mdx');
7const slugPlugin = require('remark-slug');
8
9var cache = {};
10var exitCode = 0;
11
12fsWalk.walkSync(process.argv[2], function(basedir, filename, stat, next) {
13 if (stat.isFile()) {
14 let filePath = basedir + "/" + filename
15 let filePathAbs = path.resolve(filePath)
16 let fileExt = filename.split('.').pop()
17
18 if(["mdx", "md", "html", "htm"].indexOf(fileExt) == -1){
19 return
20 }
21
22 let markdown = fs.readFileSync(filePathAbs).toString();
23 let html = ""
24
25 try {
26 html = mdx.sync(markdown, {
27 remarkPlugins: [
28 slugPlugin
29 ]
30 })
31 } catch(e) {
32 // Fail if there was an error parsing a mdx/md file
33 if (fileExt === "mdx" || fileExt == "md") {
34 console.error("Unable to parse mdx to html: " + filename)
35 throw e
36 }
37 }
38
39 if (!cache[filePathAbs]) {
40 cache[filePathAbs] = {
41 filePath,
42 filePathAbs,
43 externalLinks: [],
44 internalLinks: [],
45 ids: {}
46 }
47 }
48
49
50 if (fileExt === "md" || fileExt == "mdx") {
51 markdown.replace(/\[[^\]]*\]\(([^)]+)\)/g, (_, match) => {
52 if (!match.match(/(^https?:\/\/)|(^#)|(^[^:]+:.*)|(\.mdx?(#[a-zA-Z0-9._,-]*)?$)/)) {
53 console.warn(`Missing .md/.mdx suffix for internal link: '${match}' in file ${filePathAbs}`)
54 }
55 });
56 }
57
58 let fillCache = function(html) {
59 html.replace(/\s+(?:(?:"id":\s*)|(?:id=))"([^"]+)"/g, (_, match) => {
60 if (match && match.match) {
61 cache[filePathAbs].ids[match] = true
62 }
63 });
64
65 html.replace(/\s+(?:(?:"href":\s*)|(?:href=))"([^"]+)"/g, (_, match) => {
66 if (match && match.match) {
67 if (match.match(/^https?:\/\//)) {
68 cache[filePathAbs].externalLinks.push(match)
69 } else if (match.match(/^[^:]+:.*/)) {
70 // ignore links such as "mailto:" or "javascript:"
71 } else {
72 if (match.match(/^#/)) {
73 match = filename + match;
74 }
75
76 cache[filePathAbs].internalLinks.push({
77 original: match,
78 absolute: path.resolve(basedir + "/" + match),
79 })
80 }
81 }
82 });
83 }
84
85 fillCache(html)
86 fillCache(markdown)
87 }
88}, function(err) {
89 if (err) console.log(err);
90});
91
92for (let file in cache) {
93 let data = cache[file];
94
95 data.internalLinks.map((link) => {
96 let [targetFile, targetId] = link.absolute.split("#")
97
98 if (!cache[targetFile]) {
99 targetFile = targetFile + ".md"
100
101 if (!cache[targetFile]) {
102 targetFile = targetFile + "x"
103
104 if (!cache[targetFile]) {
105 targetFile = targetFile.trimEnd(path.sep).replace(/\.md(x)?$/i, "") + path.sep + "index.html"
106
107 if (!cache[targetFile]) {
108 // make sure linked file does not exist
109 if (fs.existsSync(path.join(path.dirname(file), link.original)) === false) {
110 exitCode = 1;
111 console.error(`Link is broken: '${link.original}' in file ${data.filePathAbs}`)
112 return;
113 }
114 }
115 }
116 }
117 }
118
119 if (targetId && !cache[targetFile].ids[targetId]) {
120 console.warn(`Anchor of link is broken: '${link.original}' in file ${data.filePathAbs}`)
121 }
122 })
123}
124
125process.exit(exitCode);