diff --git a/lib/find/find.js b/lib/find/find.js index f47d558..66d605e 100644 --- a/lib/find/find.js +++ b/lib/find/find.js @@ -1,5 +1,6 @@ 'use strict' +var fs = require('fs') var path = require('path') var URL = require('url').URL // Node 8 support var vfile = require('to-vfile') @@ -21,14 +22,21 @@ var slashes = '//' var lineExpression = /^#l\d/i -function find(ctx) { +// List from: https://github.com/github/markup#markups +var readmeExtensions = ['.markdown', '.mdown', '.mkdn', '.md'] +var readmeBasename = /^readme$/i + +function find(ctx, next) { 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 actual = 0 + var expected = 0 + var statted = [] + var added = [] var config = xtend(ctx.urlConfig, { path: absolute, base: absolute ? path.dirname(absolute) : file.cwd, @@ -38,18 +46,21 @@ function find(ctx) { space[constants.referenceId] = references space[constants.landmarkId] = landmarks - landmarks[absolute] = {'': true} + addLandmarks(absolute, '') slugs.reset() visit(ctx.tree, mark) + if (expected === 0) { + next() + } + function mark(node) { var data = node.data || {} var props = data.hProperties || {} var id = props.name || props.id || data.id var info = node.url ? urlToPath(node.url, config) : null - var refs var fp var hash @@ -58,34 +69,114 @@ function find(ctx) { } if (id) { - landmarks[absolute][id] = true + addLandmarks(absolute, id) } if (info) { fp = info.filePath hash = info.hash - refs = references[fp] || (references[fp] = {}) - add('', node) + addReference(fp, '', node) - // With a heading if (hash) { if (fileSet || fp === absolute) { - add(hash, node) + addReference(fp, hash, node) } - if (fileSet && fp && alreadyAdded.indexOf(fp) === -1) { - alreadyAdded.push(fp) - fileSet.add(vfile({cwd: file.cwd, path: path.relative(file.cwd, fp)})) + if (fileSet && fp && statted.indexOf(fp) === -1) { + addFile(fp) } } } + } + + function addLandmarks(filePath, hash) { + addLandmark(filePath, hash) + + // Note: this may add marks too many anchors as defined. + // For example, if there is both a `readme.md` and a `readme.markdown` in a + // folder, both their landmarks will be defined for their parent folder. + // To solve this, we could check whichever sorts first, and ignore the + // others. + // This is an unlikely scenario though, and adds a lot of complexity, so + // we’re ignoring it. + if (readme(filePath)) { + addLandmark(path.dirname(filePath), hash) + } + } + + function addLandmark(filePath, hash) { + var marks = landmarks[filePath] || (landmarks[filePath] = {}) + + marks[hash] = true + } + + function addReference(filePath, hash, node) { + var refs = references[filePath] || (references[filePath] = {}) + var hashes = refs[hash] || (refs[hash] = []) + + hashes.push(node) + } + + function addFile(fileOrFolderPath) { + expected++ - function add(hash, node) { - if (refs[hash]) { - refs[hash].push(node) + statted.push(fileOrFolderPath) + + fs.stat(fileOrFolderPath, onstat) + + function onstat(_, stats) { + if (stats && stats.isDirectory()) { + fs.readdir(fileOrFolderPath, onreaddir) } else { - refs[hash] = [node] + done(fileOrFolderPath) + } + } + + function onreaddir(_, entries) { + /* istanbul ignore next - unlikely that it is an unreadable directory. */ + var files = (entries || []).sort() + var length = files.length + var index = -1 + var entry + var file + var filePath + + while (++index < length) { + entry = entries[index] + + if (readme(entry)) { + file = entry + break + } + } + + // If there is no readme that we can parse, add the directory. + filePath = fileOrFolderPath + + // To do: test for no readme in directory. + + if (file) { + filePath = path.join(fileOrFolderPath, file) + statted.push(filePath) + } + + done(filePath) + } + + function done(filePath) { + if (added.indexOf(filePath) === -1) { + added.push(filePath) + + fileSet.add( + vfile({cwd: file.cwd, path: path.relative(file.cwd, filePath)}) + ) + } + + actual++ + + if (actual === expected) { + next() } } } @@ -197,3 +288,12 @@ function normalize(url, config) { return {filePath: decodeURIComponent(filePath), hash: hash} } + +function readme(filePath) { + var ext = path.extname(filePath) + + return ( + readmeExtensions.indexOf(ext) !== -1 && + readmeBasename.test(path.basename(filePath, ext)) + ) +} diff --git a/test/fixtures/folder-without-readme/example.md b/test/fixtures/folder-without-readme/example.md new file mode 100644 index 0000000..f023e4c --- /dev/null +++ b/test/fixtures/folder-without-readme/example.md @@ -0,0 +1 @@ +This file isn’t used when linking to the folder: it has to be a `readme`. diff --git a/test/fixtures/folder.md b/test/fixtures/folder.md new file mode 100644 index 0000000..eb473ec --- /dev/null +++ b/test/fixtures/folder.md @@ -0,0 +1,23 @@ +Here we link to a folder: + +Explicit: + +[OK](./folder/readme.markdown#this) + +[OK](./folder/readme.markdown#that) + +[NOK](./folder/readme.markdown#missing) + +Implicit: + +[OK](./folder#this) + +[OK](./folder#that) + +[NOK](./folder#missing) + +Other: + +[NOK](./missing#missing) + +[NOK](./folder-without-readme#missing) diff --git a/test/fixtures/folder/example.md b/test/fixtures/folder/example.md new file mode 100644 index 0000000..f023e4c --- /dev/null +++ b/test/fixtures/folder/example.md @@ -0,0 +1 @@ +This file isn’t used when linking to the folder: it has to be a `readme`. diff --git a/test/fixtures/folder/readme.asc b/test/fixtures/folder/readme.asc new file mode 100644 index 0000000..1ce4b56 --- /dev/null +++ b/test/fixtures/folder/readme.asc @@ -0,0 +1,9 @@ +readme +------ + +While GitHub would pick up on this readme for the folder, +`remark-validate-links` is for Markdown, so we ignore AsciiDoc. + +== This + +== That diff --git a/test/fixtures/folder/readme.markdown b/test/fixtures/folder/readme.markdown new file mode 100644 index 0000000..74fabf2 --- /dev/null +++ b/test/fixtures/folder/readme.markdown @@ -0,0 +1,5 @@ +# readme + +## This + +## That diff --git a/test/fixtures/folder/readme.md b/test/fixtures/folder/readme.md new file mode 100644 index 0000000..d29d2f6 --- /dev/null +++ b/test/fixtures/folder/readme.md @@ -0,0 +1 @@ +This one isn’t used (`readme.markdown` sorts before `readme.md`). diff --git a/test/index.js b/test/index.js index 5ee556a..413c9c9 100644 --- a/test/index.js +++ b/test/index.js @@ -1004,6 +1004,45 @@ test('remark-validate-links', function(t) { } }) + t.test('should support folders', function(st) { + st.plan(1) + + childProcess.exec( + [ + bin, + '--no-config', + '--no-ignore', + '--use', + '"../..=repository:\\"wooorm/test\\""', + '--use', + '../sort', + 'folder.md' + ].join(' '), + onexec + ) + + function onexec(err, stdout, stderr) { + st.deepEqual( + [err, strip(stderr)], + [ + null, + [ + 'folder.md', + ' 9:1-9:40 warning Link to unknown heading in `folder/readme.markdown`: `missing` missing-heading-in-file remark-validate-links', + ' 17:1-17:24 warning Link to unknown heading in `folder`: `missing` missing-heading-in-file remark-validate-links', + ' 21:1-21:25 warning Link to unknown file: `missing` missing-file remark-validate-links', + ' 21:1-21:25 warning Link to unknown heading in `missing`: `missing` missing-heading-in-file remark-validate-links', + ' 23:1-23:39 warning Link to unknown heading in `folder-without-readme`: `missing` missing-heading-in-file remark-validate-links', + '', + '⚠ 5 warnings', + '' + ].join('\n') + ], + 'should work' + ) + } + }) + t.test('should check images', function(st) { st.plan(1)