@rimelight/ui 0.0.14 → 0.0.16
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/package.json +3 -1
- package/src/env.d.ts +3 -0
- package/src/integrations/sri.ts +92 -92
- package/src/middleware/security.ts +9 -2
- package/src/middleware/sri.ts +78 -78
- package/src/plugins/starlightAddons/PackageManagers.astro +8 -14
- package/src/plugins/starlightAddons/Sidebar.astro +89 -0
- package/src/plugins/starlightAddons/data.ts +39 -0
- package/src/plugins/starlightAddons/index.ts +132 -63
- package/src/plugins/starlightAddons/libs/config.ts +107 -0
- package/src/plugins/starlightAddons/libs/content.ts +20 -0
- package/src/plugins/starlightAddons/libs/i18n.ts +51 -0
- package/src/plugins/starlightAddons/libs/locals.ts +12 -0
- package/src/plugins/starlightAddons/libs/pathname.ts +22 -0
- package/src/plugins/starlightAddons/libs/plugin.ts +9 -0
- package/src/plugins/starlightAddons/libs/sidebar.ts +183 -0
- package/src/plugins/starlightAddons/libs/vite.ts +46 -0
- package/src/plugins/starlightAddons/locals.d.ts +14 -0
- package/src/plugins/starlightAddons/middleware.ts +132 -0
- package/src/plugins/starlightAddons/overrides/Sidebar.astro +8 -0
- package/src/plugins/starlightAddons/schema.ts +13 -0
- package/src/plugins/starlightAddons/starlight.d.ts +6 -0
- package/src/plugins/starlightAddons/virtual.d.ts +13 -0
|
@@ -1,63 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
1
|
+
/// <reference path="./locals.d.ts" />
|
|
2
|
+
import type { StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types"
|
|
3
|
+
import virtual from "vite-plugin-virtual"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
import { z } from "astro/zod"
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
StarlightSidebarTopicsConfigSchema,
|
|
10
|
+
StarlightSidebarTopicsOptionsSchema,
|
|
11
|
+
type StarlightSidebarTopicsUserConfig,
|
|
12
|
+
type StarlightSidebarTopicsUserOptions
|
|
13
|
+
} from "./libs/config"
|
|
14
|
+
import { throwPluginError } from "./libs/plugin"
|
|
15
|
+
import { vitePluginStarlightSidebarTopics } from "./libs/vite"
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
StarlightSidebarTopicsConfig,
|
|
19
|
+
StarlightSidebarTopicsUserConfig,
|
|
20
|
+
StarlightSidebarTopicsOptions,
|
|
21
|
+
StarlightSidebarTopicsUserOptions
|
|
22
|
+
} from "./libs/config"
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
|
|
26
|
+
export interface StarlightAddonsConfig {
|
|
27
|
+
copy?: boolean
|
|
28
|
+
share?: boolean
|
|
29
|
+
packageManagers?: string[]
|
|
30
|
+
// Merged config for Topics
|
|
31
|
+
topics?: StarlightSidebarTopicsUserConfig
|
|
32
|
+
topicOptions?: StarlightSidebarTopicsUserOptions
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function starlightAddons(userConfig?: StarlightAddonsConfig): StarlightPlugin {
|
|
36
|
+
// 1. Setup default and merged config for Addons
|
|
37
|
+
const defaultAddonConfig: StarlightAddonsConfig = {
|
|
38
|
+
copy: true,
|
|
39
|
+
share: true,
|
|
40
|
+
packageManagers: ["npm"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const addonConfig = { ...defaultAddonConfig, ...userConfig }
|
|
44
|
+
|
|
45
|
+
// 2. Validate Sidebar Topics configurations if they exist
|
|
46
|
+
const parsedTopics = StarlightSidebarTopicsConfigSchema.safeParse(addonConfig.topics || [])
|
|
47
|
+
if (!parsedTopics.success) {
|
|
48
|
+
throwPluginError(
|
|
49
|
+
`Invalid topics config: ${parsedTopics.error.issues.map((i) => i.message).join("\n")}`
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsedOptions = StarlightSidebarTopicsOptionsSchema.safeParse(addonConfig.topicOptions)
|
|
54
|
+
if (!parsedOptions.success) {
|
|
55
|
+
throwPluginError(`Invalid topic options: ${z.prettifyError(parsedOptions.error)}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const topicsData = parsedTopics.data
|
|
59
|
+
const optionsData = parsedOptions.data
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name: "rimelight-ui-starlight-addons",
|
|
63
|
+
hooks: {
|
|
64
|
+
"config:setup"({
|
|
65
|
+
addIntegration,
|
|
66
|
+
updateConfig,
|
|
67
|
+
logger,
|
|
68
|
+
addRouteMiddleware,
|
|
69
|
+
command,
|
|
70
|
+
config: starlightConfig
|
|
71
|
+
}) {
|
|
72
|
+
// --- Sidebar Topics Logic ---
|
|
73
|
+
if ((command === "dev" || command === "build") && topicsData.length > 0) {
|
|
74
|
+
if (starlightConfig.sidebar) {
|
|
75
|
+
throwPluginError(
|
|
76
|
+
"Sidebar detected. To use topics, move your sidebar items into the topics config."
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const topicSidebar: StarlightUserConfig["sidebar"] = topicsData.map((topic, index) => {
|
|
81
|
+
return "items" in topic
|
|
82
|
+
? { label: String(index), items: topic.items }
|
|
83
|
+
: { label: String(index), items: [] }
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
updateConfig({
|
|
87
|
+
sidebar: topicSidebar,
|
|
88
|
+
components: {
|
|
89
|
+
Sidebar: `starlight-sidebar-topics/overrides/Sidebar.astro`
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Addons Logic ---
|
|
95
|
+
if (
|
|
96
|
+
!addonConfig.copy &&
|
|
97
|
+
!addonConfig.share &&
|
|
98
|
+
(!addonConfig.packageManagers || addonConfig.packageManagers.length === 0)
|
|
99
|
+
) {
|
|
100
|
+
logger.warn("StarlightAddons: No features enabled.")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
updateConfig({
|
|
104
|
+
components: {
|
|
105
|
+
// This will merge with the Sidebar override if both exist
|
|
106
|
+
PageTitle: path.join(__dirname, "PageTitle.astro")
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// --- Unified Integration ---
|
|
111
|
+
addIntegration({
|
|
112
|
+
name: "rimelight-ui-starlight-addons-integration",
|
|
113
|
+
hooks: {
|
|
114
|
+
"astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
|
|
115
|
+
updateAstroConfig({
|
|
116
|
+
vite: {
|
|
117
|
+
plugins: [
|
|
118
|
+
virtual({
|
|
119
|
+
"virtual:rimelight-starlight-addons-config": `export default ${JSON.stringify(addonConfig)}`
|
|
120
|
+
}),
|
|
121
|
+
// Pass the validated topic data to the vite plugin
|
|
122
|
+
vitePluginStarlightSidebarTopics(topicsData, optionsData)
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
} as Record<string, unknown>)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { StarlightUserConfig } from "@astrojs/starlight/types"
|
|
2
|
+
import { z } from "astro/zod"
|
|
3
|
+
|
|
4
|
+
const sidebarTopicBadgeSchema = z.object({
|
|
5
|
+
text: z.union([z.string(), z.record(z.string(), z.string())]),
|
|
6
|
+
variant: z.enum(["note", "danger", "success", "caution", "tip", "default"]).default("default")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const sidebarTopicBaseSchema = z.object({
|
|
10
|
+
/**
|
|
11
|
+
* An optional badge to display next to the topic label.
|
|
12
|
+
*
|
|
13
|
+
* This option accepts the same configuration as the Starlight badge sidebar item configuration.
|
|
14
|
+
*
|
|
15
|
+
* @see https://starlight.astro.build/guides/sidebar/#badges
|
|
16
|
+
*/
|
|
17
|
+
badge: z
|
|
18
|
+
.union([z.string(), sidebarTopicBadgeSchema])
|
|
19
|
+
.transform((badge) =>
|
|
20
|
+
typeof badge === "string" ? { variant: "default" as const, text: badge } : badge
|
|
21
|
+
)
|
|
22
|
+
.optional(),
|
|
23
|
+
/**
|
|
24
|
+
* The name of an optional icon to display before the topic label set to one of Starlight’s
|
|
25
|
+
* built-in icons.
|
|
26
|
+
*
|
|
27
|
+
* @see https://starlight.astro.build/reference/icons/#all-icons
|
|
28
|
+
*/
|
|
29
|
+
icon: z.string().optional(),
|
|
30
|
+
/**
|
|
31
|
+
* The topic label visible at the top of the sidebar.
|
|
32
|
+
*
|
|
33
|
+
* The value can be a string, or for multilingual sites, an object with values for each different
|
|
34
|
+
* locale. When using the object form, the keys must be BCP-47 tags (e.g. en, fr, or zh-CN).
|
|
35
|
+
*/
|
|
36
|
+
label: z.union([z.string(), z.record(z.string(), z.string())]),
|
|
37
|
+
/**
|
|
38
|
+
* The link to the topic’s content which an be a relative link to local files or the full URL of
|
|
39
|
+
* an external page.
|
|
40
|
+
*
|
|
41
|
+
* For internal links, the link can either be a page included in the items array or a different
|
|
42
|
+
* page acting as the topic’s landing page.
|
|
43
|
+
*/
|
|
44
|
+
link: z.string()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const sidebarTopicLinkSchema = sidebarTopicBaseSchema
|
|
48
|
+
|
|
49
|
+
const sidebarTopicGroupSchema = sidebarTopicBaseSchema.extend({
|
|
50
|
+
/**
|
|
51
|
+
* An optional unique identifier for the topic that can be used to associate content pages that
|
|
52
|
+
* are not listed in any topic sidebar configuration with this topic.
|
|
53
|
+
*/
|
|
54
|
+
id: z.string().optional(),
|
|
55
|
+
/**
|
|
56
|
+
* The sidebar items (links and subcategories) to display for this topic.
|
|
57
|
+
*
|
|
58
|
+
* The topic’s sidebar navigation items. This represents the sidebar displayed when the topic
|
|
59
|
+
* `link` page or any of the pages configured in the `items` array is the current page.
|
|
60
|
+
*/
|
|
61
|
+
items: z.any().array() as z.ZodType<NonNullable<StarlightUserConfig["sidebar"]>>
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const StarlightSidebarTopicsConfigSchema = z
|
|
65
|
+
.union([sidebarTopicGroupSchema, sidebarTopicLinkSchema])
|
|
66
|
+
.array()
|
|
67
|
+
|
|
68
|
+
export const StarlightSidebarTopicsOptionsSchema = z
|
|
69
|
+
.strictObject({
|
|
70
|
+
/**
|
|
71
|
+
* Defines a list of pages or glob patterns that should be excluded from any topic.
|
|
72
|
+
*
|
|
73
|
+
* This options can be useful for custom pages that use a custom site navigation sidebar which
|
|
74
|
+
* do not belong to any topic. Excluded pages will use the built-in Starlight sidebar and not
|
|
75
|
+
* render a list of topics.
|
|
76
|
+
*
|
|
77
|
+
* @default [ ]
|
|
78
|
+
*/
|
|
79
|
+
exclude: z.array(z.string()).default([]),
|
|
80
|
+
/**
|
|
81
|
+
* Defines a map of topic IDs mapped to a list of pages or glob patterns that should be
|
|
82
|
+
* associated with the topic.
|
|
83
|
+
*
|
|
84
|
+
* This option can be useful for custom pages generated and included in the sidebar by other
|
|
85
|
+
* plugins that have no knowledge of the Starlight Sidebar Topics plugin that should be
|
|
86
|
+
* associated with a specific topic.
|
|
87
|
+
*
|
|
88
|
+
* @default {}
|
|
89
|
+
*/
|
|
90
|
+
topics: z.record(z.string(), z.array(z.string())).default({})
|
|
91
|
+
})
|
|
92
|
+
.prefault({})
|
|
93
|
+
|
|
94
|
+
export type StarlightSidebarTopicsUserConfig = z.input<typeof StarlightSidebarTopicsConfigSchema>
|
|
95
|
+
export type StarlightSidebarTopicsConfig = z.output<typeof StarlightSidebarTopicsConfigSchema>
|
|
96
|
+
|
|
97
|
+
export type StarlightSidebarTopicsUserOptions = z.input<typeof StarlightSidebarTopicsOptionsSchema>
|
|
98
|
+
export type StarlightSidebarTopicsOptions = z.output<typeof StarlightSidebarTopicsOptionsSchema>
|
|
99
|
+
|
|
100
|
+
export type StarlightSidebarTopicsSharedConfig = (
|
|
101
|
+
| (z.output<typeof sidebarTopicLinkSchema> & { type: "link" })
|
|
102
|
+
| (Omit<z.output<typeof sidebarTopicGroupSchema>, "items"> & { type: "group" })
|
|
103
|
+
)[]
|
|
104
|
+
|
|
105
|
+
export type StarlightSidebarTopicsSharedOptions = StarlightSidebarTopicsOptions
|
|
106
|
+
|
|
107
|
+
export type SidebarTopicBadge = z.output<typeof sidebarTopicBadgeSchema>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StarlightRouteData } from "@astrojs/starlight/route-data"
|
|
2
|
+
|
|
3
|
+
export function isStarlightEntryWithTopic(entry: StarlightEntry): entry is StarlightEntryWithTopic {
|
|
4
|
+
// Use a runtime-friendly shape check to narrow the type safely
|
|
5
|
+
// without relying on potentially unsafe TS inferences for discriminated unions.
|
|
6
|
+
if (typeof entry !== "object" || entry === null) return false
|
|
7
|
+
// Shape-based runtime check without unsafe casts
|
|
8
|
+
if (typeof entry !== "object" || entry === null) return false
|
|
9
|
+
// entry must contain a data property which is an object with a string topic
|
|
10
|
+
// Use a minimal structural check; rely on TypeScript to narrow after this predicate
|
|
11
|
+
// We cannot access entry.data without a type assertion in TS without a cast; but the guard below
|
|
12
|
+
// ensures correctness for subsequent code that relies on this predicate.
|
|
13
|
+
const anyEntry = entry as any
|
|
14
|
+
if (!anyEntry.data) return false
|
|
15
|
+
const data = anyEntry.data
|
|
16
|
+
if (typeof data !== "object" || data === null) return false
|
|
17
|
+
return typeof data.topic === "string"
|
|
18
|
+
}
|
|
19
|
+
export type StarlightEntry = StarlightRouteData["entry"]
|
|
20
|
+
export type StarlightEntryWithTopic = StarlightEntry & { data: { topic: string } }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { APIContext } from "astro"
|
|
2
|
+
import { AstroError } from "astro/errors"
|
|
3
|
+
import starlightConfig from "virtual:starlight/user-config"
|
|
4
|
+
|
|
5
|
+
import { stripTrailingSlash } from "./pathname"
|
|
6
|
+
|
|
7
|
+
const defaultLang =
|
|
8
|
+
starlightConfig.defaultLocale.lang ?? starlightConfig.defaultLocale.locale ?? "en"
|
|
9
|
+
|
|
10
|
+
export function getLocalizedSlug(slug: string, locale: string | undefined): string {
|
|
11
|
+
const slugLocale = getLocaleFromSlug(slug)
|
|
12
|
+
if (slugLocale === locale) return slug
|
|
13
|
+
locale ??= ""
|
|
14
|
+
if (slugLocale === slug) return locale
|
|
15
|
+
|
|
16
|
+
if (slugLocale) {
|
|
17
|
+
return stripTrailingSlash(slug.replace(`${slugLocale}/`, locale ? `${locale}/` : ""))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return slug ? `${locale}/${slug}` : locale
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getLocaleFromSlug(slug: string): string | undefined {
|
|
24
|
+
const locales = Object.keys(starlightConfig.locales ?? {})
|
|
25
|
+
const baseSegment = slug.split("/")[0]
|
|
26
|
+
return baseSegment && locales.includes(baseSegment) ? baseSegment : undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getTranslation(
|
|
30
|
+
currentLocale: APIContext["currentLocale"],
|
|
31
|
+
translations: Record<string, string>,
|
|
32
|
+
link: string,
|
|
33
|
+
description: string
|
|
34
|
+
) {
|
|
35
|
+
const defaultTranslation = translations[defaultLang]
|
|
36
|
+
|
|
37
|
+
if (!defaultTranslation) {
|
|
38
|
+
throw new AstroError(
|
|
39
|
+
`The ${description} for "${link}" must have a key for the default language "${defaultLang}".`,
|
|
40
|
+
"Update the Starlight config to include a topic label for the default language."
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let translation = defaultTranslation
|
|
45
|
+
|
|
46
|
+
if (currentLocale) {
|
|
47
|
+
translation = translations[currentLocale] ?? defaultTranslation
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return translation
|
|
51
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StarlightSidebarTopicsSharedConfig } from "./config"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* We keep track of the current topic configuration using a variable stored using `Astro.locals`
|
|
5
|
+
* which will be reset for each new page. The value is tracked using an untyped symbol on purpose to
|
|
6
|
+
* avoid users to get autocomplete for it and avoid potential clashes with user-defined variables.
|
|
7
|
+
*/
|
|
8
|
+
export const StarlightSidebarTopicsLocalsSymbol = Symbol.for("starlight-sidebar-topics:locals")
|
|
9
|
+
|
|
10
|
+
export interface StarlightSidebarTopicsLocals {
|
|
11
|
+
config: StarlightSidebarTopicsSharedConfig[number]
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function arePathnamesEqual(pathnameA: string, pathnameB: string) {
|
|
2
|
+
return stripLeadingAndTrailingSlashes(pathnameA) === stripLeadingAndTrailingSlashes(pathnameB)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function stripLeadingAndTrailingSlashes(pathname: string): string {
|
|
6
|
+
return stripLeadingSlash(stripTrailingSlash(pathname))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function stripLeadingSlash(pathname: string) {
|
|
10
|
+
if (pathname.startsWith("/")) pathname = pathname.slice(1)
|
|
11
|
+
return pathname
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function stripTrailingSlash(pathname: string) {
|
|
15
|
+
if (pathname.endsWith("/")) pathname = pathname.slice(0, -1)
|
|
16
|
+
return pathname
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ensureLeadingSlash(pathname: string): string {
|
|
20
|
+
if (pathname.startsWith("/")) return pathname
|
|
21
|
+
return `/${pathname}`
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AstroError } from "astro/errors"
|
|
2
|
+
|
|
3
|
+
export function throwPluginError(message: string, hint?: string): never {
|
|
4
|
+
throw new AstroError(
|
|
5
|
+
message,
|
|
6
|
+
hint ??
|
|
7
|
+
`See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/HiDeoo/starlight-sidebar-topics/issues/new/choose`
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { StarlightRouteData } from "@astrojs/starlight/route-data"
|
|
2
|
+
|
|
3
|
+
import type { StarlightSidebarTopicsSharedConfig } from "./config"
|
|
4
|
+
import { isStarlightEntryWithTopic, type StarlightEntry } from "./content"
|
|
5
|
+
import { getLocaleFromSlug, getLocalizedSlug } from "./i18n"
|
|
6
|
+
import { arePathnamesEqual, stripLeadingAndTrailingSlashes } from "./pathname"
|
|
7
|
+
|
|
8
|
+
const absoluteLinkRegex = /^https?:\/\//
|
|
9
|
+
|
|
10
|
+
export function getCurrentTopic(
|
|
11
|
+
config: StarlightSidebarTopicsSharedConfig,
|
|
12
|
+
sidebar: SidebarEntry[],
|
|
13
|
+
currentSlug: string,
|
|
14
|
+
entry: StarlightEntry
|
|
15
|
+
): Topic | undefined {
|
|
16
|
+
// If the current page has a topic ID, use it to find the topic.
|
|
17
|
+
const topicId = getTopicIdFromEntry(entry)
|
|
18
|
+
if (topicId) return getTopicById(config, sidebar, topicId)
|
|
19
|
+
|
|
20
|
+
// Start by checking if the current page is a topic root.
|
|
21
|
+
const topicFromSlug = getTopicFromSlug(config, sidebar, currentSlug)
|
|
22
|
+
if (topicFromSlug) return topicFromSlug
|
|
23
|
+
|
|
24
|
+
// Otherwise, find the current topic by looking for the current page in the sidebar.
|
|
25
|
+
const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
|
|
26
|
+
if (!currentSidebarTopic) return undefined
|
|
27
|
+
|
|
28
|
+
const currentTopicConfig = config[Number.parseInt(currentSidebarTopic.label, 10)]
|
|
29
|
+
if (!currentTopicConfig) return undefined
|
|
30
|
+
|
|
31
|
+
return { config: currentTopicConfig, sidebar: currentSidebarTopic.entries }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isTopicFirstPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
|
|
35
|
+
const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
|
|
36
|
+
if (!currentSidebarTopic) return false
|
|
37
|
+
|
|
38
|
+
const firstPage = getSidebarFirstPage(currentSidebarTopic.entries)
|
|
39
|
+
if (!firstPage) return false
|
|
40
|
+
|
|
41
|
+
return arePathnamesEqual(stripLeadingAndTrailingSlashes(firstPage.href), currentSlug)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isTopicLastPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
|
|
45
|
+
const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
|
|
46
|
+
if (!currentSidebarTopic) return false
|
|
47
|
+
|
|
48
|
+
const lastPage = getSidebarLastPage(currentSidebarTopic.entries)
|
|
49
|
+
if (!lastPage) return false
|
|
50
|
+
|
|
51
|
+
return arePathnamesEqual(stripLeadingAndTrailingSlashes(lastPage.href), currentSlug)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSidebarFirstPage(sidebar: SidebarEntry[]) {
|
|
55
|
+
const entry = sidebar[0]
|
|
56
|
+
if (!entry) return undefined
|
|
57
|
+
if (entry.type === "link") return entry
|
|
58
|
+
|
|
59
|
+
return getSidebarFirstPage(entry.entries)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getSidebarLastPage(sidebar: SidebarEntry[]) {
|
|
63
|
+
const entry = sidebar.at(-1)
|
|
64
|
+
if (!entry) return undefined
|
|
65
|
+
if (entry.type === "link") return entry
|
|
66
|
+
|
|
67
|
+
return getSidebarLastPage(entry.entries)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getTopicFromSlug(
|
|
71
|
+
config: StarlightSidebarTopicsSharedConfig,
|
|
72
|
+
sidebar: SidebarEntry[],
|
|
73
|
+
slug: string
|
|
74
|
+
): Topic | undefined {
|
|
75
|
+
let topicConfig: Topic["config"] | undefined
|
|
76
|
+
let topicSidebar: Topic["sidebar"] | undefined
|
|
77
|
+
|
|
78
|
+
// Start by checking if the current page is a topic homepage.
|
|
79
|
+
let groupTopicIndex = -1
|
|
80
|
+
const slugLocale = getLocaleFromSlug(slug)
|
|
81
|
+
|
|
82
|
+
for (const topic of config) {
|
|
83
|
+
if (topic.type === "group") groupTopicIndex++
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
!absoluteLinkRegex.test(topic.link) &&
|
|
87
|
+
arePathnamesEqual(
|
|
88
|
+
getLocalizedSlug(stripLeadingAndTrailingSlashes(topic.link), slugLocale),
|
|
89
|
+
slug
|
|
90
|
+
) &&
|
|
91
|
+
groupTopicIndex !== -1
|
|
92
|
+
) {
|
|
93
|
+
const sidebarTopic = sidebar[groupTopicIndex]
|
|
94
|
+
|
|
95
|
+
if (sidebarTopic?.type === "group") {
|
|
96
|
+
topicConfig = topic
|
|
97
|
+
topicSidebar = sidebarTopic.entries
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!topicConfig || !topicSidebar) return undefined
|
|
105
|
+
|
|
106
|
+
return { config: topicConfig, sidebar: topicSidebar }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getTopicById(
|
|
110
|
+
config: StarlightSidebarTopicsSharedConfig,
|
|
111
|
+
sidebar: SidebarEntry[],
|
|
112
|
+
id: string
|
|
113
|
+
): Topic | undefined {
|
|
114
|
+
let topicConfig: Topic["config"] | undefined
|
|
115
|
+
let topicSidebar: Topic["sidebar"] | undefined
|
|
116
|
+
|
|
117
|
+
let groupTopicIndex = -1
|
|
118
|
+
|
|
119
|
+
for (const topic of config) {
|
|
120
|
+
if (topic.type === "group") groupTopicIndex++
|
|
121
|
+
|
|
122
|
+
if (topic.type === "group" && topic.id === id) {
|
|
123
|
+
const sidebarTopic = sidebar[groupTopicIndex]
|
|
124
|
+
|
|
125
|
+
if (sidebarTopic?.type === "group") {
|
|
126
|
+
topicConfig = topic
|
|
127
|
+
topicSidebar = sidebarTopic.entries
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!topicConfig || !topicSidebar) return undefined
|
|
135
|
+
|
|
136
|
+
return { config: topicConfig, sidebar: topicSidebar }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getCurrentSidebarTopic(sidebar: SidebarEntry[]): SidebarTopic | undefined {
|
|
140
|
+
let currentSidebarTopic: SidebarTopic | undefined
|
|
141
|
+
|
|
142
|
+
for (const topic of sidebar) {
|
|
143
|
+
if (topic.type === "link") continue
|
|
144
|
+
|
|
145
|
+
const currentSidebarEntry = getCurrentSidebarEntry(topic.entries)
|
|
146
|
+
|
|
147
|
+
if (currentSidebarEntry) {
|
|
148
|
+
currentSidebarTopic = topic
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return currentSidebarTopic
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getCurrentSidebarEntry(sidebar: SidebarEntry[]): SidebarEntry | undefined {
|
|
157
|
+
return sidebar.find((entry) => {
|
|
158
|
+
if (entry.type === "link") {
|
|
159
|
+
return entry.isCurrent
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return getCurrentSidebarEntry(entry.entries)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getTopicIdFromEntry(entry: StarlightEntry): string | undefined {
|
|
167
|
+
if (isStarlightEntryWithTopic(entry)) {
|
|
168
|
+
return entry.data.topic
|
|
169
|
+
}
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type SidebarEntry = StarlightRouteData["sidebar"][number]
|
|
174
|
+
|
|
175
|
+
interface SidebarTopic {
|
|
176
|
+
label: string
|
|
177
|
+
entries: SidebarEntry[]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface Topic {
|
|
181
|
+
config: StarlightSidebarTopicsSharedConfig[number]
|
|
182
|
+
sidebar: SidebarEntry[]
|
|
183
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ViteUserConfig } from "astro"
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
StarlightSidebarTopicsConfig,
|
|
5
|
+
StarlightSidebarTopicsOptions,
|
|
6
|
+
StarlightSidebarTopicsSharedConfig
|
|
7
|
+
} from "./config"
|
|
8
|
+
|
|
9
|
+
export function vitePluginStarlightSidebarTopics(
|
|
10
|
+
config: StarlightSidebarTopicsConfig,
|
|
11
|
+
options: StarlightSidebarTopicsOptions
|
|
12
|
+
): VitePlugin {
|
|
13
|
+
const sharedConfig: StarlightSidebarTopicsSharedConfig = config.map((topic) => {
|
|
14
|
+
if (!("items" in topic)) return { ...topic, type: "link" }
|
|
15
|
+
// Remove the items property without creating an unused binding
|
|
16
|
+
const topicWithoutItems: any = { ...topic }
|
|
17
|
+
delete topicWithoutItems.items
|
|
18
|
+
return { ...topicWithoutItems, type: "group" }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const modules: Record<string, string> = {
|
|
22
|
+
"virtual:starlight-sidebar-topics/config": `export default ${JSON.stringify(sharedConfig)}`,
|
|
23
|
+
"virtual:starlight-sidebar-topics/options": `export default ${JSON.stringify(options)}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const moduleResolutionMap = Object.fromEntries(
|
|
27
|
+
Object.keys(modules).map((key) => [resolveVirtualModuleId(key), key])
|
|
28
|
+
) as Record<string, string>
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: "vite-plugin-starlight-sidebar-topics",
|
|
32
|
+
load(id) {
|
|
33
|
+
const moduleId = moduleResolutionMap[id]
|
|
34
|
+
return moduleId ? modules[moduleId] : undefined
|
|
35
|
+
},
|
|
36
|
+
resolveId(id) {
|
|
37
|
+
return id in modules ? resolveVirtualModuleId(id) : undefined
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveVirtualModuleId<TModuleId extends string>(id: TModuleId): `\0${TModuleId}` {
|
|
43
|
+
return `\0${id}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type VitePlugin = NonNullable<ViteUserConfig["plugins"]>[number]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StarlightLocals } from "@astrojs/starlight/types"
|
|
2
|
+
import type { StarlightSidebarTopicsRouteData } from "./data"
|
|
3
|
+
|
|
4
|
+
declare namespace App {
|
|
5
|
+
interface Locals extends StarlightLocals {
|
|
6
|
+
/**
|
|
7
|
+
* Starlight Sidebar Topics data.
|
|
8
|
+
*
|
|
9
|
+
* @see https://starlight-sidebar-topics.netlify.app/docs/guides/custom-topic-list/
|
|
10
|
+
*/
|
|
11
|
+
starlightSidebarTopics: StarlightSidebarTopicsRouteData
|
|
12
|
+
starlightRoute: any
|
|
13
|
+
}
|
|
14
|
+
}
|