Skip to content

Commit a4b11d2

Browse files
committed
Support relative local file paths
Inspired by textlint-rule/textlint-rule-no-dead-link-fork@f0063e9, thanks @azu!
1 parent 80c49b4 commit a4b11d2

File tree

7 files changed

+100
-35
lines changed

7 files changed

+100
-35
lines changed

ReadMe.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,13 @@ This error is fixable and textlint will automatically replace the obsolete links
4141
### Relative Link Resolution
4242

4343
Sometimes your files contain relative URIs, which don't have domain information in an URI string.
44+
In this case, we have to somehow resolve the relative URIs and convert them into absolute URIs.
4445

45-
You can enable availability checks to such links by telling the rule how to resolve the relative links (See below for details).
46+
The resolution strategy is as follows:
47+
48+
1. If `baseURI` is specified, use that path to resolve relative URIs (See the below section for details).
49+
2. If not, try to get the path of the file being linted and use its parent folder as the base path.
50+
3. If that's not available (e.g., when you are performing linting from API), put an error `Unable to resolve the relative URI`.
4651

4752
## Options
4853

@@ -54,33 +59,30 @@ The default options are:
5459
{
5560
"rules": {
5661
"no-dead-link": {
57-
"checkRelative": false,
5862
"baseURI": null,
5963
"ignore": [],
6064
}
6165
}
6266
}
6367
```
6468

65-
### checkRelative
66-
67-
Enable the dead link checks against relative URIs.
68-
Note that you also have to specify the `baseURI` to make this option work.
69-
7069
### baseURI
7170

7271
The base URI to be used for resolving relative URIs.
7372

74-
Example:
73+
Though its name, you can pass either an URI starting with `http` or `https`, or an file path starting with `/`.
74+
75+
Examples:
7576

7677
```
77-
{
78-
"rules": {
79-
"no-dead-link": {
80-
"checkRelative": true,
81-
"baseURI": "http://example.com/"
82-
}
83-
}
78+
"no-dead-link": {
79+
"baseURI": "http://example.com/"
80+
}
81+
```
82+
83+
```
84+
"no-dead-link": {
85+
"baseURI": "/Users/textlint/path/to/parent/folder/"
8486
}
8587
```
8688

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"homepage": "https://github.com/textlint-rule/textlint-rule-no-dead-link",
2424
"repository": "textlint-rule/textlint-rule-no-dead-link",
2525
"dependencies": {
26+
"fs-extra": "^5.0.0",
2627
"isomorphic-fetch": "^2.2.1",
2728
"textlint-rule-helper": "^2.0.0"
2829
},

src/no-dead-link.js

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { RuleHelper } from 'textlint-rule-helper';
22
import fetch from 'isomorphic-fetch';
33
import URL from 'url';
4+
import path from 'path';
5+
import fs from 'fs-extra';
46

57
const DEFAULT_OPTIONS = {
68
checkRelative: false, // `true` enables availability checks for relative URIs.
7-
baseURI: null, // a base URI to resolve relative URIs.
8-
ignore: [], // URIs to be skipped from availability checks.
9+
baseURI: null, // {String|null} a base URI to resolve relative URIs.
10+
ignore: [], // {Array<String>} URIs to be skipped from availability checks.
911
};
1012

1113
// Adopted from http://stackoverflow.com/a/3809435/951517
@@ -20,6 +22,15 @@ function isRelative(uri) {
2022
return URL.parse(uri).protocol === null;
2123
}
2224

25+
/**
26+
* Returns if a given URI indicates a local file.
27+
* @param {string} uri
28+
* @return {boolean}
29+
*/
30+
function isLocal(uri) {
31+
return isRelative(uri);
32+
}
33+
2334
/**
2435
* Return `true` if the `code` is redirect status code.
2536
* @see https://fetch.spec.whatwg.org/#redirect-status
@@ -38,7 +49,7 @@ function isRedirect(code) {
3849
* @param {string} method
3950
* @return {{ ok: boolean, redirect?: string, message: string }}
4051
*/
41-
async function isAlive(uri, method = 'HEAD') {
52+
async function isAliveURI(uri, method = 'HEAD') {
4253
const opts = {
4354
method,
4455
// Disable gzip compression in Node.js
@@ -76,7 +87,7 @@ async function isAlive(uri, method = 'HEAD') {
7687
// as some servers don't accept `HEAD` requests but are OK with `GET` requests.
7788
// https://github.com/textlint-rule/textlint-rule-no-dead-link/pull/86
7889
if (method === 'HEAD') {
79-
return isAlive(uri, 'GET');
90+
return isAliveURI(uri, 'GET');
8091
}
8192

8293
return {
@@ -86,8 +97,26 @@ async function isAlive(uri, method = 'HEAD') {
8697
}
8798
}
8899

100+
/**
101+
* Check if a given file exists
102+
*/
103+
async function isAliveLocalFile(filePath) {
104+
try {
105+
await fs.access(filePath.replace(/[?#].*?$/, ''));
106+
107+
return {
108+
ok: true,
109+
};
110+
} catch (ex) {
111+
return {
112+
ok: false,
113+
message: ex.message,
114+
};
115+
}
116+
}
117+
89118
function reporter(context, options = {}) {
90-
const { Syntax, getSource, report, RuleError, fixer } = context;
119+
const { Syntax, getSource, report, RuleError, fixer, getFilePath } = context;
91120
const helper = new RuleHelper(context);
92121
const opts = Object.assign({}, DEFAULT_OPTIONS, options);
93122

@@ -103,22 +132,24 @@ function reporter(context, options = {}) {
103132
}
104133

105134
if (isRelative(uri)) {
106-
if (!opts.checkRelative) {
107-
return;
108-
}
135+
const filePath = getFilePath();
136+
const base = opts.baseURI || (filePath && path.dirname(filePath));
109137

110-
if (!opts.baseURI) {
111-
const message = 'The base URI is not specified.';
138+
if (!base) {
139+
const message =
140+
'Unable to resolve the relative URI. Please check if the base URI is correctly specified.';
112141

113-
report(node, new RuleError(message, { index: 0 }));
142+
report(node, new RuleError(message, { index }));
114143
return;
115144
}
116145

117146
// eslint-disable-next-line no-param-reassign
118-
uri = URL.resolve(opts.baseURI, uri);
147+
uri = URL.resolve(base, uri);
119148
}
120149

121-
const result = await isAlive(uri);
150+
const result = isLocal(uri)
151+
? await isAliveLocalFile(uri)
152+
: await isAliveURI(uri);
122153
const { ok, redirected, redirectTo, message } = result;
123154

124155
if (!ok) {

test/fixtures/a.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
* Good link: [b.md](b.md).
2+
* Good link: [./b.md](./b.md).
3+
* Good link: [b.md#hash](b.md#hash).
4+
* Good link: [b.md?param](b.md?param).

test/fixtures/b.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
* Bad link: [../NOTFOUND](../NOTFOUND).
2+
* Bad link: [NOTFOUND](NOTFOUND).
3+
* Bad link: [/NOTFOUND_XXXX](/NOTFOUND_XXXX).

test/no-dead-link.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable max-len */
22
import TextlintTester from 'textlint-tester';
3+
import fs from 'fs';
4+
import path from 'path';
35
import rule from '../src/no-dead-link';
46

57
const tester = new TextlintTester();
@@ -34,6 +36,12 @@ tester.run('no-dead-link', rule, {
3436
ignore: ['https://example.com/404.html'],
3537
},
3638
},
39+
{
40+
text: fs.readFileSync(path.join(__dirname, 'fixtures/a.md'), 'utf-8'),
41+
options: {
42+
baseURI: path.join(__dirname, 'fixtures/'),
43+
},
44+
},
3745
],
3846
invalid: [
3947
{
@@ -108,15 +116,13 @@ tester.run('no-dead-link', rule, {
108116
},
109117
{
110118
text:
111-
'should throw "No base URI is provided" error if checkRelative is true but baseURI is undefined: [no base](index.html)',
112-
options: {
113-
checkRelative: true,
114-
},
119+
'should throw when a relative URI cannot be resolved: [test](./a.md).',
115120
errors: [
116121
{
117-
message: 'The base URI is not specified.',
122+
message:
123+
'Unable to resolve the relative URI. Please check if the base URI is correctly specified.',
118124
line: 1,
119-
column: 97,
125+
column: 61,
120126
},
121127
],
122128
},

yarn.lock

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,14 @@ from@~0:
14491449
version "0.1.3"
14501450
resolved "https://registry.yarnpkg.com/from/-/from-0.1.3.tgz#ef63ac2062ac32acf7862e0d40b44b896f22f3bc"
14511451

1452+
fs-extra@^5.0.0:
1453+
version "5.0.0"
1454+
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
1455+
dependencies:
1456+
graceful-fs "^4.1.2"
1457+
jsonfile "^4.0.0"
1458+
universalify "^0.1.0"
1459+
14521460
fs-readdir-recursive@^1.0.0:
14531461
version "1.0.0"
14541462
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560"
@@ -1574,7 +1582,7 @@ globby@^5.0.0:
15741582
pify "^2.0.0"
15751583
pinkie-promise "^2.0.0"
15761584

1577-
graceful-fs@^4.1.2, graceful-fs@^4.1.4:
1585+
graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6:
15781586
version "4.1.11"
15791587
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
15801588

@@ -2036,6 +2044,12 @@ json5@^0.5.0, json5@^0.5.1:
20362044
version "0.5.1"
20372045
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
20382046

2047+
jsonfile@^4.0.0:
2048+
version "4.0.0"
2049+
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
2050+
optionalDependencies:
2051+
graceful-fs "^4.1.6"
2052+
20392053
jsonify@~0.0.0:
20402054
version "0.0.0"
20412055
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -3507,6 +3521,10 @@ unist-util-visit@^1.1.0:
35073521
version "1.1.1"
35083522
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.1.tgz#e917a3b137658b335cb4420c7da2e74d928e4e94"
35093523

3524+
universalify@^0.1.0:
3525+
version "0.1.1"
3526+
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
3527+
35103528
user-home@^1.1.1:
35113529
version "1.1.1"
35123530
resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"

0 commit comments

Comments
 (0)