diff --git a/index.js b/index.js index c2fb0a8..3541ac5 100644 --- a/index.js +++ b/index.js @@ -1,428 +1,3 @@ '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') diff --git a/lib/check/check-files.browser.js b/lib/check/check-files.browser.js new file mode 100644 index 0000000..7a3c8c4 --- /dev/null +++ b/lib/check/check-files.browser.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = checkIfReferencedFilesExist + +function checkIfReferencedFilesExist(ctx, next) { + next() +} diff --git a/lib/check/check-files.js b/lib/check/check-files.js new file mode 100644 index 0000000..0139998 --- /dev/null +++ b/lib/check/check-files.js @@ -0,0 +1,42 @@ +'use strict' + +var fs = require('fs') + +module.exports = checkIfReferencedFilesExist + +function checkIfReferencedFilesExist(ctx, next) { + var landmarks = ctx.landmarks + var references = ctx.references + var filePaths = [] + var filePath + var actual = 0 + var expected + + for (filePath in references) { + if (landmarks[filePath] === undefined) { + filePaths.push(filePath) + } + } + + expected = filePaths.length + + if (expected === 0) { + next() + } else { + filePaths.forEach(checkIfExists) + } + + function checkIfExists(filePath) { + fs.access(filePath, fs.F_OK, onaccess) + + function onaccess(err) { + var noEntry = err && err.code === 'ENOENT' + + landmarks[filePath] = !noEntry + + if (++actual === expected) { + next() + } + } + } +} diff --git a/lib/check/index.js b/lib/check/index.js new file mode 100644 index 0000000..cc30978 --- /dev/null +++ b/lib/check/index.js @@ -0,0 +1,13 @@ +'use strict' + +var trough = require('trough') +var mergeLandmarks = require('./merge-landmarks') +var mergeReferences = require('./merge-references') +var checkIfReferencedFilesExist = require('./check-files') +var validate = require('./validate') + +module.exports = trough() + .use(mergeLandmarks) + .use(mergeReferences) + .use(checkIfReferencedFilesExist) + .use(validate) diff --git a/lib/check/merge-landmarks.js b/lib/check/merge-landmarks.js new file mode 100644 index 0000000..3a1459b --- /dev/null +++ b/lib/check/merge-landmarks.js @@ -0,0 +1,28 @@ +'use strict' + +var constants = require('../constants') + +module.exports = mergeLandmarks + +function mergeLandmarks(ctx) { + var result = {} + var files = ctx.files + var length = files.length + var index = -1 + var file + var landmarks + var landmark + + while (++index < length) { + file = files[index] + landmarks = file.data[constants.landmarkId] + + if (landmarks) { + for (landmark in landmarks) { + result[landmark] = true + } + } + } + + ctx.landmarks = result +} diff --git a/lib/check/merge-references.js b/lib/check/merge-references.js new file mode 100644 index 0000000..f98f4e7 --- /dev/null +++ b/lib/check/merge-references.js @@ -0,0 +1,38 @@ +'use strict' + +var constants = require('../constants') + +module.exports = mergeReferences + +function mergeReferences(ctx) { + var result = {} + var files = ctx.files + var length = files.length + var index = -1 + var file + var references + var reference + + while (++index < length) { + file = files[index] + references = file.data[constants.referenceId] + + if (!references) { + continue + } + + for (reference in references) { + if (!(reference in result)) { + result[reference] = [] + } + + result[reference].push({ + file: file, + reference: reference, + nodes: references[reference] + }) + } + } + + ctx.references = result +} diff --git a/lib/check/validate.js b/lib/check/validate.js new file mode 100644 index 0000000..f747bdc --- /dev/null +++ b/lib/check/validate.js @@ -0,0 +1,105 @@ +'use strict' + +var path = require('path') +var propose = require('propose') +var constants = require('../constants') + +module.exports = validate + +function validate(ctx) { + var landmarks = ctx.landmarks + var references = ctx.references + var missing = [] + var reference + var length + var index + + for (reference in references) { + if (!landmarks[reference]) { + missing = missing.concat(references[reference]) + } + } + + length = missing.length + index = -1 + + while (++index < length) { + reference = missing[index] + warn(ctx, reference.reference, reference.file, reference.nodes) + } +} + +function warn(ctx, reference, file, nodes) { + var landmarks = ctx.landmarks + var absolute = file.path ? path.resolve(file.cwd, file.path) : '' + var base = absolute ? path.dirname(absolute) : null + var relative = base ? path.relative(base, reference) : reference + var pathname = relative + var numberSignIndex = pathname.indexOf('#') + var dictionary = [] + var subhash + var subpathname + var landmark + var relativeLandmark + var hash + var reason + var ruleId + var origin + var suggestion + + if (numberSignIndex !== -1) { + hash = pathname.slice(numberSignIndex + 1) + pathname = pathname.slice(0, numberSignIndex) + } + + if (hash) { + reason = 'Link to unknown heading' + ruleId = constants.headingRuleId + + if (base && path.join(base, pathname) !== absolute) { + reason += ' in `' + pathname + '`' + ruleId = constants.headingInFileRuleId + } + + reason += ': `' + hash + '`' + } else { + reason = 'Link to unknown file: `' + pathname + '`' + ruleId = constants.fileRuleId + } + + origin = [constants.sourceId, ruleId].join(':') + + for (landmark in landmarks) { + if (landmarks[landmark]) { + relativeLandmark = base ? path.relative(base, landmark) : landmark + subpathname = relativeLandmark + subhash = null + numberSignIndex = subpathname.indexOf('#') + + if (numberSignIndex !== -1) { + subhash = subpathname.slice(numberSignIndex + 1) + subpathname = subpathname.slice(0, numberSignIndex) + } + + if (subpathname === pathname) { + if (subhash && hash) { + dictionary.push(subhash) + } + } else if (!subhash && !hash) { + dictionary.push(subpathname) + } + } + } + + suggestion = propose(hash ? hash : pathname, dictionary, {threshold: 0.7}) + + if (suggestion) { + reason += '. Did you mean `' + suggestion + '`' + } + + nodes.forEach(one) + + function one(node) { + file.message(reason, node, origin) + } +} diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..271bbc7 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,8 @@ +'use strict' + +exports.sourceId = 'remark-validate-links' +exports.headingRuleId = 'missing-heading' +exports.headingInFileRuleId = 'missing-heading-in-file' +exports.fileRuleId = 'missing-file' +exports.landmarkId = 'remarkValidateLinksLandmarks' +exports.referenceId = 'remarkValidateLinksReferences' diff --git a/lib/find/config.js b/lib/find/config.js new file mode 100644 index 0000000..5fae954 --- /dev/null +++ b/lib/find/config.js @@ -0,0 +1,43 @@ +'use strict' + +var hostedGitInfo = require('hosted-git-info') + +module.exports = config + +var viewPaths = {github: 'blob', gitlab: 'blob', bitbucket: 'src'} +var headingPrefixes = {github: '#', gitlab: '#', bitbucket: '#markdown-header-'} +var lineLinks = {github: true, gitlab: true} + +function config(ctx) { + var repo = ctx.repository + var urlConfig = ctx.urlConfig + var info = {} + + if (urlConfig) { + return + } + + urlConfig = {prefix: '', headingPrefix: '#', lines: false, hostname: null} + + if (repo) { + info = hostedGitInfo.fromUrl(repo) + } + + if (info) { + if (info.type in viewPaths) { + urlConfig.prefix = '/' + info.path() + '/' + viewPaths[info.type] + '/' + } + + if (info.type in headingPrefixes) { + urlConfig.headingPrefix = headingPrefixes[info.type] + } + + if (info.type in lineLinks) { + urlConfig.lines = lineLinks[info.type] + } + + urlConfig.hostname = info.domain + } + + ctx.urlConfig = urlConfig +} diff --git a/lib/find/find-repo.browser.js b/lib/find/find-repo.browser.js new file mode 100644 index 0000000..9acc773 --- /dev/null +++ b/lib/find/find-repo.browser.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = findRepo + +function findRepo(ctx, next) { + next() +} diff --git a/lib/find/find-repo.js b/lib/find/find-repo.js new file mode 100644 index 0000000..06c5a8b --- /dev/null +++ b/lib/find/find-repo.js @@ -0,0 +1,76 @@ +'use strict' + +var path = require('path') +var exec = require('child_process').exec + +module.exports = findRepo + +function findRepo(ctx, next) { + var repo = ctx.repository + var file = ctx.file + var base = file.cwd + var actual = 0 + var expected = 0 + + if (file.path) { + base = path.dirname(path.resolve(base, file.path)) + } + + if (repo === null || repo === undefined) { + expected++ + exec('git remote -v', {cwd: base}, onremote) + } + + if (ctx.root === null || ctx.root === undefined) { + if (repo === null || repo === undefined) { + expected++ + exec('git rev-parse --show-cdup', {cwd: base}, oncdup) + } else { + ctx.root = ctx.file.cwd + } + } else { + ctx.root = path.resolve(file.cwd, ctx.root) + } + + if (actual === expected) { + next() + } + + function onremote(err, stdout) { + var remote + + if (err) { + expected = Infinity + return next(err) + } + + remote = stdout.match(/origin\t(.+?) \(fetch\)/) + ctx.repository = remote ? remote[1] : null + + if (!ctx.repository) { + expected = Infinity + return next(new Error('Could not find remote origin')) + } + + if (++actual === expected) { + expected = Infinity + next() + } + } + + function oncdup(err, stdout) { + var out + + if (err) { + expected = Infinity + return next(err) + } + + out = stdout.trim() + ctx.root = out ? path.join(base, out) : base + + if (++actual === expected) { + next() + } + } +} diff --git a/lib/find/find.js b/lib/find/find.js new file mode 100644 index 0000000..d624654 --- /dev/null +++ b/lib/find/find.js @@ -0,0 +1,178 @@ +'use strict' + +var path = require('path') +var vfile = require('to-vfile') +var visit = require('unist-util-visit') +var toString = require('mdast-util-to-string') +var xtend = require('xtend') +var slugs = require('github-slugger')() +var constants = require('../constants') + +module.exports = find + +var slash = '/' +var numberSign = '#' +var questionMark = '?' + +var https = 'https:' +var http = 'http:' +var slashes = '//' + +var lineExpression = /^#l\d/i + +function find(ctx) { + var file = ctx.file + var fileSet = ctx.fileSet + var absolute = file.path ? path.resolve(file.cwd, file.path) : '' + var space = file.data + var references = {} + var landmarks = {} + var alreadyAdded = [] + var config = xtend(ctx.urlConfig, { + path: absolute, + base: absolute ? path.dirname(absolute) : file.cwd, + root: ctx.root + }) + + space[constants.referenceId] = references + space[constants.landmarkId] = landmarks + + landmarks[absolute] = true + + slugs.reset() + + visit(ctx.tree, mark) + + // eslint-disable-next-line complexity + function mark(node) { + var data = node.data || {} + var props = data.hProperties || {} + var id = props.name || props.id || data.id + var filePath = node.url ? urlToPath(node.url, config) : null + var numberSignIndex + var refs + var fp + var hash + + if (!id && node.type === 'heading') { + id = slugs.slug(toString(node)) + } + + if (id) { + landmarks[absolute + numberSign + id] = true + } + + if (filePath) { + numberSignIndex = filePath.indexOf(numberSign) + fp = filePath + + if (numberSignIndex !== -1) { + fp = filePath.slice(0, numberSignIndex) + hash = filePath.slice(numberSignIndex).toLowerCase() + + // Ignore the hash if it references lines in a file or doesn’t start + // with a heading prefix. + if ( + (config.lines && lineExpression.test(hash)) || + hash.slice(0, config.headingPrefix.length) !== config.headingPrefix + ) { + numberSignIndex = -1 + } + // Use the hash if it starts with a heading prefix. + else { + filePath = fp + numberSign + hash.slice(config.headingPrefix.length) + } + } + + refs = references[fp] || (references[fp] = []) + refs.push(node) + + // With a heading + if (numberSignIndex !== -1) { + // Add with heading. + if (fileSet || fp === absolute) { + refs = references[filePath] || (references[filePath] = []) + refs.push(node) + } + + if ( + fileSet && + alreadyAdded.indexOf(fp) === -1 && + numberSignIndex !== 0 + ) { + alreadyAdded.push(fp) + fileSet.add(vfile({cwd: file.cwd, path: path.relative(file.cwd, fp)})) + } + } + } + } +} + +function urlToPath(value, config) { + var url + var questionMarkIndex + var numberSignIndex + + // Absolute paths: `/wooorm/test/blob/master/directory/example.md`. + if (value.charAt(0) === slash) { + if (!config.hostname) { + return + } + + // Create a URL. + value = https + slashes + config.hostname + value + } + + try { + url = new URL(value) + } catch (error) {} + + // URLs: `https://github.com/wooorm/test/blob/master/directory/example.md`. + if (url) { + // Exit if we don’t have hosted Git info or this is not a URL to the repo. + if ( + !config.prefix || + !config.hostname || + (url.protocol !== https && url.protocol !== http) || + url.hostname !== config.hostname || + url.pathname.slice(0, config.prefix.length) !== config.prefix + ) { + return + } + + value = url.pathname.slice(config.prefix.length) + + // 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. + value = value + .split(slash) + .slice(1) + .join(slash) + + return path.resolve(config.root, value + url.hash) + } + + // Remove the search: `?foo=bar`. + // But don’t remove stuff if it’s in the hash: `readme.md#heading?`. + numberSignIndex = value.indexOf(numberSign) + questionMarkIndex = value.indexOf(questionMark) + + if ( + questionMarkIndex !== -1 && + (numberSignIndex === -1 || numberSignIndex > questionMarkIndex) + ) { + value = + value.slice(0, questionMarkIndex) + + (numberSignIndex === -1 ? '' : value.slice(numberSignIndex)) + } + + // Local: `#heading`. + if (value.charAt(0) === numberSign) { + return config.path ? config.path + value : value + } + + // Anything else, such as `readme.md`. + return config.path ? path.resolve(config.base, value) : '' +} diff --git a/lib/find/index.js b/lib/find/index.js new file mode 100644 index 0000000..aacf065 --- /dev/null +++ b/lib/find/index.js @@ -0,0 +1,11 @@ +'use strict' + +var trough = require('trough') +var findRepo = require('./find-repo') +var config = require('./config') +var find = require('./find') + +module.exports = trough() + .use(findRepo) + .use(config) + .use(find) diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..6602677 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,48 @@ +'use strict' + +var xtend = require('xtend') +var check = require('./check') +var find = require('./find') +var constants = require('./constants') + +module.exports = validateLinks + +cliCompleter.pluginId = constants.sourceId + +function validateLinks(options, fileSet) { + var settings = options || {} + + // Attach a `completer`. + if (fileSet) { + fileSet.use(cliCompleter) + } + + return transformer + + // Find references and landmarks. + function transformer(tree, file, next) { + find.run(xtend(settings, {tree: tree, file: file, fileSet: fileSet}), done) + + function done(err) { + if (err) { + next(err) + } else if (fileSet) { + next() + } else { + checkAll([file], next) + } + } + } +} + +// Completer for the CLI (multiple files, supports parsing more files). +function cliCompleter(set, next) { + checkAll(set.valueOf(), next) +} + +function checkAll(files, next) { + // Check all references and landmarks. + check.run({files: files}, function(err) { + next(err) + }) +} diff --git a/package.json b/package.json index 399989d..6b8767c 100644 --- a/package.json +++ b/package.json @@ -27,18 +27,22 @@ "wangshijun ", "Anton Rybochkin " ], + "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" ], "dependencies": { "github-slugger": "^1.2.0", "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", @@ -47,6 +51,7 @@ "remark": "^10.0.0", "remark-cli": "^6.0.0", "remark-preset-wooorm": "^5.0.0", + "rimraf": "^2.0.0", "strip-ansi": "^5.0.0", "tape": "^4.0.0", "to-vfile": "^6.0.0", @@ -77,11 +82,7 @@ "prettier": true, "esnext": false, "rules": { - "no-eq-null": "off", - "eqeqeq": "off", - "guard-for-in": "off", - "max-lines": "off", - "complexity": "off" + "guard-for-in": "off" } }, "remarkConfig": { diff --git a/readme.md b/readme.md index d7c00c6..02b0704 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ for that). For example, this document does not have a heading named `Hello`. So if we link to that (`[welcome](#hello)`), this plugin will warn about it. -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 the file does not exist, this plugin will also warn. @@ -79,24 +79,31 @@ readme.md: no issues found ⚠ 2 warnings ``` +> 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. Say we have the following file, `example.md`: ```markdown # Alpha -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 ``` And our script, `example.js`, looks as follows: @@ -118,26 +125,38 @@ Now, running `node example` yields: ```markdown 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 ``` ## Configuration -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 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. You can define this repository in [configuration files][cli] too. An example `.remarkrc` file could look as follows: @@ -155,6 +174,35 @@ An example `.remarkrc` file could look as follows: } ``` +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 `remark-validate-links` can detect anchors on nodes through several properties @@ -239,3 +287,5 @@ abide by its terms. [no-dead-urls]: https://github.com/davidtheclark/remark-lint-no-dead-urls [package-repository]: https://docs.npmjs.com/files/package.json#repository + +[cwd]: https://github.com/vfile/vfile#vfilecwd diff --git a/test/fixtures/bitbucket.md b/test/fixtures/bitbucket.md index 77809e5..b6fbc17 100644 --- a/test/fixtures/bitbucket.md +++ b/test/fixtures/bitbucket.md @@ -4,6 +4,8 @@ This is a valid relative heading [link](#markdown-header-hello). This is an invalid relative heading [link](#markdown-header-world). +This is an ignored hash [link](#world). + ## Files This is a valid relative file [link](https://bitbucket.org/wooorm/test/src/master/examples/bitbucket.md). @@ -50,6 +52,8 @@ Invalid: [k](https://bitbucket.org/wooorm/test/src/master/examples/world.md#mark Invalid: [l](https://bitbucket.org/wooorm/test/src/foo-bar/examples/world.md#markdown-header-hello). +This is an ignored hash [j](https://bitbucket.org/wooorm/test/src/foo-bar/examples/bitbucket.md#world). + ## External These are all invalid, because they do not link to Bitbucket. diff --git a/test/fixtures/case-insensitive-headings.md b/test/fixtures/case-insensitive-headings.md new file mode 100644 index 0000000..23c9c38 --- /dev/null +++ b/test/fixtures/case-insensitive-headings.md @@ -0,0 +1,9 @@ +# Hello + +This is fine: [link](#HELLO). + +This isn’t: [link](#WORLD). + +Also fine: [link](examples/github.md#HELLO). + +Also not fine: [link](examples/github.md#WORLD). diff --git a/test/fixtures/empty.md b/test/fixtures/empty.md new file mode 100644 index 0000000..e41bef0 --- /dev/null +++ b/test/fixtures/empty.md @@ -0,0 +1 @@ +No links! diff --git a/test/fixtures/github.md b/test/fixtures/github.md index 5c56074..7ef3b08 100644 --- a/test/fixtures/github.md +++ b/test/fixtures/github.md @@ -8,6 +8,8 @@ This is an invalid relative heading [link](#world). This is a valid relative file [link](https://github.com/wooorm/test/blob/master/examples/github.md). +This is a valid absolute file [link](/wooorm/test/blob/master/examples/github.md). + So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/github.md). And this [link](./examples/github.md). @@ -18,6 +20,8 @@ This is a valid external [file](../index.js). This is an invalid relative file [link](https://github.com/wooorm/test/blob/master/examples/world.md). +This is an invalid absolute file [link](/wooorm/test/blob/master/examples/world.md). + So is this [link](https://github.com/wooorm/test/blob/foo-bar/examples/world.md). And this [link](./examples/world.md). diff --git a/test/fixtures/query-params.md b/test/fixtures/query-params.md index 699888b..eb5027e 100644 --- a/test/fixtures/query-params.md +++ b/test/fixtures/query-params.md @@ -5,3 +5,5 @@ Link to relative heading ![link](?foo=bar#query-params) Link to an ![image](./examples/image.jpg?foo=bar) And a file [link](./examples/github.md?foo=bar). + +Question mark in hash (invalid) [link](#query-params?). diff --git a/test/fixtures/self-hosted.md b/test/fixtures/self-hosted.md new file mode 100644 index 0000000..c5de4b2 --- /dev/null +++ b/test/fixtures/self-hosted.md @@ -0,0 +1,5 @@ +# Hello + +[Fine](https://gitlab.acme.com/acme/project/blob/master/self-hosted.md#hello); + +[Not fine](https://gitlab.acme.com/acme/project/blob/master/self-hosted.md#world); diff --git a/test/fixtures/small.md b/test/fixtures/small.md new file mode 100644 index 0000000..386c8a9 --- /dev/null +++ b/test/fixtures/small.md @@ -0,0 +1,5 @@ +# Hello + +This is fine: [link](#hello). + +This isn’t: [link](#world). diff --git a/test/index.js b/test/index.js index 9498634..6f62628 100644 --- a/test/index.js +++ b/test/index.js @@ -7,6 +7,7 @@ var execa = require('execa') var vfile = require('to-vfile') var remark = require('remark') var strip = require('strip-ansi') +var rimraf = require('rimraf') var sort = require('./sort') var links = require('..') @@ -28,13 +29,20 @@ test('remark-validate-links', function(t) { .process(vfile.readSync('github.md'), function(err, file) { st.deepEqual( [err].concat(file.messages.map(String)), - [null, 'github.md:5:37-5:51: Link to unknown heading: `world`'], + [ + null, + 'github.md:5:37-5:51: Link to unknown heading: `world`', + 'github.md:27:10-27:37: Link to unknown file: `examples/world.md`', + 'github.md:29:10-29:35: Link to unknown file: `examples/world.md`', + 'github.md:49:10-49:40: Link to unknown file: `examples/world.md`', + 'github.md:51:10-51:38: Link to unknown file: `examples/world.md`' + ], 'should report messages' ) }) }) - t.test('should throw on unparsable git repositories', function(st) { + t.test('should ignore invalid repositories', function(st) { st.plan(1) execa(bin, [ @@ -44,51 +52,80 @@ test('remark-validate-links', function(t) { '../..=repository:"invalid:shortcode"', '--use', '../sort', - 'definitions.md' + 'small.md' ]).then( - function() { - st.fail('should not be successful') + function(result) { + st.equal( + strip(result.stderr), + [ + 'small.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should work' + ) }, function(err) { + st.error(err) + } + ) + }) + + t.test('should throw on Gist repositories', function(st) { + st.plan(1) + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=repository:"gist:wooorm/8504606"', + '--use', + '../sort', + 'small.md' + ]).then( + function(result) { st.equal( - strip(err.stderr) - .split('\n') - .slice(0, 2) - .join('\n'), + strip(result.stderr), [ - 'definitions.md', - ' 1:1 error Error: remark-validate-links cannot parse `repository` (`invalid:shortcode`)' + 'small.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' ].join('\n'), - 'should report an error' + 'should work' ) + }, + function(err) { + st.error(err, 'should not fail') } ) }) - t.test('should throw on gist repositories', function(st) { + t.test('should ignore unfound files (#1)', function(st) { st.plan(1) execa(bin, [ '--no-config', '--no-ignore', '--use', - '../..=repository:"gist:wooorm/8504606"', + '../..', '--use', '../sort', - 'definitions.md' + 'FOOOO' ]).then( function() { st.fail('should not be successful') }, function(err) { st.equal( - strip(err.stderr) - .split('\n') - .slice(0, 2) - .join('\n'), + strip(err.stderr), [ - 'definitions.md', - ' 1:1 error Error: remark-validate-links does not support gist repositories' + 'FOOOO', + ' 1:1 error No such file or directory', + '', + '✖ 1 error', + '' ].join('\n'), 'should report an error' ) @@ -96,7 +133,7 @@ test('remark-validate-links', function(t) { ) }) - t.test('should ignore unfound files', function(st) { + t.test('should ignore unfound files (#2)', function(st) { st.plan(1) execa(bin, [ @@ -104,9 +141,9 @@ test('remark-validate-links', function(t) { '--no-ignore', '--use', '../..', - 'definitions.md', '--use', '../sort', + 'definitions.md', 'FOOOO' ]).then( function() { @@ -117,10 +154,10 @@ test('remark-validate-links', function(t) { strip(err.stderr), [ 'FOOOO', - ' 1:1 error No such file or directory', + ' 1:1 error No such file or directory', '', 'definitions.md', - ' 5:12-5:21 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 10:1-10:18 warning Link to unknown heading: `world` missing-heading remark-validate-links', '', '2 messages (✖ 1 error, ⚠ 1 warning)', '' @@ -131,6 +168,54 @@ test('remark-validate-links', function(t) { ) }) + t.test('should work if there are no links', function(st) { + st.plan(1) + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..', + '--use', + '../sort', + 'empty.md' + ]).then(function(result) { + st.equal( + strip(result.stderr), + 'empty.md: no issues found', + 'should report' + ) + }, st.error) + }) + + t.test('should work with stdin', function(st) { + st.plan(1) + + var subprocess = execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..', + '--use', + '../sort' + ]) + + fs.createReadStream('github.md').pipe(subprocess.stdin) + + subprocess.then(function(result) { + st.equal( + strip(result.stderr), + [ + '', + ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should report an error' + ) + }, st.error) + }) + t.test('should work when not all files are given', function(st) { st.plan(1) @@ -148,14 +233,14 @@ test('remark-validate-links', function(t) { [ 'github.md', ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 23:10-23:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 25:10-25:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 37:10-37:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 39:10-39:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', '', '⚠ 9 warnings' ].join('\n'), @@ -182,21 +267,21 @@ test('remark-validate-links', function(t) { [ 'examples/github.md', ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 19:10-19:29 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 29:10-29:33 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 35:10-35:32 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 35:10-35:32 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', '', 'github.md', ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 23:10-23:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 25:10-25:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 37:10-37:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 39:10-39:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', '', '⚠ 14 warnings' ].join('\n'), @@ -221,7 +306,7 @@ test('remark-validate-links', function(t) { strip(result.stderr), [ 'definitions.md', - ' 5:12-5:21 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 10:1-10:18 warning Link to unknown heading: `world` missing-heading remark-validate-links', '', '⚠ 1 warning' ].join('\n'), @@ -248,57 +333,134 @@ test('remark-validate-links', function(t) { [ 'examples/github.md', ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 15:34-15:93 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 17:12-17:72 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 19:10-19:29 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 29:10-29:33 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 31:10-31:73 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 33:10-33:74 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 35:10-35:32 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 35:10-35:32 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 37:10-37:72 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 37:10-37:72 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 39:10-39:73 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 39:10-39:73 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 15:34-15:93 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 17:12-17:72 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 31:10-31:73 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 33:10-33:74 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 37:10-37:72 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 37:10-37:72 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 39:10-39:73 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 39:10-39:73 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', '', 'github.md', ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 19:34-19:102 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 21:12-21:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 23:10-23:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 25:10-25:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 37:10-37:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 39:10-39:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 41:10-41:82 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 43:10-43:83 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 49:10-49:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 49:10-49:81 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 51:10-51:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 51:10-51:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 21:34-21:102 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 23:34-23:84 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 25:12-25:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 45:10-45:82 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 47:10-47:83 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 53:10-53:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 53:10-53:81 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 55:10-55:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 55:10-55:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', '', - '⚠ 30 warnings' + '⚠ 31 warnings' ].join('\n'), 'should report' ) }, st.error) }) - t.test('should work on GitHub URLs when with package.json', function(st) { + t.test('should work when with Git directory', function(st) { st.plan(1) - // `cwd` is moved to `test/fixtures`. - fs.writeFileSync( - './package.json', - JSON.stringify({repository: 'wooorm/test'}, 0, 2) - ) + Promise.resolve() + .then(function() { + return execa('git', ['init']) + }) + .then(function() { + return execa('git', [ + 'remote', + 'add', + 'origin', + 'git@github.com:wooorm/test.git' + ]) + }) + .then(function() { + return execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..', + '--use', + '../sort', + 'github.md', + 'examples/github.md' + ]) + }) + .then( + function(result) { + clean() + st.equal( + strip(result.stderr), + [ + 'examples/github.md', + ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 15:34-15:93 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 17:12-17:72 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 31:10-31:73 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 33:10-33:74 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 37:10-37:72 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 37:10-37:72 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 39:10-39:73 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 39:10-39:73 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', + '', + 'github.md', + ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 21:34-21:102 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 23:34-23:84 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 25:12-25:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 45:10-45:82 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 47:10-47:83 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 53:10-53:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 53:10-53:81 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 55:10-55:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 55:10-55:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + '', + '⚠ 31 warnings' + ].join('\n'), + 'should report' + ) + }, + function(err) { + clean() + st.error(err) + } + ) function clean() { - fs.unlinkSync('./package.json') + rimraf.sync('./.git') } + }) + + t.test('should fail w/o Git repository', function(st) { + st.plan(1) + + fs.renameSync('../../.git', '../../.git.bak') execa(bin, [ '--no-config', @@ -307,6 +469,177 @@ test('remark-validate-links', function(t) { '../..', '--use', '../sort', + 'github.md' + ]).then( + function() { + clean() + st.fail('should not work') + }, + function(err) { + clean() + st.ok(/not a git repository/.test(err), 'should fail') + } + ) + + function clean() { + fs.renameSync('../../.git.bak', '../../.git') + } + }) + + t.test('should fail w/o Git repository w/o remote', function(st) { + st.plan(1) + + Promise.resolve() + .then(function() { + return execa('git', ['init']) + }) + .then(function() { + return execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..', + 'github.md' + ]) + }) + .then( + function() { + clean() + st.fail('should not work') + }, + function(err) { + clean() + st.ok(/Could not find remote origin/.test(err), 'should fail') + } + ) + + function clean() { + rimraf.sync('./.git') + } + }) + + t.test('should work w/o Git repository w/ repo', function(st) { + st.plan(1) + + fs.renameSync('../../.git', '../../.git.bak') + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=repository:"wooorm/test"', + '--use', + '../sort', + 'small.md' + ]).then( + function(result) { + clean() + st.equal( + strip(result.stderr), + [ + 'small.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should work' + ) + }, + function(err) { + clean() + st.error(err, 'shoult not fail') + } + ) + + function clean() { + fs.renameSync('../../.git.bak', '../../.git') + } + }) + + t.test('should work w/o Git repository w/ remote', function(st) { + st.plan(1) + + fs.renameSync('../../.git', '../../.git.bak') + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=repository:{remote:"wooorm/test"}', + 'small.md' + ]).then( + function(result) { + clean() + st.equal( + strip(result.stderr), + [ + 'small.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should work' + ) + }, + function(err) { + clean() + st.error(err, 'shoult not fail') + } + ) + + function clean() { + fs.renameSync('../../.git.bak', '../../.git') + } + }) + + t.test('should work w/o Git repository w/ remote and root', function(st) { + st.plan(1) + + fs.renameSync('../../.git', '../../.git.bak') + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=repository:"wooorm/test",root:"../"', + 'small.md' + ]).then( + function(result) { + clean() + st.equal( + strip(result.stderr), + [ + 'small.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should work' + ) + }, + function(err) { + clean() + st.error(err, 'shoult not fail') + } + ) + + function clean() { + fs.renameSync('../../.git.bak', '../../.git') + } + }) + + t.test('should work with `repository:false`', function(st) { + st.plan(1) + + fs.renameSync('../../.git', '../../.git.bak') + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=repository:false', + '--use', + '../sort', 'github.md', 'examples/github.md' ]).then( @@ -316,40 +649,24 @@ test('remark-validate-links', function(t) { strip(result.stderr), [ 'examples/github.md', - ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 15:34-15:93 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 17:12-17:72 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 19:10-19:29 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 29:10-29:33 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 31:10-31:73 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 33:10-33:74 warning Link to unknown heading in `github.md`: `world` missing-heading-in-file remark-validate-links', - ' 35:10-35:32 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 35:10-35:32 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 37:10-37:72 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 37:10-37:72 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 39:10-39:73 warning Link to unknown file: `world.md` missing-file remark-validate-links', - ' 39:10-39:73 warning Link to unknown heading in `world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 19:10-19:29 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 29:10-29:33 warning Link to unknown heading in `../github.md`: `world` missing-heading-in-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown file: `../world.md` missing-file remark-validate-links', + ' 35:10-35:32 warning Link to unknown heading in `../world.md`: `hello` missing-heading-in-file remark-validate-links', '', 'github.md', - ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 19:34-19:102 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 21:12-21:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 23:10-23:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 25:10-25:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 37:10-37:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 39:10-39:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 41:10-41:82 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 43:10-43:83 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 45:10-45:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 47:10-47:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 49:10-49:81 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 49:10-49:81 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 51:10-51:82 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 51:10-51:82 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 5:37-5:51 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 27:10-27:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 29:10-29:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 41:10-41:41 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 43:10-43:39 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 49:10-49:40 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 51:10-51:38 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', '', - '⚠ 30 warnings' + '⚠ 14 warnings' ].join('\n'), 'should report' ) @@ -359,6 +676,35 @@ test('remark-validate-links', function(t) { st.error(err) } ) + + function clean() { + fs.renameSync('../../.git.bak', '../../.git') + } + }) + + t.test('should work when finding non-hosted Git remotes', function(st) { + st.plan(1) + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=repository:"ssh://git@domain.com/user/project.git"', + '--use', + '../sort', + 'small.md' + ]).then(function(result) { + st.equal( + strip(result.stderr), + [ + 'small.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should report' + ) + }, st.error) }) t.test('should support a GitLab shortcode', function(st) { @@ -419,22 +765,22 @@ test('remark-validate-links', function(t) { [ 'bitbucket.md', ' 5:37-5:67 warning Link to unknown heading: `world` missing-heading remark-validate-links', - ' 19:34-19:104 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 21:12-21:83 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 23:10-23:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 25:10-25:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 37:10-37:60 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', - ' 39:10-39:58 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', - ' 41:10-41:103 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', - ' 43:10-43:104 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', - ' 45:10-45:56 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 45:10-45:56 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 47:10-47:54 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 47:10-47:54 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 49:10-49:99 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 49:10-49:99 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', - ' 51:10-51:100 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', - ' 51:10-51:100 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 21:34-21:104 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 23:12-23:83 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 25:10-25:37 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 27:10-27:35 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 39:10-39:60 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', + ' 41:10-41:58 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', + ' 43:10-43:103 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', + ' 45:10-45:104 warning Link to unknown heading in `examples/bitbucket.md`: `world` missing-heading-in-file remark-validate-links', + ' 47:10-47:56 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 47:10-47:56 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 49:10-49:54 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 49:10-49:54 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 51:10-51:99 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 51:10-51:99 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', + ' 53:10-53:100 warning Link to unknown file: `examples/world.md` missing-file remark-validate-links', + ' 53:10-53:100 warning Link to unknown heading in `examples/world.md`: `hello` missing-heading-in-file remark-validate-links', '', '⚠ 17 warnings' ].join('\n'), @@ -469,7 +815,7 @@ test('remark-validate-links', function(t) { }, st.error) }) - t.test('should recognize github links to particular lines', function(st) { + t.test('should recognise links to particular lines', function(st) { st.plan(1) execa(bin, [ @@ -511,11 +857,11 @@ test('remark-validate-links', function(t) { strip(result.stderr), [ 'images.md', - ' 19:10-19:50 warning Link to unknown file: `examples/missing.jpg` missing-file remark-validate-links', - ' 21:12-21:42 warning Link to unknown file: `examples/missing.jpg` missing-file remark-validate-links', - ' 23:10-23:91 warning Link to unknown file: `examples/missing.jpg` missing-file remark-validate-links', - ' 25:10-25:49 warning Link to unknown file: `examples/missing.jpg` missing-file remark-validate-links', - ' 27:10-27:49 warning Link to unknown file: `examples/missing.jpg` missing-file remark-validate-links', + ' 19:10-19:50 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links', + ' 21:12-21:42 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links', + ' 23:10-23:91 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links', + ' 35:1-35:38 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links', + ' 37:1-37:79 warning Link to unknown file: `examples/missing.jpg`. Did you mean `examples/image.jpg` missing-file remark-validate-links', '', '⚠ 5 warnings' ].join('\n'), @@ -535,13 +881,103 @@ test('remark-validate-links', function(t) { '--use', '../sort', 'query-params.md' - ]).then(function(result) { - st.equal( - strip(result.stderr), - 'query-params.md: no issues found', - 'should report' + ]).then( + function(result) { + st.equal( + strip(result.stderr), + [ + 'query-params.md', + ' 9:33-9:55 warning Link to unknown heading: `query-params?`. Did you mean `query-params` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should report' + ) + }, + function(err) { + st.error(err, 'should not fail') + } + ) + }) + + t.test('should support query parameters', function(st) { + st.plan(1) + + execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..', + '--use', + '../sort', + 'case-insensitive-headings.md' + ]).then( + function(result) { + st.equal( + strip(result.stderr), + [ + 'case-insensitive-headings.md', + ' 5:13-5:27 warning Link to unknown heading: `world` missing-heading remark-validate-links', + ' 9:16-9:48 warning Link to unknown heading in `examples/github.md`: `world` missing-heading-in-file remark-validate-links', + '', + '⚠ 2 warnings' + ].join('\n'), + 'should report' + ) + }, + function(err) { + st.error(err, 'should not fail') + } + ) + }) + + t.test('should support self-hosted Git solutions', function(st) { + st.plan(1) + + Promise.resolve() + .then(function() { + return execa('git', ['init']) + }) + .then(function() { + return execa('git', [ + 'remote', + 'add', + 'origin', + 'git@gitlab.acme.company:acme/project.git' + ]) + }) + .then(function() { + return execa(bin, [ + '--no-config', + '--no-ignore', + '--use', + '../..=urlConfig:{hostname:"gitlab.acme.com",prefix:"/acme/project/blob/",headingPrefix:"#",lines:true}', + 'self-hosted.md' + ]) + }) + .then( + function(result) { + clean() + st.equal( + strip(result.stderr), + [ + 'self-hosted.md', + ' 5:1-5:82 warning Link to unknown heading: `world` missing-heading remark-validate-links', + '', + '⚠ 1 warning' + ].join('\n'), + 'should report' + ) + }, + function(err) { + clean() + st.error(err) + } ) - }, st.error) + + function clean() { + rimraf.sync('./.git') + } }) t.end()