starlight-sidebar-topics-dropdown 0.2.2 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # starlight-sidebar-topics-dropdown
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#31](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/31) [`3193771`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/31937716171b020f414ece6783c17181fbd5fdcf) Thanks [@trueberryless](https://github.com/trueberryless)! - ⚠️ **BREAKING CHANGE:** The Starlight Sidebar Topics Dropdown plugin no longer provides the same ["Unnested Sidebar" configuration](https://starlight-sidebar-topics-dropdown.trueberryless.org/docs/guides/unnested-sidebar/) like before. Please adapt your `astro.config.mjs` with the up-to-date guide on [how to configure a "Unnested Sidebar"](https://starlight-sidebar-topics-dropdown.trueberryless.org/docs/guides/unnested-sidebar/#configure-an-unnested-sidebar).
8
+
9
+ - [#31](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/31) [`db10526`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/db10526d49aab3ac8619159d40968dbc4748c9a0) Thanks [@trueberryless](https://github.com/trueberryless)! - ⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now version `0.32.0`.
10
+
11
+ Please use the `@astrojs/upgrade` command to upgrade your project:
12
+
13
+ ```sh
14
+ npx @astrojs/upgrade
15
+ ```
16
+
17
+ - [#31](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/31) [`db10526`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/db10526d49aab3ac8619159d40968dbc4748c9a0) Thanks [@trueberryless](https://github.com/trueberryless)! - ⚠️ **BREAKING CHANGE:** The Starlight Sidebar Topics Dropdown plugin no longer [overrides](https://starlight.astro.build/guides/overriding-components/) the [`<Pagination>` component](https://starlight.astro.build/reference/overrides/#pagination). If you were manually rendering `starlight-sidebar-topics-dropdown/overrides/Pagination.astro` in a custom override, you can now remove it.
18
+
19
+ ## 0.3.0
20
+
21
+ ### Minor Changes
22
+
23
+ - [#16](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/16) [`9edb5dc`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/9edb5dca0215df684c4471ed7e1fe878617be91a) Thanks [@trueberryless](https://github.com/trueberryless)! - Adds support for Astro v5, drops support for Astro v4.
24
+
25
+ ⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now `0.30.0`.
26
+
27
+ Please follow the [upgrade guide](https://github.com/withastro/starlight/releases/tag/%40astrojs/starlight%400.30.0) to update your project.
28
+
29
+ Note that the [`legacy.collections` flag](https://docs.astro.build/en/reference/legacy-flags/#collections) is not supported by this plugin and you should update your collections to use Astro's new Content Layer API.
30
+
31
+ ### Patch Changes
32
+
33
+ - [#18](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/18) [`2039e9a`](https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/commit/2039e9a60659b5628c7406952bde0d453e38aeda) Thanks [@trueberryless](https://github.com/trueberryless)! - Add Unnested Sidebar configuration
34
+
3
35
  ## 0.2.2
4
36
 
5
37
  ### Patch Changes
@@ -6,7 +6,7 @@ import { getRelativeLocaleUrl } from 'astro:i18n'
6
6
  import starlightConfig from 'virtual:starlight/user-config'
7
7
  import config from 'virtual:starlight-sidebar-topics-dropdown/config'
8
8
 
9
- import type { StarlightSidebarTopicsDropdownSharedConfig } from '../lib/config'
9
+ import type { StarlightSidebarTopicsDropdownSharedConfig } from '../libs/config'
10
10
 
11
11
  interface Props {
12
12
  current: StarlightSidebarTopicsDropdownSharedConfig[number]
@@ -98,9 +98,9 @@ function getTranslation(translations: Record<string, string>, link: string, desc
98
98
  .starlight-sidebar-topics-dropdown-dropdown {
99
99
  position: relative;
100
100
  display: block;
101
- width: calc(100% - 8px - 2 * 2px);
102
- margin-left: calc(8px + 2px);
103
- margin-right: calc(2px);
101
+ width: calc(100% - 2 * 2px);
102
+ margin-inline: 2px;
103
+ margin-top: 4px;
104
104
  }
105
105
 
106
106
  .starlight-sidebar-topics-dropdown-button {
@@ -0,0 +1,28 @@
1
+ ---
2
+ import { StarlightSidebarTopicsDropdownLocalsSymbol, type StarlightSidebarTopicsDropdownLocals } from '../../libs/locals'
3
+ import Topics from '../Topics.astro'
4
+ const { entry } = Astro.locals.starlightRoute
5
+ // @ts-expect-error - See `libs/locals` for more information.
6
+ const currentTopic = Astro.locals[StarlightSidebarTopicsDropdownLocalsSymbol] as StarlightSidebarTopicsDropdownLocals
7
+ ---
8
+
9
+ {entry['data'].template !== 'splash' && currentTopic && <Topics current={currentTopic.config} />}
10
+
11
+ <style is:global>
12
+ sl-sidebar-state-persist ul.top-level > li:not(:has(details)) {
13
+ margin-top: 0rem;
14
+ }
15
+ sl-sidebar-state-persist ul.top-level > li:has(details) {
16
+ margin-block: 0.5rem;
17
+ }
18
+ /* sl-sidebar-state-persist ul.top-level > li > a {
19
+ font-size: var(--sl-text-sm);
20
+ } */
21
+ sl-sidebar-state-persist ul.top-level > li > a[aria-current='page'] {
22
+ font-weight: 600;
23
+ }
24
+ sl-sidebar-state-persist ul.top-level > li > a[aria-current='false'] {
25
+ font-weight: 400;
26
+ color: var(--sl-color-gray-2);
27
+ }
28
+ </style>
package/index.ts CHANGED
@@ -1,82 +1,92 @@
1
- import type { StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types";
1
+ import type {
2
+ StarlightPlugin,
3
+ StarlightUserConfig,
4
+ } from "@astrojs/starlight/types";
2
5
 
3
6
  import {
4
- StarlightSidebarTopicsDropdownConfigSchema,
5
- type StarlightSidebarTopicsDropdownUserConfig,
6
- } from "./lib/config";
7
- import { overrideStarlightComponent, throwPluginError } from "./lib/plugin";
8
- import { vitePluginStarlightSidebarTopicsDropdown } from "./lib/vite";
7
+ StarlightSidebarTopicsDropdownConfigSchema,
8
+ type StarlightSidebarTopicsDropdownUserConfig,
9
+ } from "./libs/config";
10
+ import { overrideStarlightComponent, throwPluginError } from "./libs/plugin";
11
+ import { vitePluginStarlightSidebarTopicsDropdown } from "./libs/vite";
9
12
 
10
13
  export type {
11
- StarlightSidebarTopicsDropdownConfig,
12
- StarlightSidebarTopicsDropdownUserConfig,
13
- } from "./lib/config";
14
+ StarlightSidebarTopicsDropdownConfig,
15
+ StarlightSidebarTopicsDropdownUserConfig,
16
+ } from "./libs/config";
14
17
 
15
18
  export default function starlightSidebarTopicsDropdownPlugin(
16
- userConfig: StarlightSidebarTopicsDropdownUserConfig
19
+ userConfig: StarlightSidebarTopicsDropdownUserConfig
17
20
  ): StarlightPlugin {
18
- const parsedConfig = StarlightSidebarTopicsDropdownConfigSchema.safeParse(userConfig);
21
+ const parsedConfig =
22
+ StarlightSidebarTopicsDropdownConfigSchema.safeParse(userConfig);
19
23
 
20
- if (!parsedConfig.success) {
21
- throwPluginError(
22
- `The provided plugin configuration is invalid.\n${parsedConfig.error.issues
23
- .map((issue) => issue.message)
24
- .join("\n")}`
25
- );
26
- }
24
+ if (!parsedConfig.success) {
25
+ throwPluginError(
26
+ `The provided plugin configuration is invalid.\n${parsedConfig.error.issues
27
+ .map((issue) => issue.message)
28
+ .join("\n")}`
29
+ );
30
+ }
27
31
 
28
- const config = parsedConfig.data;
32
+ const config = parsedConfig.data;
29
33
 
30
- return {
31
- name: "starlight-sidebar-topics-dropdown-plugin",
32
- hooks: {
33
- setup({ addIntegration, command, config: starlightConfig, logger, updateConfig }) {
34
- if (command !== "dev" && command !== "build") return;
34
+ return {
35
+ name: "starlight-sidebar-topics-dropdown-plugin",
36
+ hooks: {
37
+ "config:setup"({
38
+ addIntegration,
39
+ addRouteMiddleware,
40
+ command,
41
+ config: starlightConfig,
42
+ logger,
43
+ updateConfig,
44
+ }) {
45
+ if (command !== "dev" && command !== "build") return;
35
46
 
36
- if (starlightConfig.sidebar) {
37
- throwPluginError(
38
- "It looks like you have a `sidebar` configured in your Starlight configuration. To use `starlight-sidebar-topics-dropdown`, create a new topic with your sidebar items.",
39
- "Learn more about topic configuration at https://starlight-sidebar-topics-dropdown.netlify.app/docs/configuration/"
40
- );
41
- }
47
+ if (starlightConfig.sidebar) {
48
+ throwPluginError(
49
+ "It looks like you have a `sidebar` configured in your Starlight configuration. To use `starlight-sidebar-topics-dropdown`, create a new topic with your sidebar items.",
50
+ "Learn more about topic configuration at https://starlight-sidebar-topics-dropdown.trueberryless.org/docs/configuration/"
51
+ );
52
+ }
42
53
 
43
- const sidebar: StarlightUserConfig["sidebar"] = [];
54
+ addRouteMiddleware({
55
+ entrypoint: "starlight-sidebar-topics-dropdown/middleware",
56
+ });
44
57
 
45
- for (const [index, topic] of config.entries()) {
46
- if ("items" in topic) {
47
- sidebar.push({ label: String(index), items: topic.items });
48
- }
49
- }
50
- updateConfig({
51
- components: {
52
- ...starlightConfig.components,
53
- ...overrideStarlightComponent(
54
- starlightConfig.components,
55
- logger,
56
- "Sidebar"
57
- ),
58
- ...overrideStarlightComponent(
59
- starlightConfig.components,
60
- logger,
61
- "Pagination"
62
- ),
63
- },
64
- sidebar,
65
- });
58
+ const sidebar: StarlightUserConfig["sidebar"] = [];
66
59
 
67
- addIntegration({
68
- name: "starlight-sidebar-topics-dropdown-integration",
69
- hooks: {
70
- "astro:config:setup": ({ updateConfig }) => {
71
- updateConfig({
72
- vite: {
73
- plugins: [vitePluginStarlightSidebarTopicsDropdown(config)],
74
- },
75
- });
76
- },
77
- },
78
- });
60
+ for (const [index, topic] of config.entries()) {
61
+ if ("items" in topic) {
62
+ sidebar.push({ label: String(index), items: topic.items });
63
+ }
64
+ }
65
+ updateConfig({
66
+ components: {
67
+ ...starlightConfig.components,
68
+ ...overrideStarlightComponent(
69
+ starlightConfig.components,
70
+ logger,
71
+ "Sidebar"
72
+ ),
73
+ },
74
+ sidebar,
75
+ });
76
+
77
+ addIntegration({
78
+ name: "starlight-sidebar-topics-dropdown-integration",
79
+ hooks: {
80
+ "astro:config:setup": ({ updateConfig }) => {
81
+ updateConfig({
82
+ vite: {
83
+ plugins: [vitePluginStarlightSidebarTopicsDropdown(config)],
84
+ },
85
+ });
79
86
  },
80
- },
81
- };
87
+ },
88
+ });
89
+ },
90
+ },
91
+ };
82
92
  }
package/libs/config.ts ADDED
@@ -0,0 +1,82 @@
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())]),
6
+ variant: z
7
+ .enum(["note", "danger", "success", "caution", "tip", "default"])
8
+ .default("default"),
9
+ });
10
+
11
+ const sidebarTopicBaseSchema = z.object({
12
+ /**
13
+ * An optional badge to display next to the topic label.
14
+ *
15
+ * This option accepts the same configuration as the Starlight badge sidebar item configuration.
16
+ * @see https://starlight.astro.build/guides/sidebar/#badges
17
+ */
18
+ badge: z
19
+ .union([z.string(), sidebarTopicBadgeSchema])
20
+ .transform((badge) =>
21
+ typeof badge === "string"
22
+ ? { variant: "default" as const, text: badge }
23
+ : badge
24
+ )
25
+ .optional(),
26
+ /**
27
+ * The name of an optional icon to display before the topic label set to one of Starlight’s built-in icons.
28
+ * @see https://starlight.astro.build/reference/icons/#all-icons
29
+ */
30
+ icon: z.string().optional(),
31
+ /**
32
+ * The topic label visible at the top of the sidebar.
33
+ *
34
+ * The value can be a string, or for multilingual sites, an object with values for each different locale. When using
35
+ * the object form, the keys must be BCP-47 tags (e.g. en, fr, or zh-CN).
36
+ */
37
+ label: z.union([z.string(), z.record(z.string())]),
38
+ /**
39
+ * The link to the topic’s content which an be a relative link to local files or the full URL of an external page.
40
+ *
41
+ * For internal links, the link can either be a page included in the items array or a different page acting as the
42
+ * 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 are not listed in any
52
+ * 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 `link` page or any of
59
+ * the pages configured in the `items` array is the current page.
60
+ */
61
+ items: z.any().array() as z.Schema<
62
+ NonNullable<StarlightUserConfig["sidebar"]>
63
+ >,
64
+ });
65
+
66
+ export const StarlightSidebarTopicsDropdownConfigSchema = z
67
+ .union([sidebarTopicGroupSchema, sidebarTopicLinkSchema])
68
+ .array();
69
+
70
+ export type StarlightSidebarTopicsDropdownUserConfig = z.input<
71
+ typeof StarlightSidebarTopicsDropdownConfigSchema
72
+ >;
73
+ export type StarlightSidebarTopicsDropdownConfig = z.output<
74
+ typeof StarlightSidebarTopicsDropdownConfigSchema
75
+ >;
76
+
77
+ export type StarlightSidebarTopicsDropdownSharedConfig = (
78
+ | (z.output<typeof sidebarTopicLinkSchema> & { type: "link" })
79
+ | (Omit<z.output<typeof sidebarTopicGroupSchema>, "items"> & {
80
+ type: "group";
81
+ })
82
+ )[];
@@ -0,0 +1,15 @@
1
+ import type { StarlightRouteData } from "@astrojs/starlight/route-data";
2
+ import type { TopicFrontmatterSchema } from "../schema";
3
+ export function isStarlightEntryWithTopic(
4
+ entry: StarlightEntry
5
+ ): entry is StarlightEntryWithTopic {
6
+ return (
7
+ "data" in entry &&
8
+ "topic" in (entry as StarlightEntryWithTopic).data &&
9
+ typeof (entry as StarlightEntryWithTopic).data.topic === "string"
10
+ );
11
+ }
12
+ export type StarlightEntry = StarlightRouteData["entry"];
13
+ export type StarlightEntryWithTopic = StarlightEntry & {
14
+ data: TopicFrontmatterSchema;
15
+ };
package/libs/i18n.ts ADDED
@@ -0,0 +1,27 @@
1
+ import starlightConfig from "virtual:starlight/user-config";
2
+
3
+ import { stripTrailingSlash } from "./pathname";
4
+
5
+ export function getLocalizedSlug(
6
+ slug: string,
7
+ locale: string | undefined
8
+ ): string {
9
+ const slugLocale = getLocaleFromSlug(slug);
10
+ if (slugLocale === locale) return slug;
11
+ locale ??= "";
12
+ if (slugLocale === slug) return locale;
13
+
14
+ if (slugLocale) {
15
+ return stripTrailingSlash(
16
+ slug.replace(`${slugLocale}/`, locale ? `${locale}/` : "")
17
+ );
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
+ }
package/libs/locals.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { StarlightSidebarTopicsDropdownSharedConfig } from "./config";
2
+
3
+ /**
4
+ * We keep track of the current topic configuration using a variable stored using `Astro.locals` which will be reset for
5
+ * each new page. The value is tracked using an untyped symbol on purpose to avoid users to get autocomplete for it and
6
+ * avoid potential clashes with user-defined variables.
7
+ */
8
+ export const StarlightSidebarTopicsDropdownLocalsSymbol = Symbol.for(
9
+ "starlight-sidebar-topics-dropdown:locals"
10
+ );
11
+
12
+ export interface StarlightSidebarTopicsDropdownLocals {
13
+ config: StarlightSidebarTopicsDropdownSharedConfig[number];
14
+ }
@@ -0,0 +1,20 @@
1
+ export function arePathnamesEqual(pathnameA: string, pathnameB: string) {
2
+ return (
3
+ stripLeadingAndTrailingSlashes(pathnameA) ===
4
+ stripLeadingAndTrailingSlashes(pathnameB)
5
+ );
6
+ }
7
+
8
+ export function stripLeadingAndTrailingSlashes(pathname: string): string {
9
+ return stripLeadingSlash(stripTrailingSlash(pathname));
10
+ }
11
+
12
+ export function stripLeadingSlash(pathname: string) {
13
+ if (pathname.startsWith("/")) pathname = pathname.slice(1);
14
+ return pathname;
15
+ }
16
+
17
+ export function stripTrailingSlash(pathname: string) {
18
+ if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
19
+ return pathname;
20
+ }
package/libs/plugin.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { StarlightUserConfig } from "@astrojs/starlight/types";
2
+ import type { AstroIntegrationLogger } from "astro";
3
+ import { AstroError } from "astro/errors";
4
+
5
+ export function throwPluginError(message: string, hint?: string): never {
6
+ throw new AstroError(
7
+ message,
8
+ hint ??
9
+ `See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/issues/new/choose`
10
+ );
11
+ }
12
+
13
+ export function overrideStarlightComponent(
14
+ components: StarlightUserConfig["components"],
15
+ logger: AstroIntegrationLogger,
16
+ component: keyof NonNullable<StarlightUserConfig["components"]>
17
+ ) {
18
+ if (components?.[component]) {
19
+ logger.warn(
20
+ `It looks like you already have a \`${component}\` component override in your Starlight configuration.`
21
+ );
22
+ logger.warn(
23
+ `To use \`starlight-sidebar-topics-dropdown\`, either remove your override or update it to render the content from \`starlight-sidebar-topics-dropdown/overrides/${component}.astro\`.`
24
+ );
25
+
26
+ return {};
27
+ }
28
+
29
+ return {
30
+ [component]: `starlight-sidebar-topics-dropdown/overrides/${component}.astro`,
31
+ };
32
+ }
@@ -0,0 +1,210 @@
1
+ import type { StarlightRouteData } from "@astrojs/starlight/route-data";
2
+
3
+ import type { StarlightSidebarTopicsDropdownSharedConfig } 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: StarlightSidebarTopicsDropdownSharedConfig,
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;
27
+
28
+ const currentTopicConfig =
29
+ config[Number.parseInt(currentSidebarTopic.label, 10)];
30
+ if (!currentTopicConfig) return;
31
+
32
+ const sidebarHasGroups =
33
+ currentSidebarTopic.entries.length !== 1 ||
34
+ currentSidebarTopic.entries[0]?.label !== "";
35
+
36
+ return {
37
+ config: currentTopicConfig,
38
+ sidebar: currentSidebarTopic.entries,
39
+ sidebarHasGroups,
40
+ };
41
+ }
42
+
43
+ export function isTopicFirstPage(
44
+ sidebar: SidebarEntry[],
45
+ currentSlug: string
46
+ ): boolean {
47
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
48
+ if (!currentSidebarTopic) return false;
49
+
50
+ const firstPage = getSidebarFirstPage(currentSidebarTopic.entries);
51
+ if (!firstPage) return false;
52
+
53
+ return arePathnamesEqual(
54
+ stripLeadingAndTrailingSlashes(firstPage.href),
55
+ currentSlug
56
+ );
57
+ }
58
+
59
+ export function isTopicLastPage(
60
+ sidebar: SidebarEntry[],
61
+ currentSlug: string
62
+ ): boolean {
63
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar);
64
+ if (!currentSidebarTopic) return false;
65
+
66
+ const lastPage = getSidebarLastPage(currentSidebarTopic.entries);
67
+ if (!lastPage) return false;
68
+
69
+ return arePathnamesEqual(
70
+ stripLeadingAndTrailingSlashes(lastPage.href),
71
+ currentSlug
72
+ );
73
+ }
74
+
75
+ function getSidebarFirstPage(sidebar: SidebarEntry[]) {
76
+ const entry = sidebar[0];
77
+ if (!entry) return;
78
+ if (entry.type === "link") return entry;
79
+
80
+ return getSidebarFirstPage(entry.entries);
81
+ }
82
+
83
+ function getSidebarLastPage(sidebar: SidebarEntry[]) {
84
+ const entry = sidebar.at(-1);
85
+ if (!entry) return;
86
+ if (entry.type === "link") return entry;
87
+
88
+ return getSidebarLastPage(entry.entries);
89
+ }
90
+
91
+ function getTopicFromSlug(
92
+ config: StarlightSidebarTopicsDropdownSharedConfig,
93
+ sidebar: SidebarEntry[],
94
+ slug: string
95
+ ): Topic | undefined {
96
+ let topicConfig: Topic["config"] | undefined;
97
+ let topicSidebar: Topic["sidebar"] | undefined;
98
+ let sidebarHasGroups = true;
99
+
100
+ // Start by checking if the current page is a topic homepage.
101
+ let groupTopicIndex = -1;
102
+ const slugLocale = getLocaleFromSlug(slug);
103
+
104
+ for (const topic of config) {
105
+ if (topic.type === "group") groupTopicIndex++;
106
+
107
+ if (
108
+ !absoluteLinkRegex.test(topic.link) &&
109
+ arePathnamesEqual(
110
+ getLocalizedSlug(
111
+ stripLeadingAndTrailingSlashes(topic.link),
112
+ slugLocale
113
+ ),
114
+ slug
115
+ ) &&
116
+ groupTopicIndex !== -1
117
+ ) {
118
+ const sidebarTopic = sidebar[groupTopicIndex];
119
+
120
+ if (sidebarTopic?.type === "group") {
121
+ topicConfig = topic;
122
+ topicSidebar = sidebarTopic.entries;
123
+ sidebarHasGroups =
124
+ sidebarTopic.entries.length !== 1 ||
125
+ sidebarTopic.entries[0]?.label !== "";
126
+ }
127
+
128
+ break;
129
+ }
130
+ }
131
+
132
+ if (!topicConfig || !topicSidebar) return;
133
+
134
+ return { config: topicConfig, sidebar: topicSidebar, sidebarHasGroups };
135
+ }
136
+
137
+ function getTopicById(
138
+ config: StarlightSidebarTopicsDropdownSharedConfig,
139
+ sidebar: SidebarEntry[],
140
+ id: string
141
+ ): Topic | undefined {
142
+ let topicConfig: Topic["config"] | undefined;
143
+ let topicSidebar: Topic["sidebar"] | undefined;
144
+ let sidebarHasGroups = true;
145
+ let groupTopicIndex = -1;
146
+ for (const topic of config) {
147
+ if (topic.type === "group") groupTopicIndex++;
148
+ if (topic.type === "group" && topic.id === id) {
149
+ const sidebarTopic = sidebar[groupTopicIndex];
150
+ if (sidebarTopic?.type === "group") {
151
+ topicConfig = topic;
152
+ topicSidebar = sidebarTopic.entries;
153
+ sidebarHasGroups =
154
+ sidebarTopic.entries.length !== 1 ||
155
+ sidebarTopic.entries[0]?.label !== "";
156
+ }
157
+ break;
158
+ }
159
+ }
160
+ if (!topicConfig || !topicSidebar) return;
161
+ return { config: topicConfig, sidebar: topicSidebar, sidebarHasGroups };
162
+ }
163
+
164
+ function getCurrentSidebarTopic(
165
+ sidebar: SidebarEntry[]
166
+ ): SidebarTopic | undefined {
167
+ let currentSidebarTopic: SidebarTopic | undefined;
168
+
169
+ for (const topic of sidebar) {
170
+ if (topic.type === "link") continue;
171
+
172
+ const currentSidebarEntry = getCurrentSidebarEntry(topic.entries);
173
+
174
+ if (currentSidebarEntry) {
175
+ currentSidebarTopic = topic;
176
+ break;
177
+ }
178
+ }
179
+
180
+ return currentSidebarTopic;
181
+ }
182
+
183
+ function getCurrentSidebarEntry(
184
+ sidebar: SidebarEntry[]
185
+ ): SidebarEntry | undefined {
186
+ return sidebar.find((entry) => {
187
+ if (entry.type === "link") {
188
+ return entry.isCurrent;
189
+ }
190
+
191
+ return getCurrentSidebarEntry(entry.entries);
192
+ });
193
+ }
194
+
195
+ function getTopicIdFromEntry(entry: StarlightEntry): string | undefined {
196
+ return isStarlightEntryWithTopic(entry) ? entry.data.topic : undefined;
197
+ }
198
+
199
+ type SidebarEntry = StarlightRouteData["sidebar"][number];
200
+
201
+ interface SidebarTopic {
202
+ label: string;
203
+ entries: SidebarEntry[];
204
+ }
205
+
206
+ export interface Topic {
207
+ config: StarlightSidebarTopicsDropdownSharedConfig[number];
208
+ sidebar: SidebarEntry[];
209
+ sidebarHasGroups: boolean;
210
+ }