UNPKG

@astrojs/starlight

Version:

Build beautiful, high-performance documentation websites with Astro

234 lines (221 loc) 8.44 kB
import { astroExpressiveCode, type AstroExpressiveCodeOptions, type CustomConfigPreprocessors, } from 'astro-expressive-code'; import { addClassName } from 'astro-expressive-code/hast'; import type { AstroIntegration } from 'astro'; import type { HookParameters, StarlightConfig } from '../../types'; import { absolutePathToLang } from '../shared/absolutePathToLang'; import { slugToLocale } from '../shared/slugToLocale'; import { localeToLang } from '../shared/localeToLang'; import { applyStarlightUiThemeColors, preprocessThemes, type ThemeObjectOrBundledThemeName, } from './theming'; import { addTranslations } from './translations'; export type StarlightExpressiveCodeOptions = Omit<AstroExpressiveCodeOptions, 'themes'> & { /** * Set the themes used to style code blocks. * * See the [Expressive Code `themes` documentation](https://expressive-code.com/guides/themes/) * for details of the supported theme formats. * * Starlight uses the dark and light variants of Sarah Drasner’s * [Night Owl theme](https://github.com/sdras/night-owl-vscode-theme) by default. * * If you provide at least one dark and one light theme, Starlight will automatically keep * the active code block theme in sync with the current site theme. Configure this behavior * with the [`useStarlightDarkModeSwitch`](#usestarlightdarkmodeswitch) option. * * Defaults to `['starlight-dark', 'starlight-light']`. */ themes?: ThemeObjectOrBundledThemeName[] | undefined; /** * When `true`, code blocks automatically switch between light and dark themes when the * site theme changes. * * When `false`, you must manually add CSS to handle switching between multiple themes. * * **Note**: When setting `themes`, you must provide at least one dark and one light theme * for the Starlight dark mode switch to work. * * Defaults to `true`. */ useStarlightDarkModeSwitch?: boolean | undefined; /** * When `true`, Starlight's CSS variables are used for the colors of code block UI elements * (backgrounds, buttons, shadows etc.), matching the * [site color theme](/guides/css-and-tailwind/#theming). * * When `false`, the colors provided by the active syntax highlighting theme are used for * these elements. * * Defaults to `true` if the `themes` option is not set (= you are using Starlight's * default themes), and `false` otherwise. * * **Note**: When manually setting this to `true` with your custom set of `themes`, you must * provide at least one dark and one light theme to ensure proper color contrast. */ useStarlightUiThemeColors?: boolean | undefined; }; type StarlightEcIntegrationOptions = { starlightConfig: StarlightConfig; useTranslations: HookParameters<'config:setup'>['useTranslations']; }; /** * Create an Expressive Code configuration preprocessor based on Starlight config. * Used internally to set up Expressive Code and by the `<Code>` component. */ export function getStarlightEcConfigPreprocessor({ starlightConfig, useTranslations, }: StarlightEcIntegrationOptions): CustomConfigPreprocessors['preprocessAstroIntegrationConfig'] { return (input): AstroExpressiveCodeOptions => { const astroConfig = input.astroConfig; const ecConfig = input.ecConfig as StarlightExpressiveCodeOptions; const { themes: themesInput, cascadeLayer, customizeTheme, styleOverrides: { textMarkers: textMarkersStyleOverrides, ...otherStyleOverrides } = {}, useStarlightDarkModeSwitch, useStarlightUiThemeColors = ecConfig.themes === undefined, plugins = [], ...rest } = ecConfig; // Handle the `themes` option const themes = preprocessThemes(themesInput); if (useStarlightUiThemeColors === true && themes.length < 2) { console.warn( `*** Warning: Using the config option "useStarlightUiThemeColors: true" ` + `with a single theme is not recommended. For better color contrast, ` + `please provide at least one dark and one light theme.\n` ); } // Add the `not-content` class to all rendered blocks to prevent them from being affected // by Starlight's default content styles plugins.push({ name: 'Starlight Plugin', hooks: { postprocessRenderedBlock: ({ renderData }) => { addClassName(renderData.blockAst, 'not-content'); }, }, }); // Add Expressive Code UI translations for all defined locales addTranslations(starlightConfig, useTranslations); return { themes, customizeTheme: (theme) => { if (useStarlightUiThemeColors) { applyStarlightUiThemeColors(theme); } if (customizeTheme) { theme = customizeTheme(theme) ?? theme; } return theme; }, defaultLocale: starlightConfig.defaultLocale?.lang ?? starlightConfig.defaultLocale?.locale, themeCssSelector: (theme, { styleVariants }) => { // If one dark and one light theme are available, and the user has not disabled it, // generate theme CSS selectors compatible with Starlight's dark mode switch if (useStarlightDarkModeSwitch !== false && styleVariants.length >= 2) { const baseTheme = styleVariants[0]?.theme; const altTheme = styleVariants.find((v) => v.theme.type !== baseTheme?.type)?.theme; if (theme === baseTheme || theme === altTheme) return `[data-theme='${theme.type}']`; } // Return the default selector return `[data-theme='${theme.name}']`; }, cascadeLayer: cascadeLayer ?? 'starlight.components', styleOverrides: { borderRadius: '0px', borderWidth: '1px', codePaddingBlock: '0.75rem', codePaddingInline: '1rem', codeFontFamily: 'var(--__sl-font-mono)', codeFontSize: 'var(--sl-text-code)', codeLineHeight: 'var(--sl-line-height)', uiFontFamily: 'var(--__sl-font)', textMarkers: { lineDiffIndicatorMarginLeft: '0.25rem', defaultChroma: '45', backgroundOpacity: '60%', ...textMarkersStyleOverrides, }, ...otherStyleOverrides, }, getBlockLocale: ({ file }) => { if (file.url) { const locale = slugToLocale(file.url.pathname.slice(1), starlightConfig); return localeToLang(starlightConfig, locale); } // Note that EC cannot use the `absolutePathToLang` helper passed down to plugins as this callback // is also called in the context of the `<Code>` component. return absolutePathToLang(file.path, { starlightConfig, astroConfig }); }, plugins, ...rest, }; }; } export const starlightExpressiveCode = ({ starlightConfig, useTranslations, }: StarlightEcIntegrationOptions): AstroIntegration[] => { // If Expressive Code is disabled, add a shim to prevent build errors and provide // a helpful error message in case the user tries to use the `<Code>` component if (starlightConfig.expressiveCode === false) { const modules: Record<string, string> = { 'virtual:astro-expressive-code/api': 'export default {}', 'virtual:astro-expressive-code/config': 'export default {}', 'virtual:astro-expressive-code/preprocess-config': `throw new Error("Starlight's Code component requires Expressive Code, which is disabled in your Starlight config. Please remove \`expressiveCode: false\` from your config or import Astro's built-in Code component from 'astro:components' instead.")`.replace(/\s+/g, ' '), }; return [ { name: 'astro-expressive-code-shim', hooks: { 'astro:config:setup': ({ updateConfig }) => { updateConfig({ vite: { plugins: [ { name: 'vite-plugin-astro-expressive-code-shim', enforce: 'post', resolveId: (id) => (id in modules ? `\0${id}` : undefined), load: (id) => (id?.[0] === '\0' ? modules[id.slice(1)] : undefined), }, ], }, }); }, }, }, ]; } const configArgs = typeof starlightConfig.expressiveCode === 'object' ? (starlightConfig.expressiveCode as AstroExpressiveCodeOptions) : {}; return [ astroExpressiveCode({ ...configArgs, customConfigPreprocessors: { preprocessAstroIntegrationConfig: getStarlightEcConfigPreprocessor({ starlightConfig, useTranslations, }), preprocessComponentConfig: ` import starlightConfig from 'virtual:starlight/user-config' import { useTranslations, getStarlightEcConfigPreprocessor } from '@astrojs/starlight/internal' export default getStarlightEcConfigPreprocessor({ starlightConfig, useTranslations }) `, }, }), ]; };