Skip to content

Commit

Permalink
feat: adds a new errorOnInvalidHashes option defaulting to true t…
Browse files Browse the repository at this point in the history
…o disable hash validation
  • Loading branch information
HiDeoo committed Sep 12, 2024
1 parent 1b9eee0 commit 32a92f8
Show file tree
Hide file tree
Showing 24 changed files with 373 additions and 63 deletions.
24 changes: 24 additions & 0 deletions docs/src/content/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ export default defineConfig({
})
```

### `errorOnInvalidHashes`

**Type:** `boolean`
**Default:** `true`

By default, the Starlight Links Validator plugin will error if an internal link points to an [hash](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) that does not exist in the target page.
If you want to only validate that pages exist but ignore hashes, you can set this option to `false`.

This option should be used with caution but can be useful in large documentation with many contributors where hashes always being up-to-date can be difficult to maintain and validated on a different schedule, e.g. once a week.

```js {6}
export default defineConfig({
integrations: [
starlight({
plugins: [
starlightLinksValidator({
errorOnInvalidHashes: false,
}),
],
}),
],
})
```

### `exclude`

**Type:** `string[]`
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ title: Getting Started
A [Starlight](https://starlight.astro.build) plugin to validate **_internal_** links in Markdown and MDX files.

- Validate internal links to other pages
- Validate internal links to anchors in other pages
- Validate internal links to anchors in the same page
- Validate internal links to hashes in other pages
- Validate internal links to hashes in the same page
- Ignore external links
- Run only during a production build

Expand Down
4 changes: 2 additions & 2 deletions packages/starlight-links-validator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Want to get started immediately? Check out the [getting started guide](https://s
A [Starlight](https://starlight.astro.build) plugin to validate **_internal_** links in Markdown and MDX files.

- Validate internal links to other pages
- Validate internal links to anchors in other pages
- Validate internal links to anchors in the same page
- Validate internal links to hashes in other pages
- Validate internal links to hashes in the same page
- Ignore external links
- Run only during a production build

Expand Down
8 changes: 8 additions & 0 deletions packages/starlight-links-validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ const starlightLinksValidatorOptionsSchema = z
* @default true
*/
errorOnRelativeLinks: z.boolean().default(true),
/**
* Defines whether the plugin should error on invalid hashes.
*
* When set to `false`, the plugin will only validate link pages and ignore hashes.
*
* @default true
*/
errorOnInvalidHashes: z.boolean().default(true),
/**
* Defines a list of links or glob patterns that should be excluded from validation.
*
Expand Down
16 changes: 10 additions & 6 deletions packages/starlight-links-validator/libs/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getValidationData, type Headings } from './remark'

export const ValidationErrorType = {
InconsistentLocale: 'inconsistent locale',
InvalidAnchor: 'invalid anchor',
InvalidHash: 'invalid hash',
InvalidLink: 'invalid link',
RelativeLink: 'relative link',
TrailingSlash: 'trailing slash',
Expand Down Expand Up @@ -59,7 +59,9 @@ export function validateLinks(
}

if (link.startsWith('#')) {
validateSelfAnchor(validationContext)
if (options.errorOnInvalidHashes) {
validateSelfHash(validationContext)
}
} else {
validateLink(validationContext)
}
Expand Down Expand Up @@ -151,7 +153,9 @@ function validateLink(context: ValidationContext) {
}

if (hash && !fileHeadings.includes(hash)) {
addError(errors, filePath, link, ValidationErrorType.InvalidAnchor)
if (options.errorOnInvalidHashes) {
addError(errors, filePath, link, ValidationErrorType.InvalidHash)
}
return
}

Expand All @@ -176,9 +180,9 @@ function getFileHeadings(path: string, { headings, localeConfig, options }: Vali
}

/**
* Validate a link to an anchor in the same page.
* Validate a link to an hash in the same page.
*/
function validateSelfAnchor({ errors, link, filePath, headings }: ValidationContext) {
function validateSelfHash({ errors, link, filePath, headings }: ValidationContext) {
const sanitizedHash = link.replace(/^#/, '')
const fileHeadings = headings.get(filePath)

Expand All @@ -187,7 +191,7 @@ function validateSelfAnchor({ errors, link, filePath, headings }: ValidationCont
}

if (!fileHeadings.includes(sanitizedHash)) {
addError(errors, filePath, link, 'invalid anchor')
addError(errors, filePath, link, ValidationErrorType.InvalidHash)
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/starlight-links-validator/tests/base-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ test('should validate links when the `base` Astro option is set', async () => {
['/guides/example/#description', ValidationErrorType.InvalidLink],
['/unknown', ValidationErrorType.InvalidLink],
['/unknown/', ValidationErrorType.InvalidLink],
['/test/guides/example#unknown', ValidationErrorType.InvalidAnchor],
['/test/guides/example/#unknown', ValidationErrorType.InvalidAnchor],
['/test/guides/example#unknown', ValidationErrorType.InvalidHash],
['/test/guides/example/#unknown', ValidationErrorType.InvalidHash],
['/favicon.svg', ValidationErrorType.InvalidLink],
['/guidelines/dummy.pdf', ValidationErrorType.InvalidLink],
])
Expand Down
20 changes: 10 additions & 10 deletions packages/starlight-links-validator/tests/basics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,35 +27,35 @@ test('should not build with invalid links', async () => {
['/unknown/', ValidationErrorType.InvalidLink],
['/unknown#title', ValidationErrorType.InvalidLink],
['/unknown/#title', ValidationErrorType.InvalidLink],
['#links', ValidationErrorType.InvalidAnchor],
['/guides/example/#links', ValidationErrorType.InvalidAnchor],
['#links', ValidationErrorType.InvalidHash],
['/guides/example/#links', ValidationErrorType.InvalidHash],
['/icon.svg', ValidationErrorType.InvalidLink],
['/guidelines/ui.pdf', ValidationErrorType.InvalidLink],
['/unknown-ref', ValidationErrorType.InvalidLink],
['#unknown-ref', ValidationErrorType.InvalidAnchor],
['#anotherDiv', ValidationErrorType.InvalidAnchor],
['#unknown-ref', ValidationErrorType.InvalidHash],
['#anotherDiv', ValidationErrorType.InvalidHash],
['/guides/page-with-custom-slug', ValidationErrorType.InvalidLink],
['/release/@pkg/v0.2.0', ValidationErrorType.InvalidLink],
])

expectValidationErrors(error, 'guides/example/', [
['#links', ValidationErrorType.InvalidAnchor],
['#links', ValidationErrorType.InvalidHash],
['/unknown/#links', ValidationErrorType.InvalidLink],
['/unknown', ValidationErrorType.InvalidLink],
['#anotherBlock', ValidationErrorType.InvalidAnchor],
['#anotherBlock', ValidationErrorType.InvalidHash],
['/icon.svg', ValidationErrorType.InvalidLink],
['/guidelines/ui.pdf', ValidationErrorType.InvalidLink],
['/linkcard/', ValidationErrorType.InvalidLink],
['/linkcard/#links', ValidationErrorType.InvalidLink],
['#linkcard', ValidationErrorType.InvalidAnchor],
['#linkcard', ValidationErrorType.InvalidHash],
['/linkbutton/', ValidationErrorType.InvalidLink],
['/linkbutton/#links', ValidationErrorType.InvalidLink],
['#linkbutton', ValidationErrorType.InvalidAnchor],
['#linkbutton', ValidationErrorType.InvalidHash],
])

expectValidationErrors(error, 'guides/namespacetest/', [
['#some-other-content', ValidationErrorType.InvalidAnchor],
['/guides/namespacetest/#another-content', ValidationErrorType.InvalidAnchor],
['#some-other-content', ValidationErrorType.InvalidHash],
['/guides/namespacetest/#another-content', ValidationErrorType.InvalidHash],
])

expectValidationErrors(error, 'relative/', [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ test('should validate links with custom IDs', async () => {
} catch (error) {
expectValidationErrorCount(error, 1, 1)

expectValidationErrors(error, 'test/', [['#heading-with-custom-id', ValidationErrorType.InvalidAnchor]])
expectValidationErrors(error, 'test/', [['#heading-with-custom-id', ValidationErrorType.InvalidHash]])
}
})
16 changes: 8 additions & 8 deletions packages/starlight-links-validator/tests/fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ test('should not build with invalid fallback links', async () => {
expectValidationErrors(error, 'en/', [
['/en/guides/unknown', ValidationErrorType.InvalidLink],
['/en/guides/unknown/', ValidationErrorType.InvalidLink],
['/en/guides/example#unknown', ValidationErrorType.InvalidAnchor],
['/en/guides/example/#unknown', ValidationErrorType.InvalidAnchor],
['/en/guides/example#unknown', ValidationErrorType.InvalidHash],
['/en/guides/example/#unknown', ValidationErrorType.InvalidHash],
['/es/guides/example', ValidationErrorType.InvalidLink],
['/es/guides/example/', ValidationErrorType.InvalidLink],
])

expectValidationErrors(error, 'fr/', [
['/fr/guides/unknown', ValidationErrorType.InvalidLink],
['/fr/guides/unknown/', ValidationErrorType.InvalidLink],
['/fr/guides/example#unknown', ValidationErrorType.InvalidAnchor],
['/fr/guides/example/#unknown', ValidationErrorType.InvalidAnchor],
['/fr/guides/example#unknown', ValidationErrorType.InvalidHash],
['/fr/guides/example/#unknown', ValidationErrorType.InvalidHash],
])

expectValidationErrors(error, 'fr/guides/test/', [['/', ValidationErrorType.InvalidLink]])
Expand All @@ -68,17 +68,17 @@ test('should not build with a root locale and invalid fallback links', async ()
expectValidationErrors(error, '/', [
['/guides/unknown', ValidationErrorType.InvalidLink],
['/guides/unknown/', ValidationErrorType.InvalidLink],
['/guides/example#unknown', ValidationErrorType.InvalidAnchor],
['/guides/example/#unknown', ValidationErrorType.InvalidAnchor],
['/guides/example#unknown', ValidationErrorType.InvalidHash],
['/guides/example/#unknown', ValidationErrorType.InvalidHash],
['/es/guides/example', ValidationErrorType.InvalidLink],
['/es/guides/example/', ValidationErrorType.InvalidLink],
])

expectValidationErrors(error, 'fr/', [
['/fr/guides/unknown', ValidationErrorType.InvalidLink],
['/fr/guides/unknown/', ValidationErrorType.InvalidLink],
['/fr/guides/example#unknown', ValidationErrorType.InvalidAnchor],
['/fr/guides/example/#unknown', ValidationErrorType.InvalidAnchor],
['/fr/guides/example#unknown', ValidationErrorType.InvalidHash],
['/fr/guides/example/#unknown', ValidationErrorType.InvalidHash],
])

expectValidationErrors(error, 'guides/test/', [['/en', ValidationErrorType.InvalidLink]])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { Card, CardGrid, LinkCard, LinkButton } from '@astrojs/starlight/compone

## Some links

- [Link to invalid anchor in the same page](#links)
- [Link to invalid anchor in another page](/unknown/#links)
- [Link to invalid hash in the same page](#links)
- [Link to invalid hash in another page](/unknown/#links)

<a href="/unknown">HTML link to unknown page</a>

Expand All @@ -37,10 +37,10 @@ some content

<CardGrid>
<LinkCard title="LinkCard: unknown page" href="/linkcard/" />
<LinkCard title="LinkCard: unknown page and anchor" href="/linkcard/#links" />
<LinkCard title="LinkCard: unknown anchor" href="#linkcard" />
<LinkCard title="LinkCard: unknown page and hash" href="/linkcard/#links" />
<LinkCard title="LinkCard: unknown hash" href="#linkcard" />
</CardGrid>

<LinkButton href="/linkbutton/">LinkButton: unknown page</LinkButton>
<LinkButton href="/linkbutton/#links">LinkButton: unknown page and anchor</LinkButton>
<LinkButton href="#linkbutton">LinkButton: unknown anchor</LinkButton>
<LinkButton href="/linkbutton/#links">LinkButton: unknown page and hash</LinkButton>
<LinkButton href="#linkbutton">LinkButton: unknown hash</LinkButton>
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ title: Test

# More links

- [Link to valid anchor in this page](#some-links)
- [Link to invalid anchor in this page](#links)
- [Link to valid anchor in another MDX page](/guides/example/#some-links)
- [Link to invalid anchor in another MDX page](/guides/example/#links)
- [Link to valid hash in this page](#some-links)
- [Link to invalid hash in this page](#links)
- [Link to valid hash in another MDX page](/guides/example/#some-links)
- [Link to invalid hash in another MDX page](/guides/example/#links)
- [Link to invalid asset](/icon.svg)
- [Link to another invalid asset](/guidelines/ui.pdf)

## Links with references

- [Link reference to unknwon page][ref-unknown-page]
- [Link reference to invalid anchor][ref-invalid-anchor]
- [Link reference to invalid hash][ref-invalid-hash]

[ref-unknown-page]: /unknown-ref
[ref-invalid-anchor]: #unknown-ref
[ref-invalid-hash]: #unknown-ref

<div id="aDiv">
some content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Card, CardGrid, LinkCard, LinkButton } from '@astrojs/starlight/compone

## Some links

- [Link to anchor in the same page](#some-links)
- [Link to hash in the same page](#some-links)

<a href="/test">HTML link to another page</a>

Expand All @@ -40,10 +40,10 @@ some content

<CardGrid>
<LinkCard title="LinkCard: page" href="/test/" />
<LinkCard title="LinkCard: page and anchor" href="/test/#title" />
<LinkCard title="LinkCard: anchor" href="#steps" />
<LinkCard title="LinkCard: page and hash" href="/test/#title" />
<LinkCard title="LinkCard: hash" href="#steps" />
</CardGrid>

<LinkButton href="/test/">LinkCard: unknown page</LinkButton>
<LinkButton href="/test/#title">LinkCard: unknown page and anchor</LinkButton>
<LinkButton href="#steps">LinkCard: unknown anchor</LinkButton>
<LinkButton href="/test/#title">LinkCard: unknown page and hash</LinkButton>
<LinkButton href="#steps">LinkCard: unknown hash</LinkButton>
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ title: Index

# More links

- [Link to anchor in this page](#some-links)
- [Link to anchor in another MDX page](/guides/example/#some-links)
- [Link to hash in this page](#some-links)
- [Link to hash in another MDX page](/guides/example/#some-links)
- [Link to an asset](/favicon.svg)
- [Link to another asset](/guidelines/dummy.pdf)

## A more `complex` heading

- [Link to more complex anchor](#a-more-complex-heading)
- [Link to more complex hash](#a-more-complex-heading)

## Links with references

- [ref]
- [Link reference][ref]
- [Link reference with anchor in this page][ref-with-anchor-internal]
- [Link reference with anchor in another page][ref-with-anchor-external]
- [Link reference with hash in this page][ref-with-hash-internal]
- [Link reference with hash in another page][ref-with-hash-external]

[ref]: /test
[ref-with-anchor-internal]: #some-links
[ref-with-anchor-external]: /test#title
[ref-with-hash-internal]: #some-links
[ref-with-hash-external]: /test#title

## Link to page with custom slug

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import starlight from '@astrojs/starlight'
import { defineConfig } from 'astro/config'

import starlightLinksValidator from '../..'

export default defineConfig({
integrations: [
starlight({
plugins: [starlightLinksValidator({ errorOnInvalidHashes: false })],
title: 'Starlight Links Validator Tests - invalid hashes invalid links',
}),
],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: Example
---

import { Card, CardGrid, LinkCard, LinkButton } from '@astrojs/starlight/components'

## Steps

<CardGrid stagger>
<Card title="Step 1">Do something</Card>
<Card title="Step 2">Do something else</Card>
</CardGrid>

## Some links

- [Link to invalid hash in the same page](#links)
- [Link to invalid hash in another page](/unknown/#links)

<a href="/unknown">HTML link to unknown page</a>

<div id="aBlock">
some content

some content

some content

some content

<a href="#anotherBlock">
test
</a>
</div>

<a href="/icon.svg">Link to invalid asset</a>
<a href="/guidelines/ui.pdf">Link to another invalid asset</a>

<CardGrid>
<LinkCard title="LinkCard: unknown page" href="/linkcard/" />
<LinkCard title="LinkCard: unknown page and hash" href="/linkcard/#links" />
<LinkCard title="LinkCard: unknown hash" href="#linkcard" />
</CardGrid>

<LinkButton href="/linkbutton/">LinkButton: unknown page</LinkButton>
<LinkButton href="/linkbutton/#links">LinkButton: unknown page and hash</LinkButton>
<LinkButton href="#linkbutton">LinkButton: unknown hash</LinkButton>
Loading

0 comments on commit 32a92f8

Please sign in to comment.