starlight-sidebar-topics
Version:
Starlight plugin to split your documentation into different sections, each with its own sidebar.
178 lines (133 loc) • 5.16 kB
text/typescript
import type { StarlightRouteData } from '@astrojs/starlight/route-data'
import type { StarlightSidebarTopicsSharedConfig } from './config'
import { isStarlightEntryWithTopic, type StarlightEntry } from './content'
import { getLocaleFromSlug, getLocalizedSlug } from './i18n'
import { arePathnamesEqual, stripLeadingAndTrailingSlashes } from './pathname'
const absoluteLinkRegex = /^https?:\/\//
export function getCurrentTopic(
config: StarlightSidebarTopicsSharedConfig,
sidebar: SidebarEntry[],
currentSlug: string,
entry: StarlightEntry,
): Topic | undefined {
// If the current page has a topic ID, use it to find the topic.
const topicId = getTopicIdFromEntry(entry)
if (topicId) return getTopicById(config, sidebar, topicId)
// Start by checking if the current page is a topic root.
const topicFromSlug = getTopicFromSlug(config, sidebar, currentSlug)
if (topicFromSlug) return topicFromSlug
// Otherwise, find the current topic by looking for the current page in the sidebar.
const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
if (!currentSidebarTopic) return
const currentTopicConfig = config[Number.parseInt(currentSidebarTopic.label, 10)]
if (!currentTopicConfig) return
return { config: currentTopicConfig, sidebar: currentSidebarTopic.entries }
}
export function isTopicFirstPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
if (!currentSidebarTopic) return false
const firstPage = getSidebarFirstPage(currentSidebarTopic.entries)
if (!firstPage) return false
return arePathnamesEqual(stripLeadingAndTrailingSlashes(firstPage.href), currentSlug)
}
export function isTopicLastPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
if (!currentSidebarTopic) return false
const lastPage = getSidebarLastPage(currentSidebarTopic.entries)
if (!lastPage) return false
return arePathnamesEqual(stripLeadingAndTrailingSlashes(lastPage.href), currentSlug)
}
function getSidebarFirstPage(sidebar: SidebarEntry[]) {
const entry = sidebar[0]
if (!entry) return
if (entry.type === 'link') return entry
return getSidebarFirstPage(entry.entries)
}
function getSidebarLastPage(sidebar: SidebarEntry[]) {
const entry = sidebar.at(-1)
if (!entry) return
if (entry.type === 'link') return entry
return getSidebarLastPage(entry.entries)
}
function getTopicFromSlug(
config: StarlightSidebarTopicsSharedConfig,
sidebar: SidebarEntry[],
slug: string,
): Topic | undefined {
let topicConfig: Topic['config'] | undefined
let topicSidebar: Topic['sidebar'] | undefined
// Start by checking if the current page is a topic homepage.
let groupTopicIndex = -1
const slugLocale = getLocaleFromSlug(slug)
for (const topic of config) {
if (topic.type === 'group') groupTopicIndex++
if (
!absoluteLinkRegex.test(topic.link) &&
arePathnamesEqual(getLocalizedSlug(stripLeadingAndTrailingSlashes(topic.link), slugLocale), slug) &&
groupTopicIndex !== -1
) {
const sidebarTopic = sidebar[groupTopicIndex]
if (sidebarTopic?.type === 'group') {
topicConfig = topic
topicSidebar = sidebarTopic.entries
}
break
}
}
if (!topicConfig || !topicSidebar) return
return { config: topicConfig, sidebar: topicSidebar }
}
export function getTopicById(
config: StarlightSidebarTopicsSharedConfig,
sidebar: SidebarEntry[],
id: string,
): Topic | undefined {
let topicConfig: Topic['config'] | undefined
let topicSidebar: Topic['sidebar'] | undefined
let groupTopicIndex = -1
for (const topic of config) {
if (topic.type === 'group') groupTopicIndex++
if (topic.type === 'group' && topic.id === id) {
const sidebarTopic = sidebar[groupTopicIndex]
if (sidebarTopic?.type === 'group') {
topicConfig = topic
topicSidebar = sidebarTopic.entries
}
break
}
}
if (!topicConfig || !topicSidebar) return
return { config: topicConfig, sidebar: topicSidebar }
}
function getCurrentSidebarTopic(sidebar: SidebarEntry[]): SidebarTopic | undefined {
let currentSidebarTopic: SidebarTopic | undefined
for (const topic of sidebar) {
if (topic.type === 'link') continue
const currentSidebarEntry = getCurrentSidebarEntry(topic.entries)
if (currentSidebarEntry) {
currentSidebarTopic = topic
break
}
}
return currentSidebarTopic
}
function getCurrentSidebarEntry(sidebar: SidebarEntry[]): SidebarEntry | undefined {
return sidebar.find((entry) => {
if (entry.type === 'link') {
return entry.isCurrent
}
return getCurrentSidebarEntry(entry.entries)
})
}
function getTopicIdFromEntry(entry: StarlightEntry): string | undefined {
return isStarlightEntryWithTopic(entry) ? entry.data.topic : undefined
}
type SidebarEntry = StarlightRouteData['sidebar'][number]
interface SidebarTopic {
label: string
entries: SidebarEntry[]
}
export interface Topic {
config: StarlightSidebarTopicsSharedConfig[number]
sidebar: SidebarEntry[]
}