git.iko.soy/typeable/remark-validate-links
on main help sign in
Add support for encoded characters

Closes GH-42. Closes GH-43. Co-authored-by: Titus Wormer <tituswormer@gmail.com>

commit93158a781ca39514859d95e44f794077922c0b1a authorKelly Selden <kellyselden@gmail.com> dateMon 1 Jul 2019, 19:53 (7 years ago) parent162e931
9 files changed+154−84
lib/check/check-files.js+1−1 lib/check/merge-landmarks.js+2−1 lib/check/merge-references.js+12−8 lib/check/validate.js+36−33 lib/find/find.js+56−41 test/fixtures/examples/weird#character/readme.md+1 test/fixtures/examples/weird#characters.md+1 test/fixtures/weird-characters.md+17 test/index.js+28
lib/check/check-files.js+1−1
@@ -32,7 +32,7 @@ function checkIfReferencedFilesExist(ctx, next) {
3232 function onaccess(err) {
3333 var noEntry = err && err.code === 'ENOENT'
3434
35− landmarks[filePath] = !noEntry
35+ landmarks[filePath] = {'': !noEntry}
3636
3737 if (++actual === expected) {
3838 next()
lib/check/merge-landmarks.js+2−1
@@ -1,5 +1,6 @@
11'use strict'
22
3+var xtend = require('xtend')
34var constants = require('../constants')
45
56module.exports = mergeLandmarks
@@ -19,7 +20,7 @@ function mergeLandmarks(ctx) {
1920
2021 if (landmarks) {
2122 for (landmark in landmarks) {
22− result[landmark] = true
23+ result[landmark] = xtend(result[landmark] || {}, landmarks[landmark])
2324 }
2425 }
2526 }
lib/check/merge-references.js+12−8
@@ -12,6 +12,9 @@ function mergeReferences(ctx) {
1212 var file
1313 var references
1414 var reference
15+ var all
16+ var internal
17+ var hash
1518
1619 while (++index < length) {
1720 file = files[index]
@@ -22,15 +25,16 @@ function mergeReferences(ctx) {
2225 }
2326
2427 for (reference in references) {
25− if (!(reference in result)) {
26− result[reference] = []
28+ internal = references[reference]
29+ all = result[reference] || (result[reference] = {})
30+
31+ for (hash in internal) {
32+ ;(all[hash] || (all[hash] = [])).push({
33+ file: file,
34+ reference: {filePath: reference, hash: hash},
35+ nodes: internal[hash]
36+ })
2737 }
28−
29− result[reference].push({
30− file: file,
31− reference: reference,
32− nodes: references[reference]
33− })
3438 }
3539 }
3640
lib/check/validate.js+36−33
@@ -11,12 +11,21 @@ function validate(ctx) {
1111 var references = ctx.references
1212 var missing = []
1313 var reference
14+ var hash
15+ var refs
16+ var lands
1417 var length
1518 var index
1619
1720 for (reference in references) {
18− if (!landmarks[reference]) {
19− missing = missing.concat(references[reference])
21+ refs = references[reference]
22+ /* istanbul ignore next - `else` could happen in browser. */
23+ lands = landmarks[reference] || {}
24+
25+ for (hash in refs) {
26+ if (!lands[hash]) {
27+ missing = missing.concat(refs[hash])
28+ }
2029 }
2130 }
2231
@@ -29,69 +38,63 @@ function validate(ctx) {
2938 }
3039}
3140
32−function warn(ctx, reference, file, nodes) {
41+function warn(ctx, info, file, nodes) {
3342 var landmarks = ctx.landmarks
3443 var absolute = file.path ? path.resolve(file.cwd, file.path) : ''
3544 var base = absolute ? path.dirname(absolute) : null
36− var relative = base ? path.relative(base, reference) : reference
37− var pathname = relative
38− var numberSignIndex = pathname.indexOf('#')
45+ var filePath = base ? path.relative(base, info.filePath) : info.filePath
46+ var hash = info.hash
3947 var dictionary = []
4048 var subhash
41− var subpathname
4249 var landmark
4350 var relativeLandmark
44− var hash
4551 var reason
4652 var ruleId
4753 var origin
4854 var suggestion
4955
50− if (numberSignIndex !== -1) {
51− hash = pathname.slice(numberSignIndex + 1)
52− pathname = pathname.slice(0, numberSignIndex)
53− }
54−
5556 if (hash) {
5657 reason = 'Link to unknown heading'
5758 ruleId = constants.headingRuleId
5859
59− if (base && path.join(base, pathname) !== absolute) {
60− reason += ' in `' + pathname + '`'
60+ if (base && path.join(base, filePath) !== absolute) {
61+ reason += ' in `' + filePath + '`'
6162 ruleId = constants.headingInFileRuleId
6263 }
6364
6465 reason += ': `' + hash + '`'
6566 } else {
66− reason = 'Link to unknown file: `' + pathname + '`'
67+ reason = 'Link to unknown file: `' + filePath + '`'
6768 ruleId = constants.fileRuleId
6869 }
6970
7071 origin = [constants.sourceId, ruleId].join(':')
7172
7273 for (landmark in landmarks) {
73− if (landmarks[landmark]) {
74− relativeLandmark = base ? path.relative(base, landmark) : landmark
75− subpathname = relativeLandmark
76− subhash = null
77− numberSignIndex = subpathname.indexOf('#')
78−
79− if (numberSignIndex !== -1) {
80− subhash = subpathname.slice(numberSignIndex + 1)
81− subpathname = subpathname.slice(0, numberSignIndex)
82− }
74+ // Only suggest if file exists.
75+ if (!landmarks[landmark] || !landmarks[landmark]['']) {
76+ continue
77+ }
78+
79+ relativeLandmark = base ? path.relative(base, landmark) : landmark
80+
81+ if (!hash) {
82+ dictionary.push(relativeLandmark)
83+ continue
84+ }
85+
86+ if (relativeLandmark !== filePath) {
87+ continue
88+ }
8389
84− if (subpathname === pathname) {
85− if (subhash && hash) {
86− dictionary.push(subhash)
87− }
88− } else if (!subhash && !hash) {
89− dictionary.push(subpathname)
90+ for (subhash in landmarks[landmark]) {
91+ if (subhash !== '') {
92+ dictionary.push(subhash)
9093 }
9194 }
9295 }
9396
94− suggestion = propose(hash ? hash : pathname, dictionary, {threshold: 0.7})
97+ suggestion = propose(hash ? hash : filePath, dictionary, {threshold: 0.7})
9598
9699 if (suggestion) {
97100 reason += '. Did you mean `' + suggestion + '`'
lib/find/find.js+56−41
@@ -37,19 +37,17 @@ function find(ctx) {
3737 space[constants.referenceId] = references
3838 space[constants.landmarkId] = landmarks
3939
40− landmarks[absolute] = true
40+ landmarks[absolute] = {'': true}
4141
4242 slugs.reset()
4343
4444 visit(ctx.tree, mark)
4545
46− // eslint-disable-next-line complexity
4746 function mark(node) {
4847 var data = node.data || {}
4948 var props = data.hProperties || {}
5049 var id = props.name || props.id || data.id
51− var filePath = node.url ? urlToPath(node.url, config) : null
52− var numberSignIndex
50+ var info = node.url ? urlToPath(node.url, config) : null
5351 var refs
5452 var fp
5553 var hash
@@ -59,52 +57,36 @@ function find(ctx) {
5957 }
6058
6159 if (id) {
62− landmarks[absolute + numberSign + id] = true
60+ landmarks[absolute][id] = true
6361 }
6462
65− if (filePath) {
66− numberSignIndex = filePath.indexOf(numberSign)
67− fp = filePath
68−
69− if (numberSignIndex !== -1) {
70− fp = filePath.slice(0, numberSignIndex)
71− hash = filePath.slice(numberSignIndex).toLowerCase()
72−
73− // Ignore the hash if it references lines in a file or doesn’t start
74− // with a heading prefix.
75− if (
76− (config.lines && lineExpression.test(hash)) ||
77− hash.slice(0, config.headingPrefix.length) !== config.headingPrefix
78− ) {
79− numberSignIndex = -1
80− }
81− // Use the hash if it starts with a heading prefix.
82− else {
83− filePath = fp + numberSign + hash.slice(config.headingPrefix.length)
84− }
85− }
63+ if (info) {
64+ fp = info.filePath
65+ hash = info.hash
66+ refs = references[fp] || (references[fp] = {})
8667
87− refs = references[fp] || (references[fp] = [])
88− refs.push(node)
68+ add('', node)
8969
9070 // With a heading
91− if (numberSignIndex !== -1) {
92− // Add with heading.
71+ if (hash) {
9372 if (fileSet || fp === absolute) {
94− refs = references[filePath] || (references[filePath] = [])
95− refs.push(node)
73+ add(hash, node)
9674 }
9775
98− if (
99− fileSet &&
100− alreadyAdded.indexOf(fp) === -1 &&
101− numberSignIndex !== 0
102− ) {
76+ if (fileSet && fp && alreadyAdded.indexOf(fp) === -1) {
10377 alreadyAdded.push(fp)
10478 fileSet.add(vfile({cwd: file.cwd, path: path.relative(file.cwd, fp)}))
10579 }
10680 }
10781 }
82+
83+ function add(hash, node) {
84+ if (refs[hash]) {
85+ refs[hash].push(node)
86+ } else {
87+ refs[hash] = [node]
88+ }
89+ }
10890 }
10991}
11092
@@ -151,7 +133,7 @@ function urlToPath(value, config) {
151133 .slice(1)
152134 .join(slash)
153135
154− return path.resolve(config.root, value + url.hash)
136+ return normalize(path.resolve(config.root, value + url.hash), config)
155137 }
156138
157139 // Remove the search: `?foo=bar`.
@@ -170,9 +152,42 @@ function urlToPath(value, config) {
170152
171153 // Local: `#heading`.
172154 if (value.charAt(0) === numberSign) {
173− return config.path ? config.path + value : value
155+ value = config.path ? config.path + value : value
174156 }
175−
176157 // Anything else, such as `readme.md`.
177− return config.path ? path.resolve(config.base, value) : ''
158+ else {
159+ value = config.path ? path.resolve(config.base, value) : ''
160+ }
161+
162+ return normalize(value, config)
163+}
164+
165+function normalize(url, config) {
166+ var numberSignIndex = url.indexOf(numberSign)
167+ var lines = config.lines
168+ var prefix = config.headingPrefix
169+ var filePath
170+ var hash
171+
172+ if (numberSignIndex === -1) {
173+ filePath = url
174+ } else {
175+ filePath = url.slice(0, numberSignIndex)
176+ hash = url.slice(numberSignIndex).toLowerCase()
177+
178+ // Ignore the hash if it references lines in a file or doesn’t start
179+ // with a heading prefix.
180+ if (
181+ (lines && lineExpression.test(hash)) ||
182+ hash.slice(0, prefix.length) !== prefix
183+ ) {
184+ hash = undefined
185+ }
186+ // Use the hash if it starts with a heading prefix.
187+ else {
188+ hash = hash.slice(prefix.length)
189+ }
190+ }
191+
192+ return {filePath: decodeURIComponent(filePath), hash: hash}
178193}
test/fixtures/examples/weird#character/readme.mdnew file+1
@@ -0,0 +1 @@
1+# Hello
test/fixtures/examples/weird#characters.mdnew file+1
@@ -0,0 +1 @@
1+# Hello
test/fixtures/weird-characters.mdnew file+17
@@ -0,0 +1,17 @@
1+# File names and paths with weird characters
2+
3+OK: Link to [a weird character in a file name](./examples/weird%23characters.md)
4+
5+OK: Link to [a weird character in a file path](./examples/weird%23character/readme.md)
6+
7+OK: Link to [a heading with a weird character in a file name](./examples/weird%23characters.md#hello)
8+
9+OK: Link to [a heading with a weird character in a file path](./examples/weird%23character/readme.md#hello)
10+
11+Not OK: Link to [a weird character in a file name](./examples/missing%23characters.md)
12+
13+Not OK: Link to [a weird character in a file path](./examples/missing%23character/readme.md)
14+
15+Not OK: Link to [a missing heading with a weird character in a file name](./examples/weird%23characters.md#world)
16+
17+Not OK: Link to [a missing heading with a weird character in a file path](./examples/weird%23character/readme.md#world)
test/index.js+28
@@ -841,6 +841,34 @@ test('remark-validate-links', function(t) {
841841 }, st.error)
842842 })
843843
844+ t.test('should recognise links with encoded URLs', function(st) {
845+ st.plan(1)
846+
847+ execa(bin, [
848+ '--no-config',
849+ '--no-ignore',
850+ '--use',
851+ '../..=repository:"wooorm/test"',
852+ '--use',
853+ '../sort',
854+ 'weird-characters.md'
855+ ]).then(function(result) {
856+ st.equal(
857+ strip(result.stderr),
858+ [
859+ 'weird-characters.md',
860+ ' 11:17-11:87 warning Link to unknown file: `examples/missing#characters.md`. Did you mean `examples/weird#characters.md` missing-file remark-validate-links',
861+ ' 13:17-13:93 warning Link to unknown file: `examples/missing#character/readme.md`. Did you mean `examples/weird#character/readme.md` missing-file remark-validate-links',
862+ ' 15:17-15:114 warning Link to unknown heading in `examples/weird#characters.md`: `world` missing-heading-in-file remark-validate-links',
863+ ' 17:17-17:120 warning Link to unknown heading in `examples/weird#character/readme.md`: `world` missing-heading-in-file remark-validate-links',
864+ '',
865+ '⚠ 4 warnings'
866+ ].join('\n'),
867+ 'should report'
868+ )
869+ }, st.error)
870+ })
871+
844872 t.test('should check images', function(st) {
845873 st.plan(1)
846874