Skip to content

Commit

Permalink
Add support for GitLab, Bitbucket repositories
Browse files Browse the repository at this point in the history
Closes GH-16.
Closes GH-17.
  • Loading branch information
wooorm committed Aug 22, 2017
1 parent d86b38a commit c88aed4
Show file tree
Hide file tree
Showing 11 changed files with 581 additions and 216 deletions.
123 changes: 76 additions & 47 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,37 @@ var path = require('path');
var propose = require('propose');
var visit = require('unist-util-visit');
var definitions = require('mdast-util-definitions');
var gh = require('github-url-to-object');
var hostedGitInfo = require('hosted-git-info');
var urljoin = require('urljoin');
var slug = require('remark-slug');
var xtend = require('xtend');

module.exports = attacher;

completer.pluginId = 'remark-validate-links';
var referenceId = 'remarkValidateLinksReferences';
var landmarkId = 'remarkValidateLinksLandmarks';
var sourceId = 'remark-validate-links';

completer.pluginId = sourceId;

var exists = fs.existsSync;
var parse = url.parse;

var viewPaths = {
github: 'blob',
gitlab: 'blob',
bitbucket: 'src'
};

var headingPrefixes = {
github: '#',
gitlab: '#',
bitbucket: '#markdown-header-'
};

function attacher(options, fileSet) {
var repo = (options || {}).repository;
var info;
var pack;

/* Throw when not on the CLI. */
Expand All @@ -40,45 +57,56 @@ function attacher(options, fileSet) {
repo = pack.repository ? pack.repository.url || pack.repository : '';
}

repo = repo ? gh(repo) : {};
if (repo) {
info = hostedGitInfo.fromUrl(repo);

if (!info) {
throw new Error('remark-validate-links cannot parse `repository` (`' + repo + '`)');
} else if (info.domain === 'gist.github.com') {
throw new Error('remark-validate-links does not support gist repositories');
}
}

/* Attach a `completer`. */
fileSet.use(completer);

/* Attach `slug`. */
/* Attach `slug` and a plugin that adds our transformer after it. */
this.use(slug).use(subplugin);

function subplugin() {
/* Expose transformer. */
return transformerFactory({user: repo.user, repo: repo.repo}, fileSet);
return transformerFactory(fileSet, info);
}
}

/* Completer. */
function completer(set, done) {
var exposed = {};

set.valueOf().forEach(function (file) {
var landmarks = file.data.remarkValidateLinksLandmarks;
set.valueOf().forEach(expose);
set.valueOf().forEach(check);

done();

function expose(file) {
var landmarks = file.data[landmarkId];

if (landmarks) {
exposed = xtend(exposed, landmarks);
}
});
}

set.valueOf().forEach(function (file) {
function check(file) {
/* istanbul ignore else - stdin */
if (file.path) {
validate(exposed, file);
}
});

done();
}
}

/* Factory to create a transformer based on the given
* project and set. */
function transformerFactory(project, fileSet) {
* info and set. */
function transformerFactory(fileSet, info) {
return transformer;

/* Transformer. Adds references files to the set. */
Expand All @@ -97,7 +125,7 @@ function transformerFactory(project, fileSet) {
return;
}

references = gatherReferences(file, ast, project);
references = gatherReferences(file, ast, info);
current = getPathname(filePath);

for (link in references) {
Expand All @@ -115,26 +143,26 @@ function transformerFactory(project, fileSet) {

landmarks[filePath] = true;

visit(ast, function (node) {
visit(ast, mark);

space[referenceId] = references;
space[landmarkId] = landmarks;

function mark(node) {
var data = node.data || {};
var attrs = data.hProperties || data.htmlAttributes || {};
var id = attrs.name || attrs.id || data.id;

if (id) {
landmarks[filePath + '#' + id] = true;
}
});

space.remarkValidateLinksReferences = references;
space.remarkValidateLinksLandmarks = landmarks;
}
}
}

/* Check if `file` references headings or files not in
* `exposed`. If `project` is given, normalizes GitHub blob
* URLs. */
/* Check if `file` references headings or files not in `exposed`. */
function validate(exposed, file) {
var references = file.data.remarkValidateLinksReferences;
var references = file.data[referenceId];
var filePath = file.path;
var reference;
var nodes;
Expand Down Expand Up @@ -186,19 +214,29 @@ function validate(exposed, file) {

/* Gather references: a map of file-paths references
* to be one or more nodes. */
function gatherReferences(file, tree, project) {
function gatherReferences(file, tree, info) {
var cache = {};
var filePath = file.path;
var dirname = file.dirname;
var getDefinition;
var prefix = '';
var headingPrefix = '#';

getDefinition = definitions(tree);

if (project.user && project.repo) {
prefix = '/' + project.user + '/' + project.repo + '/blob/';
if (info && info.type in viewPaths) {
prefix = '/' + info.path() + '/' + viewPaths[info.type] + '/';
}

if (info && info.type in headingPrefixes) {
headingPrefix = headingPrefixes[info.type];
}

visit(tree, 'link', onlink);
visit(tree, 'linkReference', onlink);

return cache;

/* Handle new links. */
function onlink(node) {
var link = node.url;
Expand Down Expand Up @@ -235,18 +273,13 @@ function gatherReferences(file, tree, project) {
}
}

/* Handle full links.
* Only works with GitHub.
*/
/* Handle full links. */
if (uri.hostname) {
if (!prefix) {
return;
}

if (
uri.hostname !== 'github.com' ||
uri.pathname.slice(0, prefix.length) !== prefix
) {
if (uri.hostname !== info.domain || uri.pathname.slice(0, prefix.length) !== prefix) {
return;
}

Expand All @@ -260,20 +293,18 @@ function gatherReferences(file, tree, project) {
* Currently, I’m ignoring this and just not
* supporting branches. */
link = link.slice(link.indexOf('/') + 1);

uri = parse(link);
}

/* Handle file links, or combinations of files
* and hashes. */
index = link.indexOf('#');
index = link.indexOf(headingPrefix);

if (index === -1) {
pathname = link;
hash = null;
} else {
pathname = link.slice(0, index);
hash = link.slice(index + 1);
hash = link.slice(index + headingPrefix.length);
}

if (!cache[pathname]) {
Expand All @@ -283,28 +314,26 @@ function gatherReferences(file, tree, project) {
cache[pathname].push(node);

if (hash) {
link = pathname + '#' + hash;
if (!cache[link]) {
cache[link] = [];
}

cache[link].push(node);
}
}

visit(tree, 'link', onlink);
visit(tree, 'linkReference', onlink);

return cache;
}

/* Utilitity to warn `reason` for each node in `nodes`,
* on `file`. */
function warnAll(file, nodes, reason) {
nodes.forEach(function (node) {
nodes.forEach(one);

function one(node) {
var message = file.message(reason, node);
message.source = 'remark-validate-links';
message.ruleId = 'remark-validate-links';
});
message.source = sourceId;
message.ruleId = sourceId;
}
}

/* Suggest a possible similar reference. */
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"index.js"
],
"dependencies": {
"github-url-to-object": "^4.0.0",
"hosted-git-info": "^2.5.0",
"mdast-util-definitions": "^1.0.0",
"propose": "0.0.5",
"remark-slug": "^4.2.1",
Expand Down
24 changes: 14 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,27 +81,31 @@ directory.
remark --use 'validate-links=repository:"foo/bar"' example.md
```

When a repository is given or detected, links to GitHub are normalized
to the file-system. For example,
`https://github.com/foo/bar/blob/master/example.md` becomes `example.md`.
When a repository is given or detected (supporting GitHub, GitLab, and
Bitbucket), links to the only files are normalized to the file-system.
For example, `https://github.com/foo/bar/blob/master/example.md` becomes
`example.md`.

You can define this repository in [configuration files][cli] too.
An example `.remarkrc` file could look as follows:

```json
{
"plugins": {
"validate-links": {
"repository": "foo/bar"
}
}
"plugins": [
[
"validate-links",
{
"repository": "foo/bar"
}
]
]
}
```

## Integration

**remark-validate-links** can detect anchors on nodes through
several properties on nodes:
`remark-validate-links` can detect anchors on nodes through several properties
on nodes:

* `node.data.hProperties.name` — Used by [`remark-html`][remark-html]
to create a `name` attribute, which anchors can link to
Expand Down
63 changes: 63 additions & 0 deletions test/fixtures/bitbucket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Hello

This is a valid relative heading [link](#markdown-header-hello).

This is an invalid relative heading [link](#markdown-header-world).

## Files

This is a valid relative file [link](https://bitbucket.org/wooorm/test/src/master/examples/bitbucket.md).

So is this [link](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md).

And this [link](./examples/bitbucket.md).

And this [link](examples/bitbucket.md).

This is a valid external [file](../index.js).

This is an invalid relative file [link](https://bitbucket.org/wooorm/test/src/master/examples/world.md).

So is this [link](https://bitbucket.org/wooorm/test/src/foo-bar/examples/world.md).

And this [link](./examples/world.md).

And this [link](examples/world.md).

## Combination

Valid: [a](./examples/bitbucket.md#markdown-header-hello).

Valid: [b](examples/bitbucket.md#markdown-header-hello).

Valid: [c](https://bitbucket.org/wooorm/test/src/master/examples/bitbucket.md#markdown-header-hello).

Valid: [d](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md#markdown-header-hello).

Invalid: [e](./examples/bitbucket.md#markdown-header-world).

Invalid: [f](examples/bitbucket.md#markdown-header-world).

Invalid: [g](https://bitbucket.org/wooorm/test/src/master/examples/bitbucket.md#markdown-header-world).

Invalid: [h](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md#markdown-header-world).

Invalid: [i](./examples/world.md#markdown-header-hello).

Invalid: [j](examples/world.md#markdown-header-hello).

Invalid: [k](https://bitbucket.org/wooorm/test/src/master/examples/world.md#markdown-header-hello).

Invalid: [l](https://bitbucket.org/wooorm/test/src/foo-bar/examples/world.md#markdown-header-hello).

## External

These are all invalid, because they do not link to Bitbucket.

Valid: [a](irc://foo).

Valid: [b](http://example.com).

Valid: [b](http://example.com/foo/bar/baz).

Valid: [b](http://github.com/wooorm/test/blob/foo-bar/examples/world.md#markdown-header-hello).
Loading

0 comments on commit c88aed4

Please sign in to comment.