Skip to content

Commit e447787

Browse files
authored
fix: page and link generation issue with empty slugs (#80)
1 parent c97caa0 commit e447787

File tree

7 files changed

+71
-22
lines changed

7 files changed

+71
-22
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'starlight-typedoc': patch
3+
---
4+
5+
Fixes a potential page and link generation issue with some declaration reference names such as a function named `$`.

fixtures/basics/src/functions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ export function doThingC() {
2929
export function doThingFaster() {
3030
return 'thingB'
3131
}
32+
33+
/**
34+
* A function that print dollars.
35+
*/
36+
export function $() {
37+
return '$'
38+
}

packages/starlight-typedoc/libs/starlight.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ export function getRelativeURL(url: string, baseUrl: string, pageUrl?: string):
273273

274274
let constructedUrl = typeof baseUrl === 'string' ? baseUrl : ''
275275
constructedUrl += segments.length > 0 ? `${segments.join('/')}/` : ''
276-
constructedUrl += slug(filePath.name)
276+
const fileNameSlug = slug(filePath.name)
277+
constructedUrl += fileNameSlug || filePath.name
277278
constructedUrl += '/'
278279
constructedUrl += anchor && anchor.length > 0 ? `#${anchor}` : ''
279280

packages/starlight-typedoc/libs/typedoc.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path'
33
import url from 'node:url'
44

55
import type { AstroConfig, AstroIntegrationLogger } from 'astro'
6+
import { slug } from 'github-slugger'
67
import {
78
Application,
89
PageEvent,
@@ -18,7 +19,7 @@ import type { StarlightTypeDocOptions } from '..'
1819

1920
import { StarlightTypeDocLogger } from './logger'
2021
import { addFrontmatter } from './markdown'
21-
import { getStarlightTypeDocOutputDirectory } from './starlight'
22+
import { getRelativeURL, getStarlightTypeDocOutputDirectory } from './starlight'
2223
import { StarlightTypeDocTheme } from './theme'
2324

2425
const defaultTypeDocConfig: TypeDocConfig = {
@@ -91,6 +92,7 @@ async function bootstrapApp(
9192
logger: AstroIntegrationLogger,
9293
) {
9394
const pagesToRemove: string[] = []
95+
const outputDirectory = getStarlightTypeDocOutputDirectory(output.directory, output.base)
9496

9597
const app = await Application.bootstrapWithPlugins({
9698
...defaultTypeDocConfig,
@@ -106,10 +108,10 @@ async function bootstrapApp(
106108
app.options.addReader(new TSConfigReader())
107109
app.renderer.defineTheme('starlight-typedoc', StarlightTypeDocTheme)
108110
app.renderer.on(PageEvent.BEGIN, (event) => {
109-
onRendererPageBegin(event as MarkdownPageEvent, pagination)
111+
onRendererPageBegin(event as MarkdownPageEvent, outputDirectory, pagination)
110112
})
111113
app.renderer.on(PageEvent.END, (event) => {
112-
const shouldRemovePage = onRendererPageEnd(event as MarkdownPageEvent, pagination)
114+
const shouldRemovePage = onRendererPageEnd(event as MarkdownPageEvent, outputDirectory, pagination)
113115
if (shouldRemovePage) {
114116
pagesToRemove.push(event.filename)
115117
}
@@ -118,7 +120,7 @@ async function bootstrapApp(
118120
onRendererEnd(pagesToRemove)
119121
})
120122
app.options.addDeclaration({
121-
defaultValue: getStarlightTypeDocOutputDirectory(output.directory, output.base),
123+
defaultValue: outputDirectory,
122124
help: 'The starlight-typedoc output directory containing the generated documentation markdown files relative to the `src/content/docs/` directory.',
123125
name: 'starlight-typedoc-output',
124126
type: ParameterType.String,
@@ -127,17 +129,20 @@ async function bootstrapApp(
127129
return app
128130
}
129131

130-
function onRendererPageBegin(event: MarkdownPageEvent, pagination: boolean) {
132+
function onRendererPageBegin(event: MarkdownPageEvent, outputDirectory: string, pagination: boolean) {
131133
if (event.frontmatter) {
132-
event.frontmatter['editUrl'] = false
133-
event.frontmatter['next'] = pagination
134-
event.frontmatter['prev'] = pagination
135-
event.frontmatter['title'] = event.model.name
134+
event.frontmatter = getModelFrontmatter(event, outputDirectory, {
135+
...event.frontmatter,
136+
editUrl: false,
137+
next: pagination,
138+
prev: pagination,
139+
title: event.model.name,
140+
})
136141
}
137142
}
138143

139144
// Returning `true` will delete the page from the filesystem.
140-
function onRendererPageEnd(event: MarkdownPageEvent, pagination: boolean) {
145+
function onRendererPageEnd(event: MarkdownPageEvent, outputDirectory: string, pagination: boolean) {
141146
if (!event.contents) {
142147
return false
143148
} else if (/^.+[/\\]README\.md$/.test(event.url)) {
@@ -149,13 +154,16 @@ function onRendererPageEnd(event: MarkdownPageEvent, pagination: boolean) {
149154
}
150155

151156
if (!event.frontmatter) {
152-
event.contents = addFrontmatter(event.contents, {
153-
editUrl: false,
154-
next: pagination,
155-
prev: pagination,
156-
// Wrap in quotes to prevent issue with special characters in frontmatter
157-
title: `"${event.model.name}"`,
158-
})
157+
event.contents = addFrontmatter(
158+
event.contents,
159+
getModelFrontmatter(event, outputDirectory, {
160+
editUrl: false,
161+
next: pagination,
162+
prev: pagination,
163+
// Wrap in quotes to prevent issue with special characters in frontmatter
164+
title: `"${event.model.name}"`,
165+
}),
166+
)
159167
}
160168

161169
return false
@@ -167,6 +175,20 @@ function onRendererEnd(pagesToRemove: string[]) {
167175
}
168176
}
169177

178+
function getModelFrontmatter(
179+
event: MarkdownPageEvent,
180+
outputDirectory: string,
181+
frontmatter: NonNullable<MarkdownPageEvent['frontmatter']>,
182+
) {
183+
const defaultSlug = slug(event.model.name)
184+
185+
if (defaultSlug.length === 0) {
186+
frontmatter['slug'] = getRelativeURL(event.url, outputDirectory, event.url).replaceAll(/^\/|\/$/g, '')
187+
}
188+
189+
return frontmatter
190+
}
191+
170192
export class NoReflectionsError extends Error {
171193
constructor() {
172194
super('Failed to generate TypeDoc documentation.')

packages/starlight-typedoc/tests/e2e/basics/sidebar.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ test('should generate the proper items for for a single entry point', async ({ d
6868
},
6969
{
7070
label: 'Functions',
71-
items: [{ name: 'doThingA' }, { name: 'doThingB' }, { name: 'doThingC' }, { name: 'doThingFaster' }],
71+
items: [
72+
{ name: '$' },
73+
{ name: 'doThingA' },
74+
{ name: 'doThingB' },
75+
{ name: 'doThingC' },
76+
{ name: 'doThingFaster' },
77+
],
7278
collapsed: true,
7379
},
7480
{
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect, test } from '../test'
2+
3+
test('handles pages with a name that would lead to an empty slug', async ({ docPage }) => {
4+
await docPage.goto('functions/$')
5+
6+
await expect(docPage.content.getByText('A function that print dollars.')).toBeVisible()
7+
expect(await docPage.sidebar.locator('a[aria-current="page"]').getAttribute('href')).toBe('/api/functions/$/')
8+
})

packages/starlight-typedoc/tests/e2e/fixtures/DocPage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class DocPage {
4141
return this.page.getByRole('main')
4242
}
4343

44-
get #sidebar() {
44+
get sidebar() {
4545
return this.page.getByRole('navigation', { name: 'Main' })
4646
}
4747

@@ -56,7 +56,7 @@ export class DocPage {
5656
}
5757

5858
get #typeDocSidebarRootDetails() {
59-
return this.#sidebar
59+
return this.sidebar
6060
.getByRole('listitem')
6161
.locator(`details:has(summary > div > span:has-text("${this.#expectedTypeDocSidebarLabel}"))`)
6262
}
@@ -66,7 +66,7 @@ export class DocPage {
6666
}
6767

6868
async getSidebarItems() {
69-
return this.#getTypeDocSidebarChildrenItems(this.#sidebar.locator('ul.top-level'))
69+
return this.#getTypeDocSidebarChildrenItems(this.sidebar.locator('ul.top-level'))
7070
}
7171

7272
async #getTypeDocSidebarChildrenItems(list: Locator): Promise<TypeDocSidebarItem[]> {

0 commit comments

Comments
 (0)