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 ADDED
@@ -0,0 +1,5 @@
1
+ eslint.config.mjs
2
+ fixtures
3
+ tests
4
+ tsconfig.json
5
+ tsconfig.tsbuildinfo
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', content, url: source })
36
+ const updatedContent = await callback({ type: 'file', url: source })
37
+ if (updatedContent === true) continue
38
38
 
39
- if (typeof updatedContent !== 'string') continue
39
+ const dest = new URL(entry.name, destDir)
40
40
 
41
- await fs.writeFile(new URL(entry.name, destDir), updatedContent)
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
- | { type: 'file'; content: string; url: URL }
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 { Image, Link, Root } from 'mdast'
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 processor = remark().use(remarkDirective).use(remarkMdx).use(remarkFrontmatter).use(remarkStarlightVersions)
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.splice(0, 0, file.data.version.slug)
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 SKIP
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 srcOrSrcset = node.attributes.find(
199
- (attribute) => attribute.type === 'mdxJsxAttribute' && (attribute.name === 'src' || attribute.name === 'srcset'),
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
- if (!srcOrSrcset || typeof srcOrSrcset.value !== 'string' || !isPublicAsset(srcOrSrcset.value)) return CONTINUE
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
- srcOrSrcset.value = addVersionToPublicAsset(srcOrSrcset.value, file)
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
- let slugVersionIndex = 1
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.splice(0, 0, '..')
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
- if (starlightConfig.components?.[name]) {
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, -1 * htmlExt.length))
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
- } else if ('items' in item) {
92
+ }
93
+ if ('items' in item) {
93
94
  return {
94
95
  ...item,
95
96
  items: addPrefixToSidebarConfig(prefix, item.items, excludedSlugs),
96
97
  }
97
- } else if (isSidebarLegacyAutoGroup(item)) {
98
+ }
99
+ if (isSidebarLegacyAutoGroup(item)) {
98
100
  return addPrefixToLegacyAutogeneratedGroup(prefix, item)
99
- } else if ('autogenerate' in item) {
101
+ }
102
+ if ('autogenerate' in item) {
100
103
  return {
101
104
  ...item,
102
105
  autogenerate: addPrefixToAutogenerate(prefix, item.autogenerate),
103
106
  }
104
- } else if ('slug' in item) {
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
- } else if (isAbsoluteLink(item.link)) {
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 = [...excludedDocs.values()].toSorted()
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 false
112
+ return
109
113
  }
110
114
 
111
115
  // Skip root version directories.
112
- if (entry.name in config.versionsBySlug) return true
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 false
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 (excludedDocs.has(getDocPath(docsDir, entry.url))) return false
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.content, {
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
- return getVersionURL(
212
- { ...config, current: { ...config.current, redirect: 'same-page' } },
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 && baseSlug in config.versionsBySlug) {
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.splice(0, 1)
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 && firstSegment in config.versionsBySlug) ||
490
- (firstSegment && locales.includes(firstSegment) && secondSegment && secondSegment in config.versionsBySlug)
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 (!entry.isFile() || !config.exclude.some((excludeGlob) => path.posix.matchesGlob(docPath, excludeGlob))) {
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 (!docsDirDirectories.has(version.slug)) {
527
- if (newVersion) {
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 > -1 ? entries[currentIndex + 1] : undefined,
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
- else if (config === true) return link
96
+ if (config === true) return link
97
97
  // If a link exists, update its label if needed.
98
- else if (typeof config === 'string' && link) {
98
+ if (typeof config === 'string' && link) {
99
99
  return { ...link, label: config }
100
- } else if (typeof config === 'object') {
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
- } else if (config.link && config.label) {
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.0",
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 . --cache --max-warnings=0"
66
+ "lint": "eslint . --max-warnings=0"
68
67
  }
69
68
  }