git.iko.soy/typeable/remark-validate-links
on main help sign in
60 files changed+2837−1398
since splitsnapshots
unifiedsplit
.editorconfig+1−7 .eslintignore−1 .eslintrc−9 .gitignore+2 .jscs.json−139 .mdastignore−1 .mdastrc−13 .npmrc+1 .prettierignore+3 .travis.yml+3−7 history.md−43 index.js+2−571 lib/check/check-files.browser.js+7 lib/check/check-files.js+42 lib/check/index.js+13 lib/check/merge-landmarks.js+29 lib/check/merge-references.js+42 lib/check/validate.js+108 lib/constants.js+8 lib/find/config.js+54 lib/find/find-repo.browser.js+7 lib/find/find-repo.js+76 lib/find/find.js+296 lib/find/index.js+8 lib/index.js+48 LICENSE → license package.json+83−35 readme.md+262−61 screenshot.png test/fixtures/bitbucket.md+67 test/fixtures/case-insensitive-headings.md+9 test/fixtures/empty.md+1 test/fixtures/examples/bitbucket.md+39 test/fixtures/examples/example.js+1 test/fixtures/examples/example.md−39 test/fixtures/examples/github.md+39 test/fixtures/examples/gitlab.md+39 test/fixtures/examples/image with space.jpg test/fixtures/examples/image.jpg test/fixtures/examples/weird#character/readme.md+1 test/fixtures/examples/weird#characters.md+1 test/fixtures/external.md+11 test/fixtures/file-as-folder.md+1 test/fixtures/folder-without-readme/example.md+1 test/fixtures/folder.md+23 test/fixtures/folder/example.md+1 test/fixtures/folder/readme.asc+9 test/fixtures/folder/readme.markdown+5 test/fixtures/folder/readme.md+1 test/fixtures/example.md → test/fixtures/github.md+22−14 test/fixtures/gitlab.md+67 test/fixtures/images.md+39 test/fixtures/line-links.md+9 test/fixtures/query-params.md+9 test/fixtures/self-hosted.md+5 test/fixtures/small.md+5 test/fixtures/suggestions.md+5−1 test/fixtures/weird-characters.md+17 test/index.js+1302−457 test/sort.js+13
.editorconfig+1−7
@@ -2,14 +2,8 @@ root = true
22
33[*]
44indent_style = space
5−indent_size = 4
5+indent_size = 2
66end_of_line = lf
77charset = utf-8
88trim_trailing_whitespace = true
99insert_final_newline = true
10−
11−[*.{json,html,svg,css,mdastrc,eslintrc}]
12−indent_size = 2
13−
14−[*.md]
15−trim_trailing_whitespace = false
.eslintignoredeleted−1
@@ -1 +0,0 @@
1−coverage/
.eslintrcdeleted−9
@@ -1,9 +0,0 @@
1−{
2− "extends": "eslint:recommended",
3− "env": {
4− "node": true
5− },
6− "rules": {
7− "quotes": [2, "single"]
8− }
9−}
.gitignore+2
@@ -1,4 +1,6 @@
11.DS_Store
22*.log
3+.nyc_output/
34coverage/
45node_modules/
6+yarn.lock
.jscs.jsondeleted−139
@@ -1,139 +0,0 @@
1−{
2− "excludeFiles": [
3− "coverage/",
4− "node_modules/"
5− ],
6− "jsDoc": {
7− "checkAnnotations": "jsdoc3",
8− "checkParamNames": true,
9− "checkRedundantAccess": true,
10− "checkRedundantParams": true,
11− "checkRedundantReturns": true,
12− "checkReturnTypes": true,
13− "checkTypes": "strictNativeCase",
14− "enforceExistence": true,
15− "requireHyphenBeforeDescription": true,
16− "requireNewlineAfterDescription": true,
17− "requireParamTypes": true,
18− "requireParamDescription": true,
19− "requireReturnTypes": true
20− },
21− "requireCurlyBraces": [
22− "if",
23− "else",
24− "for",
25− "while",
26− "do",
27− "try",
28− "catch"
29− ],
30− "requireSpaceAfterKeywords": [
31− "if",
32− "else",
33− "for",
34− "while",
35− "do",
36− "switch",
37− "return",
38− "try",
39− "catch"
40− ],
41− "requireSpaceBeforeBlockStatements": true,
42− "requireParenthesesAroundIIFE": true,
43− "requireSpacesInConditionalExpression": true,
44− "requireSpacesInFunctionExpression": {
45− "beforeOpeningCurlyBrace": true
46− },
47− "requireSpacesInAnonymousFunctionExpression": {
48− "beforeOpeningRoundBrace": true,
49− "beforeOpeningCurlyBrace": true
50− },
51− "requireSpacesInNamedFunctionExpression": {
52− "beforeOpeningRoundBrace": true,
53− "beforeOpeningCurlyBrace": true
54− },
55− "requireBlocksOnNewline": true,
56− "disallowEmptyBlocks": true,
57− "disallowSpacesInsideObjectBrackets": true,
58− "disallowSpacesInsideArrayBrackets": true,
59− "disallowSpacesInsideParentheses": true,
60− "requireSpacesInsideObjectBrackets": "all",
61− "disallowDanglingUnderscores": true,
62− "disallowSpaceAfterObjectKeys": true,
63− "requireCommaBeforeLineBreak": true,
64− "requireOperatorBeforeLineBreak": [
65− "?",
66− "+",
67− "-",
68− "/",
69− "*",
70− "=",
71− "==",
72− "===",
73− "!=",
74− "!==",
75− ">",
76− ">=",
77− "<",
78− "<="
79− ],
80− "requireSpaceBeforeBinaryOperators": [
81− "+",
82− "-",
83− "/",
84− "*",
85− "=",
86− "==",
87− "===",
88− "!=",
89− "!=="
90− ],
91− "requireSpaceAfterBinaryOperators": [
92− "+",
93− "-",
94− "/",
95− "*",
96− "=",
97− "==",
98− "===",
99− "!=",
100− "!=="
101− ],
102− "disallowSpaceAfterPrefixUnaryOperators": [
103− "++",
104− "--",
105− "+",
106− "-",
107− "~",
108− "!"
109− ],
110− "disallowSpaceBeforePostfixUnaryOperators": [
111− "++",
112− "--"
113− ],
114− "disallowImplicitTypeConversion": [
115− "numeric",
116− "boolean",
117− "binary",
118− "string"
119− ],
120− "requireCamelCaseOrUpperCaseIdentifiers": true,
121− "disallowKeywords": [
122− "with"
123− ],
124− "disallowMultipleLineStrings": true,
125− "disallowMultipleLineBreaks": true,
126− "validateLineBreaks": "LF",
127− "validateQuoteMarks": "'",
128− "disallowMixedSpacesAndTabs": true,
129− "disallowTrailingWhitespace": true,
130− "disallowTrailingComma": true,
131− "disallowKeywordsOnNewLine": [
132− "else"
133− ],
134− "requireLineFeedAtFileEnd": true,
135− "requireCapitalizedConstructors": true,
136− "safeContextKeyword": "self",
137− "requireDotNotation": true,
138− "disallowYodaConditions": true
139−}
.mdastignoredeleted−1
@@ -1 +0,0 @@
1−test/
.mdastrcdeleted−13
@@ -1,13 +0,0 @@
1−{
2− "output": true,
3− "plugins": [
4− "comment-config",
5− "lint",
6− "github",
7− "toc",
8− "./"
9− ],
10− "settings": {
11− "bullet": "*"
12− }
13−}
.npmrcnew file+1
@@ -0,0 +1 @@
1+package-lock=false
.prettierignorenew file+3
@@ -0,0 +1,3 @@
1+coverage/
2+*.json
3+*.md
.travis.yml+3−7
@@ -1,9 +1,5 @@
11language: node_js
2−script: npm run-script test-travis
32node_js:
4−- '0.10'
5−- '0.11'
6−- '0.12'
7−- iojs
8−sudo: false
9−after_script: npm install codecov.io && cat ./coverage/lcov.info | codecov
3+ - lts/dubnium
4+ - node
5+after_success: bash <(curl -s https://codecov.io/bash)
history.mddeleted−43
@@ -1,43 +0,0 @@
1−<!--mdast setext-->
2−
3−<!--lint disable no-multiple-toplevel-headings-->
4−
5−1.1.1 / 2015-09-18
6−==================
7−
8−* Add support for future changes in unified ([fab6a89](https://github.com/wooorm/mdast-validate-links/commit/fab6a89))
9−* Add related section, table-of-contents ([0b6c35b](https://github.com/wooorm/mdast-validate-links/commit/0b6c35b))
10−
11−1.1.0 / 2015-09-07
12−==================
13−
14−* Add support for changes in mdast-slug ([8749876](https://github.com/wooorm/mdast-validate-links/commit/8749876))
15−* Update test fixtures for updates in mdast ([e9b32d4](https://github.com/wooorm/mdast-validate-links/commit/e9b32d4))
16−
17−1.0.0 / 2015-08-20
18−==================
19−
20−* Update dependencies, dev-dependencies ([986797a](https://github.com/wooorm/mdast-validate-links/commit/986797a))
21−
22−0.3.1 / 2015-07-27
23−==================
24−
25−* Fix for changes in mdast ([e9702cf](https://github.com/wooorm/mdast-validate-links/commit/e9702cf))
26−
27−0.3.0 / 2015-07-26
28−==================
29−
30−* Add suggestions for similar references on failure ([ddabfe2](https://github.com/wooorm/mdast-validate-links/commit/ddabfe2))
31−
32−0.2.1 / 2015-07-19
33−==================
34−
35−* Update mdast ([10411ef](https://github.com/wooorm/mdast-validate-links/commit/10411ef))
36−
37−0.2.0 / 2015-07-19
38−==================
39−
40−* Add support for inspecting referenced files ([763c0d1](https://github.com/wooorm/mdast-validate-links/commit/763c0d1))
41−
42−0.1.0 / 2015-07-18
43−==================
index.js+2−571
@@ -1,572 +1,3 @@
1−/**
2− * @author Titus Wormer
3− * @copyright 2015 Titus Wormer
4− * @license MIT
5− * @module mdast:validate-links
6− * @fileoverview Validate links to headings and files in markdown.
7− */
1+'use strict'
82
9−'use strict';
10−
11−/*
12− * Dependencies.
13− */
14−
15−var url = require('url');
16−var fs = require('fs');
17−var path = require('path');
18−var propose = require('propose');
19−var visit = require('unist-util-visit');
20−var definitions = require('mdast-util-definitions');
21−var gh = require('github-url-to-object');
22−var urljoin = require('urljoin');
23−
24−/*
25− * Methods.
26− */
27−
28−var exists = fs.existsSync;
29−var parse = url.parse;
30−
31−/**
32− * Get the `pathname` of `uri`, if applicable.
33− *
34− * @todo Externalise.
35− * @param {string} uri - Reference.
36− * @return {string?} - Pathname.
37− */
38−function getPathname(uri) {
39− return parse(uri).pathname;
40−}
41−
42−/**
43− * Get the `hash` of `uri`, if applicable.
44− *
45− * @todo Externalise.
46− * @param {string} uri - Reference.
47− * @return {string?} - Hash.
48− */
49−function getHash(uri) {
50− var hash = parse(uri).hash;
51−
52− return hash ? hash.slice(1) : null;
53−}
54−
55−/**
56− * Suggest a possible similar reference.
57− *
58− * @param {string} pathname - Unfound reference.
59− * @param {Object<string, boolean>} references - All
60− * references.
61− * @return {string?} - Suggested reference.
62− */
63−function getClosest(pathname, references) {
64− var hash = getHash(pathname);
65− var base = getPathname(pathname);
66− var dictionary = [];
67− var reference;
68− var subhash;
69− var subbase;
70−
71− for (reference in references) {
72− subbase = getPathname(reference);
73− subhash = getHash(reference);
74−
75− if (getPathname(reference) === base) {
76− if (subhash && hash) {
77− dictionary.push(subhash);
78− }
79− } else {
80− if (!subhash && !hash) {
81− dictionary.push(subbase);
82− }
83− }
84− }
85−
86− return propose(hash ? hash : base, dictionary, {
87− 'threshold': 0.7
88− });
89−}
90−
91−/**
92− * Utilitity to warn `warning` for each node in `nodes`,
93− * on `file`.
94− *
95− * @param {File} file - Virtual file.
96− * @param {Array.<Node>} nodes - Offending nodes.
97− * @param {string} warning - Message.
98− */
99−function warnAll(file, nodes, warning) {
100− nodes.forEach(function (node) {
101− file.warn(warning, node);
102− });
103−}
104−
105−/**
106− * Gather references: a map of file-paths references
107− * to be one or more nodes.
108− *
109− * @example
110− * gatherReferences(new File(), {});
111− *
112− * @param {File} file - Set of virtual files.
113− * @param {Object.<string, string>} project - GitHub
114− * project, with a `user` and `repo` property
115− * (optional).
116− * @return {Object.<string, Array.<Node>>} exposed - Map of
117− * file-paths (and anchors) which are referenced.
118− */
119−function gatherReferences(file, project) {
120− var cache = {};
121− var filePath = file.filePath();
122− var directory = file.directory;
123− var space = file.namespace('mdast');
124− var getDefinition;
125− var prefix = '';
126− var ast = space.tree || /* istanbul ignore next */ space.ast;
127−
128− getDefinition = definitions(ast);
129−
130− if (project.user && project.repo) {
131− prefix = '/' + project.user + '/' + project.repo + '/blob/';
132− }
133−
134− /**
135− * Handle new links.
136− *
137− * @example
138− * link({
139− * href: 'foo/bar/baz.md#foo',
140− * children: [
141− * {
142− * type: 'text',
143− * value: 'foo'
144− * }
145− * ]
146− * });
147− *
148− * @param {Node} node - Link or link-reference node.
149− */
150− function onlink(node) {
151− var link = node.href;
152− var definition;
153− var index;
154− var uri;
155− var pathname;
156− var hash;
157−
158− /*
159− * Handle link-references.
160− */
161−
162− if (node.identifier) {
163− definition = getDefinition(node.identifier);
164− link = definition && definition.link;
165− }
166−
167− /*
168− * Ignore definitions without link.
169− */
170−
171− if (!link) {
172− return;
173− }
174−
175− uri = parse(link);
176−
177− if (!uri.hostname) {
178− /*
179− * Handle hashes, or relative files.
180− */
181− if (!uri.pathname && uri.hash) {
182− link = filePath + uri.hash;
183− uri = parse(link);
184− } else {
185− link = urljoin(directory, link) + (uri.hash || '');
186− uri = parse(link);
187− }
188− }
189−
190− /*
191− * Handle full links.
192− * Only works with GitHub.
193− */
194−
195− if (uri.hostname) {
196− if (!prefix) {
197− return;
198− }
199−
200− if (
201− uri.hostname !== 'github.com' ||
202− uri.pathname.slice(0, prefix.length) !== prefix
203− ) {
204− return;
205− }
206−
207− link = uri.pathname.slice(prefix.length) + (uri.hash || '');
208−
209− /*
210− * Things get interesting here: branches.
211− * `foo/bar/baz` could be `baz` on the
212− * `foo/bar` branch. Or, `baz` in the `bar`
213− * directory on the `foo` branch.
214− *
215− * Currently, I’m ignoring this and just not
216− * supporting branches.
217− */
218−
219− link = link.slice(link.indexOf('/') + 1);
220−
221− uri = parse(link);
222− }
223−
224− /*
225− * Handle file links, or combinations of files
226− * and hashes.
227− */
228−
229− index = link.indexOf('#');
230−
231− if (index === -1) {
232− pathname = link;
233− hash = null;
234− } else {
235− pathname = link.slice(0, index);
236− hash = link.slice(index + 1);
237− }
238−
239− if (!cache[pathname]) {
240− cache[pathname] = [];
241− }
242−
243− cache[pathname].push(node);
244−
245− if (hash) {
246− if (!cache[link]) {
247− cache[link] = [];
248− }
249−
250− cache[link].push(node);
251− }
252− }
253−
254− visit(ast, 'link', onlink);
255− visit(ast, 'linkReference', onlink);
256−
257− return cache;
258−}
259−
260−/**
261− * Factory to store all markdown files, and headings.
262− *
263− * @example
264− * var gather = gatherExposedFactory();
265− * var file = new File('# foo');
266− *
267− * gather(file).done() // {}
268− *
269− * @return {Function}
270− */
271−function gatherExposedFactory() {
272− var cache = {};
273− var hasHeadings = false;
274− var hasSlugs = false;
275−
276− /**
277− * Access found files and headings.
278− *
279− * @return {Object.<string, boolean>} - Map of
280− * file-paths, with `true` as their value.
281− * @throws {Error} - When headings are found, but no
282− * heading has a slug.
283− */
284− function done() {
285− if (hasHeadings && !hasSlugs) {
286− throw new Error(
287− 'Missing slugs. Use for example `mdast-slug` to generate ' +
288− 'heading IDs'
289− );
290− }
291−
292− return cache;
293− }
294−
295− /**
296− * Find headings in `file`.
297− *
298− * @property {Function} done - Access found links.
299− * @param {File} file - Virtual file.
300− * @returns {Function} - Itself.
301− */
302− function gather(file) {
303− var filePath = file.filePath();
304− var space = file.namespace('mdast');
305− var ast = space.tree || space.ast;
306−
307− /*
308− * Ignore files without AST or filename.
309− */
310−
311− if (filePath && ast) {
312− cache[filePath] = true;
313−
314− visit(ast, 'heading', function (node) {
315− var data = node.data || {};
316− var attributes = data.htmlAttributes || {};
317− var id = attributes.id || data.id;
318−
319− /* istanbul ignore next */
320− id = id || (node.attributes && node.attributes.id);
321−
322− hasHeadings = true;
323−
324− if (id) {
325− cache[filePath + '#' + id] = hasSlugs = true;
326− }
327− });
328− }
329−
330− return gather;
331− }
332−
333− gather.done = done;
334−
335− return gather;
336−}
337−
338−/**
339− * Check if `file` references headings or files not in
340− * `exposed`. If `project` is given, normalizes GitHub blob
341− * URLs.
342− *
343− * @example
344− * validate({'example.md': true}, new File());
345− *
346− * @param {Object.<string, boolean?>} exposed - Map of
347− * file-paths (and anchors) which can be references to.
348− * @param {File} file - Set of virtual files.
349− * @param {Object.<string, string>} project - GitHub
350− * project, with a `user` and `repo` property
351− * (optional).
352− */
353−function validate(exposed, file, project) {
354− var space = file.namespace('mdast');
355− var ast = space.tree || space.ast;
356− var references = ast ? gatherReferences(file, project) : {};
357− var filePath = file.filePath();
358− var reference;
359− var nodes;
360− var real;
361− var hash;
362− var pathname;
363− var warning;
364− var suggestion;
365−
366− for (reference in references) {
367− nodes = references[reference];
368− real = exposed[reference];
369− hash = getHash(reference);
370−
371− /*
372− * Check if files without `hash` can be linked to.
373− * Because there’s no need to inspect those files
374− * for headings they are not added to mdast. This
375− * is especially useful because they might be
376− * non-markdown files. Here we check if they exist.
377− */
378−
379− if ((real === undefined || real === null) && !hash) {
380− real = references[reference] = exists(reference);
381− }
382−
383− if (!real) {
384− if (hash) {
385− pathname = getPathname(reference);
386− warning = 'Link to unknown heading';
387−
388− if (pathname !== filePath) {
389− warning += ' in `' + pathname + '`';
390− }
391−
392− warning += ': `' + hash + '`';
393− } else {
394− warning = 'Link to unknown file: `' + reference + '`';
395− }
396−
397− suggestion = getClosest(reference, exposed);
398−
399− if (suggestion) {
400− warning += '. Did you mean `' + suggestion + '`';
401− }
402−
403− warnAll(file, nodes, warning);
404− }
405− }
406−}
407−
408−/**
409− * Factory to create a new completer.
410− *
411− * @example
412− * var completer = completerFactory({
413− * 'user': 'foo',
414− * 'repo': 'bar'
415− * });
416− *
417− * @param {Object} project - GitHub project, if applicable.
418− * @return {Function} - `completer`, bound to `project`.
419− */
420−function completerFactory(project) {
421− /**
422− * Completer.
423− *
424− * @example
425− * completer(new FileSet(), console.log)
426− *
427− * @property {*} pluginId - Unique ID so completers by
428− * this plug-in are not added multiple times to a single
429− * file-set pipeline.
430− * @param {FileSet} set - Virtual file-set.
431− * @param {function(err?)} done - Callback.
432− */
433− function completer(set, done) {
434− var gatherExposed = gatherExposedFactory();
435− var exposed;
436−
437− set.valueOf().forEach(gatherExposed);
438−
439− exposed = gatherExposed.done();
440−
441− set.valueOf().forEach(function (file) {
442− /* istanbul ignore else - stdin */
443− if (file.filePath()) {
444− validate(exposed, file, project);
445− }
446− });
447−
448− done();
449− }
450−
451− completer.pluginId = 'mdast-validate-links';
452−
453− return completer;
454−}
455−
456−/**
457− * Factory to create a transformer based on the given
458− * project and set.
459− *
460− * @example
461− * transformerFactory({}, new FileSet());
462− *
463− * @param {Object.<string, string>} project - GitHub
464− * project, with a `user` and `repo` property
465− * (optional).
466− * @param {FileSet} fileSet - Set of virtual files.
467− * @return {function(ast, file)}
468− */
469−function transformerFactory(project, fileSet) {
470− /**
471− * Transformer. Adds references files to the set.
472− *
473− * @example
474− * transformer({}, new File());
475− *
476− * @param {*} ast - Node.
477− * @param {File} file - Virtual file.
478− */
479− function transformer(ast, file) {
480− var links = [];
481− var references;
482− var current;
483− var link;
484− var pathname;
485−
486− /* istanbul ignore if - stdin */
487− if (!file.filePath()) {
488− return;
489− }
490−
491− references = gatherReferences(file, project);
492− current = getPathname(file.filePath());
493−
494− for (link in references) {
495− pathname = getPathname(link);
496−
497− if (
498− pathname !== current &&
499− getHash(link) &&
500− links.indexOf(pathname) === -1
501− ) {
502− links.push(pathname);
503−
504− fileSet.add(pathname);
505− }
506− }
507− }
508−
509− return transformer;
510−}
511−
512−/**
513− * Attacher.
514− *
515− * @example
516− * attacher(mdast())
517− * // [Error: mdast-validate-links only works on the CLI]
518− *
519− * @param {MDAST} mdast - Processor.
520− * @param {Object?} options - Settings.
521− * @param {FileSet?} fileSet - Virtual file-set.
522− * @throws {Error} - When `fileSet` is not given (when not on
523− * the CLI).
524− */
525−function attacher(mdast, options, fileSet) {
526− var repo = (options || {}).repository;
527− var pack;
528−
529− /*
530− * Throw when not on the CLI.
531− */
532−
533− if (!fileSet) {
534− throw new Error('mdast-validate-links only works on the CLI');
535− }
536−
537− /*
538− * Try to get the repo from `package.json` when not
539− * given.
540− */
541−
542− if (!repo) {
543− try {
544− pack = require(path.resolve(process.cwd(), 'package.json'));
545− } catch (exception) {
546− pack = {};
547− }
548−
549− repo = pack.repository ? pack.repository.url || pack.repository : '';
550− }
551−
552− repo = repo ? gh(repo) : {};
553−
554− repo = {
555− 'user': repo.user,
556− 'repo': repo.repo
557− };
558−
559− /*
560− * Attach a `completer`.
561− */
562−
563− fileSet.use(completerFactory(repo));
564−
565− return transformerFactory(repo, fileSet);
566−}
567−
568−/*
569− * Expose.
570− */
571−
572−module.exports = attacher;
3+module.exports = require('./lib')
lib/check/check-files.browser.jsnew file+7
@@ -0,0 +1,7 @@
1+'use strict'
2+
3+module.exports = checkIfReferencedFilesExist
4+
5+function checkIfReferencedFilesExist(ctx, next) {
6+ next()
7+}
lib/check/check-files.jsnew file+42
@@ -0,0 +1,42 @@
1+'use strict'
2+
3+var fs = require('fs')
4+
5+module.exports = checkIfReferencedFilesExist
6+
7+function checkIfReferencedFilesExist(ctx, next) {
8+ var landmarks = ctx.landmarks
9+ var references = ctx.references
10+ var filePaths = []
11+ var filePath
12+ var actual = 0
13+ var expected
14+
15+ for (filePath in references) {
16+ if (landmarks[filePath] === undefined) {
17+ filePaths.push(filePath)
18+ }
19+ }
20+
21+ expected = filePaths.length
22+
23+ if (expected === 0) {
24+ next()
25+ } else {
26+ filePaths.forEach(checkIfExists)
27+ }
28+
29+ function checkIfExists(filePath) {
30+ fs.access(filePath, fs.F_OK, onaccess)
31+
32+ function onaccess(err) {
33+ var noEntry = err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')
34+
35+ landmarks[filePath] = {'': !noEntry}
36+
37+ if (++actual === expected) {
38+ next()
39+ }
40+ }
41+ }
42+}
lib/check/index.jsnew file+13
@@ -0,0 +1,13 @@
1+'use strict'
2+
3+var trough = require('trough')
4+var mergeLandmarks = require('./merge-landmarks')
5+var mergeReferences = require('./merge-references')
6+var checkIfReferencedFilesExist = require('./check-files')
7+var validate = require('./validate')
8+
9+module.exports = trough()
10+ .use(mergeLandmarks)
11+ .use(mergeReferences)
12+ .use(checkIfReferencedFilesExist)
13+ .use(validate)
lib/check/merge-landmarks.jsnew file+29
@@ -0,0 +1,29 @@
1+'use strict'
2+
3+var xtend = require('xtend')
4+var constants = require('../constants')
5+
6+module.exports = mergeLandmarks
7+
8+function mergeLandmarks(ctx) {
9+ var result = {}
10+ var files = ctx.files
11+ var length = files.length
12+ var index = -1
13+ var file
14+ var landmarks
15+ var landmark
16+
17+ while (++index < length) {
18+ file = files[index]
19+ landmarks = file.data[constants.landmarkId]
20+
21+ if (landmarks) {
22+ for (landmark in landmarks) {
23+ result[landmark] = xtend(result[landmark] || {}, landmarks[landmark])
24+ }
25+ }
26+ }
27+
28+ ctx.landmarks = result
29+}
lib/check/merge-references.jsnew file+42
@@ -0,0 +1,42 @@
1+'use strict'
2+
3+var constants = require('../constants')
4+
5+module.exports = mergeReferences
6+
7+function mergeReferences(ctx) {
8+ var result = {}
9+ var files = ctx.files
10+ var length = files.length
11+ var index = -1
12+ var file
13+ var references
14+ var reference
15+ var all
16+ var internal
17+ var hash
18+
19+ while (++index < length) {
20+ file = files[index]
21+ references = file.data[constants.referenceId]
22+
23+ if (!references) {
24+ continue
25+ }
26+
27+ for (reference in references) {
28+ internal = references[reference]
29+ all = result[reference] || (result[reference] = {})
30+
31+ for (hash in internal) {
32+ ;(all[hash] || (all[hash] = [])).push({
33+ file: file,
34+ reference: {filePath: reference, hash: hash},
35+ nodes: internal[hash]
36+ })
37+ }
38+ }
39+ }
40+
41+ ctx.references = result
42+}
lib/check/validate.jsnew file+108
@@ -0,0 +1,108 @@
1+'use strict'
2+
3+var path = require('path')
4+var propose = require('propose')
5+var constants = require('../constants')
6+
7+module.exports = validate
8+
9+function validate(ctx) {
10+ var landmarks = ctx.landmarks
11+ var references = ctx.references
12+ var missing = []
13+ var reference
14+ var hash
15+ var refs
16+ var lands
17+ var length
18+ var index
19+
20+ for (reference in references) {
21+ refs = references[reference]
22+ /* istanbul ignore next - `else` could happen in browser. */
23+ lands = landmarks[reference] || {}
24+
25+ for (hash in refs) {
26+ if (!lands[hash]) {
27+ missing = missing.concat(refs[hash])
28+ }
29+ }
30+ }
31+
32+ length = missing.length
33+ index = -1
34+
35+ while (++index < length) {
36+ reference = missing[index]
37+ warn(ctx, reference.reference, reference.file, reference.nodes)
38+ }
39+}
40+
41+function warn(ctx, info, file, nodes) {
42+ var landmarks = ctx.landmarks
43+ var absolute = file.path ? path.resolve(file.cwd, file.path) : ''
44+ var base = absolute ? path.dirname(absolute) : null
45+ var filePath = base ? path.relative(base, info.filePath) : info.filePath
46+ var hash = info.hash
47+ var dictionary = []
48+ var subhash
49+ var landmark
50+ var relativeLandmark
51+ var reason
52+ var ruleId
53+ var origin
54+ var suggestion
55+
56+ if (hash) {
57+ reason = 'Link to unknown heading'
58+ ruleId = constants.headingRuleId
59+
60+ if (base && path.join(base, filePath) !== absolute) {
61+ reason += ' in `' + filePath + '`'
62+ ruleId = constants.headingInFileRuleId
63+ }
64+
65+ reason += ': `' + hash + '`'
66+ } else {
67+ reason = 'Link to unknown file: `' + filePath + '`'
68+ ruleId = constants.fileRuleId
69+ }
70+
71+ origin = [constants.sourceId, ruleId].join(':')
72+
73+ for (landmark in landmarks) {
74+ // Only suggest if file exists.
75+ if (!landmarks[landmark] || !landmarks[landmark]['']) {
76+ continue
77+ }
78+
79+ relativeLandmark = base ? path.relative(base, landmark) : landmark
80+
81+ if (!hash) {
82+ dictionary.push(relativeLandmark)
83+ continue
84+ }
85+
86+ if (relativeLandmark !== filePath) {
87+ continue
88+ }
89+
90+ for (subhash in landmarks[landmark]) {
91+ if (subhash !== '') {
92+ dictionary.push(subhash)
93+ }
94+ }
95+ }
96+
97+ suggestion = propose(hash ? hash : filePath, dictionary, {threshold: 0.7})
98+
99+ if (suggestion) {
100+ reason += '. Did you mean `' + suggestion + '`'
101+ }
102+
103+ nodes.forEach(one)
104+
105+ function one(node) {
106+ file.message(reason, node, origin)
107+ }
108+}
lib/constants.jsnew file+8
@@ -0,0 +1,8 @@
1+'use strict'
2+
3+exports.sourceId = 'remark-validate-links'
4+exports.headingRuleId = 'missing-heading'
5+exports.headingInFileRuleId = 'missing-heading-in-file'
6+exports.fileRuleId = 'missing-file'
7+exports.landmarkId = 'remarkValidateLinksLandmarks'
8+exports.referenceId = 'remarkValidateLinksReferences'
lib/find/config.jsnew file+54
@@ -0,0 +1,54 @@
1+'use strict'
2+
3+var hostedGitInfo = require('hosted-git-info')
4+
5+module.exports = config
6+
7+var viewPaths = {github: 'blob', gitlab: 'blob', bitbucket: 'src'}
8+var headingPrefixes = {github: '#', gitlab: '#', bitbucket: '#markdown-header-'}
9+var topAnchors = {github: '#readme', gitlab: '#readme'}
10+var lineLinks = {github: true, gitlab: true}
11+
12+function config(ctx) {
13+ var repo = ctx.repository
14+ var urlConfig = ctx.urlConfig
15+ var info = {}
16+
17+ if (urlConfig) {
18+ return
19+ }
20+
21+ urlConfig = {
22+ prefix: '',
23+ headingPrefix: '#',
24+ lines: false,
25+ hostname: null,
26+ topAnchor: null
27+ }
28+
29+ if (repo) {
30+ info = hostedGitInfo.fromUrl(repo)
31+ }
32+
33+ if (info) {
34+ if (info.type in viewPaths) {
35+ urlConfig.prefix = '/' + info.path() + '/' + viewPaths[info.type] + '/'
36+ }
37+
38+ if (info.type in headingPrefixes) {
39+ urlConfig.headingPrefix = headingPrefixes[info.type]
40+ }
41+
42+ if (info.type in lineLinks) {
43+ urlConfig.lines = lineLinks[info.type]
44+ }
45+
46+ if (info.type in topAnchors) {
47+ urlConfig.topAnchor = topAnchors[info.type]
48+ }
49+
50+ urlConfig.hostname = info.domain
51+ }
52+
53+ ctx.urlConfig = urlConfig
54+}
lib/find/find-repo.browser.jsnew file+7
@@ -0,0 +1,7 @@
1+'use strict'
2+
3+module.exports = findRepo
4+
5+function findRepo(ctx, next) {
6+ next()
7+}
lib/find/find-repo.jsnew file+76
@@ -0,0 +1,76 @@
1+'use strict'
2+
3+var path = require('path')
4+var exec = require('child_process').exec
5+
6+module.exports = findRepo
7+
8+function findRepo(ctx, next) {
9+ var repo = ctx.repository
10+ var file = ctx.file
11+ var base = file.cwd
12+ var actual = 0
13+ var expected = 0
14+
15+ if (file.path) {
16+ base = path.dirname(path.resolve(base, file.path))
17+ }
18+
19+ if (repo === null || repo === undefined) {
20+ expected++
21+ exec('git remote -v', {cwd: base}, onremote)
22+ }
23+
24+ if (ctx.root === null || ctx.root === undefined) {
25+ if (repo === null || repo === undefined) {
26+ expected++
27+ exec('git rev-parse --show-cdup', {cwd: base}, oncdup)
28+ } else {
29+ ctx.root = ctx.file.cwd
30+ }
31+ } else {
32+ ctx.root = path.resolve(file.cwd, ctx.root)
33+ }
34+
35+ if (actual === expected) {
36+ next()
37+ }
38+
39+ function onremote(err, stdout) {
40+ var remote
41+
42+ if (err) {
43+ expected = Infinity
44+ return next(err)
45+ }
46+
47+ remote = stdout.match(/origin\t(.+?) \(fetch\)/)
48+ ctx.repository = remote ? remote[1] : null
49+
50+ if (!ctx.repository) {
51+ expected = Infinity
52+ return next(new Error('Could not find remote origin'))
53+ }
54+
55+ if (++actual === expected) {
56+ expected = Infinity
57+ next()
58+ }
59+ }
60+
61+ function oncdup(err, stdout) {
62+ var out
63+
64+ if (err) {
65+ expected = Infinity
66+ return next(err)
67+ }
68+
69+ out = stdout.trim()
70+ ctx.root = out ? path.join(base, out) : base
71+
72+ if (++actual === expected) {
73+ next()
74+ }
75+ }
76+}
lib/find/find.jsnew file+296
@@ -0,0 +1,296 @@
1+'use strict'
2+
3+var fs = require('fs')
4+var path = require('path')
5+var URL = require('url').URL // Node 8 support
6+var vfile = require('to-vfile')
7+var visit = require('unist-util-visit')
8+var toString = require('mdast-util-to-string')
9+var xtend = require('xtend')
10+var slugs = require('github-slugger')()
11+var constants = require('../constants')
12+
13+module.exports = find
14+
15+var slash = '/'
16+var numberSign = '#'
17+var questionMark = '?'
18+
19+var https = 'https:'
20+var http = 'http:'
21+var slashes = '//'
22+
23+var lineExpression = /^#l\d/i
24+
25+// List from: https://github.com/github/markup#markups
26+var readmeExtensions = ['.markdown', '.mdown', '.mkdn', '.md']
27+var readmeBasename = /^readme$/i
28+
29+function find(ctx, next) {
30+ var file = ctx.file
31+ var fileSet = ctx.fileSet
32+ var absolute = file.path ? path.resolve(file.cwd, file.path) : ''
33+ var space = file.data
34+ var references = {}
35+ var landmarks = {}
36+ var actual = 0
37+ var expected = 0
38+ var statted = []
39+ var added = []
40+ var config = xtend(ctx.urlConfig, {
41+ path: absolute,
42+ base: absolute ? path.dirname(absolute) : file.cwd,
43+ root: ctx.root
44+ })
45+
46+ space[constants.referenceId] = references
47+ space[constants.landmarkId] = landmarks
48+
49+ addLandmarks(absolute, '')
50+
51+ slugs.reset()
52+
53+ visit(ctx.tree, mark)
54+
55+ if (expected === 0) {
56+ next()
57+ }
58+
59+ function mark(node) {
60+ var data = node.data || {}
61+ var props = data.hProperties || {}
62+ var id = props.name || props.id || data.id
63+ var info = node.url ? urlToPath(node.url, config) : null
64+ var fp
65+ var hash
66+
67+ if (!id && node.type === 'heading') {
68+ id = slugs.slug(toString(node))
69+ }
70+
71+ if (id) {
72+ addLandmarks(absolute, id)
73+ }
74+
75+ if (info) {
76+ fp = info.filePath
77+ hash = info.hash
78+
79+ addReference(fp, '', node)
80+
81+ if (hash) {
82+ if (fileSet || fp === absolute) {
83+ addReference(fp, hash, node)
84+ }
85+
86+ if (fileSet && fp && statted.indexOf(fp) === -1) {
87+ addFile(fp)
88+ }
89+ }
90+ }
91+ }
92+
93+ function addLandmarks(filePath, hash) {
94+ addLandmark(filePath, hash)
95+
96+ // Note: this may add marks too many anchors as defined.
97+ // For example, if there is both a `readme.md` and a `readme.markdown` in a
98+ // folder, both their landmarks will be defined for their parent folder.
99+ // To solve this, we could check whichever sorts first, and ignore the
100+ // others.
101+ // This is an unlikely scenario though, and adds a lot of complexity, so
102+ // we’re ignoring it.
103+ if (readme(filePath)) {
104+ addLandmark(path.dirname(filePath), hash)
105+ }
106+ }
107+
108+ function addLandmark(filePath, hash) {
109+ var marks = landmarks[filePath] || (landmarks[filePath] = {})
110+
111+ marks[hash] = true
112+ }
113+
114+ function addReference(filePath, hash, node) {
115+ var refs = references[filePath] || (references[filePath] = {})
116+ var hashes = refs[hash] || (refs[hash] = [])
117+
118+ hashes.push(node)
119+ }
120+
121+ function addFile(fileOrFolderPath) {
122+ expected++
123+
124+ statted.push(fileOrFolderPath)
125+
126+ fs.stat(fileOrFolderPath, onstat)
127+
128+ function onstat(_, stats) {
129+ if (stats && stats.isDirectory()) {
130+ fs.readdir(fileOrFolderPath, onreaddir)
131+ } else {
132+ done(fileOrFolderPath)
133+ }
134+ }
135+
136+ function onreaddir(_, entries) {
137+ /* istanbul ignore next - unlikely that it is an unreadable directory. */
138+ var files = (entries || []).sort()
139+ var length = files.length
140+ var index = -1
141+ var entry
142+ var file
143+ var filePath
144+
145+ while (++index < length) {
146+ entry = entries[index]
147+
148+ if (readme(entry)) {
149+ file = entry
150+ break
151+ }
152+ }
153+
154+ // If there is no readme that we can parse, add the directory.
155+ filePath = fileOrFolderPath
156+
157+ // To do: test for no readme in directory.
158+
159+ if (file) {
160+ filePath = path.join(fileOrFolderPath, file)
161+ statted.push(filePath)
162+ }
163+
164+ done(filePath)
165+ }
166+
167+ function done(filePath) {
168+ if (added.indexOf(filePath) === -1) {
169+ added.push(filePath)
170+
171+ fileSet.add(
172+ vfile({cwd: file.cwd, path: path.relative(file.cwd, filePath)})
173+ )
174+ }
175+
176+ actual++
177+
178+ if (actual === expected) {
179+ next()
180+ }
181+ }
182+ }
183+}
184+
185+function urlToPath(value, config) {
186+ var url
187+ var questionMarkIndex
188+ var numberSignIndex
189+
190+ // Absolute paths: `/wooorm/test/blob/main/directory/example.md`.
191+ if (value.charAt(0) === slash) {
192+ if (!config.hostname) {
193+ return
194+ }
195+
196+ // Create a URL.
197+ value = https + slashes + config.hostname + value
198+ }
199+
200+ try {
201+ url = new URL(value)
202+ } catch (_) {}
203+
204+ // URLs: `https://github.com/wooorm/test/blob/main/directory/example.md`.
205+ if (url) {
206+ // Exit if we don’t have hosted Git info or this is not a URL to the repo.
207+ if (
208+ !config.prefix ||
209+ !config.hostname ||
210+ (url.protocol !== https && url.protocol !== http) ||
211+ url.hostname !== config.hostname ||
212+ url.pathname.slice(0, config.prefix.length) !== config.prefix
213+ ) {
214+ return
215+ }
216+
217+ value = url.pathname.slice(config.prefix.length)
218+
219+ // Things get interesting here: branches: `foo/bar/baz` could be `baz` on
220+ // the `foo/bar` branch, or, `baz` in the `bar` directory on the `foo`
221+ // branch.
222+ // Currently, we’re ignoring this and just not supporting branches.
223+ value = value.split(slash).slice(1).join(slash)
224+
225+ return normalize(path.resolve(config.root, value + url.hash), config)
226+ }
227+
228+ // Remove the search: `?foo=bar`.
229+ // But don’t remove stuff if it’s in the hash: `readme.md#heading?`.
230+ numberSignIndex = value.indexOf(numberSign)
231+ questionMarkIndex = value.indexOf(questionMark)
232+
233+ if (
234+ questionMarkIndex !== -1 &&
235+ (numberSignIndex === -1 || numberSignIndex > questionMarkIndex)
236+ ) {
237+ value =
238+ value.slice(0, questionMarkIndex) +
239+ (numberSignIndex === -1 ? '' : value.slice(numberSignIndex))
240+ }
241+
242+ // Local: `#heading`.
243+ if (value.charAt(0) === numberSign) {
244+ value = config.path ? config.path + value : value
245+ }
246+ // Anything else, such as `readme.md`.
247+ else {
248+ value = config.path ? path.resolve(config.base, value) : ''
249+ }
250+
251+ return normalize(value, config)
252+}
253+
254+function normalize(url, config) {
255+ var numberSignIndex = url.indexOf(numberSign)
256+ var lines = config.lines
257+ var prefix = config.headingPrefix
258+ var topAnchor = config.topAnchor
259+ var filePath
260+ var hash
261+
262+ if (numberSignIndex === -1) {
263+ filePath = url
264+ } else {
265+ filePath = url.slice(0, numberSignIndex)
266+ hash = url.slice(numberSignIndex).toLowerCase()
267+
268+ // Ignore the hash if it references the top anchor of the environment
269+ if (topAnchor && hash === topAnchor) {
270+ hash = undefined
271+ }
272+ // Ignore the hash if it references lines in a file or doesn’t start
273+ // with a heading prefix.
274+ else if (
275+ (lines && lineExpression.test(hash)) ||
276+ hash.slice(0, prefix.length) !== prefix
277+ ) {
278+ hash = undefined
279+ }
280+ // Use the hash if it starts with a heading prefix.
281+ else {
282+ hash = hash.slice(prefix.length)
283+ }
284+ }
285+
286+ return {filePath: decodeURIComponent(filePath), hash: hash}
287+}
288+
289+function readme(filePath) {
290+ var ext = path.extname(filePath)
291+
292+ return (
293+ readmeExtensions.indexOf(ext) !== -1 &&
294+ readmeBasename.test(path.basename(filePath, ext))
295+ )
296+}
lib/find/index.jsnew file+8
@@ -0,0 +1,8 @@
1+'use strict'
2+
3+var trough = require('trough')
4+var findRepo = require('./find-repo')
5+var config = require('./config')
6+var find = require('./find')
7+
8+module.exports = trough().use(findRepo).use(config).use(find)
lib/index.jsnew file+48
@@ -0,0 +1,48 @@
1+'use strict'
2+
3+var xtend = require('xtend')
4+var check = require('./check')
5+var find = require('./find')
6+var constants = require('./constants')
7+
8+module.exports = validateLinks
9+
10+cliCompleter.pluginId = constants.sourceId
11+
12+function validateLinks(options, fileSet) {
13+ var settings = options || {}
14+
15+ // Attach a `completer`.
16+ if (fileSet) {
17+ fileSet.use(cliCompleter)
18+ }
19+
20+ return transformer
21+
22+ // Find references and landmarks.
23+ function transformer(tree, file, next) {
24+ find.run(xtend(settings, {tree: tree, file: file, fileSet: fileSet}), done)
25+
26+ function done(err) {
27+ if (err) {
28+ next(err)
29+ } else if (fileSet) {
30+ next()
31+ } else {
32+ checkAll([file], next)
33+ }
34+ }
35+ }
36+}
37+
38+// Completer for the CLI (multiple files, supports parsing more files).
39+function cliCompleter(set, next) {
40+ checkAll(set.valueOf(), next)
41+}
42+
43+function checkAll(files, next) {
44+ // Check all references and landmarks.
45+ check.run({files: files}, function (err) {
46+ next(err)
47+ })
48+}
LICENSE → licenserenamed

The content is the same.

package.json+83−35
@@ -1,52 +1,100 @@
11{
2− "name": "mdast-validate-links",
3− "version": "1.1.1",
4− "description": "Validate links to headings and files in markdown",
2+ "name": "remark-validate-links",
3+ "version": "10.0.2",
4+ "description": "remark plugin to validate links to headings and files",
55 "license": "MIT",
66 "keywords": [
7+ "unified",
8+ "remark",
9+ "remark-plugin",
10+ "plugin",
11+ "mdast",
12+ "markdown",
713 "validate",
814 "link",
915 "reference",
1016 "file",
11− "heading",
12− "mdast"
17+ "heading"
1318 ],
14− "dependencies": {
15− "github-url-to-object": "^2.1.0",
16− "mdast-slug": "^2.0.0",
17− "mdast-util-definitions": "^1.0.0",
18− "unist-util-visit": "^1.0.0",
19− "propose": "0.0.5",
20− "urljoin": "^0.1.5"
19+ "repository": "remarkjs/remark-validate-links",
20+ "bugs": "https://github.com/remarkjs/remark-validate-links/issues",
21+ "funding": {
22+ "type": "opencollective",
23+ "url": "https://opencollective.com/unified"
24+ },
25+ "author": "Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)",
26+ "contributors": [
27+ "Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)",
28+ "Diogo Nunes <dialex.nunes@gmail.com>",
29+ "Jonathan Haines <jonno.haines@gmail.com>",
30+ "Merlijn Vos <merlijn@soverin.net>",
31+ "Andrew Petro <andrew.w.petro@gmail.com>",
32+ "wangshijun <wangshijun2010@gmail.com>",
33+ "Anton Rybochkin <anton.rybochkin@axibase.com>"
34+ ],
35+ "browser": {
36+ "./lib/check/check-files.js": "./lib/check/check-files.browser.js",
37+ "./lib/find/find-repo.js": "./lib/find/find-repo.browser.js"
2138 },
22− "repository": "wooorm/mdast-validate-links",
23− "author": "Titus Wormer <tituswormer@gmail.com>",
2439 "files": [
40+ "lib/",
2541 "index.js"
2642 ],
43+ "dependencies": {
44+ "github-slugger": "^1.0.0",
45+ "hosted-git-info": "^3.0.0",
46+ "mdast-util-to-string": "^1.0.0",
47+ "propose": "0.0.5",
48+ "to-vfile": "^6.0.0",
49+ "trough": "^1.0.0",
50+ "unist-util-visit": "^2.0.0",
51+ "xtend": "^4.0.0"
52+ },
2753 "devDependencies": {
28− "eslint": "^1.0.0",
29− "hook-std": "^0.1.0",
30− "istanbul": "^0.4.0",
31− "jscs": "^2.0.0",
32− "jscs-jsdoc": "^1.0.0",
33− "mdast": "^2.2.2",
34− "mdast-comment-config": "^1.0.0",
35− "mdast-github": "^1.0.0",
36− "mdast-lint": "^1.0.0",
37− "mdast-toc": "^1.0.0",
38− "mocha": "^2.0.0"
54+ "nyc": "^15.0.0",
55+ "prettier": "^2.0.0",
56+ "remark": "^12.0.0",
57+ "remark-cli": "^8.0.0",
58+ "remark-preset-wooorm": "^7.0.0",
59+ "rimraf": "^3.0.0",
60+ "strip-ansi": "^6.0.0",
61+ "tape": "^5.0.0",
62+ "vfile-sort": "^2.0.0",
63+ "xo": "^0.32.0"
3964 },
4065 "scripts": {
41− "test-api": "mocha --check-leaks test/index.js",
42− "test-coverage": "istanbul cover _mocha -- test/index.js",
43− "test-travis": "npm run test-coverage",
44− "test": "npm run test-api",
45− "lint-api": "eslint .",
46− "lint-style": "jscs --reporter inline .",
47− "lint": "npm run lint-api && npm run lint-style",
48− "make": "npm run lint && npm run test-coverage",
49− "build-md": "mdast . --quiet",
50− "build": "npm run build-md"
66+ "format": "remark . -qfo --ignore-pattern test/ && prettier . --write && xo --fix",
67+ "test-api": "node test",
68+ "test-coverage": "nyc --reporter lcov tape test/index.js",
69+ "test": "npm run format && npm run test-coverage"
70+ },
71+ "nyc": {
72+ "check-coverage": true,
73+ "lines": 100,
74+ "functions": 100,
75+ "branches": 100
76+ },
77+ "prettier": {
78+ "tabWidth": 2,
79+ "useTabs": false,
80+ "singleQuote": true,
81+ "bracketSpacing": false,
82+ "semi": false,
83+ "trailingComma": "none"
84+ },
85+ "xo": {
86+ "prettier": true,
87+ "esnext": false,
88+ "rules": {
89+ "unicorn/prefer-optional-catch-binding": "off",
90+ "unicorn/no-fn-reference-in-iterator": "off",
91+ "unicorn/prefer-includes": "off",
92+ "guard-for-in": "off"
93+ }
94+ },
95+ "remarkConfig": {
96+ "plugins": [
97+ "preset-wooorm"
98+ ]
5199 }
52100}
readme.md+262−61
@@ -1,114 +1,315 @@
1−# mdast-validate-links [![Build Status](https://img.shields.io/travis/wooorm/mdast-validate-links.svg)](https://travis-ci.org/wooorm/mdast-validate-links) [![Coverage Status](https://img.shields.io/codecov/c/github/wooorm/mdast-validate-links.svg)](https://codecov.io/github/wooorm/mdast-validate-links)
1+# remark-validate-links
22
3−[**mdast**](https://github.com/wooorm/mdast) plug-in to validate if links to
4−headings and files in markdown point to existing things.
3+[![Build][build-badge]][build]
4+[![Coverage][coverage-badge]][coverage]
5+[![Downloads][downloads-badge]][downloads]
6+[![Size][size-badge]][size]
7+[![Sponsors][sponsors-badge]][collective]
8+[![Backers][backers-badge]][collective]
9+[![Chat][chat-badge]][chat]
510
6−For example, this document does not have a heading named `Hello`. So if I
7−link to that (`[welcome](#hello)`), this plug-in will warn about it.
11+[**remark**][remark] plugin to validate that Markdown links and images reference
12+existing local files and headings.
813
9−In addition, when I link to a heading in another document
10−(`examples/foo.md#hello`), if this file exists but the heading does not,
11−or if the file does not exist, this plug-in will also warn.
14+For example, this document does not have a heading named `Hello`.
15+So if we’d link to it (`[welcome](#hello)`), we’d get a warning.
1216
13−Linking to other files, such as `LICENSE` or `index.js` (when they exist)
14−is fine.
17+In addition, when there’s a link to a heading in another document
18+(`examples/foo.md#hello`), if that file exists but the heading does not, or if
19+that file does not exist, we’d also get a warning.
1520
16−## Table of Contents
21+Linking to other files, such as `license` or `index.js` (when they exist) is
22+fine.
1723
18−* [Installation](#installation)
19−* [Command line](#command-line)
20−* [Programmatic](#programmatic)
24+This plugin does not check external URLs (see
25+[`remark-lint-no-dead-urls`][no-dead-urls]) or undefined references
26+(see [`remark-lint-no-undefined-references`][no-undef-refs]).
27+
28+## Contents
29+
30+* [Install](#install)
31+* [Use](#use)
32+ * [CLI](#cli)
33+ * [API](#api)
2134* [Configuration](#configuration)
35+* [Integration](#integration)
36+* [Security](#security)
2237* [Related](#related)
38+* [Contribute](#contribute)
2339* [License](#license)
2440
25−## Installation
41+## Install
2642
27−[npm](https://docs.npmjs.com/cli/install):
43+[npm][]:
2844
29−```bash
30−npm install mdast-validate-links
45+```sh
46+npm install remark-validate-links
3147```
3248
33−## Command line
49+## Use
3450
35−![Example of how mdast-validate-links looks on screen](https://cdn.rawgit.com/wooorm/mdast-validate-links/master/screenshot.png)
51+### CLI
3652
37−Use **mdast-validate-links** together with [**mdast**](https://github.com/wooorm/mdast)
38−and [**mdast-slug**](https://github.com/wooorm/mdast-slug):
53+Use `remark-validate-links` together with [**remark**][remark]:
3954
4055```bash
41−npm install --global mdast mdast-slug mdast-validate-links
56+npm install --global remark-cli remark-validate-links
4257```
4358
4459Let’s say `readme.md` is this document, and `example.md` looks as follows:
4560
46−```md
61+```markdown
4762# Hello
4863
4964Read more [whoops, this does not exist](#world).
5065
5166This doesn’t exist either [whoops!](readme.md#foo).
5267
53−But this does exist: [LICENSE](LICENSE).
68+But this does exist: [license](license).
5469
5570So does this: [README](readme.md#installation).
5671```
5772
58−Then, to run **mdast-validate-links** on `example.md` and `readme.md`:
73+Now, running `remark -u validate-links .` yields:
5974
60−```bash
61−mdast -u mdast-slug -u mdast-validate-links example.md
62−#
63−# Yields:
64−#
65−# example.md
66−# 3:11 warning Link to unknown heading: `world`
67−# 5:27 warning Link to unknown heading in `readme.md`: `foo`
68−#
69−# ✖ 2 problems (0 errors, 2 warnings)
75+```text
76+example.md
77+ 3:11-3:48 warning Link to unknown heading: `world` missing-heading remark-validate-links
78+ 5:27-5:51 warning Link to unknown heading in `readme.md`: `foo` missing-heading-in-file remark-validate-links
79+
80+readme.md: no issues found
81+
82+⚠ 2 warnings
7083```
7184
72−## Programmatic
85+> Note: passing a file over stdin(4) may not work as expected, because it is not
86+> known where the file originates from.
7387
74−This plug-in is **not** available on the API of mdast.
88+### API
7589
76−## Configuration
90+> Note: The API checks links to headings and files.
91+> It does not check headings in other files.
92+> In a browser, only local links to headings are checked.
7793
78−You can pass a `repository`, containing anything `package.json`s
79−[`repository`](https://docs.npmjs.com/files/package.json#repository) can
80−handle. If this is omitted, **mdast-validate-links** will try
81−the `package.json` in your current working directory.
94+Say we have the following file, `example.md`:
8295
83−```bash
84−mdast --use 'validate-links=repository:"foo/bar"' example.md
96+```markdown
97+# Alpha
98+
99+Links are checked:
100+
101+This [exists](#alpha).
102+This [one does not](#does-not).
103+
104+# Bravo
105+
106+Headings in `readme.md` are [checked](readme.md#nosuchheading).
107+And [missing files are reported](missing-example.js).
108+
109+Definitions are also checked:
110+
111+[alpha]: #alpha
112+[charlie]: #charlie
113+
114+References w/o definitions are not checked: [delta]
85115```
86116
87−When a repository is given or detected, links to GitHub are normalized
88−to the file-system. E.g., `https://github.com/foo/bar/blob/master/example.md`
89−becomes `example.md`.
117+And our script, `example.js`, looks as follows:
90118
91−You can define this repository in `.mdastrc` or `package.json` [files](https://github.com/wooorm/mdast/blob/master/doc/mdastrc.5.md)
92−too. An example `.mdastrc` file could look as follows:
119+```js
120+var vfile = require('to-vfile')
121+var report = require('vfile-reporter')
122+var remark = require('remark')
123+var links = require('remark-validate-links')
124+
125+remark()
126+ .use(links)
127+ .process(vfile.readSync('example.md'), function (err, file) {
128+ console.error(report(err || file))
129+ })
130+```
131+
132+Now, running `node example` yields:
133+
134+```markdown
135+example.md
136+ 6:6-6:31 warning Link to unknown heading: `does-not` missing-heading remark-validate-links
137+ 11:5-11:53 warning Link to unknown file: `missing-example.js` missing-file remark-validate-links
138+ 16:1-16:20 warning Link to unknown heading: `charlie` missing-heading remark-validate-links
139+
140+⚠ 3 warnings
141+```
142+
143+(Note that `readme.md#nosuchheading` is not warned about, because the API
144+does not check headings in other Markdown files).
145+
146+## Configuration
147+
148+Typically, you don’t need to configure `remark-validate-links`, as it detects
149+local Git repositories.
150+If one is detected that references a known Git host (GitHub, GitLab,
151+or Bitbucket), some extra links can be checked.
152+If one is detected that does not reference a known Git host, local links still
153+work as expected.
154+If you’re not in a Git repository, you must pass `repository: false` explicitly.
155+
156+You can pass a `repository` (`string?`, `false`).
157+If `repository` is nullish, the Git origin remote is detected.
158+If the repository resolves to something [npm understands][package-repository]
159+as a Git host such as GitHub, GitLab, or Bitbucket, full URLs to that host
160+(say, `https://github.com/remarkjs/remark-validate-links/readme.md#install`)
161+can also be checked.
162+
163+```sh
164+remark --use 'validate-links=repository:"foo/bar"' example.md
165+```
166+
167+For this to work, a `root` (`string?`) is also used, referencing the local Git
168+root directory (the place where `.git` is).
169+If both `root` and `repository` are nullish, the Git root is detected.
170+If `root` is not given but `repository` is, [`file.cwd`][cwd] is used.
171+
172+You can define this repository in [configuration files][cli] too.
173+An example `.remarkrc` file could look as follows:
93174
94175```json
95176{
96− "plugins": {
97− "slug": {
98− "library": "github"
99− },
100− "validate-links": {
101− "repository": "foo/bar"
102− }
103− }
177+ "plugins": [
178+ [
179+ "validate-links",
180+ {
181+ "repository": "foo/bar"
182+ }
183+ ]
184+ ]
185+}
186+```
187+
188+If you’re self-hosting a Git server, you can provide URL information directly,
189+as `urlConfig` (`Object`).
190+
191+For this repository, `urlConfig` looks as follows:
192+
193+```js
194+{
195+ // Domain of URLs:
196+ hostname: 'github.com',
197+ // Path prefix before files:
198+ prefix: '/remarkjs/remark-validate-links/blob/',
199+ // Prefix of headings:
200+ headingPrefix: '#',
201+ // Whether lines in files can be linked:
202+ lines: true
203+}
204+```
205+
206+If this project were hosted on Bitbucket, it would be:
207+
208+```js
209+{
210+ hostname: 'bitbucket.org',
211+ prefix: '/remarkjs/remark-validate-links/src/',
212+ headingPrefix: '#markdown-header-',
213+ lines: false
104214}
105215```
106216
217+## Integration
218+
219+`remark-validate-links` can detect anchors on nodes through several properties
220+on nodes:
221+
222+* `node.data.hProperties.name` — Used by [`remark-html`][remark-html]
223+ to create a `name` attribute, which anchors can link to
224+* `node.data.hProperties.id` — Used by [`remark-html`][remark-html]
225+ to create an `id` attribute, which anchors can link to
226+* `node.data.id` — Used, in the future, by other tools to signal
227+ unique identifiers on nodes
228+
229+## Security
230+
231+`remark-validate-links`, in Node, accesses the file system based on user
232+content, and this may be dangerous.
233+In Node `git remote` and `git rev-parse` also runs for processed files.
234+
235+The tree is not modified, so there are no openings for
236+[cross-site scripting (XSS)][xss] attacks.
237+
107238## Related
108239
109−* [`mdast-lint`](https://github.com/wooorm/mdast-lint)
110− — Markdown code style linter.
240+* [`remark-lint`][remark-lint] — Markdown code style linter
241+* [`remark-lint-no-dead-urls`][no-dead-urls] — Ensure external links are alive
242+
243+## Contribute
244+
245+See [`contributing.md`][contributing] in [`remarkjs/.github`][health] for ways
246+to get started.
247+See [`support.md`][support] for ways to get help.
248+
249+This project has a [code of conduct][coc].
250+By interacting with this repository, organization, or community you agree to
251+abide by its terms.
111252
112253## License
113254
114−[MIT](LICENSE) © [Titus Wormer](http://wooorm.com)
255+[MIT][license] © [Titus Wormer][author]
256+
257+<!-- Definitions -->
258+
259+[build-badge]: https://img.shields.io/travis/remarkjs/remark-validate-links/main.svg
260+
261+[build]: https://travis-ci.org/remarkjs/remark-validate-links
262+
263+[coverage-badge]: https://img.shields.io/codecov/c/github/remarkjs/remark-validate-links.svg
264+
265+[coverage]: https://codecov.io/github/remarkjs/remark-validate-links
266+
267+[downloads-badge]: https://img.shields.io/npm/dm/remark-validate-links.svg
268+
269+[downloads]: https://www.npmjs.com/package/remark-validate-links
270+
271+[size-badge]: https://img.shields.io/bundlephobia/minzip/remark-validate-links.svg
272+
273+[size]: https://bundlephobia.com/result?p=remark-validate-links
274+
275+[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg
276+
277+[backers-badge]: https://opencollective.com/unified/backers/badge.svg
278+
279+[collective]: https://opencollective.com/unified
280+
281+[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg
282+
283+[chat]: https://github.com/remarkjs/remark/discussions
284+
285+[npm]: https://docs.npmjs.com/cli/install
286+
287+[health]: https://github.com/remarkjs/.github
288+
289+[contributing]: https://github.com/remarkjs/.github/blob/HEAD/contributing.md
290+
291+[support]: https://github.com/remarkjs/.github/blob/HEAD/support.md
292+
293+[coc]: https://github.com/remarkjs/.github/blob/HEAD/code-of-conduct.md
294+
295+[license]: license
296+
297+[author]: https://wooorm.com
298+
299+[remark]: https://github.com/remarkjs/remark
300+
301+[cli]: https://github.com/remarkjs/remark/tree/HEAD/packages/remark-cli#readme
302+
303+[remark-lint]: https://github.com/remarkjs/remark-lint
304+
305+[remark-html]: https://github.com/remarkjs/remark-html
306+
307+[no-dead-urls]: https://github.com/davidtheclark/remark-lint-no-dead-urls
308+
309+[no-undef-refs]: https://github.com/remarkjs/remark-lint/tree/master/packages/remark-lint-no-undefined-references
310+
311+[package-repository]: https://docs.npmjs.com/files/package.json#repository
312+
313+[cwd]: https://github.com/vfile/vfile#vfilecwd
314+
315+[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
screenshot.pngdeleted

Binary file not shown.

test/fixtures/bitbucket.mdnew file+67
@@ -0,0 +1,67 @@
1+# Hello
2+
3+This is a valid relative heading [link](#markdown-header-hello).
4+
5+This is an invalid relative heading [link](#markdown-header-world).
6+
7+This is an ignored hash [link](#world).
8+
9+## Files
10+
11+This is a valid relative file [link](https://bitbucket.org/wooorm/test/src/main/examples/bitbucket.md).
12+
13+So is this [link](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md).
14+
15+And this [link](./examples/bitbucket.md).
16+
17+And this [link](examples/bitbucket.md).
18+
19+This is a valid external [file](../index.js).
20+
21+This is an invalid relative file [link](https://bitbucket.org/wooorm/test/src/main/examples/world.md).
22+
23+So is this [link](https://bitbucket.org/wooorm/test/src/foo-bar/examples/world.md).
24+
25+And this [link](./examples/world.md).
26+
27+And this [link](examples/world.md).
28+
29+## Combination
30+
31+Valid: [a](./examples/bitbucket.md#markdown-header-hello).
32+
33+Valid: [b](examples/bitbucket.md#markdown-header-hello).
34+
35+Valid: [c](https://bitbucket.org/wooorm/test/src/main/examples/bitbucket.md#markdown-header-hello).
36+
37+Valid: [d](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md#markdown-header-hello).
38+
39+Invalid: [e](./examples/bitbucket.md#markdown-header-world).
40+
41+Invalid: [f](examples/bitbucket.md#markdown-header-world).
42+
43+Invalid: [g](https://bitbucket.org/wooorm/test/src/main/examples/bitbucket.md#markdown-header-world).
44+
45+Invalid: [h](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md#markdown-header-world).
46+
47+Invalid: [i](./examples/world.md#markdown-header-hello).
48+
49+Invalid: [j](examples/world.md#markdown-header-hello).
50+
51+Invalid: [k](https://bitbucket.org/wooorm/test/src/main/examples/world.md#markdown-header-hello).
52+
53+Invalid: [l](https://bitbucket.org/wooorm/test/src/foo-bar/examples/world.md#markdown-header-hello).
54+
55+This is an ignored hash [j](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md#world).
56+
57+## External
58+
59+These are all invalid, because they do not link to Bitbucket.
60+
61+Valid: [a](irc://foo).
62+
63+Valid: [b](http://example.com).
64+
65+Valid: [b](http://example.com/foo/bar/baz).
66+
67+Valid: [b](http://github.com/wooorm/test/blob/foo-bar/examples/world.md#markdown-header-hello).
test/fixtures/case-insensitive-headings.mdnew file+9
@@ -0,0 +1,9 @@
1+# Hello
2+
3+This is fine: [link](#HELLO).
4+
5+This isn’t: [link](#WORLD).
6+
7+Also fine: [link](examples/github.md#HELLO).
8+
9+Also not fine: [link](examples/github.md#WORLD).
test/fixtures/empty.mdnew file+1
@@ -0,0 +1 @@
1+No links!
test/fixtures/examples/bitbucket.mdnew file+39
@@ -0,0 +1,39 @@
1+# Hello
2+
3+This is a valid relative heading [link](#markdown-header-hello).
4+
5+This is an invalid relative heading [link](#markdown-header-world).
6+
7+## Files
8+
9+This is a valid relative file [link](https://bitbucket.org/wooorm/test/src/main/bitbucket.md).
10+
11+So is this [link](https://bitbucket.org/wooorm/test/src/foo-bar/bitbucket.md).
12+
13+And this [link](../bitbucket.md).
14+
15+This is an invalid relative file [link](https://bitbucket.org/wooorm/test/src/main/world.md).
16+
17+So is this [link](https://bitbucket.org/wooorm/test/src/foo-bar/world.md).
18+
19+And this [link](../world.md).
20+
21+## Combination
22+
23+Valid: [a](../bitbucket.md#markdown-header-hello).
24+
25+Valid: [b](https://bitbucket.org/wooorm/test/src/main/bitbucket.md#markdown-header-hello).
26+
27+Valid: [c](https://bitbucket.org/wooorm/test/src/foo-bar/bitbucket.md#markdown-header-hello).
28+
29+Invalid: [d](../bitbucket.md#markdown-header-world).
30+
31+Invalid: [e](https://bitbucket.org/wooorm/test/src/main/bitbucket.md#markdown-header-world).
32+
33+Invalid: [f](https://bitbucket.org/wooorm/test/src/foo-bar/bitbucket.md#markdown-header-world).
34+
35+Invalid: [g](../world.md#markdown-header-hello).
36+
37+Invalid: [h](https://bitbucket.org/wooorm/test/src/main/world.md#markdown-header-hello).
38+
39+Invalid: [i](https://bitbucket.org/wooorm/test/src/foo-bar/world.md#markdown-header-hello).
test/fixtures/examples/example.jsnew file+1
@@ -0,0 +1 @@
1+console.log('Hello world!')
test/fixtures/examples/example.mddeleted−39
@@ -1,39 +0,0 @@
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/fixtures/examples/github.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/main/github.md).
10+
11+So is this [link](https://github.com/wooorm/test/blob/foo-bar/github.md).
12+
13+And this [link](../github.md).
14+
15+This is an invalid relative file [link](https://github.com/wooorm/test/blob/main/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](../github.md#hello).
24+
25+Valid: [b](https://github.com/wooorm/test/blob/main/github.md#hello).
26+
27+Valid: [c](https://github.com/wooorm/test/blob/foo-bar/github.md#hello).
28+
29+Invalid: [d](../github.md#world).
30+
31+Invalid: [e](https://github.com/wooorm/test/blob/main/github.md#world).
32+
33+Invalid: [f](https://github.com/wooorm/test/blob/foo-bar/github.md#world).
34+
35+Invalid: [g](../world.md#hello).
36+
37+Invalid: [h](https://github.com/wooorm/test/blob/main/world.md#hello).
38+
39+Invalid: [i](https://github.com/wooorm/test/blob/foo-bar/world.md#hello).
test/fixtures/examples/gitlab.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://gitlab.com/wooorm/test/blob/main/gitlab.md).
10+
11+So is this [link](https://gitlab.com/wooorm/test/blob/foo-bar/gitlab.md).
12+
13+And this [link](../gitlab.md).
14+
15+This is an invalid relative file [link](https://gitlab.com/wooorm/test/blob/main/world.md).
16+
17+So is this [link](https://gitlab.com/wooorm/test/blob/foo-bar/world.md).
18+
19+And this [link](../world.md).
20+
21+## Combination
22+
23+Valid: [a](../gitlab.md#hello).
24+
25+Valid: [b](https://gitlab.com/wooorm/test/blob/main/gitlab.md#hello).
26+
27+Valid: [c](https://gitlab.com/wooorm/test/blob/foo-bar/gitlab.md#hello).
28+
29+Invalid: [d](../gitlab.md#world).
30+
31+Invalid: [e](https://gitlab.com/wooorm/test/blob/main/gitlab.md#world).
32+
33+Invalid: [f](https://gitlab.com/wooorm/test/blob/foo-bar/gitlab.md#world).
34+
35+Invalid: [g](../world.md#hello).
36+
37+Invalid: [h](https://gitlab.com/wooorm/test/blob/main/world.md#hello).
38+
39+Invalid: [i](https://gitlab.com/wooorm/test/blob/foo-bar/world.md#hello).
test/fixtures/examples/image with space.jpgnew file

Binary file not shown.

test/fixtures/examples/image.jpgnew file

Binary file not shown.

test/fixtures/examples/weird#character/readme.mdnew file+1
@@ -0,0 +1 @@
1+# Hello
test/fixtures/examples/weird#characters.mdnew file+1
@@ -0,0 +1 @@
1+# Hello
test/fixtures/external.mdnew file+11
@@ -0,0 +1,11 @@
1+# Links to external websites
2+
3+Link to [another website](https://mochajs.org)
4+
5+Link to [another website with an anchor](https://mochajs.org/#exclusive-tests)
6+
7+Link to [GitHub](https://experiments.github.com)
8+
9+Link to [`github.com`](https://github.com/pulls)
10+
11+Link to [a repo on GitHub](https://github.com/foo/bar)
test/fixtures/file-as-folder.mdnew file+1
@@ -0,0 +1 @@
1+[](file-as-folder.md/other)
test/fixtures/folder-without-readme/example.mdnew file+1
@@ -0,0 +1 @@
1+This file isn’t used when linking to the folder: it has to be a `readme`.
test/fixtures/folder.mdnew file+23
@@ -0,0 +1,23 @@
1+Here we link to a folder:
2+
3+Explicit:
4+
5+[OK](./folder/readme.markdown#this)
6+
7+[OK](./folder/readme.markdown#that)
8+
9+[NOK](./folder/readme.markdown#missing)
10+
11+Implicit:
12+
13+[OK](./folder#this)
14+
15+[OK](./folder#that)
16+
17+[NOK](./folder#missing)
18+
19+Other:
20+
21+[NOK](./missing#missing)
22+
23+[NOK](./folder-without-readme#missing)
test/fixtures/folder/example.mdnew file+1
@@ -0,0 +1 @@
1+This file isn’t used when linking to the folder: it has to be a `readme`.
test/fixtures/folder/readme.ascnew file+9
@@ -0,0 +1,9 @@
1+readme
2+------
3+
4+While GitHub would pick up on this readme for the folder,
5+`remark-validate-links` is for Markdown, so we ignore AsciiDoc.
6+
7+== This
8+
9+== That
test/fixtures/folder/readme.markdownnew file+5
@@ -0,0 +1,5 @@
1+# readme
2+
3+## This
4+
5+## That
test/fixtures/folder/readme.mdnew file+1
@@ -0,0 +1 @@
1+This one isn’t used (`readme.markdown` sorts before `readme.md`).
test/fixtures/example.md → test/fixtures/github.mdrenamed+22−14
@@ -6,17 +6,21 @@ This is an invalid relative heading [link](#world).
66
77## Files
88
9−This is a valid relative file [link](https://github.com/wooorm/test/blob/master/examples/example.md).
9+This is a valid relative file [link](https://github.com/wooorm/test/blob/main/examples/github.md).
1010
11−So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/example.md).
11+This is a valid absolute file [link](/wooorm/test/blob/main/examples/github.md).
1212
13−And this [link](./examples/example.md).
13+So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/github.md).
1414
15−And this [link](examples/example.md).
15+And this [link](./examples/github.md).
16+
17+And this [link](examples/github.md).
1618
1719This is a valid external [file](../index.js).
1820
19−This is an invalid relative file [link](https://github.com/wooorm/test/blob/master/examples/world.md).
21+This is an invalid relative file [link](https://github.com/wooorm/test/blob/main/examples/world.md).
22+
23+This is an invalid absolute file [link](/wooorm/test/blob/main/examples/world.md).
2024
2125So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/world.md).
2226
@@ -26,27 +30,27 @@ And this [link](examples/world.md).
2630
2731## Combination
2832
29−Valid: [a](./examples/example.md#hello).
33+Valid: [a](./examples/github.md#hello).
3034
31−Valid: [b](examples/example.md#hello).
35+Valid: [b](examples/github.md#hello).
3236
33−Valid: [c](https://github.com/wooorm/test/blob/master/examples/example.md#hello).
37+Valid: [c](https://github.com/wooorm/test/blob/main/examples/github.md#hello).
3438
35−Valid: [d](https://github.com/wooorm/test/blob/foo-bar/examples/example.md#hello).
39+Valid: [d](https://github.com/wooorm/test/blob/foo-bar/examples/github.md#hello).
3640
37−Invalid: [e](./examples/example.md#world).
41+Invalid: [e](./examples/github.md#world).
3842
39−Invalid: [f](examples/example.md#world).
43+Invalid: [f](examples/github.md#world).
4044
41−Invalid: [g](https://github.com/wooorm/test/blob/master/examples/example.md#world).
45+Invalid: [g](https://github.com/wooorm/test/blob/main/examples/github.md#world).
4246
43−Invalid: [h](https://github.com/wooorm/test/blob/foo-bar/examples/example.md#world).
47+Invalid: [h](https://github.com/wooorm/test/blob/foo-bar/examples/github.md#world).
4448
4549Invalid: [i](./examples/world.md#hello).
4650
4751Invalid: [j](examples/world.md#hello).
4852
49−Invalid: [k](https://github.com/wooorm/test/blob/master/examples/world.md#hello).
53+Invalid: [k](https://github.com/wooorm/test/blob/main/examples/world.md#hello).
5054
5155Invalid: [l](https://github.com/wooorm/test/blob/foo-bar/examples/world.md#hello).
5256
@@ -61,3 +65,7 @@ Valid: [b](http://example.com).
6165Valid: [b](http://example.com/foo/bar/baz).
6266
6367Valid: [b](http://bitbucket.com/wooorm/test/blob/foo-bar/examples/world.md#hello).
68+
69+## Top Anchor
70+
71+This links to the start of the document [link](#readme).
test/fixtures/gitlab.mdnew file+67
@@ -0,0 +1,67 @@
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://gitlab.com/wooorm/test/blob/main/examples/gitlab.md).
10+
11+So is this [link](https://gitlab.com/wooorm/test/blob/foo-bar/examples/gitlab.md).
12+
13+And this [link](./examples/gitlab.md).
14+
15+And this [link](examples/gitlab.md).
16+
17+This is a valid external [file](../index.js).
18+
19+This is an invalid relative file [link](https://gitlab.com/wooorm/test/blob/main/examples/world.md).
20+
21+So is this [link](https://gitlab.com/wooorm/test/blob/foo-bar/examples/world.md).
22+
23+And this [link](./examples/world.md).
24+
25+And this [link](examples/world.md).
26+
27+## Combination
28+
29+Valid: [a](./examples/gitlab.md#hello).
30+
31+Valid: [b](examples/gitlab.md#hello).
32+
33+Valid: [c](https://gitlab.com/wooorm/test/blob/main/examples/gitlab.md#hello).
34+
35+Valid: [d](https://gitlab.com/wooorm/test/blob/foo-bar/examples/gitlab.md#hello).
36+
37+Invalid: [e](./examples/gitlab.md#world).
38+
39+Invalid: [f](examples/gitlab.md#world).
40+
41+Invalid: [g](https://gitlab.com/wooorm/test/blob/main/examples/gitlab.md#world).
42+
43+Invalid: [h](https://gitlab.com/wooorm/test/blob/foo-bar/examples/gitlab.md#world).
44+
45+Invalid: [i](./examples/world.md#hello).
46+
47+Invalid: [j](examples/world.md#hello).
48+
49+Invalid: [k](https://gitlab.com/wooorm/test/blob/main/examples/world.md#hello).
50+
51+Invalid: [l](https://gitlab.com/wooorm/test/blob/foo-bar/examples/world.md#hello).
52+
53+## External
54+
55+These are all invalid, because they do not link to GitLab.
56+
57+Valid: [a](irc://foo).
58+
59+Valid: [b](http://example.com).
60+
61+Valid: [b](http://example.com/foo/bar/baz).
62+
63+Valid: [b](http://bitbucket.com/wooorm/test/blob/foo-bar/examples/world.md#hello).
64+
65+## Top Anchor
66+
67+This links to the start of the document [link](#readme).
test/fixtures/images.mdnew file+39
@@ -0,0 +1,39 @@
1+# Image references
2+
3+<!-- Valid: -->
4+
5+Relative ![image](./examples/image.jpg)
6+
7+No `./` in ![image](examples/image.jpg)
8+
9+Absolute ![image](https://github.com/wooorm/test/blob/main/examples/image.jpg)
10+
11+Relative ![image reference][rel]
12+
13+Absolute ![image reference][abs]
14+
15+Relative with whitespace ![image reference][rel-whitespace]
16+
17+<!-- Invalid: -->
18+
19+Relative ![missing image](./examples/missing.jpg)
20+
21+No `./` in ![image](examples/missing.jpg)
22+
23+Absolute ![missing image](https://github.com/wooorm/test/blob/main/examples/missing.jpg)
24+
25+Relative ![missing image reference][rel-missing]
26+
27+Absolute ![missing image reference][abs-missing]
28+
29+<!-- Definitions: -->
30+
31+[rel]: ./examples/image.jpg
32+
33+[abs]: https://github.com/wooorm/test/blob/main/examples/image.jpg
34+
35+[rel-missing]: ./examples/missing.jpg
36+
37+[abs-missing]: https://github.com/wooorm/test/blob/main/examples/missing.jpg
38+
39+[rel-whitespace]: "./examples/image with space.jpg"
test/fixtures/line-links.mdnew file+9
@@ -0,0 +1,9 @@
1+# Source Code Link
2+
3+Link to [a line](./examples/example.js#L1)
4+
5+Link to [a line in a missing file](./examples/missing.js#L1)
6+
7+[a](https://github.com/wooorm/test/blob/main/examples/example.js#L1).
8+
9+[b](https://github.com/wooorm/test/blob/main/examples/missing.js#L1).
test/fixtures/query-params.mdnew file+9
@@ -0,0 +1,9 @@
1+# Query params
2+
3+Link to relative heading ![link](?foo=bar#query-params)
4+
5+Link to an ![image](./examples/image.jpg?foo=bar)
6+
7+And a file [link](./examples/github.md?foo=bar).
8+
9+Question mark in hash (invalid) [link](#query-params?).
test/fixtures/self-hosted.mdnew file+5
@@ -0,0 +1,5 @@
1+# Hello
2+
3+[Fine](https://gitlab.acme.com/acme/project/blob/main/self-hosted.md#hello);
4+
5+[Not fine](https://gitlab.acme.com/acme/project/blob/main/self-hosted.md#world);
test/fixtures/small.mdnew file+5
@@ -0,0 +1,5 @@
1+# Hello
2+
3+This is fine: [link](#hello).
4+
5+This isn’t: [link](#world).
test/fixtures/suggestions.md+5−1
@@ -4,4 +4,8 @@ This should suggest: [link](#helloo).
44
55## Files
66
7−So should this: [k](example.md#fiiiles).
7+So should this: [k](github.md#fiiiles).
8+
9+##
10+
11+An empty header.
test/fixtures/weird-characters.mdnew file+17
@@ -0,0 +1,17 @@
1+# File names and paths with weird characters
2+
3+OK: Link to [a weird character in a file name](./examples/weird%23characters.md)
4+
5+OK: Link to [a weird character in a file path](./examples/weird%23character/readme.md)
6+
7+OK: Link to [a heading with a weird character in a file name](./examples/weird%23characters.md#hello)
8+
9+OK: Link to [a heading with a weird character in a file path](./examples/weird%23character/readme.md#hello)
10+
11+Not OK: Link to [a weird character in a file name](./examples/missing%23characters.md)
12+
13+Not OK: Link to [a weird character in a file path](./examples/missing%23character/readme.md)
14+
15+Not OK: Link to [a missing heading with a weird character in a file name](./examples/weird%23characters.md#world)
16+
17+Not OK: Link to [a missing heading with a weird character in a file path](./examples/weird%23character/readme.md#world)
test/index.js+1302−457
@@ -1,457 +1,1302 @@
1−/**
2− * @author Titus Wormer
3− * @copyright 2015 Titus Wormer
4− * @license MIT
5− * @module mdast:validate-links:test
6− * @fileoverview Test suite for `mdast-validate-links`.
7− */
8−
9−'use strict';
10−
11−/* eslint-env mocha */
12−
13−/*
14− * Dependencies.
15− */
16−
17−var assert = require('assert');
18−var hook = require('hook-std');
19−var fs = require('fs');
20−var mdast = require('mdast');
21−var cli = require('mdast/lib/cli');
22−var links = require('..');
23−
24−/*
25− * Methods.
26− */
27−
28−var equal = assert.strictEqual;
29−
30−/**
31− * Test the cli.
32− *
33− * @param {Object} config - Configuration.
34− * @param {Function} callback - Completion handler.
35− */
36−function test(config, callback) {
37− var stdout = [];
38− var stderr = [];
39− var opts = {};
40− var unhookStdout;
41− var unhookStderr;
42−
43− opts.silent = true;
44−
45− unhookStdout = hook.stdout(opts, [].push.bind(stdout));
46− unhookStderr = hook.stderr(opts, [].push.bind(stderr));
47−
48− cli(config, function (err) {
49− unhookStdout();
50− unhookStderr();
51−
52− if (err) {
53− return callback(err);
54− }
55−
56− return callback(null, {
57− 'stdout': stdout.join(''),
58− 'stderr': stderr.join('')
59− });
60− });
61−}
62−
63−/*
64− * Tests.
65− */
66−
67−describe('mdast-validate-links', function () {
68− beforeEach(function () {
69− process.chdir('./test/fixtures');
70− });
71−
72− afterEach(function () {
73− process.chdir('../..');
74− });
75−
76− it('should throw an error when not on the CLI', function () {
77− assert.throws(function () {
78− mdast().use(links);
79− }, /Error: mdast-validate-links only works on the CLI/);
80− });
81−
82− it('should warn when used without slugs', function (done) {
83− test({
84− 'files': 'example.md',
85− 'color': false,
86− 'detectRC': false,
87− 'detectIgnore': false,
88− 'plugins': [
89− '../../../index.js'
90− ]
91− }, function (err, res) {
92− equal(res.stderr.split('\n').slice(0, 2).join('\n'), [
93− 'example.md',
94− ' 1:1 error Error: Missing slugs. Use for ' +
95− 'example `mdast-slug` to generate heading IDs'
96− ].join('\n'));
97−
98− done(err);
99− });
100− });
101−
102− it('should ignore unfound files', function (done) {
103− test({
104− 'files': ['definitions.md', 'FOOOO'],
105− 'color': false,
106− 'detectRC': false,
107− 'detectIgnore': false,
108− 'plugins': [
109− '../../../node_modules/mdast-slug',
110− '../../../index.js'
111− ]
112− }, function (err, res) {
113− equal(res.stdout, '');
114−
115− equal(res.stderr, [
116− 'FOOOO',
117− ' 1:1 error No such file or directory',
118− '',
119− 'definitions.md',
120− ' 5:12-5:21 warning Link to unknown heading: `world`',
121− '',
122− '2 messages (✖ 1 error, ⚠ 1 warning)',
123− ''
124− ].join('\n'));
125−
126− done(err);
127− });
128− });
129−
130− it('should work when not all files are given', function (done) {
131− test({
132− 'files': 'example.md',
133− 'color': false,
134− 'detectRC': false,
135− 'detectIgnore': false,
136− 'plugins': [
137− '../../../node_modules/mdast-slug',
138− '../../../index.js'
139− ]
140− }, function (err, res) {
141− equal(res.stderr, [
142− 'example.md',
143− ' 5:37-5:51 warning Link to unknown heading: `world`',
144− ' 23:10-23:37 warning Link to unknown file: ' +
145− '`examples/world.md`',
146− ' 25:10-25:35 warning Link to unknown file: ' +
147− '`examples/world.md`',
148− ' 37:10-37:42 warning Link to unknown heading in ' +
149− '`examples/example.md`: `world`',
150− ' 39:10-39:40 warning Link to unknown heading in ' +
151− '`examples/example.md`: `world`',
152− ' 45:10-45:40 warning Link to unknown file: ' +
153− '`examples/world.md`',
154− ' 45:10-45:40 warning Link to unknown heading in ' +
155− '`examples/world.md`: `hello`',
156− ' 47:10-47:38 warning Link to unknown file: ' +
157− '`examples/world.md`',
158− ' 47:10-47:38 warning Link to unknown heading in ' +
159− '`examples/world.md`: `hello`',
160− '',
161− '⚠ 9 warnings',
162− ''
163− ].join('\n'));
164−
165− done(err);
166− });
167− });
168−
169− it('should work when all files are given', function (done) {
170− test({
171− 'files': [
172− 'example.md',
173− 'examples/example.md'
174− ],
175− 'color': false,
176− 'detectRC': false,
177− 'detectIgnore': false,
178− 'plugins': [
179− '../../../node_modules/mdast-slug',
180− '../../../index.js'
181− ]
182− }, function (err, res) {
183− equal(res.stdout, '');
184−
185− equal(res.stderr, [
186− 'example.md',
187− ' 5:37-5:51 warning Link to unknown heading: `world`',
188− ' 23:10-23:37 warning Link to unknown file: ' +
189− '`examples/world.md`',
190− ' 25:10-25:35 warning Link to unknown file: ' +
191− '`examples/world.md`',
192− ' 37:10-37:42 warning Link to unknown heading in ' +
193− '`examples/example.md`: `world`',
194− ' 39:10-39:40 warning Link to unknown heading in ' +
195− '`examples/example.md`: `world`',
196− ' 45:10-45:40 warning Link to unknown file: ' +
197− '`examples/world.md`',
198− ' 45:10-45:40 warning Link to unknown heading in ' +
199− '`examples/world.md`: `hello`',
200− ' 47:10-47:38 warning Link to unknown file: ' +
201− '`examples/world.md`',
202− ' 47:10-47:38 warning Link to unknown heading in ' +
203− '`examples/world.md`: `hello`',
204− '',
205− 'examples/example.md',
206− ' 5:37-5:51 warning Link to unknown heading: ' +
207− '`world`',
208− ' 19:10-19:29 warning Link to unknown file: ' +
209− '`world.md`',
210− ' 29:10-29:34 warning Link to unknown heading in ' +
211− '`example.md`: `world`',
212− ' 35:10-35:32 warning Link to unknown file: ' +
213− '`world.md`',
214− ' 35:10-35:32 warning Link to unknown heading in ' +
215− '`world.md`: `hello`',
216− '',
217− '⚠ 14 warnings',
218− ''
219− ].join('\n'));
220−
221− done(err);
222− });
223− });
224−
225− it('should work with definitions', function (done) {
226− test({
227− 'files': 'definitions.md',
228− 'color': false,
229− 'detectRC': false,
230− 'detectIgnore': false,
231− 'plugins': [
232− '../../../node_modules/mdast-slug',
233− '../../../index.js'
234− ]
235− }, function (err, res) {
236− equal(res.stderr, [
237− 'definitions.md',
238− ' 5:12-5:21 warning Link to unknown heading: `world`',
239− '',
240− '⚠ 1 warning',
241− ''
242− ].join('\n'));
243−
244− done(err);
245− });
246− });
247−
248− it('should work on GitHub URLs when given a repo', function (done) {
249− test({
250− 'files': [
251− 'example.md',
252− 'examples/example.md'
253− ],
254− 'color': false,
255− 'detectRC': false,
256− 'detectIgnore': false,
257− 'plugins': {
258− '../../../node_modules/mdast-slug': null,
259− '../../../index.js': {
260− 'repository': 'wooorm/test'
261− }
262− }
263− }, function (err, res) {
264− equal(res.stdout, '');
265−
266− equal(res.stderr, [
267− 'example.md',
268− ' 5:37-5:51 warning Link to unknown heading: ' +
269− '`world`',
270− ' 19:34-19:102 warning Link to unknown file: ' +
271− '`examples/world.md`',
272− ' 21:12-21:81 warning Link to unknown file: ' +
273− '`examples/world.md`',
274− ' 23:10-23:37 warning Link to unknown file: ' +
275− '`examples/world.md`',
276− ' 25:10-25:35 warning Link to unknown file: ' +
277− '`examples/world.md`',
278− ' 37:10-37:42 warning Link to unknown heading in ' +
279− '`examples/example.md`: `world`',
280− ' 39:10-39:40 warning Link to unknown heading in ' +
281− '`examples/example.md`: `world`',
282− ' 41:10-41:83 warning Link to unknown heading in ' +
283− '`examples/example.md`: `world`',
284− ' 43:10-43:84 warning Link to unknown heading in ' +
285− '`examples/example.md`: `world`',
286− ' 45:10-45:40 warning Link to unknown file: ' +
287− '`examples/world.md`',
288− ' 45:10-45:40 warning Link to unknown heading in ' +
289− '`examples/world.md`: `hello`',
290− ' 47:10-47:38 warning Link to unknown file: ' +
291− '`examples/world.md`',
292− ' 47:10-47:38 warning Link to unknown heading in ' +
293− '`examples/world.md`: `hello`',
294− ' 49:10-49:81 warning Link to unknown file: ' +
295− '`examples/world.md`',
296− ' 49:10-49:81 warning Link to unknown heading in ' +
297− '`examples/world.md`: `hello`',
298− ' 51:10-51:82 warning Link to unknown heading in ' +
299− '`examples/world.md`: `hello`',
300− ' 51:10-51:82 warning Link to unknown file: ' +
301− '`examples/world.md`',
302− '',
303− 'examples/example.md',
304− ' 5:37-5:51 warning Link to unknown heading: ' +
305− '`world`',
306− ' 15:34-15:93 warning Link to unknown file: ' +
307− '`world.md`',
308− ' 17:12-17:72 warning Link to unknown file: ' +
309− '`world.md`',
310− ' 19:10-19:29 warning Link to unknown file: ' +
311− '`world.md`',
312− ' 29:10-29:34 warning Link to unknown heading in ' +
313− '`example.md`: `world`',
314− ' 31:10-31:74 warning Link to unknown heading in ' +
315− '`example.md`: `world`',
316− ' 33:10-33:75 warning Link to unknown heading in ' +
317− '`example.md`: `world`',
318− ' 35:10-35:32 warning Link to unknown file: ' +
319− '`world.md`',
320− ' 35:10-35:32 warning Link to unknown heading in ' +
321− '`world.md`: `hello`',
322− ' 37:10-37:72 warning Link to unknown file: ' +
323− '`world.md`',
324− ' 37:10-37:72 warning Link to unknown heading in ' +
325− '`world.md`: `hello`',
326− ' 39:10-39:73 warning Link to unknown heading in ' +
327− '`world.md`: `hello`',
328− ' 39:10-39:73 warning Link to unknown file: ' +
329− '`world.md`',
330− '',
331− '⚠ 30 warnings',
332− ''
333− ].join('\n'));
334−
335− done(err);
336− });
337− });
338−
339− it('should work on GitHub URLs when with package.json', function (done) {
340− fs.writeFileSync('./package.json', JSON.stringify({
341− 'repository': 'wooorm/test'
342− }, 0, 2));
343−
344− test({
345− 'files': [
346− 'example.md',
347− 'examples/example.md'
348− ],
349− 'color': false,
350− 'detectRC': false,
351− 'detectIgnore': false,
352− 'plugins': [
353− '../../../node_modules/mdast-slug',
354− '../../../index.js'
355− ]
356− }, function (err, res) {
357− fs.unlinkSync('./package.json');
358−
359− equal(res.stderr, [
360− 'example.md',
361− ' 5:37-5:51 warning Link to unknown heading: ' +
362− '`world`',
363− ' 19:34-19:102 warning Link to unknown file: ' +
364− '`examples/world.md`',
365− ' 21:12-21:81 warning Link to unknown file: ' +
366− '`examples/world.md`',
367− ' 23:10-23:37 warning Link to unknown file: ' +
368− '`examples/world.md`',
369− ' 25:10-25:35 warning Link to unknown file: ' +
370− '`examples/world.md`',
371− ' 37:10-37:42 warning Link to unknown heading in ' +
372− '`examples/example.md`: `world`',
373− ' 39:10-39:40 warning Link to unknown heading in ' +
374− '`examples/example.md`: `world`',
375− ' 41:10-41:83 warning Link to unknown heading in ' +
376− '`examples/example.md`: `world`',
377− ' 43:10-43:84 warning Link to unknown heading in ' +
378− '`examples/example.md`: `world`',
379− ' 45:10-45:40 warning Link to unknown file: ' +
380− '`examples/world.md`',
381− ' 45:10-45:40 warning Link to unknown heading in ' +
382− '`examples/world.md`: `hello`',
383− ' 47:10-47:38 warning Link to unknown file: ' +
384− '`examples/world.md`',
385− ' 47:10-47:38 warning Link to unknown heading in ' +
386− '`examples/world.md`: `hello`',
387− ' 49:10-49:81 warning Link to unknown file: ' +
388− '`examples/world.md`',
389− ' 49:10-49:81 warning Link to unknown heading in ' +
390− '`examples/world.md`: `hello`',
391− ' 51:10-51:82 warning Link to unknown heading in ' +
392− '`examples/world.md`: `hello`',
393− ' 51:10-51:82 warning Link to unknown file: ' +
394− '`examples/world.md`',
395− '',
396− 'examples/example.md',
397− ' 5:37-5:51 warning Link to unknown heading: ' +
398− '`world`',
399− ' 15:34-15:93 warning Link to unknown file: ' +
400− '`world.md`',
401− ' 17:12-17:72 warning Link to unknown file: ' +
402− '`world.md`',
403− ' 19:10-19:29 warning Link to unknown file: ' +
404− '`world.md`',
405− ' 29:10-29:34 warning Link to unknown heading in ' +
406− '`example.md`: `world`',
407− ' 31:10-31:74 warning Link to unknown heading in ' +
408− '`example.md`: `world`',
409− ' 33:10-33:75 warning Link to unknown heading in ' +
410− '`example.md`: `world`',
411− ' 35:10-35:32 warning Link to unknown file: ' +
412− '`world.md`',
413− ' 35:10-35:32 warning Link to unknown heading in ' +
414− '`world.md`: `hello`',
415− ' 37:10-37:72 warning Link to unknown file: ' +
416− '`world.md`',
417− ' 37:10-37:72 warning Link to unknown heading in ' +
418− '`world.md`: `hello`',
419− ' 39:10-39:73 warning Link to unknown heading in ' +
420− '`world.md`: `hello`',
421− ' 39:10-39:73 warning Link to unknown file: ' +
422− '`world.md`',
423− '',
424− '⚠ 30 warnings',
425− ''
426− ].join('\n'));
427−
428− done(err);
429− });
430− });
431−
432− it('should suggest similar links', function (done) {
433− test({
434− 'files': ['suggestions.md'],
435− 'color': false,
436− 'detectRC': false,
437− 'detectIgnore': false,
438− 'plugins': [
439− '../../../node_modules/mdast-slug',
440− '../../../index.js'
441− ]
442− }, function (err, res) {
443− equal(res.stderr, [
444− 'suggestions.md',
445− ' 3:22-3:37 warning Link to unknown heading: `helloo`. ' +
446− 'Did you mean `hello`',
447− ' 7:17-7:40 warning Link to unknown heading in ' +
448− '`example.md`: `fiiiles`. Did you mean `files`',
449− '',
450− '⚠ 2 warnings',
451− ''
452− ].join('\n'));
453−
454− done(err);
455− });
456− });
457−});
1+'use strict'
2+
3+var fs = require('fs')
4+var path = require('path')
5+var childProcess = require('child_process')
6+var test = require('tape')
7+var vfile = require('to-vfile')
8+var remark = require('remark')
9+var strip = require('strip-ansi')
10+var rimraf = require('rimraf')
11+var sort = require('./sort')
12+var links = require('..')
13+
14+process.chdir(path.resolve(process.cwd(), 'test', 'fixtures'))
15+
16+test.onFinish(function () {
17+ process.chdir(path.resolve(process.cwd(), '..', '..'))
18+})
19+
20+var bin = path.join('..', '..', 'node_modules', '.bin', 'remark')
21+
22+test('remark-validate-links', function (t) {
23+ t.test('should work on the API', function (st) {
24+ st.plan(1)
25+
26+ remark()
27+ .use(links)
28+ .use(sort)
29+ .process(vfile.readSync('github.md'), function (err, file) {
30+ st.deepEqual(
31+ [err].concat(file.messages.map(String)),
32+ [
33+ null,
34+ 'github.md:5:37-5:51: Link to unknown heading: `world`',
35+ 'github.md:27:10-27:37: Link to unknown file: `examples/world.md`',
36+ 'github.md:29:10-29:35: Link to unknown file: `examples/world.md`',
37+ 'github.md:49:10-49:40: Link to unknown file: `examples/world.md`',
38+ 'github.md:51:10-51:38: Link to unknown file: `examples/world.md`'
39+ ],
40+ 'should report messages'
41+ )
42+ })
43+ })
44+
45+ t.test('should ignore invalid repositories', function (st) {
46+ st.plan(1)
47+
48+ childProcess.exec(
49+ [
50+ bin,
51+ '--no-config',
52+ '--no-ignore',
53+ '--use',
54+ '"../..=repository:\\"invalid:shortcode\\""',
55+ '--use',
56+ '../sort',
57+ 'small.md'
58+ ].join(' '),
59+ onexec
60+ )
61+
62+ function onexec(err, stdout, stderr) {
63+ st.deepEqual(
64+ [err, strip(stderr)],
65+ [
66+ null,
67+ [
68+ 'small.md',
69+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
70+ '',
71+ '⚠ 1 warning',
72+ ''
73+ ].join('\n')
74+ ],
75+ 'should work'
76+ )
77+ }
78+ })
79+
80+ t.test('should throw on Gist repositories', function (st) {
81+ st.plan(1)
82+
83+ childProcess.exec(
84+ [
85+ bin,
86+ '--no-config',
87+ '--no-ignore',
88+ '--use',
89+ '"../..=repository:\\"gist:wooorm/8504606\\""',
90+ '--use',
91+ '../sort',
92+ 'small.md'
93+ ].join(' '),
94+ onexec
95+ )
96+
97+ function onexec(err, stdout, stderr) {
98+ st.deepEqual(
99+ [err, strip(stderr)],
100+ [
101+ null,
102+ [
103+ 'small.md',
104+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
105+ '',
106+ '⚠ 1 warning',
107+ ''
108+ ].join('\n')
109+ ],
110+ 'should work'
111+ )
112+ }
113+ })
114+
115+ t.test('should ignore unfound files (#1)', function (st) {
116+ st.plan(1)
117+
118+ childProcess.exec(
119+ [
120+ bin,
121+ '--no-config',
122+ '--no-ignore',
123+ '--use',
124+ '../..',
125+ '--use',
126+ '../sort',
127+ 'FOOOO'
128+ ].join(' '),
129+ onexec
130+ )
131+
132+ function onexec(err, stdout, stderr) {
133+ st.deepEqual(
134+ [/command failed/i.test(err), strip(stderr)],
135+ [
136+ true,
137+ [
138+ 'FOOOO',
139+ ' 1:1 error No such file or directory',
140+ '',
141+ '✖ 1 error',
142+ ''
143+ ].join('\n')
144+ ],
145+ 'should work'
146+ )
147+ }
148+ })
149+
150+ t.test('should ignore unfound files (#2)', function (st) {
151+ st.plan(1)
152+
153+ childProcess.exec(
154+ [
155+ bin,
156+ '--no-config',
157+ '--no-ignore',
158+ '--use',
159+ '../..',
160+ '--use',
161+ '../sort',
162+ 'definitions.md',
163+ 'FOOOO'
164+ ].join(' '),
165+ onexec
166+ )
167+
168+ function onexec(err, stdout, stderr) {
169+ st.deepEqual(
170+ [/command failed/i.test(err), strip(stderr)],
171+ [
172+ true,
173+ [
174+ 'FOOOO',
175+ ' 1:1 error No such file or directory',
176+ '',
177+ 'definitions.md',
178+ ' 10:1-10:18 warning Link to unknown heading: `world` missing-heading remark-validate-links',
179+ '',
180+ '2 messages (✖ 1 error, ⚠ 1 warning)',
181+ ''
182+ ].join('\n')
183+ ],
184+ 'should work'
185+ )
186+ }
187+ })
188+
189+ t.test('should work if there are no links', function (st) {
190+ st.plan(1)
191+
192+ childProcess.exec(
193+ [
194+ bin,
195+ '--no-config',
196+ '--no-ignore',
197+ '--use',
198+ '../..',
199+ '--use',
200+ '../sort',
201+ 'empty.md'
202+ ].join(' '),
203+ onexec
204+ )
205+
206+ function onexec(err, stdout, stderr) {
207+ st.deepEqual(
208+ [err, strip(stderr)],
209+ [null, 'empty.md: no issues found\n'],
210+ 'should work'
211+ )
212+ }
213+ })
214+
215+ t.test('should work with stdin', function (st) {
216+ st.plan(1)
217+
218+ var subprocess = childProcess.exec(
219+ [
220+ bin,
221+ '--no-config',
222+ '--no-ignore',
223+ '--use',
224+ '../..',
225+ '--use',
226+ '../sort'
227+ ].join(' '),
228+ onexec
229+ )
230+
231+ fs.createReadStream('github.md').pipe(subprocess.stdin)
232+
233+ function onexec(err, stdout, stderr) {
234+ st.deepEqual(
235+ [err, strip(stderr)],
236+ [
237+ null,
238+ [
239+ '<stdin>',
240+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
241+ '',
242+ '⚠ 1 warning',
243+ ''
244+ ].join('\n')
245+ ],
246+ 'should work'
247+ )
248+ }
249+ })
250+
251+ t.test('should work when not all files are given', function (st) {
252+ st.plan(1)
253+
254+ childProcess.exec(
255+ [
256+ bin,
257+ '--no-config',
258+ '--no-ignore',
259+ '--use',
260+ '../..',
261+ '--use',
262+ '../sort',
263+ 'github.md'
264+ ].join(' '),
265+ onexec
266+ )
267+
268+ function onexec(err, stdout, stderr) {
269+ st.deepEqual(
270+ [err, strip(stderr)],
271+ [
272+ null,
273+ [
274+ 'github.md',
275+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
276+ ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
277+ ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
278+ ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
279+ ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
280+ ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
281+ ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
282+ ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
283+ ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
284+ '',
285+ '⚠ 9 warnings',
286+ ''
287+ ].join('\n')
288+ ],
289+ 'should work'
290+ )
291+ }
292+ })
293+
294+ t.test('should work when all files are given', function (st) {
295+ st.plan(1)
296+
297+ childProcess.exec(
298+ [
299+ bin,
300+ '--no-config',
301+ '--no-ignore',
302+ '--use',
303+ '../..',
304+ '--use',
305+ '../sort',
306+ 'github.md',
307+ 'examples/github.md'
308+ ].join(' '),
309+ onexec
310+ )
311+
312+ function onexec(err, stdout, stderr) {
313+ st.deepEqual(
314+ [err, strip(stderr)],
315+ [
316+ null,
317+ [
318+ 'examples/github.md',
319+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
320+ ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
321+ ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
322+ ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
323+ ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
324+ '',
325+ 'github.md',
326+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
327+ ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
328+ ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
329+ ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
330+ ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
331+ ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
332+ ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
333+ ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
334+ ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
335+ '',
336+ '⚠ 14 warnings',
337+ ''
338+ ].join('\n')
339+ ],
340+ 'should work'
341+ )
342+ }
343+ })
344+
345+ t.test('should work with definitions', function (st) {
346+ st.plan(1)
347+
348+ childProcess.exec(
349+ [
350+ bin,
351+ '--no-config',
352+ '--no-ignore',
353+ '--use',
354+ '../..',
355+ '--use',
356+ '../sort',
357+ 'definitions.md'
358+ ].join(' '),
359+ onexec
360+ )
361+
362+ function onexec(err, stdout, stderr) {
363+ st.deepEqual(
364+ [err, strip(stderr)],
365+ [
366+ null,
367+ [
368+ 'definitions.md',
369+ ' 10:1-10:18 warning Link to unknown heading: `world` missing-heading remark-validate-links',
370+ '',
371+ '⚠ 1 warning',
372+ ''
373+ ].join('\n')
374+ ],
375+ 'should work'
376+ )
377+ }
378+ })
379+
380+ t.test('should work on GitHub URLs when given a repo', function (st) {
381+ st.plan(1)
382+
383+ childProcess.exec(
384+ [
385+ bin,
386+ '--no-config',
387+ '--no-ignore',
388+ '--use',
389+ '"../..=repository:\\"wooorm/test\\""',
390+ '--use',
391+ '../sort',
392+ 'github.md',
393+ 'examples/github.md'
394+ ].join(' '),
395+ onexec
396+ )
397+
398+ function onexec(err, stdout, stderr) {
399+ st.deepEqual(
400+ [err, strip(stderr)],
401+ [
402+ null,
403+ [
404+ 'examples/github.md',
405+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
406+ ' 15:34-15:91 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
407+ ' 17:12-17:72 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
408+ ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
409+ ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
410+ ' 31:10-31:71 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
411+ ' 33:10-33:74 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
412+ ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
413+ ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
414+ ' 37:10-37:70 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
415+ ' 37:10-37:70 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
416+ ' 39:10-39:73 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
417+ ' 39:10-39:73 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
418+ '',
419+ 'github.md',
420+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
421+ ' 21:34-21:100 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
422+ ' 23:34-23:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
423+ ' 25:12-25:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
424+ ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
425+ ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
426+ ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
427+ ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
428+ ' 45:10-45:80 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
429+ ' 47:10-47:83 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
430+ ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
431+ ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
432+ ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
433+ ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
434+ ' 53:10-53:79 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
435+ ' 53:10-53:79 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
436+ ' 55:10-55:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
437+ ' 55:10-55:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
438+ '',
439+ '⚠ 31 warnings',
440+ ''
441+ ].join('\n')
442+ ],
443+ 'should work'
444+ )
445+ }
446+ })
447+
448+ t.test('should work when with Git directory', function (st) {
449+ st.plan(1)
450+
451+ childProcess.exec('git init', oninit)
452+
453+ function oninit(err) {
454+ if (err) {
455+ t.ifErr(err)
456+ } else {
457+ childProcess.exec(
458+ 'git remote add origin git@github.com:wooorm/test.git',
459+ onremote
460+ )
461+ }
462+ }
463+
464+ function onremote(err) {
465+ if (err) {
466+ clean()
467+ t.ifErr(err)
468+ } else {
469+ childProcess.exec(
470+ [
471+ bin,
472+ '--no-config',
473+ '--no-ignore',
474+ '--use',
475+ '../..',
476+ '--use',
477+ '../sort',
478+ 'github.md',
479+ 'examples/github.md'
480+ ].join(' '),
481+ onexec
482+ )
483+ }
484+ }
485+
486+ function onexec(err, stdout, stderr) {
487+ clean()
488+ st.deepEqual(
489+ [err, strip(stderr)],
490+ [
491+ null,
492+ [
493+ 'examples/github.md',
494+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
495+ ' 15:34-15:91 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
496+ ' 17:12-17:72 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
497+ ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
498+ ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
499+ ' 31:10-31:71 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
500+ ' 33:10-33:74 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
501+ ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
502+ ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
503+ ' 37:10-37:70 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
504+ ' 37:10-37:70 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
505+ ' 39:10-39:73 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
506+ ' 39:10-39:73 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
507+ '',
508+ 'github.md',
509+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
510+ ' 21:34-21:100 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
511+ ' 23:34-23:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
512+ ' 25:12-25:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
513+ ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
514+ ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
515+ ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
516+ ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
517+ ' 45:10-45:80 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
518+ ' 47:10-47:83 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
519+ ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
520+ ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
521+ ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
522+ ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
523+ ' 53:10-53:79 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
524+ ' 53:10-53:79 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
525+ ' 55:10-55:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
526+ ' 55:10-55:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
527+ '',
528+ '⚠ 31 warnings',
529+ ''
530+ ].join('\n')
531+ ],
532+ 'should work'
533+ )
534+ }
535+
536+ function clean() {
537+ rimraf.sync('./.git')
538+ }
539+ })
540+
541+ t.test('should fail w/o Git repository', function (st) {
542+ st.plan(1)
543+
544+ fs.renameSync('../../.git', '../../.git.bak')
545+
546+ childProcess.exec(
547+ [
548+ bin,
549+ '--no-config',
550+ '--no-ignore',
551+ '--use',
552+ '../..',
553+ '--use',
554+ '../sort',
555+ 'github.md'
556+ ].join(' '),
557+ onexec
558+ )
559+
560+ function onexec(err) {
561+ fs.renameSync('../../.git.bak', '../../.git')
562+ st.ok(/not a git repository/i.test(err), 'should fail')
563+ }
564+ })
565+
566+ t.test('should fail w/o Git repository w/o remote', function (st) {
567+ st.plan(1)
568+
569+ childProcess.exec('git init', oninit)
570+
571+ function oninit(err) {
572+ if (err) {
573+ t.ifErr(err)
574+ } else {
575+ childProcess.exec(
576+ [
577+ bin,
578+ '--no-config',
579+ '--no-ignore',
580+ '--use',
581+ '../..',
582+ 'github.md'
583+ ].join(' '),
584+ onexec
585+ )
586+ }
587+ }
588+
589+ function onexec(err) {
590+ rimraf.sync('./.git')
591+ st.ok(/Could not find remote origin/.test(err), 'should fail')
592+ }
593+ })
594+
595+ t.test('should work w/o Git repository w/ repo', function (st) {
596+ st.plan(1)
597+
598+ fs.renameSync('../../.git', '../../.git.bak')
599+
600+ childProcess.exec(
601+ [
602+ bin,
603+ '--no-config',
604+ '--no-ignore',
605+ '--use',
606+ '"../..=repository:\\"wooorm/test\\""',
607+ '--use',
608+ '../sort',
609+ 'small.md'
610+ ].join(' '),
611+ onexec
612+ )
613+
614+ function onexec(err, stdout, stderr) {
615+ fs.renameSync('../../.git.bak', '../../.git')
616+ st.deepEqual(
617+ [err, strip(stderr)],
618+ [
619+ null,
620+ [
621+ 'small.md',
622+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
623+ '',
624+ '⚠ 1 warning',
625+ ''
626+ ].join('\n')
627+ ],
628+ 'should work'
629+ )
630+ }
631+ })
632+
633+ t.test('should work w/o Git repository w/ remote', function (st) {
634+ st.plan(1)
635+
636+ fs.renameSync('../../.git', '../../.git.bak')
637+
638+ childProcess.exec(
639+ [
640+ bin,
641+ '--no-config',
642+ '--no-ignore',
643+ '--use',
644+ '"../..=repository:{remote:\\"wooorm/test\\"}"',
645+ 'small.md'
646+ ].join(' '),
647+ onexec
648+ )
649+
650+ function onexec(err, stdout, stderr) {
651+ fs.renameSync('../../.git.bak', '../../.git')
652+ st.deepEqual(
653+ [err, strip(stderr)],
654+ [
655+ null,
656+ [
657+ 'small.md',
658+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
659+ '',
660+ '⚠ 1 warning',
661+ ''
662+ ].join('\n')
663+ ],
664+ 'should work'
665+ )
666+ }
667+ })
668+
669+ t.test('should work w/o Git repository w/ remote and root', function (st) {
670+ st.plan(1)
671+
672+ fs.renameSync('../../.git', '../../.git.bak')
673+
674+ childProcess.exec(
675+ [
676+ bin,
677+ '--no-config',
678+ '--no-ignore',
679+ '--use',
680+ '"../..=repository:\\"wooorm/test\\",root:\\"../\\""',
681+ 'small.md'
682+ ].join(' '),
683+ onexec
684+ )
685+
686+ function onexec(err, stdout, stderr) {
687+ fs.renameSync('../../.git.bak', '../../.git')
688+ st.deepEqual(
689+ [err, strip(stderr)],
690+ [
691+ null,
692+ [
693+ 'small.md',
694+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
695+ '',
696+ '⚠ 1 warning',
697+ ''
698+ ].join('\n')
699+ ],
700+ 'should work'
701+ )
702+ }
703+ })
704+
705+ t.test('should work with `repository:false`', function (st) {
706+ st.plan(1)
707+
708+ fs.renameSync('../../.git', '../../.git.bak')
709+
710+ childProcess.exec(
711+ [
712+ bin,
713+ '--no-config',
714+ '--no-ignore',
715+ '--use',
716+ '"../..=repository:false"',
717+ '--use',
718+ '../sort',
719+ 'github.md',
720+ 'examples/github.md'
721+ ].join(' '),
722+ onexec
723+ )
724+
725+ function onexec(err, stdout, stderr) {
726+ fs.renameSync('../../.git.bak', '../../.git')
727+ st.deepEqual(
728+ [err, strip(stderr)],
729+ [
730+ null,
731+ [
732+ 'examples/github.md',
733+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
734+ ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
735+ ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links',
736+ ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links',
737+ ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links',
738+ '',
739+ 'github.md',
740+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
741+ ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
742+ ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
743+ ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
744+ ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
745+ ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
746+ ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
747+ ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
748+ ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
749+ ' 71:41-71:56 warning Link to unknown heading: `readme` missing-heading remark-validate-links',
750+ '',
751+ '⚠ 15 warnings',
752+ ''
753+ ].join('\n')
754+ ],
755+ 'should work'
756+ )
757+ }
758+ })
759+
760+ t.test('should work when finding non-hosted Git remotes', function (st) {
761+ st.plan(1)
762+
763+ childProcess.exec(
764+ [
765+ bin,
766+ '--no-config',
767+ '--no-ignore',
768+ '--use',
769+ '"../..=repository:\\"ssh://git@domain.com/user/project.git\\""',
770+ '--use',
771+ '../sort',
772+ 'small.md'
773+ ].join(' '),
774+ onexec
775+ )
776+
777+ function onexec(err, stdout, stderr) {
778+ st.deepEqual(
779+ [err, strip(stderr)],
780+ [
781+ null,
782+ [
783+ 'small.md',
784+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
785+ '',
786+ '⚠ 1 warning',
787+ ''
788+ ].join('\n')
789+ ],
790+ 'should work'
791+ )
792+ }
793+ })
794+
795+ t.test('should support a GitLab shortcode', function (st) {
796+ st.plan(1)
797+
798+ childProcess.exec(
799+ [
800+ bin,
801+ '--no-config',
802+ '--no-ignore',
803+ '--use',
804+ '"../..=repository:\\"gitlab:wooorm/test\\""',
805+ '--use',
806+ '../sort',
807+ 'gitlab.md'
808+ ].join(' '),
809+ onexec
810+ )
811+
812+ function onexec(err, stdout, stderr) {
813+ st.deepEqual(
814+ [err, strip(stderr)],
815+ [
816+ null,
817+ [
818+ 'gitlab.md',
819+ ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links',
820+ ' 19:34-19:100 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
821+ ' 21:12-21:81 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
822+ ' 23:10-23:37 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
823+ ' 25:10-25:35 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
824+ ' 37:10-37:41 warning Link to unknown heading in `examples/gitlab.md`: `world` missing-heading-in-file remark-validate-links',
825+ ' 39:10-39:39 warning Link to unknown heading in `examples/gitlab.md`: `world` missing-heading-in-file remark-validate-links',
826+ ' 41:10-41:80 warning Link to unknown heading in `examples/gitlab.md`: `world` missing-heading-in-file remark-validate-links',
827+ ' 43:10-43:83 warning Link to unknown heading in `examples/gitlab.md`: `world` missing-heading-in-file remark-validate-links',
828+ ' 45:10-45:40 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
829+ ' 45:10-45:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
830+ ' 47:10-47:38 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
831+ ' 47:10-47:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
832+ ' 49:10-49:79 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
833+ ' 49:10-49:79 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
834+ ' 51:10-51:82 warning Link to unknown file: `examples/world.md`. Did you mean `examples/gitlab.md` missing-file remark-validate-links',
835+ ' 51:10-51:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
836+ '',
837+ '⚠ 17 warnings',
838+ ''
839+ ].join('\n')
840+ ],
841+ 'should work'
842+ )
843+ }
844+ })
845+
846+ t.test('should support a Bitbucket shortcode', function (st) {
847+ st.plan(1)
848+
849+ childProcess.exec(
850+ [
851+ bin,
852+ '--no-config',
853+ '--no-ignore',
854+ '--use',
855+ '"../..=repository:\\"bitbucket:wooorm/test\\""',
856+ '--use',
857+ '../sort',
858+ 'bitbucket.md'
859+ ].join(' '),
860+ onexec
861+ )
862+
863+ function onexec(err, stdout, stderr) {
864+ st.deepEqual(
865+ [err, strip(stderr)],
866+ [
867+ null,
868+ [
869+ 'bitbucket.md',
870+ ' 5:37-5:67 warning Link to unknown heading: `world` missing-heading remark-validate-links',
871+ ' 21:34-21:102 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
872+ ' 23:12-23:83 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
873+ ' 25:10-25:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
874+ ' 27:10-27:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
875+ ' 39:10-39:60 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links',
876+ ' 41:10-41:58 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links',
877+ ' 43:10-43:101 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links',
878+ ' 45:10-45:104 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links',
879+ ' 47:10-47:56 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
880+ ' 47:10-47:56 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
881+ ' 49:10-49:54 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
882+ ' 49:10-49:54 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
883+ ' 51:10-51:97 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
884+ ' 51:10-51:97 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
885+ ' 53:10-53:100 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links',
886+ ' 53:10-53:100 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links',
887+ '',
888+ '⚠ 17 warnings',
889+ ''
890+ ].join('\n')
891+ ],
892+ 'should work'
893+ )
894+ }
895+ })
896+
897+ t.test('should suggest similar links', function (st) {
898+ st.plan(1)
899+
900+ childProcess.exec(
901+ [
902+ bin,
903+ '--no-config',
904+ '--no-ignore',
905+ '--use',
906+ '../..',
907+ '--use',
908+ '../sort',
909+ 'suggestions.md'
910+ ].join(' '),
911+ onexec
912+ )
913+
914+ function onexec(err, stdout, stderr) {
915+ st.deepEqual(
916+ [err, strip(stderr)],
917+ [
918+ null,
919+ [
920+ 'suggestions.md',
921+ ' 3:22-3:37 warning Link to unknown heading: `helloo`. Did you mean `hello` missing-heading remark-validate-links',
922+ ' 7:17-7:39 warning Link to unknown heading in `github.md`: `fiiiles`. Did you mean `files` missing-heading-in-file remark-validate-links',
923+ '',
924+ '⚠ 2 warnings',
925+ ''
926+ ].join('\n')
927+ ],
928+ 'should work'
929+ )
930+ }
931+ })
932+
933+ t.test('should recognise links to particular lines', function (st) {
934+ st.plan(1)
935+
936+ childProcess.exec(
937+ [
938+ bin,
939+ '--no-config',
940+ '--no-ignore',
941+ '--use',
942+ '"../..=repository:\\"wooorm/test\\""',
943+ '--use',
944+ '../sort',
945+ 'line-links.md'
946+ ].join(' '),
947+ onexec
948+ )
949+
950+ function onexec(err, stdout, stderr) {
951+ st.deepEqual(
952+ [err, strip(stderr)],
953+ [
954+ null,
955+ [
956+ 'line-links.md',
957+ ' 5:9-5:61 warning Link to unknown file: `examples/missing.js` missing-file remark-validate-links',
958+ ' 9:1-9:69 warning Link to unknown file: `examples/missing.js` missing-file remark-validate-links',
959+ '',
960+ '⚠ 2 warnings',
961+ ''
962+ ].join('\n')
963+ ],
964+ 'should work'
965+ )
966+ }
967+ })
968+
969+ t.test('should recognise links with encoded URLs', function (st) {
970+ st.plan(1)
971+
972+ childProcess.exec(
973+ [
974+ bin,
975+ '--no-config',
976+ '--no-ignore',
977+ '--use',
978+ '"../..=repository:\\"wooorm/test\\""',
979+ '--use',
980+ '../sort',
981+ 'weird-characters.md'
982+ ].join(' '),
983+ onexec
984+ )
985+
986+ function onexec(err, stdout, stderr) {
987+ st.deepEqual(
988+ [err, strip(stderr)],
989+ [
990+ null,
991+ [
992+ 'weird-characters.md',
993+ ' 11:17-11:87 warning Link to unknown file: `examples/missing#characters.md`. Did you mean `examples/weird#characters.md` missing-file remark-validate-links',
994+ ' 13:17-13:93 warning Link to unknown file: `examples/missing#character/readme.md`. Did you mean `examples/weird#character/readme.md` missing-file remark-validate-links',
995+ ' 15:17-15:114 warning Link to unknown heading in `examples/weird#characters.md`: `world` missing-heading-in-file remark-validate-links',
996+ ' 17:17-17:120 warning Link to unknown heading in `examples/weird#character/readme.md`: `world` missing-heading-in-file remark-validate-links',
997+ '',
998+ '⚠ 4 warnings',
999+ ''
1000+ ].join('\n')
1001+ ],
1002+ 'should work'
1003+ )
1004+ }
1005+ })
1006+
1007+ t.test('should support folders', function (st) {
1008+ st.plan(1)
1009+
1010+ childProcess.exec(
1011+ [
1012+ bin,
1013+ '--no-config',
1014+ '--no-ignore',
1015+ '--use',
1016+ '"../..=repository:\\"wooorm/test\\""',
1017+ '--use',
1018+ '../sort',
1019+ 'folder.md'
1020+ ].join(' '),
1021+ onexec
1022+ )
1023+
1024+ function onexec(err, stdout, stderr) {
1025+ st.deepEqual(
1026+ [err, strip(stderr)],
1027+ [
1028+ null,
1029+ [
1030+ 'folder.md',
1031+ ' 9:1-9:40 warning Link to unknown heading in `folder/readme.markdown`: `missing` missing-heading-in-file remark-validate-links',
1032+ ' 17:1-17:24 warning Link to unknown heading in `folder`: `missing` missing-heading-in-file remark-validate-links',
1033+ ' 21:1-21:25 warning Link to unknown file: `missing` missing-file remark-validate-links',
1034+ ' 21:1-21:25 warning Link to unknown heading in `missing`: `missing` missing-heading-in-file remark-validate-links',
1035+ ' 23:1-23:39 warning Link to unknown heading in `folder-without-readme`: `missing` missing-heading-in-file remark-validate-links',
1036+ '',
1037+ '⚠ 5 warnings',
1038+ ''
1039+ ].join('\n')
1040+ ],
1041+ 'should work'
1042+ )
1043+ }
1044+ })
1045+
1046+ t.test('should warn on files as folders', function (st) {
1047+ st.plan(1)
1048+
1049+ childProcess.exec(
1050+ [
1051+ bin,
1052+ '--no-config',
1053+ '--no-ignore',
1054+ '--use',
1055+ '../..',
1056+ '--use',
1057+ '../sort',
1058+ 'file-as-folder.md'
1059+ ].join(' '),
1060+ onexec
1061+ )
1062+
1063+ function onexec(err, stdout, stderr) {
1064+ st.deepEqual(
1065+ [err, strip(stderr)],
1066+ [
1067+ null,
1068+ [
1069+ 'file-as-folder.md',
1070+ ' 1:1-1:28 warning Link to unknown file: `file-as-folder.md/other`. Did you mean `file-as-folder.md` missing-file remark-validate-links',
1071+ '',
1072+ '⚠ 1 warning',
1073+ ''
1074+ ].join('\n')
1075+ ],
1076+ 'should work'
1077+ )
1078+ }
1079+ })
1080+
1081+ t.test('should check images', function (st) {
1082+ st.plan(1)
1083+
1084+ childProcess.exec(
1085+ [
1086+ bin,
1087+ '--no-config',
1088+ '--no-ignore',
1089+ '--use',
1090+ '"../..=repository:\\"wooorm/test\\""',
1091+ '--use',
1092+ '../sort',
1093+ 'images.md'
1094+ ].join(' '),
1095+ onexec
1096+ )
1097+
1098+ function onexec(err, stdout, stderr) {
1099+ st.deepEqual(
1100+ [err, strip(stderr)],
1101+ [
1102+ null,
1103+ [
1104+ 'images.md',
1105+ ' 19:10-19:50 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links',
1106+ ' 21:12-21:42 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links',
1107+ ' 23:10-23:89 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links',
1108+ ' 35:1-35:38 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links',
1109+ ' 37:1-37:77 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links',
1110+ '',
1111+ '⚠ 5 warnings',
1112+ ''
1113+ ].join('\n')
1114+ ],
1115+ 'should work'
1116+ )
1117+ }
1118+ })
1119+
1120+ t.test('should support query parameters', function (st) {
1121+ st.plan(1)
1122+
1123+ childProcess.exec(
1124+ [
1125+ bin,
1126+ '--no-config',
1127+ '--no-ignore',
1128+ '--use',
1129+ '"../..=repository:\\"wooorm/test\\""',
1130+ '--use',
1131+ '../sort',
1132+ 'query-params.md'
1133+ ].join(' '),
1134+ onexec
1135+ )
1136+
1137+ function onexec(err, stdout, stderr) {
1138+ st.deepEqual(
1139+ [err, strip(stderr)],
1140+ [
1141+ null,
1142+ [
1143+ 'query-params.md',
1144+ ' 9:33-9:55 warning Link to unknown heading: `query-params?`. Did you mean `query-params` missing-heading remark-validate-links',
1145+ '',
1146+ '⚠ 1 warning',
1147+ ''
1148+ ].join('\n')
1149+ ],
1150+ 'should work'
1151+ )
1152+ }
1153+ })
1154+
1155+ t.test('should support case insensitive headings', function (st) {
1156+ st.plan(1)
1157+
1158+ childProcess.exec(
1159+ [
1160+ bin,
1161+ '--no-config',
1162+ '--no-ignore',
1163+ '--use',
1164+ '../..',
1165+ '--use',
1166+ '../sort',
1167+ 'case-insensitive-headings.md'
1168+ ].join(' '),
1169+ onexec
1170+ )
1171+
1172+ function onexec(err, stdout, stderr) {
1173+ st.deepEqual(
1174+ [err, strip(stderr)],
1175+ [
1176+ null,
1177+ [
1178+ 'case-insensitive-headings.md',
1179+ ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links',
1180+ ' 9:16-9:48 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links',
1181+ '',
1182+ '⚠ 2 warnings',
1183+ ''
1184+ ].join('\n')
1185+ ],
1186+ 'should work'
1187+ )
1188+ }
1189+ })
1190+
1191+ t.test('should ignore external links', function (st) {
1192+ st.plan(1)
1193+
1194+ childProcess.exec(
1195+ [
1196+ bin,
1197+ '--no-config',
1198+ '--no-ignore',
1199+ '--use',
1200+ '"../..=repository:\\"wooorm/test\\""',
1201+ '--use',
1202+ '../sort',
1203+ 'external.md'
1204+ ].join(' '),
1205+ onexec
1206+ )
1207+
1208+ function onexec(err, stdout, stderr) {
1209+ st.deepEqual(
1210+ [err, strip(stderr)],
1211+ [null, 'external.md: no issues found\n'],
1212+ 'should work'
1213+ )
1214+ }
1215+ })
1216+
1217+ t.test('should ignore external links (without repo)', function (st) {
1218+ st.plan(1)
1219+
1220+ childProcess.exec(
1221+ [
1222+ bin,
1223+ '--no-config',
1224+ '--no-ignore',
1225+ '--use',
1226+ '../..=repository:false',
1227+ '--use',
1228+ '../sort',
1229+ 'external.md'
1230+ ].join(' '),
1231+ onexec
1232+ )
1233+
1234+ function onexec(err, stdout, stderr) {
1235+ st.deepEqual(
1236+ [err, strip(stderr)],
1237+ [null, 'external.md: no issues found\n'],
1238+ 'should work'
1239+ )
1240+ }
1241+ })
1242+
1243+ t.test('should support self-hosted Git solutions', function (st) {
1244+ st.plan(1)
1245+
1246+ childProcess.exec('git init', oninit)
1247+
1248+ function oninit(err) {
1249+ if (err) {
1250+ t.ifErr(err)
1251+ } else {
1252+ childProcess.exec(
1253+ 'git remote add origin git@gitlab.acme.company:acme/project.git',
1254+ onremote
1255+ )
1256+ }
1257+ }
1258+
1259+ function onremote(err) {
1260+ if (err) {
1261+ clean()
1262+ t.ifErr(err)
1263+ } else {
1264+ childProcess.exec(
1265+ [
1266+ bin,
1267+ '--no-config',
1268+ '--no-ignore',
1269+ '--use',
1270+ '"../..=urlConfig:{hostname:\\"gitlab.acme.com\\",prefix:\\"/acme/project/blob/\\",headingPrefix:\\"#\\",lines:true}"',
1271+ 'self-hosted.md'
1272+ ].join(' '),
1273+ onexec
1274+ )
1275+ }
1276+ }
1277+
1278+ function onexec(err, stdout, stderr) {
1279+ clean()
1280+ st.deepEqual(
1281+ [err, strip(stderr)],
1282+ [
1283+ null,
1284+ [
1285+ 'self-hosted.md',
1286+ ' 5:1-5:80 warning Link to unknown heading: `world` missing-heading remark-validate-links',
1287+ '',
1288+ '⚠ 1 warning',
1289+ ''
1290+ ].join('\n')
1291+ ],
1292+ 'should work'
1293+ )
1294+ }
1295+
1296+ function clean() {
1297+ rimraf.sync('./.git')
1298+ }
1299+ })
1300+
1301+ t.end()
1302+})
test/sort.jsnew file+13
@@ -0,0 +1,13 @@
1+'use strict'
2+
3+var vfileSort = require('vfile-sort')
4+
5+module.exports = sort
6+
7+function sort() {
8+ return transform
9+}
10+
11+function transform(tree, file) {
12+ vfileSort(file)
13+}