starlight-links-validator 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/libs/remark.ts +8 -8
- package/libs/validation.ts +35 -28
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# starlight-links-validator
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#118](https://github.com/HiDeoo/starlight-links-validator/pull/118) [`efef54a`](https://github.com/HiDeoo/starlight-links-validator/commit/efef54a647f65072d33b70ad92a2ea90b52ddb57) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds [hyperlinks (OSC 8)](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) support to validation terminal output.
|
|
8
|
+
|
|
9
|
+
In [supported terminals](https://github.com/Alhadis/OSC8-Adoption/), error slugs can be conveniently used (e.g. with `Ctrl+Click`, `Opt+Click`, `Cmd+Click`, or a context menu) to open the corresponding file using the default associated application.
|
|
10
|
+
|
|
3
11
|
## 0.17.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/libs/remark.ts
CHANGED
|
@@ -41,11 +41,11 @@ export const remarkStarlightLinksValidator: Plugin<[RemarkStarlightLinksValidato
|
|
|
41
41
|
|
|
42
42
|
if (file.data.astro?.frontmatter?.['draft']) return
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
44
|
+
const path = file.history[0]
|
|
45
|
+
if (!path) throw new Error('Missing file path to validate links.')
|
|
46
46
|
|
|
47
47
|
const slugger = new GitHubSlugger()
|
|
48
|
-
const
|
|
48
|
+
const id = normalizeId(base, srcDir, path)
|
|
49
49
|
const slug: string | undefined =
|
|
50
50
|
typeof file.data.astro?.frontmatter?.['slug'] === 'string' ? file.data.astro.frontmatter['slug'] : undefined
|
|
51
51
|
|
|
@@ -159,8 +159,8 @@ export const remarkStarlightLinksValidator: Plugin<[RemarkStarlightLinksValidato
|
|
|
159
159
|
}
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
data.set(
|
|
163
|
-
file:
|
|
162
|
+
data.set(getValidationDataId(base, id, slug), {
|
|
163
|
+
file: path,
|
|
164
164
|
headings: fileHeadings,
|
|
165
165
|
links: fileLinks,
|
|
166
166
|
})
|
|
@@ -201,15 +201,15 @@ function getLinkToValidate(link: string, { options, site }: RemarkStarlightLinks
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function
|
|
204
|
+
function getValidationDataId(base: string, id: string, slug: string | undefined) {
|
|
205
205
|
if (slug) {
|
|
206
206
|
return nodePath.posix.join(stripLeadingSlash(base), stripLeadingSlash(ensureTrailingSlash(slug)))
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
return
|
|
209
|
+
return id
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function
|
|
212
|
+
function normalizeId(base: string, srcDir: URL, filePath: string) {
|
|
213
213
|
const path = nodePath
|
|
214
214
|
.relative(nodePath.join(fileURLToPath(srcDir), 'content/docs'), filePath)
|
|
215
215
|
.replace(/\.\w+$/, '')
|
package/libs/validation.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { statSync } from 'node:fs'
|
|
2
2
|
import { posix } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
import type { StarlightUserConfig as StarlightUserConfigWithPlugins } from '@astrojs/starlight/types'
|
|
6
6
|
import type { AstroConfig, AstroIntegrationLogger } from 'astro'
|
|
7
7
|
import { bgGreen, black, blue, dim, green, red } from 'kleur/colors'
|
|
8
8
|
import picomatch from 'picomatch'
|
|
9
|
+
import terminalLink from 'terminal-link'
|
|
9
10
|
|
|
10
11
|
import type { StarlightLinksValidatorOptions } from '..'
|
|
11
12
|
|
|
@@ -49,13 +50,14 @@ export function validateLinks(
|
|
|
49
50
|
|
|
50
51
|
const errors: ValidationErrors = new Map()
|
|
51
52
|
|
|
52
|
-
for (const [
|
|
53
|
+
for (const [id, { links: fileLinks, file }] of validationData) {
|
|
53
54
|
for (const link of fileLinks) {
|
|
54
55
|
const validationContext: ValidationContext = {
|
|
55
56
|
astroConfig,
|
|
56
57
|
customPages,
|
|
57
58
|
errors,
|
|
58
|
-
|
|
59
|
+
file,
|
|
60
|
+
id,
|
|
59
61
|
link,
|
|
60
62
|
localeConfig,
|
|
61
63
|
options,
|
|
@@ -85,7 +87,10 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
|
|
|
85
87
|
return
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const errorCount = [...errors.values()].reduce(
|
|
90
|
+
const errorCount = [...errors.values()].reduce(
|
|
91
|
+
(acc, { errors: validationErrors }) => acc + validationErrors.length,
|
|
92
|
+
0,
|
|
93
|
+
)
|
|
89
94
|
|
|
90
95
|
logger.error(
|
|
91
96
|
red(
|
|
@@ -98,8 +103,8 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
|
|
|
98
103
|
|
|
99
104
|
let hasInvalidLinkToCustomPage = false
|
|
100
105
|
|
|
101
|
-
for (const [
|
|
102
|
-
logger.info(`${red('▶')} ${blue(file)}`)
|
|
106
|
+
for (const [id, { errors: validationErrors, file }] of errors) {
|
|
107
|
+
logger.info(`${red('▶')} ${blue(terminalLink(id, pathToFileURL(file).toString(), { fallback: false }))}`)
|
|
103
108
|
|
|
104
109
|
for (const [index, validationError] of validationErrors.entries()) {
|
|
105
110
|
logger.info(
|
|
@@ -120,14 +125,14 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
|
|
|
120
125
|
* Validate a link to another internal page that may or may not have a hash.
|
|
121
126
|
*/
|
|
122
127
|
function validateLink(context: ValidationContext) {
|
|
123
|
-
const { astroConfig, customPages, errors,
|
|
128
|
+
const { astroConfig, customPages, errors, id, file, link, localeConfig, options, pages } = context
|
|
124
129
|
|
|
125
130
|
if (isExcludedLink(link, context)) {
|
|
126
131
|
return
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
if (link.error) {
|
|
130
|
-
addError(errors,
|
|
135
|
+
addError(errors, id, file, link, link.error)
|
|
131
136
|
return
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -144,7 +149,7 @@ function validateLink(context: ValidationContext) {
|
|
|
144
149
|
|
|
145
150
|
if (path.startsWith('.') || (!linkToValidate.startsWith('/') && !linkToValidate.startsWith('?'))) {
|
|
146
151
|
if (options.errorOnRelativeLinks) {
|
|
147
|
-
addError(errors,
|
|
152
|
+
addError(errors, id, file, link, ValidationErrorType.RelativeLink)
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
return
|
|
@@ -162,7 +167,8 @@ function validateLink(context: ValidationContext) {
|
|
|
162
167
|
if (!isValidPage || !fileHeadings) {
|
|
163
168
|
addError(
|
|
164
169
|
errors,
|
|
165
|
-
|
|
170
|
+
id,
|
|
171
|
+
file,
|
|
166
172
|
link,
|
|
167
173
|
customPages.has(stripTrailingSlash(sanitizedPath))
|
|
168
174
|
? ValidationErrorType.InvalidLinkToCustomPage
|
|
@@ -171,24 +177,24 @@ function validateLink(context: ValidationContext) {
|
|
|
171
177
|
return
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
if (options.errorOnInconsistentLocale && localeConfig && isInconsistentLocaleLink(
|
|
175
|
-
addError(errors,
|
|
180
|
+
if (options.errorOnInconsistentLocale && localeConfig && isInconsistentLocaleLink(id, link.raw, localeConfig)) {
|
|
181
|
+
addError(errors, id, file, link, ValidationErrorType.InconsistentLocale)
|
|
176
182
|
return
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
if (hash && !fileHeadings.includes(hash)) {
|
|
180
186
|
if (options.errorOnInvalidHashes) {
|
|
181
|
-
addError(errors,
|
|
187
|
+
addError(errors, id, file, link, ValidationErrorType.InvalidHash)
|
|
182
188
|
}
|
|
183
189
|
return
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
if (path.length > 0) {
|
|
187
193
|
if (astroConfig.trailingSlash === 'always' && !path.endsWith('/')) {
|
|
188
|
-
addError(errors,
|
|
194
|
+
addError(errors, id, file, link, ValidationErrorType.TrailingSlashMissing)
|
|
189
195
|
return
|
|
190
196
|
} else if (astroConfig.trailingSlash === 'never' && path.endsWith('/')) {
|
|
191
|
-
addError(errors,
|
|
197
|
+
addError(errors, id, file, link, ValidationErrorType.TrailingSlashForbidden)
|
|
192
198
|
return
|
|
193
199
|
}
|
|
194
200
|
}
|
|
@@ -208,7 +214,7 @@ function getFileHeadings(path: string, { astroConfig, localeConfig, options, val
|
|
|
208
214
|
* Validate a link to an hash in the same page.
|
|
209
215
|
*/
|
|
210
216
|
function validateSelfHash(context: ValidationContext) {
|
|
211
|
-
const { errors, link,
|
|
217
|
+
const { errors, link, id, file, validationData } = context
|
|
212
218
|
|
|
213
219
|
if (isExcludedLink(link, context)) {
|
|
214
220
|
return
|
|
@@ -216,14 +222,14 @@ function validateSelfHash(context: ValidationContext) {
|
|
|
216
222
|
|
|
217
223
|
const hash = link.raw.split('#')[1] ?? link.raw
|
|
218
224
|
const sanitizedHash = hash.replace(/^#/, '')
|
|
219
|
-
const fileHeadings = validationData.get(
|
|
225
|
+
const fileHeadings = validationData.get(id)?.headings
|
|
220
226
|
|
|
221
227
|
if (!fileHeadings) {
|
|
222
|
-
throw new Error(`Failed to find headings for the file at '${
|
|
228
|
+
throw new Error(`Failed to find headings for the file at '${id}'.`)
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
if (!fileHeadings.includes(sanitizedHash)) {
|
|
226
|
-
addError(errors,
|
|
232
|
+
addError(errors, id, file, link, ValidationErrorType.InvalidHash)
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
|
|
@@ -254,16 +260,16 @@ function isValidAsset(path: string, context: ValidationContext) {
|
|
|
254
260
|
/**
|
|
255
261
|
* Check if a link is excluded from validation by the user.
|
|
256
262
|
*/
|
|
257
|
-
function isExcludedLink(link: Link, {
|
|
263
|
+
function isExcludedLink(link: Link, { id, options, validationData }: ValidationContext) {
|
|
258
264
|
if (Array.isArray(options.exclude)) return picomatch(options.exclude)(stripQueryString(link.raw))
|
|
259
265
|
|
|
260
|
-
const file = validationData.get(
|
|
266
|
+
const file = validationData.get(id)?.file
|
|
261
267
|
if (!file) throw new Error('Missing file path to check exclusion.')
|
|
262
268
|
|
|
263
269
|
return options.exclude({
|
|
264
270
|
file,
|
|
265
271
|
link: link.raw,
|
|
266
|
-
slug: stripTrailingSlash(
|
|
272
|
+
slug: stripTrailingSlash(id),
|
|
267
273
|
})
|
|
268
274
|
}
|
|
269
275
|
|
|
@@ -271,11 +277,11 @@ function stripQueryString(path: string): string {
|
|
|
271
277
|
return path.split('?')[0] ?? path
|
|
272
278
|
}
|
|
273
279
|
|
|
274
|
-
function addError(errors: ValidationErrors,
|
|
275
|
-
const fileErrors = errors.get(
|
|
276
|
-
fileErrors.push({ link: link.raw, type })
|
|
280
|
+
function addError(errors: ValidationErrors, id: string, file: string, link: Link, type: ValidationErrorType) {
|
|
281
|
+
const fileErrors = errors.get(id) ?? { errors: [], file }
|
|
282
|
+
fileErrors.errors.push({ link: link.raw, type })
|
|
277
283
|
|
|
278
|
-
errors.set(
|
|
284
|
+
errors.set(id, fileErrors)
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
function pluralize(count: number, singular: string) {
|
|
@@ -289,7 +295,7 @@ function formatValidationError(error: ValidationError, site: AstroConfig['site']
|
|
|
289
295
|
}
|
|
290
296
|
|
|
291
297
|
// The validation errors keyed by file path.
|
|
292
|
-
type ValidationErrors = Map<string, ValidationError[]>
|
|
298
|
+
type ValidationErrors = Map<string, { errors: ValidationError[]; file: string }>
|
|
293
299
|
|
|
294
300
|
export type ValidationErrorType = (typeof ValidationErrorType)[keyof typeof ValidationErrorType]
|
|
295
301
|
|
|
@@ -308,7 +314,8 @@ interface ValidationContext {
|
|
|
308
314
|
astroConfig: AstroConfig
|
|
309
315
|
customPages: Set<string>
|
|
310
316
|
errors: ValidationErrors
|
|
311
|
-
|
|
317
|
+
id: string
|
|
318
|
+
file: string
|
|
312
319
|
link: Link
|
|
313
320
|
localeConfig: LocaleConfig | undefined
|
|
314
321
|
options: StarlightLinksValidatorOptions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-links-validator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Starlight plugin to validate internal links.",
|
|
6
6
|
"author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"mdast-util-mdx-jsx": "^3.1.3",
|
|
20
20
|
"mdast-util-to-string": "^4.0.0",
|
|
21
21
|
"picomatch": "^4.0.2",
|
|
22
|
+
"terminal-link": "^5.0.0",
|
|
22
23
|
"unist-util-visit": "^5.0.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|