@astrojs/starlight
Version:
Build beautiful, high-performance documentation websites with Astro
234 lines (221 loc) • 8.44 kB
text/typescript
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 })
`,
},
}),
];
};