starlight-versions 0.10.0 → 0.10.2
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/.npmignore +5 -0
- package/CHANGELOG.md +20 -0
- package/libs/fs.ts +16 -8
- package/libs/markdown.ts +96 -23
- package/libs/plugin.ts +2 -1
- package/libs/starlight.ts +12 -7
- package/libs/versions.ts +38 -26
- package/middleware.ts +7 -5
- package/package.json +4 -5
package/.npmignore
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# starlight-versions
|
|
2
2
|
|
|
3
|
+
## 0.10.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#86](https://github.com/HiDeoo/starlight-versions/pull/86) [`27959fb`](https://github.com/HiDeoo/starlight-versions/commit/27959fb72e19fbad44a79fe673a2246ab64ad05a) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes versioning of public assets in media elements using both `src` and `srcset` attributes.
|
|
8
|
+
|
|
9
|
+
- [#84](https://github.com/HiDeoo/starlight-versions/pull/84) [`7cc6fdf`](https://github.com/HiDeoo/starlight-versions/commit/7cc6fdfe49f66df7a77b1f1b18891ebe50a37b1e) Thanks [@dgesteves](https://github.com/dgesteves)! - Fixes versioning of Markdown files using syntax that is not valid in MDX, like `<->` arrows.
|
|
10
|
+
|
|
11
|
+
- [#86](https://github.com/HiDeoo/starlight-versions/pull/86) [`27959fb`](https://github.com/HiDeoo/starlight-versions/commit/27959fb72e19fbad44a79fe673a2246ab64ad05a) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes versioning of public assets in `srcset` attributes with multiple images or descriptors.
|
|
12
|
+
|
|
13
|
+
- [#86](https://github.com/HiDeoo/starlight-versions/pull/86) [`27959fb`](https://github.com/HiDeoo/starlight-versions/commit/27959fb72e19fbad44a79fe673a2246ab64ad05a) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes versioning of images inside links.
|
|
14
|
+
|
|
15
|
+
- [#86](https://github.com/HiDeoo/starlight-versions/pull/86) [`27959fb`](https://github.com/HiDeoo/starlight-versions/commit/27959fb72e19fbad44a79fe673a2246ab64ad05a) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes versioning of public assets in inline media elements in MDX files.
|
|
16
|
+
|
|
17
|
+
## 0.10.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#80](https://github.com/HiDeoo/starlight-versions/pull/80) [`dd385c8`](https://github.com/HiDeoo/starlight-versions/commit/dd385c88940cc36e5733d0f082a04907f44bb2f0) Thanks [@kozmagabor](https://github.com/kozmagabor)! - Fixes versioning of assets stored alongside documentation pages.
|
|
22
|
+
|
|
3
23
|
## 0.10.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/libs/fs.ts
CHANGED
|
@@ -32,13 +32,17 @@ export async function copyDirectory(sourceDir: URL, destDir: URL, callback: Copy
|
|
|
32
32
|
await copyDirectory(source, dest, callback, false)
|
|
33
33
|
} else if (entry.isFile()) {
|
|
34
34
|
const source = new URL(entry.name, sourceDir)
|
|
35
|
-
const content = await fs.readFile(source, 'utf8')
|
|
36
35
|
|
|
37
|
-
const updatedContent = await callback({ type: 'file',
|
|
36
|
+
const updatedContent = await callback({ type: 'file', url: source })
|
|
37
|
+
if (updatedContent === true) continue
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
const dest = new URL(entry.name, destDir)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
if (updatedContent === undefined) {
|
|
42
|
+
await copyFile(source, dest)
|
|
43
|
+
} else if (typeof updatedContent === 'string') {
|
|
44
|
+
await fs.writeFile(dest, updatedContent)
|
|
45
|
+
}
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -72,8 +76,12 @@ export async function isDirectoryEntry(entry: Dirent) {
|
|
|
72
76
|
return stats.isDirectory()
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Defines how entries are handled when copying a directory with `copyDirectory()`.
|
|
81
|
+
*
|
|
82
|
+
* - For files, return `undefined` to copy as-is, a string to write transformed content, or `true` to skip.
|
|
83
|
+
* - For directories, return `undefined` to use the default destination, a `URL` to override it, or `true` to skip.
|
|
84
|
+
*/
|
|
75
85
|
export type CopyDirectoryCallback = (
|
|
76
|
-
entry:
|
|
77
|
-
|
|
78
|
-
| { type: 'directory'; name: string; isRoot: boolean; source: URL; dest: URL },
|
|
79
|
-
) => Promise<string | boolean | URL>
|
|
86
|
+
entry: { type: 'file'; url: URL } | { type: 'directory'; name: string; isRoot: boolean; source: URL; dest: URL },
|
|
87
|
+
) => Promise<string | true | URL | undefined>
|
package/libs/markdown.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { Element } from 'hast'
|
|
4
|
+
import { fromHtml } from 'hast-util-from-html'
|
|
5
|
+
import type { Html, Image, Link, Root } from 'mdast'
|
|
4
6
|
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
|
|
5
7
|
import type { MdxjsEsm } from 'mdast-util-mdxjs-esm'
|
|
6
8
|
import { remark } from 'remark'
|
|
@@ -12,6 +14,7 @@ import type { VFile } from 'vfile'
|
|
|
12
14
|
|
|
13
15
|
import {
|
|
14
16
|
ensureTrailingSlash,
|
|
17
|
+
getExtension,
|
|
15
18
|
isAbsoluteLink,
|
|
16
19
|
stripLeadingAndTrailingSlashes,
|
|
17
20
|
stripLeadingSlash,
|
|
@@ -22,12 +25,17 @@ import type { Version, VersionAsset } from './versions'
|
|
|
22
25
|
|
|
23
26
|
const importPathRegex = /(from ?["'])([^"']*)(["'];?\s?)$/gm
|
|
24
27
|
const astroAssetRegex = /\.(png|jpg|jpeg|tiff|webp|gif|svg|avif|.+\?raw)$/i
|
|
28
|
+
const htmlAttributeRegex = /^(?<prefix>[^=]+=\s*(?<quote>["']?))(?<value>.*)\k<quote>$/s
|
|
29
|
+
const srcsetUrlRegex = /(?<=(?:^|,)\s*)[^\s,]\S*?(?=,*(?:\s|$))/g
|
|
25
30
|
|
|
26
31
|
const mediaElements = new Set(['img', 'source', 'Image', 'audio', 'video'])
|
|
27
32
|
|
|
28
|
-
const
|
|
33
|
+
const markdownProcessor = remark().use(remarkDirective).use(remarkFrontmatter).use(remarkStarlightVersions)
|
|
34
|
+
const mdxProcessor = markdownProcessor().use(remarkMdx)
|
|
29
35
|
|
|
30
36
|
export async function transformMarkdown(markdown: string, context: TransformContext) {
|
|
37
|
+
const processor = getExtension(context.url.pathname) === '.mdx' ? mdxProcessor : markdownProcessor
|
|
38
|
+
|
|
31
39
|
const file = await processor.process({
|
|
32
40
|
data: { ...context },
|
|
33
41
|
value: markdown,
|
|
@@ -46,17 +54,11 @@ export function remarkStarlightVersions() {
|
|
|
46
54
|
case 'link': {
|
|
47
55
|
return handleLinks(node, file)
|
|
48
56
|
}
|
|
49
|
-
case 'mdxJsxTextElement': {
|
|
50
|
-
if (node.name === 'a') {
|
|
51
|
-
return handleLinkElements(node, file)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return CONTINUE
|
|
55
|
-
}
|
|
56
57
|
case 'mdxjsEsm': {
|
|
57
58
|
return handleImports(node, file)
|
|
58
59
|
}
|
|
59
|
-
case 'mdxJsxFlowElement':
|
|
60
|
+
case 'mdxJsxFlowElement':
|
|
61
|
+
case 'mdxJsxTextElement': {
|
|
60
62
|
if (node.name === 'a') {
|
|
61
63
|
return handleLinkElements(node, file)
|
|
62
64
|
}
|
|
@@ -70,6 +72,9 @@ export function remarkStarlightVersions() {
|
|
|
70
72
|
case 'image': {
|
|
71
73
|
return handleImages(node, file)
|
|
72
74
|
}
|
|
75
|
+
case 'html': {
|
|
76
|
+
return handleHtml(node, file)
|
|
77
|
+
}
|
|
73
78
|
default: {
|
|
74
79
|
return CONTINUE
|
|
75
80
|
}
|
|
@@ -102,7 +107,7 @@ function handleFrontmatter(tree: Root, file: VFile) {
|
|
|
102
107
|
if (file.data.locale && segments[0] === file.data.locale) {
|
|
103
108
|
segments.splice(1, 0, file.data.version.slug)
|
|
104
109
|
} else {
|
|
105
|
-
segments.
|
|
110
|
+
segments.unshift(file.data.version.slug)
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
frontmatter.slug = segments.join('/')
|
|
@@ -144,7 +149,7 @@ function handleFrontmatter(tree: Root, file: VFile) {
|
|
|
144
149
|
function handleLinks(node: Link, file: VFile) {
|
|
145
150
|
node.url = transformLink(node.url, file)
|
|
146
151
|
|
|
147
|
-
return
|
|
152
|
+
return CONTINUE
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
function handleLinkElements(node: MdxJsxFlowElement | MdxJsxTextElement, file: VFile) {
|
|
@@ -194,18 +199,74 @@ function handleImages(node: Image, file: VFile) {
|
|
|
194
199
|
return SKIP
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
function handleMediaElements(node: MdxJsxFlowElement, file: VFile) {
|
|
198
|
-
const
|
|
199
|
-
(attribute
|
|
200
|
-
)
|
|
202
|
+
function handleMediaElements(node: MdxJsxFlowElement | MdxJsxTextElement, file: VFile) {
|
|
203
|
+
for (const attribute of node.attributes) {
|
|
204
|
+
if (attribute.type !== 'mdxJsxAttribute' || typeof attribute.value !== 'string') continue
|
|
201
205
|
|
|
202
|
-
|
|
206
|
+
if (attribute.name === 'src' && isPublicAsset(attribute.value)) {
|
|
207
|
+
attribute.value = addVersionToPublicAsset(attribute.value, file)
|
|
208
|
+
} else if (attribute.name === 'srcset' || attribute.name === 'srcSet') {
|
|
209
|
+
attribute.value = addVersionToPublicSrcset(attribute.value, file)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
203
212
|
|
|
204
|
-
|
|
213
|
+
return CONTINUE
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Raw HTML nodes only exist in non-MDX files (MDX parses HTML as JSX) and are not re-serialized to preserve them as
|
|
217
|
+
// authored, e.g. unclosed inline tags, and only the transformed attribute values are updated.
|
|
218
|
+
function handleHtml(node: Html, file: VFile) {
|
|
219
|
+
const tree = fromHtml(node.value, { fragment: true, verbose: true })
|
|
220
|
+
// Updates are keyed by attribute offset as misnested HTML can lead the parser to recreate elements which share the
|
|
221
|
+
// source position of the original element and would be updated multiple times.
|
|
222
|
+
const updates = new Map<number, HtmlAttribute>()
|
|
223
|
+
|
|
224
|
+
visit(tree, 'element', (element) => {
|
|
225
|
+
if (element.tagName === 'a') {
|
|
226
|
+
const href = getHtmlAttribute(node.value, element, 'href')
|
|
227
|
+
if (!href || updates.has(href.start)) return
|
|
228
|
+
|
|
229
|
+
updates.set(href.start, { ...href, value: transformLink(href.value, file) })
|
|
230
|
+
} else if (mediaElements.has(element.tagName)) {
|
|
231
|
+
const src = getHtmlAttribute(node.value, element, 'src')
|
|
232
|
+
|
|
233
|
+
if (src && !updates.has(src.start) && isPublicAsset(src.value)) {
|
|
234
|
+
updates.set(src.start, { ...src, value: addVersionToPublicAsset(src.value, file) })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const srcset = getHtmlAttribute(node.value, element, 'srcSet')
|
|
238
|
+
|
|
239
|
+
if (srcset && !updates.has(srcset.start)) {
|
|
240
|
+
updates.set(srcset.start, { ...srcset, value: addVersionToPublicSrcset(srcset.value, file) })
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Update attributes from the end to keep the offsets of the previous ones valid.
|
|
246
|
+
const sortedUpdates = updates
|
|
247
|
+
.values()
|
|
248
|
+
.toArray()
|
|
249
|
+
.toSorted((a, b) => b.start - a.start)
|
|
250
|
+
|
|
251
|
+
for (const { end, start, value } of sortedUpdates) {
|
|
252
|
+
node.value = `${node.value.slice(0, start)}${value}${node.value.slice(end)}`
|
|
253
|
+
}
|
|
205
254
|
|
|
206
255
|
return SKIP
|
|
207
256
|
}
|
|
208
257
|
|
|
258
|
+
function getHtmlAttribute(html: string, element: Element, property: string): HtmlAttribute | undefined {
|
|
259
|
+
const position = element.data?.position.properties?.[property]
|
|
260
|
+
if (position?.start.offset === undefined || position.end.offset === undefined) return
|
|
261
|
+
|
|
262
|
+
const match = htmlAttributeRegex.exec(html.slice(position.start.offset, position.end.offset))
|
|
263
|
+
if (!match?.groups?.['prefix'] || !match.groups['value']) return
|
|
264
|
+
|
|
265
|
+
const start = position.start.offset + match.groups['prefix'].length
|
|
266
|
+
|
|
267
|
+
return { end: start + match.groups['value'].length, start, value: match.groups['value'] }
|
|
268
|
+
}
|
|
269
|
+
|
|
209
270
|
function handleImports(node: MdxjsEsm, file: VFile) {
|
|
210
271
|
node.value = node.value.replaceAll(importPathRegex, (match, start, importPath: string, end) => {
|
|
211
272
|
if (!importPath.startsWith('../')) return match
|
|
@@ -232,8 +293,7 @@ function addVersionToLink(link: string, file: VFile) {
|
|
|
232
293
|
|
|
233
294
|
const segments = link.split('/')
|
|
234
295
|
|
|
235
|
-
|
|
236
|
-
if (file.data.locale && segments[1] === file.data.locale) slugVersionIndex = 2
|
|
296
|
+
const slugVersionIndex = file.data.locale && segments[1] === file.data.locale ? 2 : 1
|
|
237
297
|
|
|
238
298
|
segments.splice(slugVersionIndex, 0, file.data.version.slug)
|
|
239
299
|
|
|
@@ -246,15 +306,17 @@ function addVersionToLink(link: string, file: VFile) {
|
|
|
246
306
|
|
|
247
307
|
function addVersionToAstroAsset(asset: string, file: VFile) {
|
|
248
308
|
assert.ok(file.data.version, 'A version must be provided to add a version to an Astro asset.')
|
|
309
|
+
assert.ok(file.data.docsDir, 'A docs directory must be provided to add a version to an Astro asset.')
|
|
249
310
|
|
|
250
311
|
const source = new URL(asset, file.data.url)
|
|
312
|
+
if (source.href.startsWith(file.data.docsDir.href)) return asset
|
|
251
313
|
|
|
252
314
|
const segments = asset.split('/')
|
|
253
|
-
segments.splice(-1, 0, file.data.version.slug)
|
|
315
|
+
segments.splice(Math.max(segments.length - 1, 0), 0, file.data.version.slug)
|
|
254
316
|
|
|
255
317
|
addVersionAsset(file, { source, dest: new URL(segments.join('/'), file.data.url) })
|
|
256
318
|
|
|
257
|
-
segments.
|
|
319
|
+
segments.unshift('..')
|
|
258
320
|
|
|
259
321
|
return segments.join('/')
|
|
260
322
|
}
|
|
@@ -265,13 +327,17 @@ function addVersionToPublicAsset(asset: string, file: VFile) {
|
|
|
265
327
|
const source = new URL(`.${asset}`, file.data.publicDir)
|
|
266
328
|
|
|
267
329
|
const segments = asset.split('/')
|
|
268
|
-
segments.splice(-1, 0, file.data.version.slug)
|
|
330
|
+
segments.splice(Math.max(segments.length - 1, 0), 0, file.data.version.slug)
|
|
269
331
|
|
|
270
332
|
addVersionAsset(file, { source, dest: new URL(`.${segments.join('/')}`, file.data.publicDir) })
|
|
271
333
|
|
|
272
334
|
return segments.join('/')
|
|
273
335
|
}
|
|
274
336
|
|
|
337
|
+
function addVersionToPublicSrcset(srcset: string, file: VFile) {
|
|
338
|
+
return srcset.replaceAll(srcsetUrlRegex, (url) => (isPublicAsset(url) ? addVersionToPublicAsset(url, file) : url))
|
|
339
|
+
}
|
|
340
|
+
|
|
275
341
|
function addVersionAsset(file: VFile, asset: VersionAsset) {
|
|
276
342
|
file.data.assets?.push(asset)
|
|
277
343
|
}
|
|
@@ -287,6 +353,7 @@ function isPublicAsset(asset: string) {
|
|
|
287
353
|
export interface TransformContext {
|
|
288
354
|
assets: VersionAsset[]
|
|
289
355
|
base: string
|
|
356
|
+
docsDir: URL
|
|
290
357
|
excludedSlugs: string[]
|
|
291
358
|
locale: string | undefined
|
|
292
359
|
publicDir: URL
|
|
@@ -295,6 +362,12 @@ export interface TransformContext {
|
|
|
295
362
|
version: Version
|
|
296
363
|
}
|
|
297
364
|
|
|
365
|
+
interface HtmlAttribute {
|
|
366
|
+
end: number
|
|
367
|
+
start: number
|
|
368
|
+
value: string
|
|
369
|
+
}
|
|
370
|
+
|
|
298
371
|
declare module 'vfile' {
|
|
299
372
|
interface DataMap extends TransformContext {}
|
|
300
373
|
}
|
package/libs/plugin.ts
CHANGED
|
@@ -17,7 +17,8 @@ export function overrideComponents(
|
|
|
17
17
|
): StarlightUserConfig['components'] {
|
|
18
18
|
const components = { ...starlightConfig.components }
|
|
19
19
|
for (const { name, fallback } of overrides) {
|
|
20
|
-
|
|
20
|
+
const override = starlightConfig.components?.[name]
|
|
21
|
+
if (override) {
|
|
21
22
|
logger.warn(`A \`<${name}>\` component override is already defined in your Starlight configuration.`)
|
|
22
23
|
logger.warn(
|
|
23
24
|
`To use \`starlight-versions\`, either remove this override or manually render the content from \`starlight-versions/components/${fallback}.astro\`.`,
|
package/libs/starlight.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function getURLSlug(url: URL) {
|
|
|
59
59
|
} else if (segments.at(-1)?.endsWith(htmlExt)) {
|
|
60
60
|
// Remove trailing `.html`.
|
|
61
61
|
const last = segments.pop()
|
|
62
|
-
if (last) segments.push(last.slice(0, -
|
|
62
|
+
if (last) segments.push(last.slice(0, -htmlExt.length))
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
return segments.filter(Boolean).join('/')
|
|
@@ -70,7 +70,7 @@ export function getDocLocale(slug: string, starlightConfig: StarlightUserConfig)
|
|
|
70
70
|
|
|
71
71
|
if (locales.length === 0) return undefined
|
|
72
72
|
|
|
73
|
-
const localeSegment = slug.split('/')[0]
|
|
73
|
+
const localeSegment = slug.split('/', 1)[0]
|
|
74
74
|
|
|
75
75
|
if (!localeSegment) return undefined
|
|
76
76
|
|
|
@@ -89,24 +89,29 @@ export function addPrefixToSidebarConfig(
|
|
|
89
89
|
return sidebar.map((item) => {
|
|
90
90
|
if (typeof item === 'string') {
|
|
91
91
|
return isExcludedInternalSidebarSlug(item, excludedSlugs) ? item : addPrefixToSlug(prefix, item)
|
|
92
|
-
}
|
|
92
|
+
}
|
|
93
|
+
if ('items' in item) {
|
|
93
94
|
return {
|
|
94
95
|
...item,
|
|
95
96
|
items: addPrefixToSidebarConfig(prefix, item.items, excludedSlugs),
|
|
96
97
|
}
|
|
97
|
-
}
|
|
98
|
+
}
|
|
99
|
+
if (isSidebarLegacyAutoGroup(item)) {
|
|
98
100
|
return addPrefixToLegacyAutogeneratedGroup(prefix, item)
|
|
99
|
-
}
|
|
101
|
+
}
|
|
102
|
+
if ('autogenerate' in item) {
|
|
100
103
|
return {
|
|
101
104
|
...item,
|
|
102
105
|
autogenerate: addPrefixToAutogenerate(prefix, item.autogenerate),
|
|
103
106
|
}
|
|
104
|
-
}
|
|
107
|
+
}
|
|
108
|
+
if ('slug' in item) {
|
|
105
109
|
return {
|
|
106
110
|
...item,
|
|
107
111
|
slug: isExcludedInternalSidebarSlug(item.slug, excludedSlugs) ? item.slug : addPrefixToSlug(prefix, item.slug),
|
|
108
112
|
}
|
|
109
|
-
}
|
|
113
|
+
}
|
|
114
|
+
if (isAbsoluteLink(item.link)) {
|
|
110
115
|
return item
|
|
111
116
|
}
|
|
112
117
|
|
package/libs/versions.ts
CHANGED
|
@@ -40,6 +40,9 @@ import {
|
|
|
40
40
|
|
|
41
41
|
const currentVersionSidebarGroupLabel = Symbol('StarlightVersionsCurrentVersionSidebarGroupLabel')
|
|
42
42
|
|
|
43
|
+
// https://github.com/withastro/starlight/blob/23207221169b31e0cc210451a39a19c80fa1f69e/packages/starlight/loaders.ts#L6
|
|
44
|
+
const docsExtensions = new Set(['.markdown', '.mdown', '.mkdn', '.mkd', '.mdwn', '.md', '.mdx'])
|
|
45
|
+
|
|
43
46
|
export const VersionBaseSchema = z.object({
|
|
44
47
|
/**
|
|
45
48
|
* The version redirect strategy used when navigating to this version:
|
|
@@ -79,16 +82,17 @@ export async function ensureNewVersion(
|
|
|
79
82
|
) {
|
|
80
83
|
const docsDir = new URL('content/docs/', astroConfig.srcDir)
|
|
81
84
|
const newVersion = await checkForNewVersion(config, docsDir)
|
|
82
|
-
const locales = Object.keys(starlightConfig.locales ?? {})
|
|
83
85
|
|
|
84
86
|
if (!newVersion) return
|
|
85
87
|
|
|
88
|
+
const locales = Object.keys(starlightConfig.locales ?? {})
|
|
89
|
+
|
|
86
90
|
const excludedDocs = await getExcludedDocs(
|
|
87
91
|
config,
|
|
88
92
|
locales.filter((locale) => locale !== 'root'),
|
|
89
93
|
docsDir,
|
|
90
94
|
)
|
|
91
|
-
const excludedSlugs =
|
|
95
|
+
const excludedSlugs = excludedDocs.values().toArray().toSorted()
|
|
92
96
|
|
|
93
97
|
const assets: VersionAsset[] = []
|
|
94
98
|
|
|
@@ -105,28 +109,30 @@ export async function ensureNewVersion(
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
// Do not skip other non-root directories.
|
|
108
|
-
return
|
|
112
|
+
return
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
// Skip root version directories.
|
|
112
|
-
if (
|
|
116
|
+
if (Object.hasOwn(config.versionsBySlug, entry.name)) return true
|
|
113
117
|
|
|
114
118
|
const localeDir = locales.find((locale) => locale === entry.name)
|
|
115
119
|
|
|
116
120
|
// Copy root directories not matching any locale.
|
|
117
|
-
if (!localeDir) return
|
|
121
|
+
if (!localeDir) return
|
|
118
122
|
|
|
119
123
|
// Otherwise, swap the locale and version directories.
|
|
120
124
|
return new URL(`../../${localeDir}/${newVersion.slug}/`, entry.dest)
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
if (
|
|
127
|
+
if (!docsExtensions.has(getExtension(entry.url.pathname))) return
|
|
128
|
+
if (excludedDocs.has(getDocPath(docsDir, entry.url))) return true
|
|
124
129
|
|
|
125
130
|
const slug = getDocSlug(docsDir, entry.url)
|
|
126
131
|
|
|
127
|
-
const md = await transformMarkdown(entry.
|
|
132
|
+
const md = await transformMarkdown(await fs.readFile(entry.url, 'utf8'), {
|
|
128
133
|
assets: [],
|
|
129
134
|
base: stripTrailingSlash(astroConfig.base),
|
|
135
|
+
docsDir,
|
|
130
136
|
excludedSlugs,
|
|
131
137
|
locale: getDocLocale(slug, starlightConfig),
|
|
132
138
|
publicDir: astroConfig.publicDir,
|
|
@@ -208,12 +214,8 @@ export function getVersionURL(
|
|
|
208
214
|
const canonicalSlug = getCanonicalSlug(config, starlightConfig, url)
|
|
209
215
|
|
|
210
216
|
if (version && isSlugExcludedFromVersion(config, version, canonicalSlug)) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
starlightConfig,
|
|
214
|
-
url,
|
|
215
|
-
undefined,
|
|
216
|
-
)
|
|
217
|
+
config = { ...config, current: { ...config.current, redirect: 'same-page' } }
|
|
218
|
+
version = undefined
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
const versionURL = new URL(url)
|
|
@@ -231,7 +233,7 @@ export function getVersionURL(
|
|
|
231
233
|
let localeSegment: string | undefined
|
|
232
234
|
|
|
233
235
|
const isHTML = getExtension(versionURL.pathname) === '.html'
|
|
234
|
-
const [, firstSegment, secondSegment] = versionURL.pathname.split('/')
|
|
236
|
+
const [, firstSegment, secondSegment] = versionURL.pathname.split('/', 3)
|
|
235
237
|
|
|
236
238
|
if (starlightConfig.isMultilingual || starlightConfig.locales) {
|
|
237
239
|
const versionOrLocale = firstSegment?.replace('.html', '')
|
|
@@ -249,11 +251,11 @@ export function getVersionURL(
|
|
|
249
251
|
const isRootHTML = baseSegment && getExtension(baseSegment) === '.html'
|
|
250
252
|
const baseSlug = baseSegment && isRootHTML ? stripExtension(baseSegment) : baseSegment
|
|
251
253
|
|
|
252
|
-
if (baseSlug &&
|
|
254
|
+
if (baseSlug && Object.hasOwn(config.versionsBySlug, baseSlug)) {
|
|
253
255
|
if (versionSlug) {
|
|
254
256
|
versionURL.pathname =
|
|
255
257
|
versionRedirect === 'same-page'
|
|
256
|
-
? versionURL.pathname.replace(baseSlug, versionSlug)
|
|
258
|
+
? versionURL.pathname.replace(baseSlug, () => versionSlug)
|
|
257
259
|
: `${versionSlug}${isHTML ? '.html' : '/'}`
|
|
258
260
|
} else if (isRootHTML) {
|
|
259
261
|
versionURL.pathname = '/index.html'
|
|
@@ -453,7 +455,7 @@ export function getVersionFromPaginationLink(
|
|
|
453
455
|
|
|
454
456
|
if (locale) {
|
|
455
457
|
// Remove the locale segment if the current locale is not a root locale.
|
|
456
|
-
segments.
|
|
458
|
+
segments.shift()
|
|
457
459
|
}
|
|
458
460
|
|
|
459
461
|
const versionSegment = segments[0]
|
|
@@ -483,11 +485,14 @@ async function getExcludedDocs(config: StarlightVersionsConfig, locales: string[
|
|
|
483
485
|
const docPath = getDocPath(docsDir, entryURL)
|
|
484
486
|
|
|
485
487
|
if (isDirectory) {
|
|
486
|
-
const [firstSegment, secondSegment] = docPath.split('/')
|
|
488
|
+
const [firstSegment, secondSegment] = docPath.split('/', 2)
|
|
487
489
|
|
|
488
490
|
if (
|
|
489
|
-
(firstSegment &&
|
|
490
|
-
(firstSegment &&
|
|
491
|
+
(firstSegment && Object.hasOwn(config.versionsBySlug, firstSegment)) ||
|
|
492
|
+
(firstSegment &&
|
|
493
|
+
secondSegment &&
|
|
494
|
+
locales.includes(firstSegment) &&
|
|
495
|
+
Object.hasOwn(config.versionsBySlug, secondSegment))
|
|
491
496
|
) {
|
|
492
497
|
continue
|
|
493
498
|
}
|
|
@@ -497,7 +502,11 @@ async function getExcludedDocs(config: StarlightVersionsConfig, locales: string[
|
|
|
497
502
|
continue
|
|
498
503
|
}
|
|
499
504
|
|
|
500
|
-
if (
|
|
505
|
+
if (
|
|
506
|
+
!entry.isFile() ||
|
|
507
|
+
!docsExtensions.has(getExtension(docPath)) ||
|
|
508
|
+
config.exclude.every((excludeGlob) => !path.posix.matchesGlob(docPath, excludeGlob))
|
|
509
|
+
) {
|
|
501
510
|
continue
|
|
502
511
|
}
|
|
503
512
|
|
|
@@ -523,12 +532,15 @@ async function checkForNewVersion(config: StarlightVersionsConfig, docsDir: URL)
|
|
|
523
532
|
}
|
|
524
533
|
|
|
525
534
|
for (const version of config.versions) {
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
throw new Error('Only one new version can be configured at a time.')
|
|
529
|
-
}
|
|
530
|
-
newVersion = version
|
|
535
|
+
if (docsDirDirectories.has(version.slug)) {
|
|
536
|
+
continue
|
|
531
537
|
}
|
|
538
|
+
|
|
539
|
+
if (newVersion) {
|
|
540
|
+
throw new Error('Only one new version can be configured at a time.')
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
newVersion = version
|
|
532
544
|
}
|
|
533
545
|
|
|
534
546
|
return newVersion
|
package/middleware.ts
CHANGED
|
@@ -74,7 +74,7 @@ function getPaginationFromSidebar(
|
|
|
74
74
|
route.pagination.prev,
|
|
75
75
|
)
|
|
76
76
|
const next = applyPaginationConfig(
|
|
77
|
-
currentIndex
|
|
77
|
+
currentIndex === -1 ? undefined : entries[currentIndex + 1],
|
|
78
78
|
starlightConfig.pagination,
|
|
79
79
|
route.entry.data.next,
|
|
80
80
|
route.pagination.next,
|
|
@@ -93,11 +93,12 @@ function applyPaginationConfig(
|
|
|
93
93
|
// Explicitly remove the link.
|
|
94
94
|
if (config === false) return undefined
|
|
95
95
|
// Use the generated link if any.
|
|
96
|
-
|
|
96
|
+
if (config === true) return link
|
|
97
97
|
// If a link exists, update its label if needed.
|
|
98
|
-
|
|
98
|
+
if (typeof config === 'string' && link) {
|
|
99
99
|
return { ...link, label: config }
|
|
100
|
-
}
|
|
100
|
+
}
|
|
101
|
+
if (typeof config === 'object') {
|
|
101
102
|
if (link) {
|
|
102
103
|
// If a link exists, update both its label and href if needed.
|
|
103
104
|
return {
|
|
@@ -107,7 +108,8 @@ function applyPaginationConfig(
|
|
|
107
108
|
// Explicitly remove sidebar link attributes for prev/next links.
|
|
108
109
|
attrs: {},
|
|
109
110
|
}
|
|
110
|
-
}
|
|
111
|
+
}
|
|
112
|
+
if (config.link && config.label) {
|
|
111
113
|
// If there is no link and the frontmatter contains both a URL and a label, use it.
|
|
112
114
|
return existing
|
|
113
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-versions",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Starlight plugin to version documentation pages.",
|
|
6
6
|
"author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@pagefind/default-ui": "^1.3.0",
|
|
24
24
|
"github-slugger": "^2.0.0",
|
|
25
|
+
"hast-util-from-html": "^2.0.3",
|
|
25
26
|
"mdast-util-mdx-jsx": "^3.2.0",
|
|
26
27
|
"mdast-util-mdxjs-esm": "^2.0.1",
|
|
27
28
|
"remark": "^15.0.1",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"yaml": "^2.8.2"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
37
|
+
"@types/hast": "^3.0.4",
|
|
36
38
|
"@types/mdast": "^4.0.4",
|
|
37
39
|
"fast-glob": "^3.3.3",
|
|
38
40
|
"vitest": "^4.0.18"
|
|
@@ -40,9 +42,6 @@
|
|
|
40
42
|
"peerDependencies": {
|
|
41
43
|
"@astrojs/starlight": ">=0.39.0"
|
|
42
44
|
},
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=22.12.0"
|
|
45
|
-
},
|
|
46
45
|
"publishConfig": {
|
|
47
46
|
"access": "public"
|
|
48
47
|
},
|
|
@@ -64,6 +63,6 @@
|
|
|
64
63
|
"bugs": "https://github.com/HiDeoo/starlight-versions/issues",
|
|
65
64
|
"scripts": {
|
|
66
65
|
"test": "vitest",
|
|
67
|
-
"lint": "eslint . --
|
|
66
|
+
"lint": "eslint . --max-warnings=0"
|
|
68
67
|
}
|
|
69
68
|
}
|