Skip to content

Commit 889c7c7

Browse files
committed
fix: fixes various issues with IDs and links with HTML embedded in Markdown
1 parent cc14af9 commit 889c7c7

File tree

11 files changed

+728
-218
lines changed

11 files changed

+728
-218
lines changed

example/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"lint": "prettier -c --cache . && eslint . --cache --max-warnings=0"
1414
},
1515
"dependencies": {
16-
"@astrojs/starlight": "0.2.0",
17-
"astro": "2.6.6",
16+
"@astrojs/starlight": "0.7.2",
17+
"astro": "2.10.9",
1818
"starlight-links-validator": "workspace:*"
1919
},
2020
"engines": {

example/src/content/docs/guides/example.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,17 @@ Writing a good guide requires thinking about what your users are trying to do.
1616
- Check the [links](#links) section of this guide
1717
- Check the [reference](/reference/example/#further-reading) page
1818
- Check the [links](/guides/links/) page
19+
20+
<div id="aBlock">
21+
some content
22+
23+
some content
24+
25+
some content
26+
27+
some content
28+
29+
<a href="#anotherBlock">
30+
test
31+
</a>
32+
</div>

packages/starlight-links-validator/libs/remark.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import 'mdast-util-mdx-jsx'
33
import nodePath from 'node:path'
44

55
import { slug } from 'github-slugger'
6+
import type { Nodes } from 'hast'
7+
import { fromHtml } from 'hast-util-from-html'
8+
import { hasProperty } from 'hast-util-has-property'
69
import type { Root } from 'mdast'
10+
import type { MdxJsxAttribute, MdxJsxExpressionAttribute } from 'mdast-util-mdx-jsx'
711
import { toString } from 'mdast-util-to-string'
812
import type { Plugin } from 'unified'
913
import { visit } from 'unist-util-visit'
@@ -20,7 +24,7 @@ export const remarkStarlightLinksValidator: Plugin<[], Root> = function () {
2024
const fileHeadings: string[] = []
2125
const fileLinks: string[] = []
2226

23-
visit(tree, ['heading', 'link', 'mdxJsxFlowElement'], (node) => {
27+
visit(tree, ['heading', 'html', 'link', 'mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
2428
// https://github.com/syntax-tree/mdast#nodes
2529
// https://github.com/syntax-tree/mdast-util-mdx-jsx#nodes
2630
switch (node.type) {
@@ -43,6 +47,12 @@ export const remarkStarlightLinksValidator: Plugin<[], Root> = function () {
4347
break
4448
}
4549
case 'mdxJsxFlowElement': {
50+
for (const attribute of node.attributes) {
51+
if (isMdxIdAttribute(attribute)) {
52+
fileHeadings.push(attribute.value)
53+
}
54+
}
55+
4656
if (node.name !== 'a') {
4757
break
4858
}
@@ -61,6 +71,37 @@ export const remarkStarlightLinksValidator: Plugin<[], Root> = function () {
6171
}
6272
}
6373

74+
break
75+
}
76+
case 'mdxJsxTextElement': {
77+
for (const attribute of node.attributes) {
78+
if (isMdxIdAttribute(attribute)) {
79+
fileHeadings.push(attribute.value)
80+
}
81+
}
82+
83+
break
84+
}
85+
case 'html': {
86+
const htmlTree = fromHtml(node.value, { fragment: true })
87+
88+
// @ts-expect-error - https://github.com/microsoft/TypeScript/issues/51188
89+
visit(htmlTree, (htmlNode: Nodes) => {
90+
if (hasProperty(htmlNode, 'id') && typeof htmlNode.properties.id === 'string') {
91+
fileHeadings.push(htmlNode.properties.id)
92+
}
93+
94+
if (
95+
htmlNode.type === 'element' &&
96+
htmlNode.tagName === 'a' &&
97+
hasProperty(htmlNode, 'href') &&
98+
typeof htmlNode.properties.href === 'string' &&
99+
isInternalLink(htmlNode.properties.href)
100+
) {
101+
fileLinks.push(htmlNode.properties.href)
102+
}
103+
})
104+
64105
break
65106
}
66107
}
@@ -91,5 +132,15 @@ function normalizeFilePath(filePath?: string) {
91132
.replace(/\/?$/, '/')
92133
}
93134

135+
function isMdxIdAttribute(attribute: MdxJsxAttribute | MdxJsxExpressionAttribute): attribute is MdxIdAttribute {
136+
return attribute.type === 'mdxJsxAttribute' && attribute.name === 'id' && typeof attribute.value === 'string'
137+
}
138+
94139
export type Headings = Map<string, string[]>
95140
export type Links = Map<string, string[]>
141+
142+
interface MdxIdAttribute {
143+
name: 'id'
144+
type: 'mdxJsxAttribute'
145+
value: string
146+
}

packages/starlight-links-validator/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
},
1616
"dependencies": {
1717
"github-slugger": "2.0.0",
18+
"hast-util-from-html": "2.0.1",
19+
"hast-util-has-property": "3.0.0",
1820
"kleur": "4.1.5",
1921
"mdast-util-to-string": "3.2.0",
2022
"unist-util-visit": "4.1.2"
2123
},
2224
"devDependencies": {
25+
"@types/hast": "3.0.0",
2326
"@types/mdast": "3.0.11",
2427
"@types/node": "18.16.18",
28+
"astro": "2.10.9",
2529
"mdast-util-mdx-jsx": "2.1.4",
2630
"typescript": "5.1.3",
2731
"unified": "10.1.2",

packages/starlight-links-validator/tests/fixtures/base/astro.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import starlight from '@astrojs/starlight'
22
import { defineConfig } from 'astro/config'
33

4-
import starlightLinksValidator from '../../src'
4+
import starlightLinksValidator from '../..'
55

66
export default defineConfig({
77
integrations: [

packages/starlight-links-validator/tests/fixtures/with-invalid-links/src/content/docs/guides/example.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,17 @@ import { Card, CardGrid } from '@astrojs/starlight/components'
1717
- [Link to invalid anchor in another page](/unknown/#links)
1818

1919
<a href="/unknown">HTML link to unknown page</a>
20+
21+
<div id="aBlock">
22+
some content
23+
24+
some content
25+
26+
some content
27+
28+
some content
29+
30+
<a href="#anotherBlock">
31+
test
32+
</a>
33+
</div>

packages/starlight-links-validator/tests/fixtures/with-invalid-links/src/content/docs/test.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,17 @@ title: Test
2020
- [Link to invalid anchor in this page](#links)
2121
- [Link to valid anchor in another MDX page](/guides/example/#some-links)
2222
- [Link to invalid anchor in another MDX page](/guides/example/#links)
23+
24+
<div id="aDiv">
25+
some content
26+
27+
some content
28+
29+
some content
30+
31+
some content
32+
33+
<a href="#anotherDiv">
34+
test
35+
</a>
36+
</div>

packages/starlight-links-validator/tests/fixtures/with-valid-links/src/content/docs/guides/example.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,21 @@ import { Card, CardGrid } from '@astrojs/starlight/components'
1616
- [Link to anchor in the same page](#some-links)
1717

1818
<a href="/test">HTML link to another page</a>
19+
20+
<div id="aBlock">
21+
some content
22+
23+
some content
24+
25+
some <span id="aText">other</span> content
26+
27+
some content
28+
29+
<a href="#aBlock">test block</a>
30+
31+
some content
32+
33+
<a href="#aText">
34+
test text
35+
</a>
36+
</div>

packages/starlight-links-validator/tests/fixtures/with-valid-links/src/content/docs/test.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,17 @@ title: Test
77
Some content.
88

99
- [Link to same page](/test)
10+
11+
<div id="aDiv">
12+
some content
13+
14+
some content
15+
16+
some content
17+
18+
some content
19+
20+
<a href="#aDiv">
21+
test
22+
</a>
23+
</div>

packages/starlight-links-validator/tests/validation.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test('should not build with invalid links', async () => {
1616
try {
1717
await loadFixture('with-invalid-links')
1818
} catch (error) {
19-
expect(error).toMatch(/Found 10 invalid links in 2 files./)
19+
expect(error).toMatch(/Found 12 invalid links in 2 files./)
2020

2121
expect(error).toMatch(
2222
new RegExp(`▶ test/
@@ -26,14 +26,16 @@ test('should not build with invalid links', async () => {
2626
├─ /unknown#title
2727
├─ /unknown/#title
2828
├─ #links
29-
└─ /guides/example/#links`)
29+
├─ /guides/example/#links
30+
└─ #anotherDiv`)
3031
)
3132

3233
expect(error).toMatch(
3334
new RegExp(`▶ guides/example/
3435
├─ #links
3536
├─ /unknown/#links
36-
└─ /unknown`)
37+
├─ /unknown
38+
└─ #anotherBlock`)
3739
)
3840
}
3941
})

0 commit comments

Comments
 (0)