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 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 originalPath = file.history[0]
45
- if (!originalPath) throw new Error('Missing file path to validate links.')
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 filePath = normalizeFilePath(base, srcDir, originalPath)
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(getFilePath(base, filePath, slug), {
163
- file: originalPath,
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 getFilePath(base: string, filePath: string, slug: string | undefined) {
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 filePath
209
+ return id
210
210
  }
211
211
 
212
- function normalizeFilePath(base: string, srcDir: URL, filePath: string) {
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+$/, '')
@@ -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 [filePath, { links: fileLinks }] of validationData) {
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
- filePath,
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((acc, links) => acc + links.length, 0)
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 [file, validationErrors] of errors) {
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, filePath, link, localeConfig, options, pages } = context
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, filePath, link, link.error)
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, filePath, link, ValidationErrorType.RelativeLink)
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
- filePath,
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(filePath, link.raw, localeConfig)) {
175
- addError(errors, filePath, link, ValidationErrorType.InconsistentLocale)
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, filePath, link, ValidationErrorType.InvalidHash)
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, filePath, link, ValidationErrorType.TrailingSlashMissing)
194
+ addError(errors, id, file, link, ValidationErrorType.TrailingSlashMissing)
189
195
  return
190
196
  } else if (astroConfig.trailingSlash === 'never' && path.endsWith('/')) {
191
- addError(errors, filePath, link, ValidationErrorType.TrailingSlashForbidden)
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, filePath, validationData } = context
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(filePath)?.headings
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 '${filePath}'.`)
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, filePath, link, ValidationErrorType.InvalidHash)
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, { filePath, options, validationData }: ValidationContext) {
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(filePath)?.file
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(filePath),
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, filePath: string, link: Link, type: ValidationErrorType) {
275
- const fileErrors = errors.get(filePath) ?? []
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(filePath, fileErrors)
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
- filePath: string
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.17.2",
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": {