git.iko.soy/typeable/remark-validate-links
on main help sign in
Initial commit
commit88079b027d1350d9bdddd35cbe0ab30e0bf9ad75 authorTitus Wormer <tituswormer@gmail.com> dateSat 18 Jul 2015, 09:43 (11 years ago)
18 files changed+1268
.editorconfig+15 .eslintignore+1 .eslintrc+8 .gitignore+4 .jscs.json+147 .mdastignore+1 .mdastrc+12 .travis.yml+9 LICENSE+22 history.md+9 index.js+384 package.json+57 readme.md+105 screen-shot.png test/fixtures/definitions.md+10 test/fixtures/example.md+61 test/fixtures/examples/example.md+39 test/index.js+384
.editorconfignew file+15
@@ -0,0 +1,15 @@
1+root = true
2+
3+[*]
4+indent_style = space
5+indent_size = 4
6+end_of_line = lf
7+charset = utf-8
8+trim_trailing_whitespace = true
9+insert_final_newline = true
10+
11+[*.{json,html,svg,css,mdastrc,eslintrc}]
12+indent_size = 2
13+
14+[*.md]
15+trim_trailing_whitespace = false
.eslintignorenew file+1
@@ -0,0 +1 @@
1+coverage/
.eslintrcnew file+8
@@ -0,0 +1,8 @@
1+{
2+ "env": {
3+ "node": true
4+ },
5+ "rules": {
6+ "quotes": [2, "single"]
7+ }
8+}
.gitignorenew file+4
@@ -0,0 +1,4 @@
1+.DS_Store
2+*.log
3+coverage/
4+node_modules/
.jscs.jsonnew file+147
@@ -0,0 +1,147 @@
1+{
2+ "excludeFiles": [
3+ "coverage/",
4+ "node_modules/"
5+ ],
6+ "plugins": [
7+ "jscs-jsdoc"
8+ ],
9+ "jsDoc": {
10+ "checkAnnotations": "jsdoc3",
11+ "checkParamNames": true,
12+ "requireParamTypes": true,
13+ "checkRedundantParams": true,
14+ "checkReturnTypes": true,
15+ "checkRedundantReturns": true,
16+ "requireReturnTypes": true,
17+ "checkTypes": "strictNativeCase",
18+ "checkRedundantAccess": true,
19+ "enforceExistence": true,
20+ "requireHyphenBeforeDescription": true
21+ },
22+ "requireCurlyBraces": [
23+ "if",
24+ "else",
25+ "for",
26+ "while",
27+ "do",
28+ "try",
29+ "catch"
30+ ],
31+ "requireSpaceAfterKeywords": [
32+ "if",
33+ "else",
34+ "for",
35+ "while",
36+ "do",
37+ "switch",
38+ "return",
39+ "try",
40+ "catch"
41+ ],
42+ "requireSpaceBeforeBlockStatements": true,
43+ "requireParenthesesAroundIIFE": true,
44+ "requireSpacesInConditionalExpression": true,
45+ "requireSpacesInAnonymousFunctionExpression": {
46+ "beforeOpeningRoundBrace": true,
47+ "beforeOpeningCurlyBrace": true
48+ },
49+ "requireSpacesInNamedFunctionExpression": {
50+ "beforeOpeningRoundBrace": true,
51+ "beforeOpeningCurlyBrace": true
52+ },
53+ "requireSpacesInFunctionExpression": {
54+ "beforeOpeningCurlyBrace": true
55+ },
56+ "requireBlocksOnNewline": true,
57+ "disallowPaddingNewlinesInBlocks": true,
58+ "disallowEmptyBlocks": true,
59+ "disallowSpacesInsideObjectBrackets": true,
60+ "disallowSpacesInsideArrayBrackets": true,
61+ "disallowSpacesInsideParentheses": true,
62+ "requireSpacesInsideObjectBrackets": "all",
63+ "disallowDanglingUnderscores": true,
64+ "disallowSpaceAfterObjectKeys": true,
65+ "requireCommaBeforeLineBreak": true,
66+ "requireOperatorBeforeLineBreak": [
67+ "?",
68+ "+",
69+ "-",
70+ "/",
71+ "*",
72+ "=",
73+ "==",
74+ "===",
75+ "!=",
76+ "!==",
77+ ">",
78+ ">=",
79+ "<",
80+ "<="
81+ ],
82+ "requireSpaceBeforeBinaryOperators": [
83+ "+",
84+ "-",
85+ "/",
86+ "*",
87+ "=",
88+ "==",
89+ "===",
90+ "!=",
91+ "!=="
92+ ],
93+ "requireSpaceAfterBinaryOperators": [
94+ "+",
95+ "-",
96+ "/",
97+ "*",
98+ "=",
99+ "==",
100+ "===",
101+ "!=",
102+ "!=="
103+ ],
104+ "disallowSpaceAfterPrefixUnaryOperators": [
105+ "++",
106+ "--",
107+ "+",
108+ "-",
109+ "~",
110+ "!"
111+ ],
112+ "disallowSpaceBeforePostfixUnaryOperators": [
113+ "++",
114+ "--"
115+ ],
116+ "disallowImplicitTypeConversion": [
117+ "numeric",
118+ "boolean",
119+ "binary",
120+ "string"
121+ ],
122+ "requireCamelCaseOrUpperCaseIdentifiers": true,
123+ "disallowKeywords": [
124+ "with"
125+ ],
126+ "disallowMultipleLineStrings": true,
127+ "disallowMultipleLineBreaks": true,
128+ "validateLineBreaks": "LF",
129+ "validateQuoteMarks": "'",
130+ "disallowMixedSpacesAndTabs": true,
131+ "disallowTrailingWhitespace": true,
132+ "disallowTrailingComma": true,
133+ "disallowKeywordsOnNewLine": [
134+ "else"
135+ ],
136+ "requireLineFeedAtFileEnd": true,
137+ "maximumLineLength": 78,
138+ "requireCapitalizedConstructors": true,
139+ "safeContextKeyword": "self",
140+ "requireDotNotation": true,
141+ "disallowYodaConditions": true,
142+ "validateJSDoc": {
143+ "checkParamNames": true,
144+ "checkRedundantParams": true,
145+ "requireParamTypes": true
146+ }
147+}
.mdastignorenew file+1
@@ -0,0 +1 @@
1+test/
.mdastrcnew file+12
@@ -0,0 +1,12 @@
1+{
2+ "plugins": [
3+ "slug",
4+ "./",
5+ "lint",
6+ "github",
7+ "yaml-config"
8+ ],
9+ "settings": {
10+ "bullet": "*"
11+ }
12+}
.travis.ymlnew file+9
@@ -0,0 +1,9 @@
1+language: node_js
2+script: npm run-script test-travis
3+node_js:
4+- '0.10'
5+- '0.11'
6+- '0.12'
7+- iojs
8+sudo: false
9+after_script: npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls
LICENSEnew file+22
@@ -0,0 +1,22 @@
1+(The MIT License)
2+
3+Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>
4+
5+Permission is hereby granted, free of charge, to any person obtaining
6+a copy of this software and associated documentation files (the
7+'Software'), to deal in the Software without restriction, including
8+without limitation the rights to use, copy, modify, merge, publish,
9+distribute, sublicense, and/or sell copies of the Software, and to
10+permit persons to whom the Software is furnished to do so, subject to
11+the following conditions:
12+
13+The above copyright notice and this permission notice shall be
14+included in all copies or substantial portions of the Software.
15+
16+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
history.mdnew file+9
@@ -0,0 +1,9 @@
1+---
2+mdast:
3+ setext: true
4+---
5+
6+<!--lint disable no-multiple-toplevel-headings-->
7+
8+0.0.0 / 2015-07-16
9+==================
index.jsnew file+384
@@ -0,0 +1,384 @@
1+'use strict';
2+
3+/*
4+ * Dependencies.
5+ */
6+
7+var url = require('url');
8+var fs = require('fs');
9+var path = require('path');
10+var visit = require('mdast-util-visit');
11+var gh = require('github-url-to-object');
12+var urljoin = require('urljoin');
13+
14+/*
15+ * Methods.
16+ */
17+
18+var exists = fs.existsSync;
19+var parse = url.parse;
20+
21+/**
22+ * Factory to store all markdown files, and headings.
23+ *
24+ * @example
25+ * var gather = gatherFactory();
26+ * var file = new File('# foo');
27+ *
28+ * gather(file).done() // {}
29+ *
30+ * @return {Function}
31+ */
32+function gatherFactory() {
33+ var cache = {};
34+ var hasHeadings = false;
35+ var hasSlugs = false;
36+
37+ /**
38+ * Access found files and headings.
39+ *
40+ * @return {Object.<string, boolean>} - Map of
41+ * file-paths, with `true` as their value.
42+ * @throws {Error} - When headings are found, but no
43+ * heading has a slug.
44+ */
45+ function done() {
46+ if (hasHeadings && !hasSlugs) {
47+ throw new Error(
48+ 'Missing slugs. Use for example `mdast-slug` to generate ' +
49+ 'heading IDs'
50+ );
51+ }
52+
53+ return cache;
54+ }
55+
56+ /**
57+ * Find headings in `file`.
58+ *
59+ * @property {Function} done - Access found links.
60+ * @param {File} file - Virtual file.
61+ * @returns {Function} - Itself.
62+ */
63+ function gather(file) {
64+ var filePath = file.filePath();
65+
66+ cache[filePath] = true;
67+
68+ /*
69+ * Ignore files without AST or filename.
70+ */
71+
72+ if (filePath && file.ast) {
73+ visit(file.ast, 'heading', function (node) {
74+ var id = node.attributes && node.attributes.id;
75+
76+ hasHeadings = true;
77+
78+ if (id) {
79+ cache[filePath + '#' + id] = hasSlugs = true;
80+ }
81+ });
82+ }
83+
84+ return gather;
85+ }
86+
87+ gather.done = done;
88+
89+ return gather;
90+}
91+
92+/**
93+ * Resolve a single `file`.
94+ *
95+ * @example
96+ * var file = new File('[](foo.md)')
97+ *
98+ * resolve(file, {
99+ * 'foo.md': true
100+ * }, {});
101+ * // no warnings.
102+ *
103+ * @param {File} file - Virtual file.
104+ * @param {Object} cache - Found links.
105+ * @param {Object} project - GitHub project, if applicable.
106+ */
107+function resolve(file, cache, project) {
108+ var filePath = file.filePath();
109+ var directory = file.directory;
110+ var ast = file.ast;
111+ var definitions = {};
112+ var prefix = '';
113+
114+ if (project.user && project.repo) {
115+ prefix = '/' + project.user + '/' + project.repo + '/blob/';
116+ }
117+
118+ /*
119+ * Store link definitions.
120+ */
121+
122+ visit(ast, 'definition', function (node) {
123+ definitions[node.identifier.toUpperCase()] = node;
124+ });
125+
126+ /**
127+ * Handle new links.
128+ *
129+ * @example
130+ * link({
131+ * href: 'foo/bar/baz.md#foo',
132+ * children: [
133+ * {
134+ * type: 'text',
135+ * value: 'foo'
136+ * }
137+ * ]
138+ * });
139+ *
140+ * @param {Node} node - Link or link-reference node.
141+ */
142+ function onlink(node) {
143+ var link = node.href;
144+ var definition;
145+ var index;
146+ var pathname;
147+ var hash;
148+ var uri;
149+ var warning;
150+
151+ /*
152+ * Handle link-references.
153+ */
154+
155+ if (node.identifier) {
156+ definition = definitions[node.identifier.toUpperCase()];
157+ link = definition && definition.link;
158+ }
159+
160+ /*
161+ * Ignore definitions without link.
162+ */
163+
164+ if (!link) {
165+ return;
166+ }
167+
168+ uri = parse(link);
169+
170+ if (!uri.hostname) {
171+ /*
172+ * Handle hashes, or relative files.
173+ */
174+ if (!uri.pathname && uri.hash) {
175+ link = filePath + uri.hash;
176+ uri = parse(link);
177+ } else {
178+ link = urljoin(directory, link) + (uri.hash || '');
179+ uri = parse(link);
180+ }
181+ }
182+
183+ /*
184+ * Handle full links.
185+ * Only works with GitHub.
186+ */
187+
188+ if (uri.hostname) {
189+ if (!prefix) {
190+ return;
191+ }
192+
193+ if (
194+ uri.hostname !== 'github.com' ||
195+ uri.pathname.slice(0, prefix.length) !== prefix
196+ ) {
197+ return;
198+ }
199+
200+ link = uri.pathname.slice(prefix.length) + (uri.hash || '');
201+
202+ /*
203+ * Things get interesting here: branches.
204+ * `foo/bar/baz` could be `baz` on the
205+ * `foo/bar` branch. Or, `baz` in the `bar`
206+ * directory on the `foo` branch.
207+ *
208+ * Currently, I’m ignoring this and just not
209+ * supporting branches.
210+ */
211+
212+ link = link.slice(link.indexOf('/') + 1);
213+
214+ uri = parse(link);
215+ }
216+
217+ /*
218+ * Handle file links, or combinations of files
219+ * and hashes.
220+ */
221+
222+ index = link.indexOf('#');
223+
224+ if (index === -1) {
225+ pathname = link;
226+ hash = null;
227+ } else {
228+ pathname = link.slice(0, index);
229+ hash = link.slice(index + 1);
230+ }
231+
232+ if (cache[pathname] !== true) {
233+ /*
234+ * Ignore actually existing, but not to mdast
235+ * visible files (unless the link has a hash).
236+ */
237+
238+ if (!exists(pathname) || hash) {
239+ warning = 'Link to unknown file: `' + pathname + '`';
240+ }
241+ } else if (cache[link] !== true) {
242+ warning = 'Link to unknown heading';
243+
244+ if (pathname !== filePath) {
245+ warning += ' in `' + pathname + '`';
246+ }
247+
248+ warning += ': `' + hash + '`';
249+ }
250+
251+ if (warning) {
252+ file.warn(warning, node);
253+ }
254+ }
255+
256+ visit(ast, 'link', onlink);
257+ visit(ast, 'linkReference', onlink);
258+}
259+
260+/**
261+ * Resolve all files in `set`.
262+ *
263+ * @example
264+ * var set = new FileSet();
265+ * set.add(new File('[](foo.md)'));
266+ *
267+ * resolveAll(set, {
268+ * 'foo.md': true
269+ * }, {});
270+ * // no warnings.
271+ *
272+ * @param {FileSet} set - Virtual file-set.
273+ * @param {Object} cache - Found links.
274+ * @param {Object} project - GitHub project, if applicable.
275+ */
276+function resolveAll(set, cache, project) {
277+ set.valueOf().forEach(function (file) {
278+ /*
279+ * Ignore failed files.
280+ */
281+
282+ if (!file.hasFailed()) {
283+ resolve(file, cache, project);
284+ }
285+ });
286+}
287+
288+/**
289+ * Factory to create a new completer.
290+ *
291+ * @example
292+ * var completer = completerFactory({
293+ * 'user': 'foo',
294+ * 'repo': 'bar'
295+ * });
296+ *
297+ * @param {Object} project - GitHub project, if applicable.
298+ * @return {Function} - `completer`, bound to `project`.
299+ */
300+function completerFactory(project) {
301+ /**
302+ * Completer.
303+ *
304+ * @example
305+ * completer(new FileSet(), console.log)
306+ *
307+ * @property {*} pluginId - Unique ID so completers by
308+ * this plug-in are not added multiple times to a single
309+ * file-set pipeline.
310+ * @param {FileSet} set - Virtual file-set.
311+ * @param {function(err?)} done - Callback.
312+ */
313+ function completer(set, done) {
314+ var gather = gatherFactory();
315+
316+ set.valueOf().forEach(gather);
317+
318+ resolveAll(set, gather.done(), project);
319+
320+ done();
321+ }
322+
323+ completer.pluginId = 'mdast-validate-links';
324+
325+ return completer;
326+}
327+
328+/**
329+ * Attacher.
330+ *
331+ * @example
332+ * attacher(mdast())
333+ * // [Error: mdast-validate-links only works on the CLI]
334+ *
335+ * @param {MDAST} mdast - Processor.
336+ * @param {Object?} options - Settings.
337+ * @param {FileSet?} set - Virtual file-set.
338+ * @throws {Error} - When `set` is not given (when not on
339+ * the CLI).
340+ */
341+function attacher(mdast, options, set) {
342+ var repo = (options || {}).repository;
343+ var pack;
344+
345+ /*
346+ * Throw when not on the CLI.
347+ */
348+
349+ if (!set) {
350+ throw new Error('mdast-validate-links only works on the CLI');
351+ }
352+
353+ /*
354+ * Try to get the repo from `package.json` when not
355+ * given.
356+ */
357+
358+ if (!repo) {
359+ try {
360+ pack = require(path.resolve(process.cwd(), 'package.json'));
361+ } catch (exception) {
362+ pack = {};
363+ }
364+
365+ repo = pack.repository ? pack.repository.url || pack.repository : '';
366+ }
367+
368+ repo = repo ? gh(repo) : {};
369+
370+ /*
371+ * Attach a `completer`.
372+ */
373+
374+ set.use(completerFactory({
375+ 'user': repo.user,
376+ 'repo': repo.repo
377+ }));
378+}
379+
380+/*
381+ * Expose.
382+ */
383+
384+module.exports = attacher;
package.jsonnew file+57
@@ -0,0 +1,57 @@
1+{
2+ "name": "mdast-validate-links",
3+ "version": "0.0.0",
4+ "description": "Validate links to headings and files in markdown",
5+ "license": "MIT",
6+ "keywords": [
7+ "validate",
8+ "link",
9+ "reference",
10+ "file",
11+ "heading",
12+ "mdast"
13+ ],
14+ "dependencies": {
15+ "github-url-to-object": "^1.5.2",
16+ "mdast-slug": "^0.1.1",
17+ "mdast-util-visit": "^0.1.1",
18+ "urljoin": "^0.1.5"
19+ },
20+ "repository": {
21+ "type": "git",
22+ "url": "https://github.com/wooorm/mdast-validate-links.git"
23+ },
24+ "author": {
25+ "name": "Titus Wormer",
26+ "email": "tituswormer@gmail.com"
27+ },
28+ "files": [
29+ "index.js",
30+ "LICENSE"
31+ ],
32+ "devDependencies": {
33+ "cept": "^1.0.0",
34+ "eslint": "^0.24.0",
35+ "istanbul": "^0.3.0",
36+ "jscs": "^1.0.0",
37+ "jscs-jsdoc": "^1.0.0",
38+ "mdast": "^0.27.0-rc.1",
39+ "mdast-github": "^0.3.0",
40+ "mdast-lint": "^0.4.0",
41+ "mdast-yaml-config": "^0.2.0",
42+ "mocha": "^2.0.0"
43+ },
44+ "scripts": {
45+ "test-api": "mocha --check-leaks test/index.js",
46+ "test-coveralls": "istanbul cover _mocha --report lcovonly -- --check-leaks test/index.js",
47+ "test-coverage": "istanbul cover _mocha -- test/index.js",
48+ "test-travis": "npm run test-coveralls",
49+ "test": "npm run test-api",
50+ "lint-api": "eslint .",
51+ "lint-style": "jscs --reporter inline .",
52+ "lint": "npm run lint-api && npm run lint-style",
53+ "make": "npm run lint && npm run test-coverage",
54+ "build-md": "mdast . --output --quiet",
55+ "build": "npm run build-md"
56+ }
57+}
readme.mdnew file+105
@@ -0,0 +1,105 @@
1+# mdast-validate-links [![Build Status](https://img.shields.io/travis/wooorm/mdast-validate-links.svg?style=flat)](https://travis-ci.org/wooorm/mdast-validate-links) [![Coverage Status](https://img.shields.io/coveralls/wooorm/mdast-validate-links.svg?style=flat)](https://coveralls.io/r/wooorm/mdast-validate-links?branch=master)
2+
3+> Currently, this only works with the current beta version of
4+> **mdast**: [0.27.0-rc.1](https://github.com/wooorm/mdast/blob/5f3d914fd0a59fd42bd1a2852ba17e1d1f6360db/history.md).
5+
6+[**mdast**](https://github.com/wooorm/mdast) plug-in to validate if links to
7+headings and files in markdown point to existing things.
8+
9+For example, this document does not have a heading named `Hello`. So if I
10+link to that (`[welcome](#hello)`), this plug-in will warn about it.
11+
12+In addition, when I link to a heading in another document
13+(`examples/foo.md#hello`), if this file exists but the heading does not,
14+or if the file does not exist, this plug-in will also warn.
15+
16+Linking to other files, such as `LICENSE` or `index.js` (when they exist)
17+is fine.
18+
19+## Installation
20+
21+[npm](https://docs.npmjs.com/cli/install):
22+
23+```bash
24+npm install mdast-validate-links
25+```
26+
27+## Command line
28+
29+![Example of how mdast-validate-links looks on screen](https://cdn.rawgit.com/wooorm/mdast-validate-links/master/screen-shot.png)
30+
31+Use **mdast-validate-links** together with [**mdast**](https://github.com/wooorm/mdast)
32+and [**mdast-slug**](https://github.com/wooorm/mdast-slug):
33+
34+```bash
35+npm install --global mdast mdast-slug mdast-validate-links
36+```
37+
38+Let’s say `readme.md` is this document, and `example.md` looks as follows:
39+
40+```md
41+# Hello
42+
43+Read more [whoops, this does not exist](#world).
44+
45+This doesn’t exist either [whoops!](readme.md#foo).
46+
47+But this does exist: [LICENSE](LICENSE).
48+
49+So does this: [README](readme.md#installation).
50+```
51+
52+Then, to run **mdast-validate-links** on `example.md` and `readme.md`:
53+
54+```bash
55+mdast -u mdast-slug -u mdast-validate-links example.md readme.md
56+#
57+# Yields:
58+#
59+# example.md
60+# 3:11 warning Link to unknown heading: `world`
61+# 5:27 warning Link to unknown heading in `readme.md`: `foo`
62+#
63+# ✖ 2 problems (0 errors, 2 warnings)
64+#
65+# readme.md: done.
66+```
67+
68+## Programmatic
69+
70+This plug-in is **not** available on the API of mdast.
71+
72+## Configuration
73+
74+You can pass a `repository`, containing anything `package.json`s
75+[`repository`](https://docs.npmjs.com/files/package.json#repository) can
76+handle. If this is omitted, **mdast-validate-links** will try
77+the `package.json` in your current working directory.
78+
79+```bash
80+mdast --use 'validate-links=repository:"foo/bar"' example.md
81+```
82+
83+When a repository is given or detected, links to GitHub are normalized
84+to the file-system. E.g., `https://github.com/foo/bar/blob/master/example.md`
85+becomes `example.md`.
86+
87+You can define this repository in `.mdastrc` or `package.json` [files](https://github.com/wooorm/mdast/blob/master/doc/mdastrc.5.md)
88+too. An example `.mdastrc` file could look as follows:
89+
90+```json
91+{
92+ "plugins": {
93+ "slug": {
94+ "library": "github"
95+ },
96+ "validate-links": {
97+ "repository": "foo/bar"
98+ }
99+ }
100+}
101+```
102+
103+## License
104+
105+[MIT](LICENSE) © [Titus Wormer](http://wooorm.com)
screen-shot.pngnew file

Binary file not shown.

test/fixtures/definitions.mdnew file+10
@@ -0,0 +1,10 @@
1+# Hello
2+
3+This is a [valid] definition.
4+
5+This is an [invalid] definition.
6+
7+this [doesn’t] have a definition.
8+
9+[valid]: #hello
10+[invalid]: #world
test/fixtures/example.mdnew file+61
@@ -0,0 +1,61 @@
1+# Hello
2+
3+This is a valid relative heading [link](#hello).
4+
5+This is an invalid relative heading [link](#world).
6+
7+## Files
8+
9+This is a valid relative file [link](https://github.com/wooorm/test/blob/master/examples/example.md).
10+
11+So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/example.md).
12+
13+And this [link](./examples/example.md).
14+
15+And this [link](examples/example.md).
16+
17+This is an invalid relative file [link](https://github.com/wooorm/test/blob/master/examples/world.md).
18+
19+So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/world.md).
20+
21+And this [link](./examples/world.md).
22+
23+And this [link](examples/world.md).
24+
25+## Combination
26+
27+Valid: [a](./examples/example.md#hello).
28+
29+Valid: [b](examples/example.md#hello).
30+
31+Valid: [c](https://github.com/wooorm/test/blob/master/examples/example.md#hello).
32+
33+Valid: [d](https://github.com/wooorm/test/blob/foo-bar/examples/example.md#hello).
34+
35+Invalid: [e](./examples/example.md#world).
36+
37+Invalid: [f](examples/example.md#world).
38+
39+Invalid: [g](https://github.com/wooorm/test/blob/master/examples/example.md#world).
40+
41+Invalid: [h](https://github.com/wooorm/test/blob/foo-bar/examples/example.md#world).
42+
43+Invalid: [i](./examples/world.md#hello).
44+
45+Invalid: [j](examples/world.md#hello).
46+
47+Invalid: [k](https://github.com/wooorm/test/blob/master/examples/world.md#hello).
48+
49+Invalid: [l](https://github.com/wooorm/test/blob/foo-bar/examples/world.md#hello).
50+
51+## External
52+
53+These are all invalid, because they do not link to GitHub.
54+
55+Valid: [a](irc://foo).
56+
57+Valid: [b](http://example.com).
58+
59+Valid: [b](http://example.com/foo/bar/baz).
60+
61+Valid: [b](http://bitbucket.com/wooorm/test/blob/foo-bar/examples/world.md#hello).
test/fixtures/examples/example.mdnew file+39
@@ -0,0 +1,39 @@
1+# Hello
2+
3+This is a valid relative heading [link](#hello).
4+
5+This is an invalid relative heading [link](#world).
6+
7+## Files
8+
9+This is a valid relative file [link](https://github.com/wooorm/test/blob/master/example.md).
10+
11+So is this [link](https://github.com/wooorm/test/blob/foo-bar/example.md).
12+
13+And this [link](../example.md).
14+
15+This is an invalid relative file [link](https://github.com/wooorm/test/blob/master/world.md).
16+
17+So is this [link](https://github.com/wooorm/test/blob/foo-bar/world.md).
18+
19+And this [link](../world.md).
20+
21+## Combination
22+
23+Valid: [a](../example.md#hello).
24+
25+Valid: [b](https://github.com/wooorm/test/blob/master/example.md#hello).
26+
27+Valid: [c](https://github.com/wooorm/test/blob/foo-bar/example.md#hello).
28+
29+Invalid: [d](../example.md#world).
30+
31+Invalid: [e](https://github.com/wooorm/test/blob/master/example.md#world).
32+
33+Invalid: [f](https://github.com/wooorm/test/blob/foo-bar/example.md#world).
34+
35+Invalid: [g](../world.md#hello).
36+
37+Invalid: [h](https://github.com/wooorm/test/blob/master/world.md#hello).
38+
39+Invalid: [i](https://github.com/wooorm/test/blob/foo-bar/world.md#hello).
test/index.jsnew file+384
@@ -0,0 +1,384 @@
1+'use strict';
2+
3+/* eslint-env mocha */
4+
5+/*
6+ * Dependencies.
7+ */
8+
9+var assert = require('assert');
10+var fs = require('fs');
11+var cept = require('cept');
12+var mdast = require('mdast');
13+var cli = require('mdast/lib/cli');
14+var links = require('..');
15+
16+/*
17+ * Cache to store output and errors by mdast.
18+ */
19+
20+var std = {
21+ 'out': '',
22+ 'err': ''
23+};
24+
25+/**
26+ * Factory to store a bound type.
27+ *
28+ * @param {string} type - `"out"` or `"err"`.
29+ * @return {Function} - Stores the bound type.
30+ */
31+function queue(type) {
32+ /**
33+ * Store a bound type.
34+ */
35+ return function () {
36+ std[type] += [].slice.call(arguments).join('');
37+ };
38+}
39+
40+/**
41+ * Intercept both `console.log` to `std.out` and
42+ * `console.error` to `std.err`;
43+ *
44+ * @return {Function} - Stores output.
45+ */
46+function intercept() {
47+ var stopOut = cept(console, 'log', queue('out'));
48+ var stopErr = cept(console, 'error', queue('err'));
49+
50+ /**
51+ * Stops intercepting. Returns the captured `std`.
52+ *
53+ * @return {Object} - Output.
54+ */
55+ return function () {
56+ var current = {
57+ 'out': std.out.trim(),
58+ 'err': std.err.trim()
59+ };
60+
61+ stopOut();
62+ stopErr();
63+
64+ std.out = '';
65+ std.err = '';
66+
67+ return current;
68+ };
69+}
70+
71+/*
72+ * Tests.
73+ */
74+
75+describe('mdast-validate-links', function () {
76+ beforeEach(function () {
77+ process.chdir('./test/fixtures');
78+ });
79+
80+ afterEach(function () {
81+ process.chdir('../..');
82+ });
83+
84+ it('should throw an error when not on the CLI', function () {
85+ assert.throws(function () {
86+ mdast().use(links);
87+ }, /Error: mdast-validate-links only works on the CLI/);
88+ });
89+
90+ it('should warn when used without slugs', function (done) {
91+ var stop = intercept();
92+
93+ cli({
94+ 'files': 'example.md',
95+ 'color': false,
96+ 'detectRC': false,
97+ 'detectIgnore': false,
98+ 'plugins': ['./index.js']
99+ }, function (err) {
100+ var res = stop();
101+
102+ assert.equal(res.out, '');
103+
104+ assert.equal(res.err, [
105+ 'example.md',
106+ ' 0:0 error Missing slugs. Use for example ' +
107+ '`mdast-slug` to generate heading IDs',
108+ '',
109+ '✖ 1 problem (1 error, 0 warnings)'
110+ ].join('\n'));
111+
112+ done(err);
113+ });
114+ });
115+
116+ it('should ignore unfound files', function (done) {
117+ var stop = intercept();
118+
119+ cli({
120+ 'files': ['definitions.md', 'FOOOO'],
121+ 'color': false,
122+ 'detectRC': false,
123+ 'detectIgnore': false,
124+ 'plugins': ['slug', './index.js']
125+ }, function (err) {
126+ var res = stop();
127+
128+ assert.equal(res.out, '');
129+
130+ assert.equal(res.err, [
131+ 'definitions.md',
132+ ' 5:12 warning Link to unknown heading: `world`',
133+ '',
134+ '✖ 1 problem (0 errors, 1 warning)',
135+ '',
136+ 'FOOOO',
137+ ' 0:0 error No such file or directory',
138+ '',
139+ '✖ 1 problem (1 error, 0 warnings)'
140+ ].join('\n'));
141+
142+ done(err);
143+ });
144+ });
145+
146+ it('should work when not all files are given', function (done) {
147+ var stop = intercept();
148+
149+ cli({
150+ 'files': 'example.md',
151+ 'color': false,
152+ 'detectRC': false,
153+ 'detectIgnore': false,
154+ 'plugins': ['slug', './index.js']
155+ }, function (err) {
156+ var res = stop();
157+
158+ assert.equal(res.err, [
159+ 'example.md',
160+ ' 5:37 warning Link to unknown heading: `world`',
161+ ' 21:10 warning Link to unknown file: `examples/world.md`',
162+ ' 23:10 warning Link to unknown file: `examples/world.md`',
163+ ' 27:8 warning Link to unknown file: ' +
164+ '`examples/example.md`',
165+ ' 29:8 warning Link to unknown file: ' +
166+ '`examples/example.md`',
167+ ' 35:10 warning Link to unknown file: ' +
168+ '`examples/example.md`',
169+ ' 37:10 warning Link to unknown file: ' +
170+ '`examples/example.md`',
171+ ' 43:10 warning Link to unknown file: `examples/world.md`',
172+ ' 45:10 warning Link to unknown file: `examples/world.md`',
173+ '',
174+ '✖ 9 problems (0 errors, 9 warnings)'
175+ ].join('\n'));
176+
177+ done(err);
178+ });
179+ });
180+
181+ it('should work when all files are given', function (done) {
182+ var stop = intercept();
183+
184+ cli({
185+ 'files': [
186+ 'example.md',
187+ 'examples/example.md'
188+ ],
189+ 'color': false,
190+ 'detectRC': false,
191+ 'detectIgnore': false,
192+ 'plugins': ['slug', './index.js']
193+ }, function (err) {
194+ var res = stop();
195+
196+ assert.equal(res.out, '');
197+
198+ assert.equal(res.err, [
199+ 'example.md',
200+ ' 5:37 warning Link to unknown heading: `world`',
201+ ' 21:10 warning Link to unknown file: `examples/world.md`',
202+ ' 23:10 warning Link to unknown file: `examples/world.md`',
203+ ' 35:10 warning Link to unknown heading in ' +
204+ '`examples/example.md`: `world`',
205+ ' 37:10 warning Link to unknown heading in ' +
206+ '`examples/example.md`: `world`',
207+ ' 43:10 warning Link to unknown file: `examples/world.md`',
208+ ' 45:10 warning Link to unknown file: `examples/world.md`',
209+ '',
210+ '✖ 7 problems (0 errors, 7 warnings)',
211+ '',
212+ 'examples/example.md',
213+ ' 5:37 warning Link to unknown heading: `world`',
214+ ' 19:10 warning Link to unknown file: `world.md`',
215+ ' 29:10 warning Link to unknown heading in ' +
216+ '`example.md`: `world`',
217+ ' 35:10 warning Link to unknown file: `world.md`',
218+ '',
219+ '✖ 4 problems (0 errors, 4 warnings)'
220+ ].join('\n'));
221+
222+ done(err);
223+ });
224+ });
225+
226+ it('should work with definitions', function (done) {
227+ var stop = intercept();
228+
229+ cli({
230+ 'files': 'definitions.md',
231+ 'color': false,
232+ 'detectRC': false,
233+ 'detectIgnore': false,
234+ 'plugins': ['slug', './index.js']
235+ }, function (err) {
236+ var res = stop();
237+
238+ assert.equal(res.err, [
239+ 'definitions.md',
240+ ' 5:12 warning Link to unknown heading: `world`',
241+ '',
242+ '✖ 1 problem (0 errors, 1 warning)'
243+ ].join('\n'));
244+
245+ done(err);
246+ });
247+ });
248+
249+ it('should work on GitHub URLs when given a repo', function (done) {
250+ var stop = intercept();
251+
252+ cli({
253+ 'files': [
254+ 'example.md',
255+ 'examples/example.md'
256+ ],
257+ 'color': false,
258+ 'detectRC': false,
259+ 'detectIgnore': false,
260+ 'plugins': {
261+ 'slug': null,
262+ './index.js': {
263+ 'repository': 'wooorm/test'
264+ }
265+ }
266+ }, function (err) {
267+ var res = stop();
268+
269+ assert.equal(res.out, '');
270+
271+ assert.equal(res.err, [
272+ 'example.md',
273+ ' 5:37 warning Link to unknown heading: `world`',
274+ ' 17:34 warning Link to unknown file: `examples/world.md`',
275+ ' 19:12 warning Link to unknown file: `examples/world.md`',
276+ ' 21:10 warning Link to unknown file: `examples/world.md`',
277+ ' 23:10 warning Link to unknown file: `examples/world.md`',
278+ ' 35:10 warning Link to unknown heading in ' +
279+ '`examples/example.md`: `world`',
280+ ' 37:10 warning Link to unknown heading in ' +
281+ '`examples/example.md`: `world`',
282+ ' 39:10 warning Link to unknown heading in ' +
283+ '`examples/example.md`: `world`',
284+ ' 41:10 warning Link to unknown heading in ' +
285+ '`examples/example.md`: `world`',
286+ ' 43:10 warning Link to unknown file: `examples/world.md`',
287+ ' 45:10 warning Link to unknown file: `examples/world.md`',
288+ ' 47:10 warning Link to unknown file: `examples/world.md`',
289+ ' 49:10 warning Link to unknown file: `examples/world.md`',
290+ '',
291+ '✖ 13 problems (0 errors, 13 warnings)',
292+ '',
293+ 'examples/example.md',
294+ ' 5:37 warning Link to unknown heading: `world`',
295+ ' 15:34 warning Link to unknown file: `world.md`',
296+ ' 17:12 warning Link to unknown file: `world.md`',
297+ ' 19:10 warning Link to unknown file: `world.md`',
298+ ' 29:10 warning Link to unknown heading in ' +
299+ '`example.md`: `world`',
300+ ' 31:10 warning Link to unknown heading in ' +
301+ '`example.md`: `world`',
302+ ' 33:10 warning Link to unknown heading in ' +
303+ '`example.md`: `world`',
304+ ' 35:10 warning Link to unknown file: `world.md`',
305+ ' 37:10 warning Link to unknown file: `world.md`',
306+ ' 39:10 warning Link to unknown file: `world.md`',
307+ '',
308+ '✖ 10 problems (0 errors, 10 warnings)'
309+ ].join('\n'));
310+
311+ done(err);
312+ });
313+ });
314+
315+ it('should work on GitHub URLs when with package.json', function (done) {
316+ var stop = intercept();
317+
318+ fs.writeFileSync('./package.json', JSON.stringify({
319+ 'repository': 'wooorm/test'
320+ }, 0, 2));
321+
322+ cli({
323+ 'files': [
324+ 'example.md',
325+ 'examples/example.md'
326+ ],
327+ 'color': false,
328+ 'detectRC': false,
329+ 'detectIgnore': false,
330+ 'plugins': [
331+ '../../../node_modules/mdast-slug',
332+ '../../../index.js'
333+ ]
334+ }, function (err) {
335+ var res = stop();
336+
337+ fs.unlinkSync('./package.json');
338+
339+ assert.equal(res.out, '');
340+
341+ assert.equal(res.err, [
342+ 'example.md',
343+ ' 5:37 warning Link to unknown heading: `world`',
344+ ' 17:34 warning Link to unknown file: `examples/world.md`',
345+ ' 19:12 warning Link to unknown file: `examples/world.md`',
346+ ' 21:10 warning Link to unknown file: `examples/world.md`',
347+ ' 23:10 warning Link to unknown file: `examples/world.md`',
348+ ' 35:10 warning Link to unknown heading in ' +
349+ '`examples/example.md`: `world`',
350+ ' 37:10 warning Link to unknown heading in ' +
351+ '`examples/example.md`: `world`',
352+ ' 39:10 warning Link to unknown heading in ' +
353+ '`examples/example.md`: `world`',
354+ ' 41:10 warning Link to unknown heading in ' +
355+ '`examples/example.md`: `world`',
356+ ' 43:10 warning Link to unknown file: `examples/world.md`',
357+ ' 45:10 warning Link to unknown file: `examples/world.md`',
358+ ' 47:10 warning Link to unknown file: `examples/world.md`',
359+ ' 49:10 warning Link to unknown file: `examples/world.md`',
360+ '',
361+ '✖ 13 problems (0 errors, 13 warnings)',
362+ '',
363+ 'examples/example.md',
364+ ' 5:37 warning Link to unknown heading: `world`',
365+ ' 15:34 warning Link to unknown file: `world.md`',
366+ ' 17:12 warning Link to unknown file: `world.md`',
367+ ' 19:10 warning Link to unknown file: `world.md`',
368+ ' 29:10 warning Link to unknown heading in ' +
369+ '`example.md`: `world`',
370+ ' 31:10 warning Link to unknown heading in ' +
371+ '`example.md`: `world`',
372+ ' 33:10 warning Link to unknown heading in ' +
373+ '`example.md`: `world`',
374+ ' 35:10 warning Link to unknown file: `world.md`',
375+ ' 37:10 warning Link to unknown file: `world.md`',
376+ ' 39:10 warning Link to unknown file: `world.md`',
377+ '',
378+ '✖ 10 problems (0 errors, 10 warnings)'
379+ ].join('\n'));
380+
381+ done(err);
382+ });
383+ });
384+});