1 | "use strict";
|
2 |
|
3 | const fs = require('fs');
|
4 | const path = require('path');
|
5 | const fsWalk = require('fs-walk');
|
6 | const mdx = require('@mdx-js/mdx');
|
7 | const slugPlugin = require('remark-slug');
|
8 |
|
9 | var cache = {};
|
10 | var exitCode = 0;
|
11 |
|
12 | fsWalk.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 |
|
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 |
|
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 |
|
92 | for (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 |
|
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 |
|
125 | process.exit(exitCode);
|