UNPKG

@astrojs/starlight

Version:

Build beautiful, high-performance documentation websites with Astro

127 lines (104 loc) 4.61 kB
import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { AstroConfig } from 'astro'; import { rehypeHeadingIds } from '@astrojs/markdown-remark'; import type { Root as RehypeRoot } from 'hast'; import type { Root as RemarkRoot } from 'mdast'; import remarkDirective from 'remark-directive'; import type { Plugin } from 'unified'; import type { VFile } from 'vfile'; import { resolveCollectionPath } from '../utils/collection-fs'; import type { HookParameters, StarlightConfig } from '../types'; import { remarkAsides } from './asides'; import { rehypeRtlCodeSupport } from './code-rtl-support'; import rehypeAutolinkHeadings from './heading-links'; /** List of remark plugins to apply. */ export function starlightRemarkPlugins(options: RemarkRehypePluginOptions): RemarkPlugin[] { return [remarkDirective, remarkPlugins(options)]; } /** List of rehype plugins to apply. */ export function starlightRehypePlugins(options: RemarkRehypePluginOptions): RehypePlugin[] { return [ ...(options.starlightConfig.markdown.headingLinks ? [ [ rehypeHeadingIds, { experimentalHeadingIdCompat: options.astroConfig.experimental?.headingIdCompat }, ], ] : []), rehypePlugins(options), ] as RehypePlugin[]; } /** Remark plugin applying other Starlight remark plugins if necessary. */ function remarkPlugins(options: RemarkRehypePluginOptions): RemarkPlugin { const remarkRehypePaths = getRemarkRehypePaths(options); return function attacher(this) { const remarkAsidesTransformer = remarkAsides(options).call(this)!; return async function transformer(...args) { const [, file] = args; if (!shouldTransformFile(file, remarkRehypePaths)) return; await remarkAsidesTransformer(...args); }; }; } /** Rehype plugin applying other Starlight rehype plugins if necessary. */ function rehypePlugins(options: RemarkRehypePluginOptions): RehypePlugin { const remarkRehypePaths = getRemarkRehypePaths(options); return function attacher(this) { const rehypeRtlCodeSupportTransformer = rehypeRtlCodeSupport(options).call(this); const rehypeAutolinkHeadingsTransformer = rehypeAutolinkHeadings(options).call(this); return async function transformer(...args) { const [, file] = args; if (!shouldTransformFile(file, remarkRehypePaths)) return; await rehypeRtlCodeSupportTransformer(...args); if (options.starlightConfig.markdown.headingLinks) { await rehypeAutolinkHeadingsTransformer(...args); } }; }; } /** * Returns the paths to the Starlight docs collection and any additional paths defined in the * `starlightConfig.markdown.processedDirs` option that can be used with the * `shouldTransformFile()` utility to determine if a file should be transformed by a plugin or not. */ function getRemarkRehypePaths(options: RemarkRehypePluginOptions): string[] { const paths = [normalizePath(resolveCollectionPath('docs', options.astroConfig.srcDir))]; for (const processedDir of options.starlightConfig.markdown.processedDirs) { paths.push(normalizePath(resolve(fileURLToPath(options.astroConfig.root), processedDir))); } return paths; } /** * Determines if a file should be transformed by a remark/rehype plugin, e.g. files without a known * path or files that are not part of the allowed remark/rehype paths are skipped. */ function shouldTransformFile(file: VFile, remarkRehypePaths: string[]) { // If the content is rendered using the content loader `renderMarkdown()` API, a file path // is not provided. // In that case, we skip the file. if (!file?.path) return false; const normalizedPath = normalizePath(file.path); // If the document is not part of the allowed remark/rehype paths, skip it. return remarkRehypePaths.some((path) => normalizedPath.startsWith(path)); } /** * File path separators seems to be inconsistent on Windows between remark/rehype plugins used on * Markdown vs MDX files. * For the time being, we normalize all paths to unix style paths. */ const backSlashRegex = /\\/g; function normalizePath(path: string) { return path.replace(backSlashRegex, '/'); } export interface RemarkRehypePluginOptions { starlightConfig: Pick<StarlightConfig, 'defaultLocale' | 'locales' | 'markdown'>; astroConfig: Pick<AstroConfig, 'root' | 'srcDir'> & { experimental: Pick<AstroConfig['experimental'], 'headingIdCompat'>; }; useTranslations: HookParameters<'config:setup'>['useTranslations']; absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang']; } type RemarkPlugin = Plugin<[], RemarkRoot>; type RehypePlugin = Plugin<[], RehypeRoot>;