Skip to content

Commit

Permalink
Add support for linking to directories
Browse files Browse the repository at this point in the history
Previously, a link to a directory such as `folder#anchor`, didn’t
work.  This commit adds support for, in such a case:

1.  Loading the particular readme file that vendors such as GitHub,
    GitLab, and BitBucket display as an index on those page
2.  For every processed file that is a readme of a directory, marking
    its anchors as valid

Closes GH-49.
Closes GH-50.
  • Loading branch information
wooorm authored Jan 23, 2020
1 parent 9ff5548 commit 0c3a479
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 16 deletions.
132 changes: 116 additions & 16 deletions lib/find/find.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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()
}
}
}
Expand Down Expand Up @@ -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))
)
}
1 change: 1 addition & 0 deletions test/fixtures/folder-without-readme/example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file isn’t used when linking to the folder: it has to be a `readme`.
23 changes: 23 additions & 0 deletions test/fixtures/folder.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions test/fixtures/folder/example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file isn’t used when linking to the folder: it has to be a `readme`.
9 changes: 9 additions & 0 deletions test/fixtures/folder/readme.asc
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions test/fixtures/folder/readme.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# readme

## This

## That
1 change: 1 addition & 0 deletions test/fixtures/folder/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This one isn’t used (`readme.markdown` sorts before `readme.md`).
39 changes: 39 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 0c3a479

Please sign in to comment.