starlight-links-validator 0.16.0 → 0.17.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 +10 -0
- package/index.ts +28 -3
- package/libs/i18n.ts +3 -3
- package/libs/remark.ts +25 -16
- package/libs/validation.ts +29 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# starlight-links-validator
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#108](https://github.com/HiDeoo/starlight-links-validator/pull/108) [`82f8ec5`](https://github.com/HiDeoo/starlight-links-validator/commit/82f8ec5cff97d5b9e343440666a3bb67de216b00) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for [excluding](https://starlight-links-validator.vercel.app/configuration#exclude) links from validation using a function.
|
|
8
|
+
|
|
9
|
+
When using the function syntax, the function should return `true` for any link that should be excluded from validation or `false` otherwise. The function will be called for each link to validate and will receive an object containing various properties to help determine whether to exclude the link or not.
|
|
10
|
+
|
|
11
|
+
Check out the [`exclude` configuration option](https://starlight-links-validator.vercel.app/configuration#exclude) documentation for more details and examples.
|
|
12
|
+
|
|
3
13
|
## 0.16.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/index.ts
CHANGED
|
@@ -61,13 +61,38 @@ const starlightLinksValidatorOptionsSchema = z
|
|
|
61
61
|
*/
|
|
62
62
|
errorOnLocalLinks: z.boolean().default(true),
|
|
63
63
|
/**
|
|
64
|
-
* Defines a list of links or glob patterns that should be excluded from validation
|
|
64
|
+
* Defines a list of links or glob patterns that should be excluded from validation or a function that will be
|
|
65
|
+
* called for each link to determine if it should be excluded from validation or not.
|
|
65
66
|
*
|
|
66
|
-
* The links in this list will be ignored by the plugin and will not be
|
|
67
|
+
* The links in this list or links where the function returns `true` will be ignored by the plugin and will not be
|
|
68
|
+
* validated.
|
|
67
69
|
*
|
|
68
70
|
* @default []
|
|
69
71
|
*/
|
|
70
|
-
exclude: z
|
|
72
|
+
exclude: z
|
|
73
|
+
.union([
|
|
74
|
+
z.array(z.string()),
|
|
75
|
+
z
|
|
76
|
+
.function()
|
|
77
|
+
.args(
|
|
78
|
+
z.object({
|
|
79
|
+
/**
|
|
80
|
+
* The absolute path to the file where the link is defined.
|
|
81
|
+
*/
|
|
82
|
+
file: z.string(),
|
|
83
|
+
/**
|
|
84
|
+
* The link to validate as authored in the content.
|
|
85
|
+
*/
|
|
86
|
+
link: z.string(),
|
|
87
|
+
/**
|
|
88
|
+
* The slug of the page where the link is defined.
|
|
89
|
+
*/
|
|
90
|
+
slug: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
.returns(z.boolean()),
|
|
94
|
+
])
|
|
95
|
+
.default([]),
|
|
71
96
|
/**
|
|
72
97
|
* Defines the policy for external links with an origin matching the Astro `site` option.
|
|
73
98
|
*
|
package/libs/i18n.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingSlash } from './path'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ValidationData } from './remark'
|
|
3
3
|
import type { StarlightUserConfig } from './validation'
|
|
4
4
|
|
|
5
5
|
export function getLocaleConfig(config: StarlightUserConfig): LocaleConfig | undefined {
|
|
@@ -30,7 +30,7 @@ export function getLocaleConfig(config: StarlightUserConfig): LocaleConfig | und
|
|
|
30
30
|
|
|
31
31
|
export function getFallbackHeadings(
|
|
32
32
|
path: string,
|
|
33
|
-
|
|
33
|
+
validationData: ValidationData,
|
|
34
34
|
localeConfig: LocaleConfig | undefined,
|
|
35
35
|
base: string,
|
|
36
36
|
): string[] | undefined {
|
|
@@ -48,7 +48,7 @@ export function getFallbackHeadings(
|
|
|
48
48
|
(localeConfig.defaultLocale === '' ? localeConfig.defaultLocale : `${localeConfig.defaultLocale}/`),
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
return
|
|
51
|
+
return validationData.get(fallbackPath === '' ? '/' : fallbackPath)?.headings
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
package/libs/remark.ts
CHANGED
|
@@ -25,10 +25,8 @@ const builtInComponents: StarlightLinksValidatorOptions['components'] = [
|
|
|
25
25
|
['LinkCard', 'href'],
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
-
// All the
|
|
29
|
-
const
|
|
30
|
-
// All the internal links keyed by file path.
|
|
31
|
-
const links: Links = new Map()
|
|
28
|
+
// All the validation data keyed by file path.
|
|
29
|
+
const data: ValidationData = new Map()
|
|
32
30
|
|
|
33
31
|
export const remarkStarlightLinksValidator: Plugin<[RemarkStarlightLinksValidatorConfig], Root> = function (config) {
|
|
34
32
|
const { base, options, srcDir } = config
|
|
@@ -40,8 +38,11 @@ export const remarkStarlightLinksValidator: Plugin<[RemarkStarlightLinksValidato
|
|
|
40
38
|
return (tree, file) => {
|
|
41
39
|
if (file.data.astro?.frontmatter?.['draft']) return
|
|
42
40
|
|
|
41
|
+
const originalPath = file.history[0]
|
|
42
|
+
if (!originalPath) throw new Error('Missing file path to validate links.')
|
|
43
|
+
|
|
43
44
|
const slugger = new GitHubSlugger()
|
|
44
|
-
const filePath = normalizeFilePath(base, srcDir,
|
|
45
|
+
const filePath = normalizeFilePath(base, srcDir, originalPath)
|
|
45
46
|
const slug: string | undefined =
|
|
46
47
|
typeof file.data.astro?.frontmatter?.['slug'] === 'string' ? file.data.astro.frontmatter['slug'] : undefined
|
|
47
48
|
|
|
@@ -155,13 +156,16 @@ export const remarkStarlightLinksValidator: Plugin<[RemarkStarlightLinksValidato
|
|
|
155
156
|
}
|
|
156
157
|
})
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
data.set(getFilePath(base, filePath, slug), {
|
|
160
|
+
file: originalPath,
|
|
161
|
+
headings: fileHeadings,
|
|
162
|
+
links: fileLinks,
|
|
163
|
+
})
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
export function getValidationData() {
|
|
164
|
-
return
|
|
167
|
+
export function getValidationData(): ValidationData {
|
|
168
|
+
return data
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
function getLinkToValidate(link: string, { options, site }: RemarkStarlightLinksValidatorConfig): Link | undefined {
|
|
@@ -202,11 +206,7 @@ function getFilePath(base: string, filePath: string, slug: string | undefined) {
|
|
|
202
206
|
return filePath
|
|
203
207
|
}
|
|
204
208
|
|
|
205
|
-
function normalizeFilePath(base: string, srcDir: URL, filePath
|
|
206
|
-
if (!filePath) {
|
|
207
|
-
throw new Error('Missing file path to validate links.')
|
|
208
|
-
}
|
|
209
|
-
|
|
209
|
+
function normalizeFilePath(base: string, srcDir: URL, filePath: string) {
|
|
210
210
|
const path = nodePath
|
|
211
211
|
.relative(nodePath.join(fileURLToPath(srcDir), 'content/docs'), filePath)
|
|
212
212
|
.replace(/\.\w+$/, '')
|
|
@@ -234,8 +234,17 @@ export interface RemarkStarlightLinksValidatorConfig {
|
|
|
234
234
|
srcDir: URL
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
export type
|
|
238
|
-
|
|
237
|
+
export type ValidationData = Map<
|
|
238
|
+
string,
|
|
239
|
+
{
|
|
240
|
+
// The absolute path to the file.
|
|
241
|
+
file: string
|
|
242
|
+
// All the headings.
|
|
243
|
+
headings: string[]
|
|
244
|
+
// All the internal links.
|
|
245
|
+
links: Link[]
|
|
246
|
+
}
|
|
247
|
+
>
|
|
239
248
|
|
|
240
249
|
export interface Link {
|
|
241
250
|
error?: ValidationErrorType
|
package/libs/validation.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { StarlightLinksValidatorOptions } from '..'
|
|
|
11
11
|
|
|
12
12
|
import { getFallbackHeadings, getLocaleConfig, isInconsistentLocaleLink, type LocaleConfig } from './i18n'
|
|
13
13
|
import { ensureTrailingSlash, stripLeadingSlash, stripTrailingSlash } from './path'
|
|
14
|
-
import { getValidationData, type
|
|
14
|
+
import { getValidationData, type Link, type ValidationData } from './remark'
|
|
15
15
|
|
|
16
16
|
export const ValidationErrorType = {
|
|
17
17
|
InconsistentLocale: 'inconsistent locale',
|
|
@@ -36,7 +36,7 @@ export function validateLinks(
|
|
|
36
36
|
process.stdout.write(`\n${bgGreen(black(` validating links `))}\n`)
|
|
37
37
|
|
|
38
38
|
const localeConfig = getLocaleConfig(starlightConfig)
|
|
39
|
-
const
|
|
39
|
+
const validationData = getValidationData()
|
|
40
40
|
const allPages: Pages = new Set(
|
|
41
41
|
pages.map((page) =>
|
|
42
42
|
ensureTrailingSlash(
|
|
@@ -49,19 +49,19 @@ export function validateLinks(
|
|
|
49
49
|
|
|
50
50
|
const errors: ValidationErrors = new Map()
|
|
51
51
|
|
|
52
|
-
for (const [filePath, fileLinks] of
|
|
52
|
+
for (const [filePath, { links: fileLinks }] of validationData) {
|
|
53
53
|
for (const link of fileLinks) {
|
|
54
54
|
const validationContext: ValidationContext = {
|
|
55
55
|
astroConfig,
|
|
56
56
|
customPages,
|
|
57
57
|
errors,
|
|
58
58
|
filePath,
|
|
59
|
-
headings,
|
|
60
59
|
link,
|
|
61
60
|
localeConfig,
|
|
62
61
|
options,
|
|
63
62
|
outputDir,
|
|
64
63
|
pages: allPages,
|
|
64
|
+
validationData,
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (link.raw.startsWith('#') || link.raw.startsWith('?')) {
|
|
@@ -194,23 +194,29 @@ function validateLink(context: ValidationContext) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
function getFileHeadings(path: string, { astroConfig,
|
|
198
|
-
let
|
|
197
|
+
function getFileHeadings(path: string, { astroConfig, localeConfig, options, validationData }: ValidationContext) {
|
|
198
|
+
let headings = validationData.get(path === '' ? '/' : path)?.headings
|
|
199
199
|
|
|
200
|
-
if (!options.errorOnFallbackPages && !
|
|
201
|
-
|
|
200
|
+
if (!options.errorOnFallbackPages && !headings && localeConfig) {
|
|
201
|
+
headings = getFallbackHeadings(path, validationData, localeConfig, astroConfig.base)
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
return
|
|
204
|
+
return headings
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Validate a link to an hash in the same page.
|
|
209
209
|
*/
|
|
210
|
-
function validateSelfHash(
|
|
210
|
+
function validateSelfHash(context: ValidationContext) {
|
|
211
|
+
const { errors, link, filePath, validationData } = context
|
|
212
|
+
|
|
213
|
+
if (isExcludedLink(link, context)) {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
211
217
|
const hash = link.raw.split('#')[1] ?? link.raw
|
|
212
218
|
const sanitizedHash = hash.replace(/^#/, '')
|
|
213
|
-
const fileHeadings =
|
|
219
|
+
const fileHeadings = validationData.get(filePath)?.headings
|
|
214
220
|
|
|
215
221
|
if (!fileHeadings) {
|
|
216
222
|
throw new Error(`Failed to find headings for the file at '${filePath}'.`)
|
|
@@ -248,8 +254,17 @@ function isValidAsset(path: string, context: ValidationContext) {
|
|
|
248
254
|
/**
|
|
249
255
|
* Check if a link is excluded from validation by the user.
|
|
250
256
|
*/
|
|
251
|
-
function isExcludedLink(link: Link,
|
|
252
|
-
return picomatch(
|
|
257
|
+
function isExcludedLink(link: Link, { filePath, options, validationData }: ValidationContext) {
|
|
258
|
+
if (Array.isArray(options.exclude)) return picomatch(options.exclude)(stripQueryString(link.raw))
|
|
259
|
+
|
|
260
|
+
const file = validationData.get(filePath)?.file
|
|
261
|
+
if (!file) throw new Error('Missing file path to check exclusion.')
|
|
262
|
+
|
|
263
|
+
return options.exclude({
|
|
264
|
+
file,
|
|
265
|
+
link: link.raw,
|
|
266
|
+
slug: stripTrailingSlash(filePath),
|
|
267
|
+
})
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
function stripQueryString(path: string): string {
|
|
@@ -294,12 +309,12 @@ interface ValidationContext {
|
|
|
294
309
|
customPages: Set<string>
|
|
295
310
|
errors: ValidationErrors
|
|
296
311
|
filePath: string
|
|
297
|
-
headings: Headings
|
|
298
312
|
link: Link
|
|
299
313
|
localeConfig: LocaleConfig | undefined
|
|
300
314
|
options: StarlightLinksValidatorOptions
|
|
301
315
|
outputDir: URL
|
|
302
316
|
pages: Pages
|
|
317
|
+
validationData: ValidationData
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
export type StarlightUserConfig = Omit<StarlightUserConfigWithPlugins, 'plugins'>
|