remark-validate-links
Advanced tools
Comparing version 8.0.3 to 9.0.0
427
index.js
'use strict' | ||
var url = require('url') | ||
var propose = require('propose') | ||
var visit = require('unist-util-visit') | ||
var definitions = require('mdast-util-definitions') | ||
var toString = require('mdast-util-to-string') | ||
var hostedGitInfo = require('hosted-git-info') | ||
var urljoin = require('urljoin') | ||
var slugs = require('github-slugger')() | ||
var xtend = require('xtend/mutable.js') | ||
// Optional Node dependencies. | ||
var fs | ||
var path | ||
try { | ||
fs = require('fs') | ||
path = require('path') | ||
} catch (error) {} | ||
module.exports = validateLinks | ||
var referenceId = 'remarkValidateLinksReferences' | ||
var landmarkId = 'remarkValidateLinksLandmarks' | ||
var sourceId = 'remark-validate-links' | ||
var headingRuleId = 'missing-heading' | ||
var headingInFileRuleId = 'missing-heading-in-file' | ||
var fileRuleId = 'missing-file' | ||
cliCompleter.pluginId = sourceId | ||
/* eslint-disable node/no-deprecated-api */ | ||
var parse = url.parse | ||
var format = url.format | ||
/* eslint-enable node/no-deprecated-api */ | ||
var viewPaths = { | ||
github: 'blob', | ||
gitlab: 'blob', | ||
bitbucket: 'src' | ||
} | ||
var headingPrefixes = { | ||
github: '#', | ||
gitlab: '#', | ||
bitbucket: '#markdown-header-' | ||
} | ||
var lineLinks = { | ||
github: true, | ||
gitlab: true | ||
} | ||
var lineExpression = /^#?l\d/i | ||
function validateLinks(options, fileSet) { | ||
var repo = (options || {}).repository | ||
var info | ||
var pack | ||
// Try to get the repo from `package.json` when not given. | ||
if (!repo && fs && fileSet) { | ||
try { | ||
pack = fileSet.files[0].cwd | ||
pack = JSON.parse(fs.readFileSync(path.resolve(pack, 'package.json'))) | ||
} catch (error) { | ||
pack = {} | ||
} | ||
repo = pack.repository ? pack.repository.url || pack.repository : '' | ||
} | ||
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 plugin that adds our transformer after it. | ||
this.use(subplugin) | ||
// Attach a `completer`. | ||
if (fileSet) { | ||
fileSet.use(cliCompleter) | ||
} else { | ||
this.use(apiCompleter) | ||
} | ||
function subplugin() { | ||
// Expose transformer. | ||
return transformerFactory(fileSet, info) | ||
} | ||
} | ||
// Completer for the API (one file, only headings are checked). | ||
function apiCompleter() { | ||
return apiTransform | ||
} | ||
function apiTransform(tree, file) { | ||
checkFactory(file.data[landmarkId])(file) | ||
} | ||
// Completer for the CLI (multiple files, and support to add more). | ||
function cliCompleter(set, done) { | ||
var exposed = {} | ||
set.valueOf().forEach(expose) | ||
set.valueOf().forEach(checkFactory(exposed)) | ||
done() | ||
function expose(file) { | ||
var landmarks = file.data[landmarkId] | ||
if (landmarks) { | ||
xtend(exposed, landmarks) | ||
} | ||
} | ||
} | ||
function checkFactory(exposed) { | ||
return check | ||
function check(file) { | ||
/* istanbul ignore else - stdin */ | ||
if (file.path) { | ||
validate(exposed, file) | ||
} | ||
} | ||
} | ||
// Factory to create a transformer based on the given info and set. | ||
function transformerFactory(fileSet, info) { | ||
return transformer | ||
// Transformer. Adds references files to the set. | ||
function transformer(ast, file) { | ||
var filePath = file.path | ||
var space = file.data | ||
var links = [] | ||
var landmarks = {} | ||
var references | ||
var current | ||
var link | ||
var pathname | ||
/* istanbul ignore if - stdin */ | ||
if (!filePath) { | ||
return | ||
} | ||
references = gatherReferences(file, ast, info, fileSet) | ||
current = getPathname(filePath) | ||
for (link in references) { | ||
pathname = getPathname(link) | ||
if ( | ||
fileSet && | ||
pathname !== current && | ||
getHash(link) && | ||
links.indexOf(pathname) === -1 | ||
) { | ||
links.push(pathname) | ||
fileSet.add(pathname) | ||
} | ||
} | ||
landmarks[filePath] = true | ||
slugs.reset() | ||
visit(ast, mark) | ||
space[referenceId] = references | ||
space[landmarkId] = landmarks | ||
function mark(node) { | ||
var data = node.data || {} | ||
var props = data.hProperties || {} | ||
var id = props.name || props.id || data.id | ||
if (!id && node.type === 'heading') { | ||
id = slugs.slug(toString(node)) | ||
} | ||
if (id) { | ||
landmarks[filePath + '#' + id] = true | ||
} | ||
} | ||
} | ||
} | ||
// Check if `file` references headings or files not in `exposed`. | ||
function validate(exposed, file) { | ||
var references = file.data[referenceId] | ||
var filePath = file.path | ||
var reference | ||
var nodes | ||
var real | ||
var hash | ||
var pathname | ||
var warning | ||
var suggestion | ||
var ruleId | ||
for (reference in references) { | ||
nodes = references[reference] | ||
real = exposed[reference] | ||
hash = getHash(reference) | ||
// Check if files without `hash` can be linked to. Because there’s no need | ||
// to inspect those files for headings they are not added to remark. This | ||
// is especially useful because they might be non-markdown files. Here we | ||
// check if they exist. | ||
if ((real === undefined || real === null) && !hash && fs) { | ||
real = fs.existsSync(path.join(file.cwd, decodeURI(reference))) | ||
references[reference] = real | ||
} | ||
if (!real) { | ||
if (hash) { | ||
pathname = getPathname(reference) | ||
warning = 'Link to unknown heading' | ||
ruleId = headingRuleId | ||
if (pathname !== filePath) { | ||
warning += ' in `' + pathname + '`' | ||
ruleId = headingInFileRuleId | ||
} | ||
warning += ': `' + hash + '`' | ||
} else { | ||
warning = 'Link to unknown file: `' + decodeURI(reference) + '`' | ||
ruleId = fileRuleId | ||
} | ||
suggestion = getClosest(reference, exposed) | ||
if (suggestion) { | ||
warning += '. Did you mean `' + suggestion + '`' | ||
} | ||
warnAll(file, nodes, warning, ruleId) | ||
} | ||
} | ||
} | ||
// Gather references: a map of file-paths references to be one or more nodes. | ||
function gatherReferences(file, tree, info, fileSet) { | ||
var cache = {} | ||
var getDefinition = definitions(tree) | ||
var prefix = '' | ||
var headingPrefix = '#' | ||
var lines | ||
if (info && info.type in viewPaths) { | ||
prefix = '/' + info.path() + '/' + viewPaths[info.type] + '/' | ||
} | ||
if (info && info.type in headingPrefixes) { | ||
headingPrefix = headingPrefixes[info.type] | ||
} | ||
lines = info && info.type in lineLinks ? lineLinks[info.type] : false | ||
visit(tree, ['link', 'image', 'linkReference', 'imageReference'], onresource) | ||
return cache | ||
// Handle resources. | ||
function onresource(node) { | ||
var link = node.url | ||
var definition | ||
var index | ||
var uri | ||
var pathname | ||
var hash | ||
// Handle references. | ||
if (node.identifier) { | ||
definition = getDefinition(node.identifier) | ||
link = definition && definition.url | ||
} | ||
// Ignore definitions without url. | ||
if (!link) { | ||
return | ||
} | ||
uri = parse(link) | ||
// Drop `?search` | ||
uri.search = '' | ||
link = format(uri) | ||
if (!fileSet && (uri.hostname || uri.pathname)) { | ||
return | ||
} | ||
if (!uri.hostname) { | ||
if (lines && lineExpression.test(uri.hash)) { | ||
uri.hash = '' | ||
} | ||
// Handle hashes, or relative files. | ||
if (!uri.pathname && uri.hash) { | ||
link = file.path + uri.hash | ||
uri = parse(link) | ||
} else { | ||
link = urljoin(file.dirname, link) | ||
if (uri.hash) { | ||
link += uri.hash | ||
} | ||
uri = parse(link) | ||
} | ||
} | ||
// Handle full links. | ||
if (uri.hostname) { | ||
if (!prefix || !fileSet) { | ||
return | ||
} | ||
if ( | ||
uri.hostname !== info.domain || | ||
uri.pathname.slice(0, prefix.length) !== prefix | ||
) { | ||
return | ||
} | ||
link = uri.pathname.slice(prefix.length) + (uri.hash || '') | ||
// Things get interesting here: branches: `foo/bar/baz` could be `baz` on | ||
// the `foo/bar` branch, or, `baz` in the `bar` directory on the `foo` | ||
// branch. | ||
// Currently, we’re ignoring this and just not supporting branches. | ||
link = link.slice(link.indexOf('/') + 1) | ||
} | ||
// Handle file links, or combinations of files and hashes. | ||
index = link.indexOf(headingPrefix) | ||
if (index === -1) { | ||
pathname = link | ||
hash = null | ||
} else { | ||
pathname = link.slice(0, index) | ||
hash = link.slice(index + headingPrefix.length) | ||
if (lines && lineExpression.test(hash)) { | ||
hash = null | ||
} | ||
} | ||
if (!cache[pathname]) { | ||
cache[pathname] = [] | ||
} | ||
cache[pathname].push(node) | ||
if (hash) { | ||
link = pathname + '#' + hash | ||
if (!cache[link]) { | ||
cache[link] = [] | ||
} | ||
cache[link].push(node) | ||
} | ||
} | ||
} | ||
// Utility to warn `reason` for each node in `nodes` on `file`. | ||
function warnAll(file, nodes, reason, ruleId) { | ||
nodes.forEach(one) | ||
function one(node) { | ||
file.message(reason, node, [sourceId, ruleId].join(':')) | ||
} | ||
} | ||
// Suggest a possible similar reference. | ||
function getClosest(pathname, references) { | ||
var hash = getHash(pathname) | ||
var base = getPathname(pathname) | ||
var dictionary = [] | ||
var reference | ||
var subhash | ||
var subbase | ||
for (reference in references) { | ||
subbase = getPathname(reference) | ||
subhash = getHash(reference) | ||
if (getPathname(reference) === base) { | ||
if (subhash && hash) { | ||
dictionary.push(subhash) | ||
} | ||
} else if (!subhash && !hash) { | ||
dictionary.push(subbase) | ||
} | ||
} | ||
return propose(hash ? hash : base, dictionary, {threshold: 0.7}) | ||
} | ||
// Get the `hash` of `uri`, if applicable. | ||
function getHash(uri) { | ||
var hash = parse(uri).hash | ||
return hash ? hash.slice(1) : null | ||
} | ||
// Get the `pathname` of `uri`, if applicable. | ||
function getPathname(uri) { | ||
return parse(uri).pathname | ||
} | ||
module.exports = require('./lib') |
{ | ||
"name": "remark-validate-links", | ||
"version": "8.0.3", | ||
"version": "9.0.0", | ||
"description": "remark plugin to validate links to headings and files", | ||
@@ -30,3 +30,8 @@ "license": "MIT", | ||
], | ||
"browser": { | ||
"lib/check/check-files.js": "lib/check/check-files.browser.js", | ||
"lib/find/find-repo.js": "lib/find/find-repo.browser.js" | ||
}, | ||
"files": [ | ||
"lib/", | ||
"index.js" | ||
@@ -37,11 +42,10 @@ ], | ||
"hosted-git-info": "^2.5.0", | ||
"mdast-util-definitions": "^1.0.0", | ||
"mdast-util-to-string": "^1.0.4", | ||
"propose": "0.0.5", | ||
"trough": "^1.0.0", | ||
"unist-util-visit": "^1.0.0", | ||
"urljoin": "^0.1.5", | ||
"xtend": "^4.0.1" | ||
"xtend": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"execa": "^1.0.0", | ||
"execa": "^2.0.0", | ||
"nyc": "^14.0.0", | ||
@@ -52,2 +56,3 @@ "prettier": "^1.0.0", | ||
"remark-preset-wooorm": "^5.0.0", | ||
"rimraf": "^2.0.0", | ||
"strip-ansi": "^5.0.0", | ||
@@ -83,7 +88,3 @@ "tape": "^4.0.0", | ||
"rules": { | ||
"no-eq-null": "off", | ||
"eqeqeq": "off", | ||
"guard-for-in": "off", | ||
"max-lines": "off", | ||
"complexity": "off" | ||
"guard-for-in": "off" | ||
} | ||
@@ -90,0 +91,0 @@ }, |
@@ -19,3 +19,3 @@ # remark-validate-links | ||
In addition, when I link to a heading in another document | ||
In addition, when there’s a link to a heading in another document | ||
(`examples/foo.md#hello`), if this file exists but the heading does not, or if | ||
@@ -83,6 +83,10 @@ the file does not exist, this plugin will also warn. | ||
> Note: passing a file over stdin(4) may not work as expected, because it is not | ||
> known where the file originates from. | ||
### API | ||
> Note: The API only checks links to headings. | ||
> Other URLs are not checked. | ||
> Note: The API checks links to headings and files. | ||
> It does not check headings in other files. | ||
> In a browser, only local links to headings are checked. | ||
@@ -94,10 +98,13 @@ Say we have the following file, `example.md`: | ||
This [exists](#alpha). This [exists][alpha] too. | ||
This [exists](#alpha). | ||
This [one does not](#does-not). | ||
References and definitions are [checked][alpha] [too][charlie]. | ||
# Bravo | ||
This is [not checked](readme.md#bravo). | ||
Headings in `readme.md` are [not checked](readme.md#bravo). | ||
But [missing files are reported](missing-example.js). | ||
[alpha]: #alpha | ||
[charlie]: #charlie | ||
``` | ||
@@ -124,5 +131,7 @@ | ||
example.md | ||
4:6-4:31 warning Link to unknown heading: `does-not` remark-validate-links remark-validate-links | ||
4:6-4:31 warning Link to unknown heading: `does-not` missing-heading remark-validate-links | ||
10:5-10:53 warning Link to unknown file: `missing-example.js` missing-file remark-validate-links | ||
13:1-13:20 warning Link to unknown heading: `charlie` missing-heading remark-validate-links | ||
⚠ 1 warning | ||
⚠ 3 warnings | ||
``` | ||
@@ -132,7 +141,17 @@ | ||
You can pass a `repository`, containing anything `package.json`s | ||
[`repository`][package-repository] can handle. | ||
If this is not given, `remark-validate-links` will try the `package.json` in | ||
the current working directory. | ||
Typically, you don’t need to configure `remark-validate-links`, as it detects | ||
local Git repositories. | ||
If one is detected that references a known Git host, some extra links can be | ||
checked. | ||
If one is detected that does not reference a known Git host, local links still | ||
work as expected. | ||
If you’re not in a Git repository, you must pass `repository: false` explicitly. | ||
You can pass a `repository` (`string?`, `false`). | ||
If `repository` is nully, the Git origin remote is detected. | ||
If the repository resolves to something [npm understands][package-repository] | ||
as a Git host such as GitHub, GitLab, or Bitbucket, full URLs to that host | ||
(say, `https://github.com/remarkjs/remark-validate-links/readme.md#install`) | ||
can also be checked. | ||
```sh | ||
@@ -142,6 +161,6 @@ remark --use 'validate-links=repository:"foo/bar"' example.md | ||
When a repository is given or detected (supporting GitHub, GitLab, and | ||
Bitbucket), links to the files are normalized to the file system. | ||
For example, `https://github.com/foo/bar/blob/master/example.md` becomes | ||
`example.md`. | ||
For this to work, a `root` (`string?`) is also used, referencing the local Git | ||
root directory (the place where `.git` is). | ||
If both `root` and `repository` are nully, the Git root is detected. | ||
If `root` is not given but `repository` is, [`file.cwd`][cwd] is used. | ||
@@ -164,2 +183,31 @@ You can define this repository in [configuration files][cli] too. | ||
If you’re self-hosting a Git server, you can provide URL information directly, | ||
as `urlConfig` (`Object`). | ||
For this repository, `urlConfig` looks as follows: | ||
```js | ||
{ | ||
// Domain of URLs: | ||
hostname: 'github.com', | ||
// Path prefix before files: | ||
prefix: '/remarkjs/remark-validate-links/blob/', | ||
// Prefix of headings: | ||
headingPrefix: '#', | ||
// Whether lines in files can be linked: | ||
lines: true | ||
} | ||
``` | ||
If this project were hosted on Bitbucket, it would be: | ||
```js | ||
{ | ||
hostname: 'bitbucket.org', | ||
prefix: '/remarkjs/remark-validate-links/src/', | ||
headingPrefix: '#markdown-header-', | ||
lines: false | ||
} | ||
``` | ||
## Integration | ||
@@ -249,1 +297,3 @@ | ||
[package-repository]: https://docs.npmjs.com/files/package.json#repository | ||
[cwd]: https://github.com/vfile/vfile#vfilecwd |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25375
7
17
504
292
12
2
+ Addedtrough@^1.0.0
+ Addedtrough@1.0.5(transitive)
- Removedmdast-util-definitions@^1.0.0
- Removedurljoin@^0.1.5
- Removedextend@2.0.2(transitive)
- Removedmdast-util-definitions@1.2.5(transitive)
- Removedurljoin@0.1.5(transitive)
Updatedxtend@^4.0.0