Skip to content

Commit 5dc1eda

Browse files
authored
Merge pull request #17 from ep-linden/add-link-mapping
Add link mapping functionality.
2 parents 5e87439 + 0d29490 commit 5dc1eda

File tree

7 files changed

+207
-31
lines changed

7 files changed

+207
-31
lines changed

README.md

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ textlint --rule textlint-rule-no-dead-relative-link README.md
4949

5050
### resolve-as-markdown
5151

52-
This option takes an array of file extension values and treats files with those extension as if they are markdown files.
53-
For eg. With the following configuration
52+
This option takes an array of file extension values and treats files with those extensions as if they are markdown files.
53+
54+
For e.g. With the following configuration
5455
```json
5556
{
5657
"rules": {
@@ -61,7 +62,60 @@ For eg. With the following configuration
6162
}
6263
```
6364

64-
and `[README](README.html)` as input, this rule will check for the existance of `README.md` file.
65+
and `[README](README.html)` as input, this rule will check for the existence of `README.md` file.
66+
67+
### route-map
68+
Use this option when relative links need to be validated using an alternate file path.
69+
70+
This option takes an array of source and destination pairs. The source value is a `Regex`, and the destination value is
71+
a `String` that can include capture groups from the `source` using the `$` notation.
72+
73+
For e.g. With the following configuration
74+
```json
75+
{
76+
"rules": {
77+
"no-dead-relative-link": {
78+
"route-map": [
79+
{
80+
"source": "^../javadocs/(\\d+\\.\\d+\\.\\w+)",
81+
"destination": "../../static/javadocs/$1"
82+
}
83+
]
84+
}
85+
}
86+
}
87+
```
88+
89+
and `../javadocs/1.0.x/overview-summary.html` as the link being checked, this rule checks for the existence of the
90+
`overview-summary.html` file at `../../static/javadocs/1.0.x/overview-summary.html`.
91+
92+
##### Note #####
93+
Ensure each `route-map` pair is specific because the route-map option will validate relative links using the first
94+
matching regex found in the configuration.
95+
96+
For e.g. With the following configuration
97+
```json
98+
{
99+
"rules": {
100+
"no-dead-relative-link": {
101+
"route-map": [
102+
{
103+
"source": "../../javadocs/",
104+
"destination": "../website/static/javadocs/"
105+
},
106+
{
107+
"source": "../javadocs/1.0.x/",
108+
"destination": "../../static/javadocs/1.0.x/"
109+
}
110+
]
111+
}
112+
}
113+
}
114+
```
115+
116+
and `../../javadocs/1.0.x/overview-summary.html` as the link being checked, this rule checks for the existence of the
117+
`overview-summary.html` file at the first destination `../website/static/javadocs/1.0.x/overview-summary.html` instead
118+
of the second destination `../../static/javadocs/1.0.x/overview-summary.html`.
65119

66120
## License
67121

src/no-dead-relative-link.js

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import GithubSlugger from 'github-slugger';
88
import util from 'util';
99
import { wrapReportHandler} from 'textlint-rule-helper';
1010

11-
const fileExists = util.promisify(fs.exists);
1211
const fileRead = util.promisify(fs.readFile);
1312

1413
//https://stackoverflow.com/a/31991870
@@ -39,21 +38,58 @@ async function validateLinkNode(linkNode, context, options) {
3938
}
4039

4140
async function validateRelativeLink(linkNode, context, options) {
42-
let linkAbsoutePath = path.resolve(path.dirname(context.getFilePath()), linkNode.url);
43-
let linkURL = new URL("file://" + linkAbsoutePath);
44-
let linkedFileExtension = path.extname(linkURL.pathname);
41+
let linkURL = getLinkURL(linkNode.url, context, options);
42+
if (!await fileExists(url.fileURLToPath(linkURL))) {
43+
if (options["route-map"]) {
44+
linkURL = await getRoutedLink(linkNode, context, options);
45+
if (linkURL && !await fileExists(url.fileURLToPath(linkURL))) {
46+
reportError(linkNode, context, `${path.basename(linkURL.pathname)} does not exist`);
47+
return;
48+
}
49+
} else {
50+
reportError(linkNode, context, `${path.basename(linkURL.pathname)} does not exist`);
51+
return;
52+
}
53+
}
4554

55+
if (linkURL && linkURL.hash && path.extname(linkURL.pathname) === ".md") {
56+
return validateAnchorLink(url.fileURLToPath(linkURL), linkURL.hash.slice(1), linkNode, context);
57+
}
58+
}
4659

60+
async function getRoutedLink(linkNode, context, options) {
61+
let linkRouteMaps = options["route-map"];
62+
let nodeUrl = linkNode.url;
63+
64+
for (const mapping of linkRouteMaps) {
65+
let sourceRegex = new RegExp(mapping["source"], "g");
66+
let mappedDestination = mapping["destination"]
67+
if (sourceRegex.test(nodeUrl)) {
68+
let routedUrl = nodeUrl.replace(sourceRegex, mappedDestination);
69+
let linkURL = getLinkURL(routedUrl, context, options);
70+
return linkURL;
71+
}
72+
}
73+
}
74+
75+
function getLinkURL(nodeURL, context, options) {
76+
let linkAbsolutePath = path.resolve(path.dirname(context.getFilePath()), nodeURL);
77+
let linkURL = new URL("file://" + linkAbsolutePath);
78+
let linkedFileExtension = path.extname(linkURL.pathname);
4779
if (linkedFileExtension !== ".md" && options["resolve-as-markdown"] && options["resolve-as-markdown"].includes(linkedFileExtension)) {
4880
linkURL.pathname = linkURL.pathname.replace(linkedFileExtension, ".md");
4981
}
5082

51-
if (!await fileExists(url.fileURLToPath(linkURL))) {
52-
reportError(linkNode, context, `${path.basename(linkURL.pathname)} does not exist`);
53-
return;
54-
}
55-
if(linkURL.hash && path.extname(linkURL.pathname) === ".md") {
56-
return validateAnchorLink(url.fileURLToPath(linkURL), linkURL.hash.slice(1), linkNode, context);
83+
return linkURL;
84+
}
85+
86+
async function fileExists(url) {
87+
let access = util.promisify(fs.access);
88+
try {
89+
await access(url);
90+
return true;
91+
} catch (e) {
92+
return false;
5793
}
5894
}
5995

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
This invalid link to a file with an anchor has no valid routing [invalidAnchorLink](../../invalidLink.md#does-not-exist)
2+
This invalid link to a file has no valid routing [invalidLink](../../invalidLink.md)
3+
This invalid link to a html file with an anchor that needs to be resolved as markdown has no valid routing [invalidAnchorLink](../dir/invalidLink.html#does-not-exist)
4+
This invalid link to html file that needs to be resolved as markdown has no valid routing [invalidLink](../dir/invalidLink.html)
5+
This invalid link to a file with an anchor is routed to a valid link with an invalid anchor [invalidAnchorLink](../../subdir/linkTestFile.md#header-7)
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
This is a link to file that does not exists [invalidLink](invalidLink.md)
1+
This is a link to file that does not exist [invalidLink](invalidLink.md)
22
This is an invalid link to txt file in current directory [invalidLink](invalidLink.txt)
33
This is an invalid html link that needs to be resolved as markdown [invalidHtmlLink](invalidHtmlLink.html)
44
This is an anchor link to invalid anchor in current file [invalidAnchorCurrentFile](#valid-links)
55
This is an anchor link to an invalid anchor [invalidAnchorLink](../linkTestFile.md#header-7)
66
This is an anchor link to an invalid anchor in file that needs to resolved as markdown [invalidAnchorLink](../linkTestFile.html#header-7)
7-
This is an anchor link to a file that does not exist [invalidAnchorLink](invalidLink.md#does-not-exist)
8-
> If these property values do not exist in the settings.xml, the source code might not be following the guidelines at [Setup Guide](../setup-introduction.md)
7+
This is an anchor link to a file that does not exist [invalidAnchorLink](invalidLink.md#does-not-exist)
8+
> If these property values do not exist in the settings.xml, the source code might not be following the guidelines at [Setup Guide](../setup-introduction.md)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
This is an invalid link routed to a valid link [invalidLink](../../linkTestFile.md)
2+
This is an invalid link routed to a valid link with an anchor [invalidAnchorLink](../../linkTestFile.md#fundamentals)
3+
This is an invalid link routed to a valid link [invalidLink](../dir/linkTestFile.md)
4+
This is an invalid link routed to a valid link with an anchor [invalidAnchorLink](../dir/linkTestFile.md#fundamentals)
5+
This is an invalid link routed to a valid link [invalidLink](../../subdir/linkTestFile.md)
6+
This is an invalid link routed to a valid link with an anchor [invalidAnchorLink](../../subdir/linkTestFile.md)
7+
This is an invalid html file that needs to be resolved as markdown routed to a valid link [invalidHtmlLink](../../linkTestFile.html)
8+
This is an invalid html file that needs to be resolved as markdown routed to a valid link with an anchor [invalidHtmlAnchorLink](../../linkTestFile.html#fundamentals)

test/fixtures/testFiles/validLinkTest.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Valid Links
2-
This is an valid empty link [emptyLink]()
2+
This is a valid empty link [emptyLink]()
33
This is valid link in the current directory [validLink](linkTestFile.md)
44
This is valid link in the current directory to txt file [validLink](linkTestTxtFile.txt)
55
This is valid link to file in sub directory [validSubLink](./subDir/linkTestFile.md)
@@ -15,6 +15,6 @@ This is a link to valid anchor [validAnchorLink](../linkTestFile.md#header-6)
1515
This is a link to valid anchor in same file [validSameFileAnchor](#valid-links)
1616
This is a link to valid anchor in same file [validSameFileAnchor](#no-paragraph)
1717
This is a linkt to valid anchor in file to be resolved as markdown [validMDAnchorLink](../linkTestFile.html#header-5)
18-
>This is valid link in the textlint repo [textlint](https://textlint.github.io)
18+
>This is valid link in the textlint repo [textlint](https://textlint.github.io)
1919
20-
## No Paragraph
20+
## No Paragraph

test/no-dead-relative-link-test.js

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@ import validateRelativeLinks from '../src/no-dead-relative-link';
33
import path from 'path';
44
const tester = new TextLintTester();
55

6-
tester.run("no-dead-relative-links", validateRelativeLinks, {
6+
tester.run(
7+
"no-dead-relative-links: with resolve-as-markdown option",
8+
{
9+
rules: [
10+
{
11+
ruleId: "no-dead-relative-link",
12+
rule: validateRelativeLinks,
13+
options: {
14+
"resolve-as-markdown": ".html"
15+
}
16+
}
17+
]
18+
},
19+
{
720
valid: [
821
{
922
inputPath: path.resolve("./test/fixtures/testFiles/validLinkTest.md"),
10-
options: {
11-
"resolve-as-markdown": ".html"
12-
}
1323
}
1424
],
1525
invalid: [
@@ -19,7 +29,7 @@ tester.run("no-dead-relative-links", validateRelativeLinks, {
1929
{
2030
message: "invalidLink.md does not exist",
2131
line: 1,
22-
column: 59
32+
column: 58
2333
},
2434
{
2535
message: "invalidLink.txt does not exist",
@@ -34,17 +44,17 @@ tester.run("no-dead-relative-links", validateRelativeLinks, {
3444
{
3545
message: "Anchor #valid-links does not exist in invalidLinkTest.md",
3646
line: 4,
37-
column: 85
47+
column: 85
3848
},
3949
{
4050
message: "Anchor #header-7 does not exist in linkTestFile.md",
4151
line: 5,
42-
column: 65
52+
column: 65
4353
},
4454
{
4555
message: "Anchor #header-7 does not exist in linkTestFile.md",
4656
line: 6,
47-
column: 108
57+
column: 108
4858
},
4959
{
5060
message: "invalidLink.md does not exist",
@@ -54,12 +64,75 @@ tester.run("no-dead-relative-links", validateRelativeLinks, {
5464
{
5565
message:"setup-introduction.md does not exist",
5666
line: 8,
57-
column: 134
67+
column: 133
68+
}
69+
]
70+
}
71+
]
72+
});
73+
tester.run(
74+
"no-dead-relative-links: with route-map and resolve-as-markdown options",
75+
{
76+
rules: [
77+
{
78+
ruleId: "no-dead-relative-link",
79+
rule: validateRelativeLinks,
80+
options: {
81+
"resolve-as-markdown": ".html",
82+
"route-map": [
83+
{
84+
"source": "../../invalidLink.md",
85+
"destination": "../invalidLink.md"
86+
},
87+
{
88+
"source": "../dir/",
89+
"destination": "../"
90+
},
91+
{
92+
"source": "../../(subdir)/",
93+
"destination": "$1/"
94+
}
95+
]
5896
}
59-
],
60-
options: {
61-
"resolve-as-markdown": ".html"
6297
}
98+
]
99+
},
100+
{
101+
valid: [
102+
{
103+
inputPath: path.resolve("./test/fixtures/testFiles/validLinkRoutingTest.md"),
104+
}
105+
],
106+
invalid: [
107+
{
108+
inputPath: path.resolve("./test/fixtures/testFiles/invalidLinkRoutingTest.md"),
109+
errors: [
110+
{
111+
message: "invalidLink.md does not exist",
112+
line: 1,
113+
column: 85
114+
},
115+
{
116+
message: "invalidLink.md does not exist",
117+
line: 2,
118+
column: 64
119+
},
120+
{
121+
message: "invalidLink.md does not exist",
122+
line: 3,
123+
column: 128
124+
},
125+
{
126+
message: "invalidLink.md does not exist",
127+
line: 4,
128+
column: 105
129+
},
130+
{
131+
message: "Anchor #header-7 does not exist in linkTestFile.md",
132+
line: 5,
133+
column: 113
134+
}
135+
]
63136
}
64137
]
65138
});

0 commit comments

Comments
 (0)