forked from todogroup/repolinter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfile-no-broken-links.js
More file actions
146 lines (138 loc) · 5.29 KB
/
file-no-broken-links.js
File metadata and controls
146 lines (138 loc) · 5.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright 2017 TODO Group. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
const { HtmlChecker } = require('broken-link-checker')
const path = require('path')
const { URL } = require('url')
const GitHubMarkup = require('../lib/github_markup')
const Result = require('../lib/result')
// eslint-disable-next-line no-unused-vars
const FileSystem = require('../lib/file_system')
/**
* Searches for a renderable markup document, renders it, and then
* checks for broken links by scanning the html.
*
* @param {FileSystem} fs A filesystem object configured with filter paths and target directories
* @param {object} options The rule configuration
* @returns {Promise<Result>} The lint rule result
* @ignore
*/
async function fileNoBrokenLinks(fs, options) {
const files = await fs.findAllFiles(options.globsAll, !!options.nocase)
if (files.length === 0) {
return new Result(
'Did not find file matching the specified patterns',
options.globsAll.map(f => {
return { passed: false, pattern: f }
}),
!!options['succeed-on-non-existent']
)
}
// for every file check every broken link
const results = await Promise.all(
files.map(async f => {
// render it, if possible
const absMdPath = path.posix.resolve(fs.targetDir, f)
const rendered = await GitHubMarkup.renderMarkup(absMdPath)
if (rendered === null) {
return {
passed: true,
path: f,
message: 'Ignored due to unknown file format.'
}
}
// scan the rendered HTML for broken links
const linkRes = await new Promise((resolve, reject) => {
const linkBuf = []
const htmlChecker = new HtmlChecker(
{
...options,
excludedKeywords: ['#*'] // exclude local section links
},
{
link: res => linkBuf.push(res),
complete: () => resolve(linkBuf)
}
)
const didScan = htmlChecker.scan(
rendered,
new URL(`file://${path.posix.join(fs.targetDir, f)}`)
)
if (!didScan)
reject(Error('Failed to scan HTML with broken link checker'))
})
// find all relative links, and double check the filesystem for their existence
// filter down to broken links
const brokenLinks = linkRes.filter(link => link.broken)
// split into invalid and otherwise failing
const { failing, invalid } = brokenLinks.reduce(
(res, linkRes) => {
linkRes.brokenReason === 'BLC_INVALID'
? res.invalid.push(linkRes)
: res.failing.push(linkRes)
return res
},
{ failing: [], invalid: [] }
)
// make the messages for the failing URLs
const failingMessages = failing.map(
({ brokenReason, url: { original }, http: { response } }) =>
`\`${original}\` (${
brokenReason.includes('HTTP')
? `status code ${response && response.status}`
: `unknown error ${brokenReason}`
})`
)
// process the invalid links to check if they're actually filesystem paths
// returning the message for invalid URLs
const failingInvalidMessagesWithNulls = await Promise.all(
invalid.map(async b => {
const originalURL = b.url.original
const baseURL = b.base.resolved
let url
// parse the URL, and if it fails to parse it's invalid
try {
url = new URL(originalURL, baseURL)
if (url.protocol !== 'file:' || !url.pathname)
return `\`${originalURL}\` (invalid URL)`
} catch (e) {
return `\`${originalURL}\` (invalid path)`
}
// verify the path is relative, else the path is invalid
if (path.posix.isAbsolute(originalURL))
return `\`${originalURL}\` (invalid path)`
// verify the path doesn't traverse outside the project, else the path is excluded
const targetDir = path.posix.resolve(fs.targetDir)
const filePath = path.posix.join('/', url.host, url.pathname)
const absPath = path.posix.resolve(targetDir, filePath)
const relPath = path.posix.relative(targetDir, absPath)
if (relPath.startsWith('..')) {
if (options['pass-external-relative-links']) return null
else return `\`${originalURL}\` (relative link outside project)`
}
// verify the file exists (or at least that we have access to it)
if (!(await fs.relativeFileExists(relPath)))
return `\`${originalURL}\` (file does not exist)`
return null
})
)
// remove messages which didn't fail
const failingInvalidMessages = failingInvalidMessagesWithNulls.filter(
m => m !== null
)
// join all the messages together to form the result
const allMessages = failingInvalidMessages.concat(failingMessages)
return {
passed: allMessages.length === 0,
path: f,
message:
allMessages.length === 0
? 'All links are valid'
: allMessages.join(', ')
}
})
)
// return the final result
const passed = results.every(({ passed }) => passed)
return new Result(passed ? '' : 'Found broken links', results, passed)
}
module.exports = fileNoBrokenLinks