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 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 validated.
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.array(z.string()).default([]),
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 { Headings } from './remark'
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
- headings: Headings,
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 headings.get(fallbackPath === '' ? '/' : fallbackPath)
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 headings keyed by file path.
29
- const headings: Headings = new Map()
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, file.history[0])
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
- headings.set(getFilePath(base, filePath, slug), fileHeadings)
159
- links.set(getFilePath(base, filePath, slug), fileLinks)
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 { headings, links }
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?: string) {
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 Headings = Map<string, string[]>
238
- export type Links = Map<string, Link[]>
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
@@ -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 Headings, type Link } from './remark'
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 { headings, links } = getValidationData()
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 links) {
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, headings, localeConfig, options }: ValidationContext) {
198
- let heading = headings.get(path === '' ? '/' : path)
197
+ function getFileHeadings(path: string, { astroConfig, localeConfig, options, validationData }: ValidationContext) {
198
+ let headings = validationData.get(path === '' ? '/' : path)?.headings
199
199
 
200
- if (!options.errorOnFallbackPages && !heading && localeConfig) {
201
- heading = getFallbackHeadings(path, headings, localeConfig, astroConfig.base)
200
+ if (!options.errorOnFallbackPages && !headings && localeConfig) {
201
+ headings = getFallbackHeadings(path, validationData, localeConfig, astroConfig.base)
202
202
  }
203
203
 
204
- return heading
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({ errors, link, filePath, headings }: ValidationContext) {
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 = headings.get(filePath)
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, context: ValidationContext) {
252
- return picomatch(context.options.exclude)(stripQueryString(link.raw))
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'>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-links-validator",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "license": "MIT",
5
5
  "description": "Starlight plugin to validate internal links.",
6
6
  "author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",