3 * @copyright 2015 Titus Wormer
5 * @module mdast:validate-links
6 * @fileoverview Validate links to headings and files in markdown.
15var url = require('url');
16var fs = require('fs');
17var path = require('path');
18var propose = require('propose');
19var visit = require('unist-util-visit');
20var definitions = require('mdast-util-definitions');
21var gh = require('github-url-to-object');
22var urljoin = require('urljoin');
28var exists = fs.existsSync;
32 * Get the `pathname` of `uri`, if applicable.
35 * @param {string} uri - Reference.
36 * @return {string?} - Pathname.
38function getPathname(uri) {
39 return parse(uri).pathname;
43 * Get the `hash` of `uri`, if applicable.
46 * @param {string} uri - Reference.
47 * @return {string?} - Hash.
49function getHash(uri) {
50 var hash = parse(uri).hash;
52 return hash ? hash.slice(1) : null;
56 * Suggest a possible similar reference.
58 * @param {string} pathname - Unfound reference.
59 * @param {Object<string, boolean>} references - All
61 * @return {string?} - Suggested reference.
63function getClosest(pathname, references) {
64 var hash = getHash(pathname);
65 var base = getPathname(pathname);
71 for (reference in references) {
72 subbase = getPathname(reference);
73 subhash = getHash(reference);
75 if (getPathname(reference) === base) {
76 if (subhash && hash) {
77 dictionary.push(subhash);
80 if (!subhash && !hash) {
81 dictionary.push(subbase);
86 return propose(hash ? hash : base, dictionary, {
92 * Utilitity to warn `warning` for each node in `nodes`,
95 * @param {File} file - Virtual file.
96 * @param {Array.<Node>} nodes - Offending nodes.
97 * @param {string} warning - Message.
99function warnAll(file, nodes, warning) {
100 nodes.forEach(function (node) {
101 file.warn(warning, node);
106 * Gather references: a map of file-paths references
107 * to be one or more nodes.
110 * gatherReferences(new File(), {});
112 * @param {File} file - Set of virtual files.
113 * @param {Object.<string, string>} project - GitHub
114 * project, with a `user` and `repo` property
116 * @return {Object.<string, Array.<Node>>} exposed - Map of
117 * file-paths (and anchors) which are referenced.
119function gatherReferences(file, project) {
121 var filePath = file.filePath();
122 var directory = file.directory;
123 var space = file.namespace('mdast');
124 var ast = space.tree || space.ast;
128 getDefinition = definitions(ast);
130 if (project.user && project.repo) {
131 prefix = '/' + project.user + '/' + project.repo + '/blob/';
139 * href: 'foo/bar/baz.md#foo',
148 * @param {Node} node - Link or link-reference node.
150 function onlink(node) {
151 var link = node.href;
159 * Handle link-references.
162 if (node.identifier) {
163 definition = getDefinition(node.identifier);
164 link = definition && definition.link;
168 * Ignore definitions without link.
179 * Handle hashes, or relative files.
181 if (!uri.pathname && uri.hash) {
182 link = filePath + uri.hash;
185 link = urljoin(directory, link) + (uri.hash || '');
192 * Only works with GitHub.
201 uri.hostname !== 'github.com' ||
202 uri.pathname.slice(0, prefix.length) !== prefix
207 link = uri.pathname.slice(prefix.length) + (uri.hash || '');
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.
215 * Currently, I’m ignoring this and just not
216 * supporting branches.
219 link = link.slice(link.indexOf('/') + 1);
225 * Handle file links, or combinations of files
229 index = link.indexOf('#');
235 pathname = link.slice(0, index);
236 hash = link.slice(index + 1);
239 if (!cache[pathname]) {
240 cache[pathname] = [];
243 cache[pathname].push(node);
250 cache[link].push(node);
254 visit(ast, 'link', onlink);
255 visit(ast, 'linkReference', onlink);
261 * Factory to store all markdown files, and headings.
264 * var gather = gatherExposedFactory();
265 * var file = new File('# foo');
267 * gather(file).done() // {}
271function gatherExposedFactory() {
273 var hasHeadings = false;
274 var hasSlugs = false;
277 * Access found files and headings.
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.
285 if (hasHeadings && !hasSlugs) {
287 'Missing slugs. Use for example `mdast-slug` to generate ' +
296 * Find headings in `file`.
298 * @property {Function} done - Access found links.
299 * @param {File} file - Virtual file.
300 * @returns {Function} - Itself.
302 function gather(file) {
303 var filePath = file.filePath();
304 var space = file.namespace('mdast');
305 var ast = space.tree || space.ast;
308 * Ignore files without AST or filename.
311 if (filePath && ast) {
312 cache[filePath] = true;
314 visit(ast, 'heading', function (node) {
315 var data = node.data || {};
316 var attributes = data.htmlAttributes || {};
317 var id = attributes.id || data.id;
319 /* istanbul ignore next */
320 id = id || (node.attributes && node.attributes.id);
325 cache[filePath + '#' + id] = hasSlugs = true;
339 * Check if `file` references headings or files not in
340 * `exposed`. If `project` is given, normalizes GitHub blob
344 * validate({'example.md': true}, new File());
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
353function 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();
366 for (reference in references) {
367 nodes = references[reference];
368 real = exposed[reference];
369 hash = getHash(reference);
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.
379 if ((real === undefined || real === null) && !hash) {
380 real = references[reference] = exists(reference);
385 pathname = getPathname(reference);
386 warning = 'Link to unknown heading';
388 if (pathname !== filePath) {
389 warning += ' in `' + pathname + '`';
392 warning += ': `' + hash + '`';
394 warning = 'Link to unknown file: `' + reference + '`';
397 suggestion = getClosest(reference, exposed);
400 warning += '. Did you mean `' + suggestion + '`';
403 warnAll(file, nodes, warning);
409 * Factory to create a new completer.
412 * var completer = completerFactory({
417 * @param {Object} project - GitHub project, if applicable.
418 * @return {Function} - `completer`, bound to `project`.
420function completerFactory(project) {
425 * completer(new FileSet(), console.log)
427 * @property {*} pluginId - Unique ID so completers by
428 * this plug-in are not added multiple times to a single
430 * @param {FileSet} set - Virtual file-set.
431 * @param {function(err?)} done - Callback.
433 function completer(set, done) {
434 var gatherExposed = gatherExposedFactory();
437 set.valueOf().forEach(gatherExposed);
439 exposed = gatherExposed.done();
441 set.valueOf().forEach(function (file) {
442 validate(exposed, file, project);
448 completer.pluginId = 'mdast-validate-links';
454 * Factory to create a transformer based on the given
458 * transformerFactory({}, new FileSet());
460 * @param {Object.<string, string>} project - GitHub
461 * project, with a `user` and `repo` property
463 * @param {FileSet} fileSet - Set of virtual files.
464 * @return {function(ast, file)}
466function transformerFactory(project, fileSet) {
468 * Transformer. Adds references files to the set.
471 * transformer({}, new File());
473 * @param {*} ast - Node.
474 * @param {File} file - Virtual file.
476 function transformer(ast, file) {
477 var references = gatherReferences(file, project);
479 var current = getPathname(file.filePath());
483 for (link in references) {
484 pathname = getPathname(link);
487 pathname !== current &&
489 links.indexOf(pathname) === -1
491 links.push(pathname);
493 fileSet.add(pathname);
506 * // [Error: mdast-validate-links only works on the CLI]
508 * @param {MDAST} mdast - Processor.
509 * @param {Object?} options - Settings.
510 * @param {FileSet?} fileSet - Virtual file-set.
511 * @throws {Error} - When `fileSet` is not given (when not on
514function attacher(mdast, options, fileSet) {
515 var repo = (options || {}).repository;
519 * Throw when not on the CLI.
523 throw new Error('mdast-validate-links only works on the CLI');
527 * Try to get the repo from `package.json` when not
533 pack = require(path.resolve(process.cwd(), 'package.json'));
534 } catch (exception) {
538 repo = pack.repository ? pack.repository.url || pack.repository : '';
541 repo = repo ? gh(repo) : {};
549 * Attach a `completer`.
552 fileSet.use(completerFactory(repo));
554 return transformerFactory(repo, fileSet);
561module.exports = attacher;
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list