@levino/shipyard-docs 0.6.2 → 0.6.3
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/README.md +40 -1
- package/astro/Layout.astro +159 -5
- package/package.json +4 -2
- package/src/index.ts +797 -146
- package/src/rehypeVersionLinks.test.ts +319 -0
- package/src/rehypeVersionLinks.ts +156 -0
- package/src/routeHelpers.test.ts +657 -1
- package/src/routeHelpers.ts +221 -0
- package/src/sidebarEntries.test.ts +154 -1
- package/src/sidebarEntries.ts +33 -0
- package/src/versionHelpers.ts +64 -0
- package/src/versionSchema.test.ts +404 -0
- package/src/virtual-module.d.ts +68 -0
package/src/routeHelpers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CollectionEntry } from 'astro:content'
|
|
2
|
+
import type { SingleVersionConfig, VersionConfig } from './index'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Configuration for generating static paths for a docs collection.
|
|
@@ -76,3 +77,223 @@ export const getDocPath = (
|
|
|
76
77
|
* Type for docs collection entry with inferred data structure.
|
|
77
78
|
*/
|
|
78
79
|
export type DocsEntry = CollectionEntry<'docs'>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the URL path segment for a version.
|
|
83
|
+
* Uses the version's `path` property if defined, otherwise uses the version string.
|
|
84
|
+
*
|
|
85
|
+
* @param version - The version string to look up
|
|
86
|
+
* @param versions - The version configuration
|
|
87
|
+
* @returns The path segment to use in URLs, or undefined if version not found
|
|
88
|
+
*/
|
|
89
|
+
export const getVersionPath = (
|
|
90
|
+
version: string,
|
|
91
|
+
versions: VersionConfig,
|
|
92
|
+
): string | undefined => {
|
|
93
|
+
const versionConfig = versions.available.find((v) => v.version === version)
|
|
94
|
+
if (!versionConfig) return undefined
|
|
95
|
+
return versionConfig.path ?? versionConfig.version
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the current/default version from version configuration.
|
|
100
|
+
*
|
|
101
|
+
* @param versions - The version configuration
|
|
102
|
+
* @returns The current version string
|
|
103
|
+
*/
|
|
104
|
+
export const getCurrentVersion = (versions: VersionConfig): string => {
|
|
105
|
+
return versions.current
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all available versions from version configuration.
|
|
110
|
+
*
|
|
111
|
+
* @param versions - The version configuration
|
|
112
|
+
* @returns Array of available version configurations
|
|
113
|
+
*/
|
|
114
|
+
export const getAvailableVersions = (
|
|
115
|
+
versions: VersionConfig,
|
|
116
|
+
): SingleVersionConfig[] => {
|
|
117
|
+
return versions.available
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a version is deprecated.
|
|
122
|
+
*
|
|
123
|
+
* @param version - The version string to check
|
|
124
|
+
* @param versions - The version configuration
|
|
125
|
+
* @returns True if the version is in the deprecated list
|
|
126
|
+
*/
|
|
127
|
+
export const isVersionDeprecated = (
|
|
128
|
+
version: string,
|
|
129
|
+
versions: VersionConfig,
|
|
130
|
+
): boolean => {
|
|
131
|
+
return versions.deprecated?.includes(version) ?? false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the stable version from configuration.
|
|
136
|
+
* Falls back to current if stable is not explicitly set.
|
|
137
|
+
*
|
|
138
|
+
* @param versions - The version configuration
|
|
139
|
+
* @returns The stable version string
|
|
140
|
+
*/
|
|
141
|
+
export const getStableVersion = (versions: VersionConfig): string => {
|
|
142
|
+
return versions.stable ?? versions.current
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Find a version configuration by its version string or path.
|
|
147
|
+
*
|
|
148
|
+
* @param versionOrPath - The version string or path segment
|
|
149
|
+
* @param versions - The version configuration
|
|
150
|
+
* @returns The matching version config, or undefined if not found
|
|
151
|
+
*/
|
|
152
|
+
export const findVersionConfig = (
|
|
153
|
+
versionOrPath: string,
|
|
154
|
+
versions: VersionConfig,
|
|
155
|
+
): SingleVersionConfig | undefined => {
|
|
156
|
+
return versions.available.find(
|
|
157
|
+
(v) => v.version === versionOrPath || v.path === versionOrPath,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate the full URL path for a versioned doc entry.
|
|
163
|
+
*
|
|
164
|
+
* URL structure with versions:
|
|
165
|
+
* - Without i18n: /[routeBasePath]/[version]/[...slug]
|
|
166
|
+
* - With i18n: /[locale]/[routeBasePath]/[version]/[...slug]
|
|
167
|
+
*
|
|
168
|
+
* @param id - The doc entry ID
|
|
169
|
+
* @param routeBasePath - The base path for routes
|
|
170
|
+
* @param hasI18n - Whether i18n is enabled
|
|
171
|
+
* @param version - The version path segment (not the version string, use getVersionPath first)
|
|
172
|
+
* @param currentLocale - The current locale (for i18n)
|
|
173
|
+
* @returns The full URL path including version
|
|
174
|
+
*/
|
|
175
|
+
export const getVersionedDocPath = (
|
|
176
|
+
id: string,
|
|
177
|
+
routeBasePath: string,
|
|
178
|
+
hasI18n: boolean,
|
|
179
|
+
version: string,
|
|
180
|
+
currentLocale?: string,
|
|
181
|
+
): string => {
|
|
182
|
+
// Normalize base path - remove leading and trailing slashes
|
|
183
|
+
let normalizedBasePath = routeBasePath
|
|
184
|
+
while (normalizedBasePath.startsWith('/')) {
|
|
185
|
+
normalizedBasePath = normalizedBasePath.slice(1)
|
|
186
|
+
}
|
|
187
|
+
while (normalizedBasePath.endsWith('/')) {
|
|
188
|
+
normalizedBasePath = normalizedBasePath.slice(0, -1)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Normalize version - remove leading and trailing slashes
|
|
192
|
+
let normalizedVersion = version
|
|
193
|
+
while (normalizedVersion.startsWith('/')) {
|
|
194
|
+
normalizedVersion = normalizedVersion.slice(1)
|
|
195
|
+
}
|
|
196
|
+
while (normalizedVersion.endsWith('/')) {
|
|
197
|
+
normalizedVersion = normalizedVersion.slice(0, -1)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (hasI18n && currentLocale) {
|
|
201
|
+
// Remove locale prefix from id (e.g., 'en/guide/intro' -> 'guide/intro')
|
|
202
|
+
const pathWithoutLocale = id.includes('/')
|
|
203
|
+
? id.slice(id.indexOf('/') + 1)
|
|
204
|
+
: id
|
|
205
|
+
return `/${currentLocale}/${normalizedBasePath}/${normalizedVersion}/${pathWithoutLocale}`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `/${normalizedBasePath}/${normalizedVersion}/${id}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate route parameters from a versioned doc entry's slug.
|
|
213
|
+
*
|
|
214
|
+
* @param slug - The slug from the doc entry (may include version prefix)
|
|
215
|
+
* @param hasI18n - Whether i18n is enabled
|
|
216
|
+
* @param version - The version path segment
|
|
217
|
+
* @returns Route parameters object including version
|
|
218
|
+
*/
|
|
219
|
+
export const getVersionedRouteParams = (
|
|
220
|
+
slug: string,
|
|
221
|
+
hasI18n: boolean,
|
|
222
|
+
version: string,
|
|
223
|
+
) => {
|
|
224
|
+
const baseParams = getRouteParams(slug, hasI18n)
|
|
225
|
+
return {
|
|
226
|
+
...baseParams,
|
|
227
|
+
version,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Convert a path from one version to another.
|
|
233
|
+
* Useful for "view this page in another version" links.
|
|
234
|
+
*
|
|
235
|
+
* @param currentPath - The current full URL path
|
|
236
|
+
* @param targetVersion - The target version path segment
|
|
237
|
+
* @param currentVersion - The current version path segment
|
|
238
|
+
* @returns The path with version replaced
|
|
239
|
+
*/
|
|
240
|
+
export const switchVersionInPath = (
|
|
241
|
+
currentPath: string,
|
|
242
|
+
targetVersion: string,
|
|
243
|
+
currentVersion: string,
|
|
244
|
+
): string => {
|
|
245
|
+
// Replace the version segment in the path
|
|
246
|
+
// Handle both /docs/v1/page and /en/docs/v1/page patterns
|
|
247
|
+
return currentPath.replace(
|
|
248
|
+
new RegExp(`/${escapeRegex(currentVersion)}/`),
|
|
249
|
+
`/${targetVersion}/`,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Escape special regex characters in a string.
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
const escapeRegex = (str: string): string => {
|
|
258
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Creates a Map for efficient version path lookups.
|
|
263
|
+
* Use this when processing many documents to avoid repeated array.find() calls.
|
|
264
|
+
*
|
|
265
|
+
* @param versions - The version configuration
|
|
266
|
+
* @returns A Map from version string to URL path segment
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* const versionPathMap = createVersionPathMap(versionsConfig)
|
|
271
|
+
* // Now use versionPathMap.get(version) instead of getVersionPath(version, versionsConfig)
|
|
272
|
+
* for (const doc of docs) {
|
|
273
|
+
* const version = getVersionFromDocId(doc.id)
|
|
274
|
+
* const versionPath = versionPathMap.get(version) ?? version
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
export const createVersionPathMap = (
|
|
279
|
+
versions: VersionConfig,
|
|
280
|
+
): Map<string, string> => {
|
|
281
|
+
const map = new Map<string, string>()
|
|
282
|
+
for (const v of versions.available) {
|
|
283
|
+
map.set(v.version, v.path ?? v.version)
|
|
284
|
+
}
|
|
285
|
+
return map
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Creates a Set of deprecated versions for efficient lookup.
|
|
290
|
+
* Use this when checking many documents for deprecation status.
|
|
291
|
+
*
|
|
292
|
+
* @param versions - The version configuration
|
|
293
|
+
* @returns A Set of deprecated version strings
|
|
294
|
+
*/
|
|
295
|
+
export const createDeprecatedVersionSet = (
|
|
296
|
+
versions: VersionConfig,
|
|
297
|
+
): Set<string> => {
|
|
298
|
+
return new Set(versions.deprecated ?? [])
|
|
299
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { DocsData } from './sidebarEntries'
|
|
3
|
-
import { toSidebarEntries } from './sidebarEntries'
|
|
3
|
+
import { filterDocsForVersion, toSidebarEntries } from './sidebarEntries'
|
|
4
4
|
|
|
5
5
|
describe('toSidebarEntries', () => {
|
|
6
6
|
it('should create a basic sidebar structure from flat docs', () => {
|
|
@@ -309,3 +309,156 @@ describe('toSidebarEntries', () => {
|
|
|
309
309
|
expect(entries.page.collapsed).toBeUndefined()
|
|
310
310
|
})
|
|
311
311
|
})
|
|
312
|
+
|
|
313
|
+
describe('filterDocsForVersion', () => {
|
|
314
|
+
it('should filter docs to only include entries for the specified version', () => {
|
|
315
|
+
const docs: DocsData[] = [
|
|
316
|
+
{ id: 'v1/intro.md', title: 'Intro v1', path: '/docs/v1/intro' },
|
|
317
|
+
{ id: 'v1/guide.md', title: 'Guide v1', path: '/docs/v1/guide' },
|
|
318
|
+
{ id: 'v2/intro.md', title: 'Intro v2', path: '/docs/v2/intro' },
|
|
319
|
+
{ id: 'v2/guide.md', title: 'Guide v2', path: '/docs/v2/guide' },
|
|
320
|
+
{
|
|
321
|
+
id: 'v2/new-feature.md',
|
|
322
|
+
title: 'New Feature',
|
|
323
|
+
path: '/docs/v2/new-feature',
|
|
324
|
+
},
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
const v1Docs = filterDocsForVersion(docs, 'v1')
|
|
328
|
+
expect(v1Docs).toHaveLength(2)
|
|
329
|
+
expect(v1Docs.map((d) => d.title)).toEqual(['Intro v1', 'Guide v1'])
|
|
330
|
+
|
|
331
|
+
const v2Docs = filterDocsForVersion(docs, 'v2')
|
|
332
|
+
expect(v2Docs).toHaveLength(3)
|
|
333
|
+
expect(v2Docs.map((d) => d.title)).toEqual([
|
|
334
|
+
'Intro v2',
|
|
335
|
+
'Guide v2',
|
|
336
|
+
'New Feature',
|
|
337
|
+
])
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should strip version prefix from doc IDs', () => {
|
|
341
|
+
const docs: DocsData[] = [
|
|
342
|
+
{
|
|
343
|
+
id: 'v2/getting-started.md',
|
|
344
|
+
title: 'Getting Started',
|
|
345
|
+
path: '/docs/v2/getting-started',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: 'v2/en/intro.md',
|
|
349
|
+
title: 'Introduction',
|
|
350
|
+
path: '/docs/v2/en/intro',
|
|
351
|
+
},
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
const filtered = filterDocsForVersion(docs, 'v2')
|
|
355
|
+
|
|
356
|
+
expect(filtered[0].id).toBe('getting-started.md')
|
|
357
|
+
expect(filtered[1].id).toBe('en/intro.md')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should preserve other doc properties unchanged', () => {
|
|
361
|
+
const docs: DocsData[] = [
|
|
362
|
+
{
|
|
363
|
+
id: 'v1/page.md',
|
|
364
|
+
title: 'Page Title',
|
|
365
|
+
path: '/docs/v1/page',
|
|
366
|
+
sidebarPosition: 5,
|
|
367
|
+
sidebarLabel: 'Custom Label',
|
|
368
|
+
sidebarClassName: 'special-class',
|
|
369
|
+
pagination_next: 'next-page',
|
|
370
|
+
pagination_prev: 'prev-page',
|
|
371
|
+
link: true,
|
|
372
|
+
},
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
const filtered = filterDocsForVersion(docs, 'v1')
|
|
376
|
+
expect(filtered[0]).toEqual({
|
|
377
|
+
id: 'page.md',
|
|
378
|
+
title: 'Page Title',
|
|
379
|
+
path: '/docs/v1/page',
|
|
380
|
+
sidebarPosition: 5,
|
|
381
|
+
sidebarLabel: 'Custom Label',
|
|
382
|
+
sidebarClassName: 'special-class',
|
|
383
|
+
pagination_next: 'next-page',
|
|
384
|
+
pagination_prev: 'prev-page',
|
|
385
|
+
link: true,
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('should return empty array when no docs match the version', () => {
|
|
390
|
+
const docs: DocsData[] = [
|
|
391
|
+
{ id: 'v1/intro.md', title: 'Intro', path: '/docs/v1/intro' },
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
const filtered = filterDocsForVersion(docs, 'v3')
|
|
395
|
+
expect(filtered).toEqual([])
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should handle versioned docs with i18n locale prefixes', () => {
|
|
399
|
+
const docs: DocsData[] = [
|
|
400
|
+
{
|
|
401
|
+
id: 'v2/en/getting-started.md',
|
|
402
|
+
title: 'Getting Started (EN)',
|
|
403
|
+
path: '/en/docs/v2/getting-started',
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: 'v2/de/getting-started.md',
|
|
407
|
+
title: 'Erste Schritte (DE)',
|
|
408
|
+
path: '/de/docs/v2/getting-started',
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: 'v1/en/getting-started.md',
|
|
412
|
+
title: 'Getting Started v1',
|
|
413
|
+
path: '/en/docs/v1/getting-started',
|
|
414
|
+
},
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
const filtered = filterDocsForVersion(docs, 'v2')
|
|
418
|
+
expect(filtered).toHaveLength(2)
|
|
419
|
+
// IDs should have version stripped but locale preserved
|
|
420
|
+
expect(filtered.map((d) => d.id)).toEqual([
|
|
421
|
+
'en/getting-started.md',
|
|
422
|
+
'de/getting-started.md',
|
|
423
|
+
])
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should work correctly with semantic version strings', () => {
|
|
427
|
+
const docs: DocsData[] = [
|
|
428
|
+
{ id: 'v1.0/intro.md', title: 'Intro v1.0', path: '/docs/v1.0/intro' },
|
|
429
|
+
{
|
|
430
|
+
id: 'v2.0.0/intro.md',
|
|
431
|
+
title: 'Intro v2.0.0',
|
|
432
|
+
path: '/docs/v2.0.0/intro',
|
|
433
|
+
},
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
const v1Docs = filterDocsForVersion(docs, 'v1.0')
|
|
437
|
+
expect(v1Docs).toHaveLength(1)
|
|
438
|
+
expect(v1Docs[0].title).toBe('Intro v1.0')
|
|
439
|
+
|
|
440
|
+
const v2Docs = filterDocsForVersion(docs, 'v2.0.0')
|
|
441
|
+
expect(v2Docs).toHaveLength(1)
|
|
442
|
+
expect(v2Docs[0].title).toBe('Intro v2.0.0')
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('should work with special version names like latest and next', () => {
|
|
446
|
+
const docs: DocsData[] = [
|
|
447
|
+
{
|
|
448
|
+
id: 'latest/intro.md',
|
|
449
|
+
title: 'Latest Intro',
|
|
450
|
+
path: '/docs/latest/intro',
|
|
451
|
+
},
|
|
452
|
+
{ id: 'next/intro.md', title: 'Next Intro', path: '/docs/next/intro' },
|
|
453
|
+
{ id: 'v1/intro.md', title: 'V1 Intro', path: '/docs/v1/intro' },
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
const latestDocs = filterDocsForVersion(docs, 'latest')
|
|
457
|
+
expect(latestDocs).toHaveLength(1)
|
|
458
|
+
expect(latestDocs[0].title).toBe('Latest Intro')
|
|
459
|
+
|
|
460
|
+
const nextDocs = filterDocsForVersion(docs, 'next')
|
|
461
|
+
expect(nextDocs).toHaveLength(1)
|
|
462
|
+
expect(nextDocs[0].title).toBe('Next Intro')
|
|
463
|
+
})
|
|
464
|
+
})
|
package/src/sidebarEntries.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Entry } from '@levino/shipyard-base'
|
|
2
|
+
import { getVersionFromDocId, stripVersionFromDocId } from './versionHelpers'
|
|
2
3
|
|
|
3
4
|
export interface DocsData {
|
|
4
5
|
id: string
|
|
@@ -19,6 +20,38 @@ export interface DocsData {
|
|
|
19
20
|
paginationPrev?: string | null
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Filters docs data to only include entries for a specific version
|
|
25
|
+
* and strips the version prefix from doc IDs for proper sidebar tree building.
|
|
26
|
+
*
|
|
27
|
+
* @param docs - Array of DocsData entries
|
|
28
|
+
* @param version - The version to filter by
|
|
29
|
+
* @returns Filtered docs with version prefix stripped from IDs
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* // Input: [{ id: 'v2/getting-started', path: '/docs/v2/getting-started', ... }]
|
|
34
|
+
* // Output: [{ id: 'getting-started', path: '/docs/v2/getting-started', ... }]
|
|
35
|
+
* const sidebarDocs = filterDocsForVersion(allDocs, 'v2')
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const filterDocsForVersion = (
|
|
39
|
+
docs: readonly DocsData[],
|
|
40
|
+
version: string,
|
|
41
|
+
): DocsData[] => {
|
|
42
|
+
return docs
|
|
43
|
+
.filter((doc) => {
|
|
44
|
+
const docVersion = getVersionFromDocId(doc.id)
|
|
45
|
+
return docVersion === version
|
|
46
|
+
})
|
|
47
|
+
.map((doc) => ({
|
|
48
|
+
...doc,
|
|
49
|
+
// Strip version from ID so tree builds correctly
|
|
50
|
+
// e.g., "v2/en/getting-started" -> "en/getting-started"
|
|
51
|
+
id: stripVersionFromDocId(doc.id),
|
|
52
|
+
}))
|
|
53
|
+
}
|
|
54
|
+
|
|
22
55
|
interface TreeNode {
|
|
23
56
|
readonly key: string
|
|
24
57
|
readonly label: string
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for parsing and manipulating version strings in doc IDs.
|
|
3
|
+
* These are low-level utilities used by other modules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a string looks like a version identifier.
|
|
8
|
+
* Matches: v1.0, v2.0.0, latest, next, main, etc.
|
|
9
|
+
*/
|
|
10
|
+
export const isVersionLikeString = (str: string): boolean => {
|
|
11
|
+
// Match common version patterns
|
|
12
|
+
return /^(v?\d+(\.\d+)*|latest|next|main|master|canary|beta|alpha|rc\d*|stable)$/i.test(
|
|
13
|
+
str,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracts the version from a versioned document ID.
|
|
19
|
+
* The version is expected to be the first path segment.
|
|
20
|
+
*
|
|
21
|
+
* @param docId - The document ID (e.g., "v1.0/en/getting-started")
|
|
22
|
+
* @returns The version string or undefined if not found
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* getVersionFromDocId("v1.0/en/getting-started") // "v1.0"
|
|
27
|
+
* getVersionFromDocId("latest/en/index") // "latest"
|
|
28
|
+
* getVersionFromDocId("en/getting-started") // undefined (non-versioned)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const getVersionFromDocId = (docId: string): string | undefined => {
|
|
32
|
+
const parts = docId.split('/')
|
|
33
|
+
// For versioned docs, first part is the version
|
|
34
|
+
// We check if it looks like a version (starts with 'v' and has numbers, or is 'latest', 'next', etc.)
|
|
35
|
+
if (parts.length > 0) {
|
|
36
|
+
const potentialVersion = parts[0]
|
|
37
|
+
if (isVersionLikeString(potentialVersion)) {
|
|
38
|
+
return potentialVersion
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Strips the version prefix from a versioned document ID.
|
|
46
|
+
* Returns the ID without the version for use in routing.
|
|
47
|
+
*
|
|
48
|
+
* @param docId - The document ID (e.g., "v1.0/en/getting-started")
|
|
49
|
+
* @returns The ID without version prefix (e.g., "en/getting-started")
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* stripVersionFromDocId("v1.0/en/getting-started") // "en/getting-started"
|
|
54
|
+
* stripVersionFromDocId("latest/en/index") // "en/index"
|
|
55
|
+
* stripVersionFromDocId("en/getting-started") // "en/getting-started" (unchanged)
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export const stripVersionFromDocId = (docId: string): string => {
|
|
59
|
+
const version = getVersionFromDocId(docId)
|
|
60
|
+
if (version) {
|
|
61
|
+
return docId.slice(version.length + 1) // +1 for the slash
|
|
62
|
+
}
|
|
63
|
+
return docId
|
|
64
|
+
}
|