diff options
author | Chris Swithinbank | 2025-04-16 11:37:52 +0200 |
---|---|---|
committer | GitHub | 2025-04-16 11:37:52 +0200 |
commit | 8c19678e57c0270d3d80d4678f23a6fc287ebf12 (patch) | |
tree | 698fc83a5729254f34fe89bb358e13ed8abce922 | |
parent | eda22726299e8771be790444ea86a6dd7deab153 (diff) | |
download | IT.starlight-8c19678e57c0270d3d80d4678f23a6fc287ebf12.tar.gz IT.starlight-8c19678e57c0270d3d80d4678f23a6fc287ebf12.tar.bz2 IT.starlight-8c19678e57c0270d3d80d4678f23a6fc287ebf12.zip |
Add built-in heading anchor link support (#3033)
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
57 files changed, 692 insertions, 45 deletions
diff --git a/.changeset/late-onions-sparkle.md b/.changeset/late-onions-sparkle.md new file mode 100644 index 00000000..a70a0ef0 --- /dev/null +++ b/.changeset/late-onions-sparkle.md @@ -0,0 +1,26 @@ +--- +'@astrojs/starlight': minor +--- + +Adds support for generating clickable anchor links for headings. + +By default, Starlight now renders an anchor link beside headings in Markdown and MDX content. A new `<AnchorHeading>` component is available to achieve the same thing in custom pages built using `<StarlightPage>`. + +If you want to disable this new Markdown processing set the `markdown.headingLinks` option in your Starlight config to `false`: + +```js +starlight({ + title: 'My docs', + markdown: { + headingLinks: false, + }, +}), +``` + +⚠️ **BREAKING CHANGE:** The minimum supported version of Astro is now v5.5.0. + +Please update Starlight and Astro together: + +```sh +npx @astrojs/upgrade +``` diff --git a/.changeset/quick-items-dream.md b/.changeset/quick-items-dream.md new file mode 100644 index 00000000..cb127773 --- /dev/null +++ b/.changeset/quick-items-dream.md @@ -0,0 +1,24 @@ +--- +'@astrojs/starlight-markdoc': minor +--- + +Adds support for generating clickable anchor links for headings. + +By default, the Starlight Markdoc preset now includes a default `heading` node, which renders an anchor link beside headings in your Markdoc content. + +If you want to disable this new feature, pass `headingLinks: false` in your Markdoc config: + +```js +export default defineMarkdocConfig({ + // Disable the default heading anchor link support + extends: [starlightMarkdoc({ headingLinks: false })], +}); +``` + +⚠️ **BREAKING CHANGE:** The minimum supported peer version of Starlight is now v0.34.0. + +Please update Starlight and the Starlight Markdoc preset together: + +```sh +npx @astrojs/upgrade +``` diff --git a/.prettierignore b/.prettierignore index 79270814..5cbeb0c1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,5 +18,8 @@ pnpm-lock.yaml # https://github.com/withastro/prettier-plugin-astro/issues/337 packages/starlight/user-components/Tabs.astro +# Prettier forces whitespace between elements we want to avoid for consistency with the rehype version +packages/starlight/components/AnchorHeading.astro + # Malformed YAML file used for testing packages/starlight/__tests__/i18n/malformed-yaml-src/content/i18n/*.yml diff --git a/docs/src/content/docs/guides/authoring-content.mdx b/docs/src/content/docs/guides/authoring-content.mdx index 15334b24..be27ca80 100644 --- a/docs/src/content/docs/guides/authoring-content.mdx +++ b/docs/src/content/docs/guides/authoring-content.mdx @@ -650,3 +650,22 @@ If you already have a Starlight site and want to add Markdoc, follow these steps </Steps> To learn more about the Markdoc syntax and features, see the [Markdoc documentation](https://markdoc.dev/docs/syntax) or the [Astro Markdoc integration guide](https://docs.astro.build/en/guides/integrations-guide/markdoc/). + +### Configuring the Markdoc preset + +The `starlightMarkdoc()` preset accepts the following configuration options: + +#### `headingLinks` + +**type:** `boolean` +**default:** `true` + +Controls whether or not headings are rendered with a clickable anchor link. +Equivalent to the [`markdown.headingLinks`](/reference/configuration/#markdown) option, which applies to Markdown and MDX files. + +```js "headingLinks: false" +export default defineMarkdocConfig({ + // Disable the default heading anchor link support + extends: [starlightMarkdoc({ headingLinks: false })], +}); +``` diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index 663bea68..5f9a2eca 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -3,6 +3,8 @@ title: Pages description: Learn how to create and manage your documentation site’s pages with Starlight. sidebar: order: 1 +tableOfContents: + maxHeadingLevel: 4 --- Starlight generates your site’s HTML pages based on your content, with flexible options provided via Markdown frontmatter. @@ -73,28 +75,49 @@ Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/ ### Using Starlight’s design in custom pages -To use the Starlight layout in custom pages, wrap your page content with the `<StarlightPage />` component. +To use the Starlight layout in custom pages, wrap your page content with the [`<StarlightPage>` component](#starlightpage-component). This can be helpful if you are generating content dynamically but still want to use Starlight’s design. +To add anchor links to headings that match Starlight’s Markdown anchor link styles, you can use the [`<AnchorHeading>` component](#anchorheading-component) in your custom pages. + ```astro --- // src/pages/custom-page/example.astro import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; +import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro'; import CustomComponent from './CustomComponent.astro'; --- <StarlightPage frontmatter={{ title: 'My custom page' }}> <p>This is a custom page with a custom component:</p> <CustomComponent /> + + <AnchorHeading level="2" id="learn-more">Learn more</AnchorHeading> + <p> + <a href="https://starlight.astro.build/">Read more in the Starlight docs</a> + </p> </StarlightPage> ``` -#### Props +#### `<StarlightPage>` component + +The `<StarlightPage />` component renders a full page of content using Starlight’s layout and styles. + +```astro +--- +import StarlightPage from '@astrojs/starlight/components/AnchorHeading.astro'; +--- + +<StarlightPage frontmatter={{ title: 'My custom page' }}> + <!-- Custom page content --> +</StarlightPage> +``` The `<StarlightPage />` component accepts the following props. -##### `frontmatter` (required) +##### `frontmatter` +**required** **type:** `StarlightPageFrontmatter` Set the [frontmatter properties](/reference/frontmatter/) for this page, similar to frontmatter in Markdown pages. @@ -173,3 +196,33 @@ Set the BCP-47 language tag for this page’s content, e.g. `en`, `zh-CN`, or `p **default:** `false` Indicate if this page is using [fallback content](/guides/i18n/#fallback-content) because there is no translation for the current language. + +#### `<AnchorHeading>` component + +The `<AnchorHeading />` component renders an HTML heading element with a clickable anchor link that matches Starlight’s Markdown styles. + +```astro +--- +import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro'; +--- + +<AnchorHeading level="2" id="sub-heading">Sub heading</AnchorHeading> +``` + +It accepts the following props as well as any other valid [global HTML attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes). + +##### `level` + +**required** +**type:** `1 | 2 | 3 | 4 | 5 | 6` + +The heading level to render. +For example, `level="1"` would render an `<h1>` element. + +##### `id` + +**required** +**type:** `string` + +The unique ID for this heading. +This will be used as the `id` attribute of the rendered heading and the anchor icon will link to it. diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx index dbdb277a..7166da59 100644 --- a/docs/src/content/docs/reference/configuration.mdx +++ b/docs/src/content/docs/reference/configuration.mdx @@ -372,6 +372,29 @@ starlight({ }); ``` +### `markdown` + +**type:** `{ headingLinks?: boolean }` +**default:** `{ headingLinks: true }` + +Configure Starlight’s Markdown processing. + +#### `headingLinks` + +**type:** `boolean` +**default:** `true` + +Controls whether or not headings are rendered with a clickable anchor link. + +```js +starlight({ + markdown: { + // Disable Starlight’s clickable heading anchor links. + headingLinks: false, + }, +}), +``` + ### `expressiveCode` **type:** `StarlightExpressiveCodeOptions | boolean` diff --git a/package.json b/package.json index cf8d314d..1ff32e1d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "examples/basics/dist/_astro/*.css", "!examples/basics/dist/_astro/print.*.css" ], - "limit": "14.5 kB", + "limit": "14.75 kB", "gzip": true } ], diff --git a/packages/markdoc/components.ts b/packages/markdoc/components.ts index 62b21695..2eaa0b47 100644 --- a/packages/markdoc/components.ts +++ b/packages/markdoc/components.ts @@ -1 +1,2 @@ export { default as Code } from './Code.astro'; +export { default as Heading } from '@astrojs/starlight/components/AnchorHeading.astro'; diff --git a/packages/markdoc/index.mjs b/packages/markdoc/index.mjs index a2ac12cf..82a472f4 100644 --- a/packages/markdoc/index.mjs +++ b/packages/markdoc/index.mjs @@ -1,4 +1,4 @@ -import { component } from '@astrojs/markdoc/config'; +import { component, nodes } from '@astrojs/markdoc/config'; import { WellKnownElementAttributes, WellKnownAnchorAttributes } from './html.mjs'; /** @@ -287,7 +287,23 @@ export const StarlightMarkdocPreset = { }, }; -/** @return {import('@astrojs/markdoc/config').AstroMarkdocConfig} */ -export default function starlightMarkdoc() { - return StarlightMarkdocPreset; +/** + * Markdoc preset that configures Starlight’s built-in components. + * @return {import('@astrojs/markdoc/config').AstroMarkdocConfig} + */ +export default function starlightMarkdoc({ headingLinks = true } = {}) { + return { + ...StarlightMarkdocPreset, + nodes: { + ...StarlightMarkdocPreset.nodes, + ...(headingLinks + ? { + heading: { + ...nodes.heading, + render: component('@astrojs/starlight-markdoc/components', 'Heading'), + }, + } + : {}), + }, + }; } diff --git a/packages/markdoc/package.json b/packages/markdoc/package.json index 3902ac97..962d354d 100644 --- a/packages/markdoc/package.json +++ b/packages/markdoc/package.json @@ -23,7 +23,7 @@ }, "peerDependencies": { "@astrojs/markdoc": ">=0.12.1", - "@astrojs/starlight": ">=0.30.0" + "@astrojs/starlight": ">=0.34.0" }, "publishConfig": { "provenance": true diff --git a/packages/starlight/__e2e__/components.test.ts b/packages/starlight/__e2e__/components.test.ts index b4021c47..6a1b7e5b 100644 --- a/packages/starlight/__e2e__/components.test.ts +++ b/packages/starlight/__e2e__/components.test.ts @@ -374,6 +374,25 @@ test.describe('whitespaces', () => { }); }); +test.describe('anchor headings', () => { + test('renders the same content for Markdown headings and Astro component', async ({ + getProdServer, + page, + }) => { + const starlight = await getProdServer(); + + await starlight.goto('/anchor-heading'); + const markdownContent = page.locator('.sl-markdown-content'); + const markdownHtml = await markdownContent.innerHTML(); + + await starlight.goto('/anchor-heading-component'); + const componentContent = page.locator('.sl-markdown-content'); + const componentHtml = await componentContent.innerHTML(); + + expect(markdownHtml).toEqual(componentHtml); + }); +}); + async function expectSelectedTab(tabs: Locator, label: string, panel?: string) { expect( ( diff --git a/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx new file mode 100644 index 00000000..245ce62c --- /dev/null +++ b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx @@ -0,0 +1,9 @@ +--- +title: Anchor Headings Component +--- + +import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro'; + +<AnchorHeading level="2" id="an-anchor-heading">An anchor heading</AnchorHeading> + +<AnchorHeading level="3" id="another-anchor-heading">Another anchor heading</AnchorHeading> diff --git a/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md new file mode 100644 index 00000000..ce659560 --- /dev/null +++ b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md @@ -0,0 +1,7 @@ +--- +title: Anchor Headings +--- + +## An anchor heading + +### Another anchor heading diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts index 6f9dad71..c64cdcc4 100644 --- a/packages/starlight/__tests__/basics/config-errors.test.ts +++ b/packages/starlight/__tests__/basics/config-errors.test.ts @@ -63,6 +63,9 @@ test('parses bare minimum valid config successfully', () => { "isUsingBuiltInDefaultLocale": true, "lastUpdated": false, "locales": undefined, + "markdown": { + "headingLinks": true, + }, "pagefind": { "ranking": { "pageLength": 0.1, diff --git a/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts b/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts new file mode 100644 index 00000000..f7b8bb5c --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts @@ -0,0 +1,85 @@ +import { createMarkdownProcessor, type MarkdownProcessor } from '@astrojs/markdown-remark'; +import { expect, test } from 'vitest'; +import { createTranslationSystemFromFs } from '../../utils/translations-fs'; +import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config'; +import { absolutePathToLang as getAbsolutePathFromLang } from '../../integrations/shared/absolutePathToLang'; +import { starlightAutolinkHeadings } from '../../integrations/heading-links'; + +const starlightConfig = StarlightConfigSchema.parse({ + title: 'Anchor Links Tests', + locales: { en: { label: 'English' }, fr: { label: 'French' } }, + defaultLocale: 'en', +} satisfies StarlightUserConfig); + +const astroConfig = { + root: new URL(import.meta.url), + srcDir: new URL('./_src/', import.meta.url), +}; + +const useTranslations = createTranslationSystemFromFs( + starlightConfig, + // Using non-existent `_src/` to ignore custom files in this test fixture. + { srcDir: new URL('./_src/', import.meta.url) } +); + +function absolutePathToLang(path: string) { + return getAbsolutePathFromLang(path, { astroConfig, starlightConfig }); +} + +const processor = await createMarkdownProcessor({ + rehypePlugins: [ + ...starlightAutolinkHeadings({ + starlightConfig, + astroConfig: { experimental: { headingIdCompat: false } }, + useTranslations, + absolutePathToLang, + }), + ], +}); + +function renderMarkdown( + content: string, + options: { fileURL?: URL; processor?: MarkdownProcessor } = {} +) { + return (options.processor ?? processor).render( + content, + // @ts-expect-error fileURL is part of MarkdownProcessor's options + { fileURL: options.fileURL ?? new URL(`./_src/content/docs/index.md`, import.meta.url) } + ); +} + +test('generates anchor link markup', async () => { + const res = await renderMarkdown(` +## Some text +`); + await expect(res.code).toMatchFileSnapshot('./snapshots/generates-anchor-link-markup.html'); +}); + +test('generates an accessible link label', async () => { + const res = await renderMarkdown(` +## Some text +`); + expect(res.code).includes('<span class="sr-only">Section titled “Some text”</span>'); +}); + +test('strips HTML markup in accessible link label', async () => { + const res = await renderMarkdown(` +## Some _important nested \`HTML\`_ +`); + // Heading renders HTML + expect(res.code).includes('Some <em>important nested <code>HTML</code></em>'); + // Visually hidden label renders plain text + expect(res.code).includes( + '<span class="sr-only">Section titled “Some important nested HTML”</span>' + ); +}); + +test('localizes accessible label for the current language', async () => { + const res = await renderMarkdown( + ` +## Some text +`, + { fileURL: new URL('./_src/content/docs/fr/index.md', import.meta.url) } + ); + expect(res.code).includes('<span class="sr-only">Section intitulée « Some text »</span>'); +}); diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html new file mode 100644 index 00000000..7a3b3aae --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html @@ -0,0 +1 @@ +<div class="sl-heading-wrapper level-h2"><h2 id="some-text">Some text</h2><a class="sl-anchor-link" href="#some-text"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only">Section titled “Some text”</span></a></div>
\ No newline at end of file diff --git a/packages/starlight/components/AnchorHeading.astro b/packages/starlight/components/AnchorHeading.astro new file mode 100644 index 00000000..c8b10402 --- /dev/null +++ b/packages/starlight/components/AnchorHeading.astro @@ -0,0 +1,53 @@ +--- +import type { HTMLAttributes } from 'astro/types'; +import { transform } from 'ultrahtml'; +import sanitize from 'ultrahtml/transformers/sanitize'; +// These styles are included globally by default, but can be removed when `markdown.headingLinks` is +// set to `false`. We import them here to ensure they are included if the component is used with the +// global Markdown option disabled. +import '../style/anchor-links.css'; +import { AstroError } from 'astro/errors'; + +const headingLevels = [1, 2, 3, 4, 5, 6, '1', '2', '3', '4', '5', '6'] as const; +interface Props extends HTMLAttributes<'h1'> { + level: 1 | 2 | 3 | 4 | 5 | 6 | `${1 | 2 | 3 | 4 | 5 | 6}`; + id: string; +} + +const { level, id, ...attrs } = Astro.props; + +if (!id) { + throw new AstroError( + 'Missing `id` attribute passed to `<AnchorHeading>` component', + `The \`<AnchorHeading>\` component requires an \`id\` attribute, but received \`${typeof id === 'string' ? '""' : id}\`.` + ); +} +if (!headingLevels.includes(level)) { + throw new AstroError( + 'Invalid `level` attribute passed to `<AnchorHeading>` component', + `The \`<AnchorHeading>\` component expects a \`level\` attribute of \`1 | 2 | 3 | 4 | 5 | 6\`, but received \`${level}\`.` + ); +} + +const HeadingElement = `h${level}` as const; +const headingHTML = await Astro.slots.render('default'); +const headingString = await transform(headingHTML, [sanitize({ unblockElements: [] })]); +const accessibleLabel = Astro.locals.t('heading.anchorLabel', { + title: headingString, + interpolation: { escapeValue: false }, +}) +--- + +{/* The spacing in this component is a little awkward to ensure whitespace matches what the rehype plugin produces. */} +<div class={`sl-heading-wrapper level-h${level}`} + ><HeadingElement {id} {...attrs} set:html={headingHTML} /><a class="sl-anchor-link" href={`#${id}`} + ><span aria-hidden="true" class="sl-anchor-icon" + ><svg width="16" height="16" viewBox="0 0 24 24" + ><path + fill="currentcolor" + d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z" + ></path></svg + ></span + ><span class="sr-only" set:text={accessibleLabel} /></a + ></div +> diff --git a/packages/starlight/components/Page.astro b/packages/starlight/components/Page.astro index 0076e4b8..7edf4217 100644 --- a/packages/starlight/components/Page.astro +++ b/packages/starlight/components/Page.astro @@ -10,6 +10,7 @@ import '../style/props.css'; import '../style/reset.css'; import '../style/asides.css'; import '../style/util.css'; +import 'virtual:starlight/optional-css'; import Banner from 'virtual:starlight/components/Banner'; import ContentPanel from 'virtual:starlight/components/ContentPanel'; diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts index fb80b2a5..96cd63de 100644 --- a/packages/starlight/index.ts +++ b/packages/starlight/index.ts @@ -26,6 +26,7 @@ import { } from './utils/plugins'; import { processI18nConfig } from './utils/i18n'; import type { StarlightConfig } from './types'; +import { starlightAutolinkHeadings } from './integrations/heading-links'; export default function StarlightIntegration( userOpts: StarlightUserConfigWithPlugins @@ -127,7 +128,16 @@ export default function StarlightIntegration( absolutePathToLang, }), ], - rehypePlugins: [rehypeRtlCodeSupport()], + rehypePlugins: [ + rehypeRtlCodeSupport(), + // Process headings and add anchor links. + ...starlightAutolinkHeadings({ + starlightConfig, + astroConfig: config, + useTranslations, + absolutePathToLang, + }), + ], }, scopedStyleStrategy: 'where', // If not already configured, default to prefetching all links on hover. diff --git a/packages/starlight/integrations/heading-links.ts b/packages/starlight/integrations/heading-links.ts new file mode 100644 index 00000000..f5914486 --- /dev/null +++ b/packages/starlight/integrations/heading-links.ts @@ -0,0 +1,104 @@ +import { rehypeHeadingIds } from '@astrojs/markdown-remark'; +import type { AstroConfig, AstroUserConfig } from 'astro'; +import type { Nodes, Root } from 'hast'; +import { toString } from 'hast-util-to-string'; +import { h } from 'hastscript'; +import type { Transformer } from 'unified'; +import { SKIP, visit } from 'unist-util-visit'; +import type { HookParameters, StarlightConfig } from '../types'; + +const AnchorLinkIcon = h( + 'span', + { ariaHidden: 'true', class: 'sl-anchor-icon' }, + h( + 'svg', + { width: 16, height: 16, viewBox: '0 0 24 24' }, + h('path', { + fill: 'currentcolor', + d: 'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z', + }) + ) +); + +/** + * Add anchor links to headings. + */ +export default function rehypeAutolinkHeadings( + useTranslationsForLang: HookParameters<'config:setup'>['useTranslations'], + absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang'] +) { + const transformer: Transformer<Root> = (tree, file) => { + const pageLang = absolutePathToLang(file.path); + const t = useTranslationsForLang(pageLang); + + visit(tree, 'element', function (node, index, parent) { + if (!headingRank(node) || !node.properties.id || typeof index !== 'number' || !parent) { + return; + } + + const accessibleLabel = t('heading.anchorLabel', { + title: toString(node), + interpolation: { escapeValue: false }, + }); + + // Wrap the heading in a div and append the anchor link. + parent.children[index] = h( + 'div', + { class: `sl-heading-wrapper level-${node.tagName}` }, + // Heading + node, + // Anchor link + { + type: 'element', + tagName: 'a', + properties: { class: 'sl-anchor-link', href: '#' + node.properties.id }, + children: [AnchorLinkIcon, h('span', { class: 'sr-only' }, accessibleLabel)], + } + ); + + return SKIP; + }); + }; + + return function attacher() { + return transformer; + }; +} + +interface AutolinkHeadingsOptions { + starlightConfig: Pick<StarlightConfig, 'markdown'>; + astroConfig: { experimental: Pick<AstroConfig['experimental'], 'headingIdCompat'> }; + useTranslations: HookParameters<'config:setup'>['useTranslations']; + absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang']; +} +type RehypePlugins = NonNullable<NonNullable<AstroUserConfig['markdown']>['rehypePlugins']>; + +export const starlightAutolinkHeadings = ({ + starlightConfig, + astroConfig, + useTranslations, + absolutePathToLang, +}: AutolinkHeadingsOptions): RehypePlugins => + starlightConfig.markdown.headingLinks + ? [ + [ + rehypeHeadingIds, + { experimentalHeadingIdCompat: astroConfig.experimental?.headingIdCompat }, + ], + rehypeAutolinkHeadings(useTranslations, absolutePathToLang), + ] + : []; + +// This utility is inlined from https://github.com/syntax-tree/hast-util-heading-rank +// Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com> +// MIT License: https://github.com/syntax-tree/hast-util-heading-rank/blob/main/license +/** + * Get the rank (`1` to `6`) of headings (`h1` to `h6`). + * @param node Node to check. + * @returns Rank of the heading or `undefined` if not a heading. + */ +function headingRank(node: Nodes): number | undefined { + const name = node.type === 'element' ? node.tagName.toLowerCase() : ''; + const code = name.length === 2 && name.charCodeAt(0) === 104 /* `h` */ ? name.charCodeAt(1) : 0; + return code > 48 /* `0` */ && code < 55 /* `7` */ ? code - 48 /* `0` */ : undefined; +} diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts index 22c86ccc..47685cbf 100644 --- a/packages/starlight/integrations/virtual-user-config.ts +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -84,6 +84,15 @@ export function vitePluginStarlightUserConfig( : `import { makeAPI } from ${resolveLocalPath('../utils/gitInlined.ts')};` + `const api = makeAPI(${JSON.stringify(getAllNewestCommitDate(rootPath, docsPath))});`) + 'export const getNewestCommitDate = api.getNewestCommitDate;', + /** + * Module containing styles for features that can be toggled on or off such as heading anchor links. + */ + 'virtual:starlight/optional-css': opts.markdown.headingLinks + ? `import ${resolveLocalPath('../style/anchor-links.css')};` + : '', + /** + * Module containing imports of user-specified custom CSS files. + */ 'virtual:starlight/user-css': opts.customCss.map((id) => `import ${resolveId(id)};`).join(''), 'virtual:starlight/user-images': opts.logo ? 'src' in opts.logo diff --git a/packages/starlight/package.json b/packages/starlight/package.json index 2848606c..39544e83 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -109,6 +109,10 @@ "types": "./components/StarlightPage.astro.tsx", "import": "./components/StarlightPage.astro" }, + "./components/AnchorHeading.astro": { + "types": "./components/AnchorHeading.astro.tsx", + "import": "./components/AnchorHeading.astro" + }, "./components/Footer.astro": { "types": "./components/Footer.astro.tsx", "import": "./components/Footer.astro" @@ -179,10 +183,9 @@ "./style/markdown.css": "./style/markdown.css" }, "peerDependencies": { - "astro": "^5.1.5" + "astro": "^5.5.0" }, "devDependencies": { - "@astrojs/markdown-remark": "^6.3.1", "@playwright/test": "^1.45.0", "@types/node": "^18.16.19", "@vitest/coverage-v8": "^3.0.5", @@ -191,6 +194,7 @@ "vitest": "^3.0.5" }, "dependencies": { + "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", @@ -213,6 +217,7 @@ "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", + "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" diff --git a/packages/starlight/schemas/i18n.ts b/packages/starlight/schemas/i18n.ts index a4c7c8b2..62047d9e 100644 --- a/packages/starlight/schemas/i18n.ts +++ b/packages/starlight/schemas/i18n.ts @@ -158,6 +158,8 @@ function starlightI18nSchema() { .describe( 'Label for the “Built with Starlight” badge optionally displayed in the site footer.' ), + + 'heading.anchorLabel': z.string().describe('Label for anchor links in Markdown content.'), }) .partial(); } diff --git a/packages/starlight/style/anchor-links.css b/packages/starlight/style/anchor-links.css new file mode 100644 index 00000000..04749443 --- /dev/null +++ b/packages/starlight/style/anchor-links.css @@ -0,0 +1,125 @@ +/* +How does anchor link placement work? + +Because anchor links need to placed inline at the end of a heading, but are not children of the +heading element itself, positioning them to behave in a desirable way is a tiny bit tricky. Here’s +how we do it. + +1. We wrap the heading and anchor link in a div and make the heading element inline: + <div class="sl-heading-wrapper"> + <h2>...</h2><a class="sl-anchor-link">...</a> + </a> + +2. We need to avoid the anchor link wrapping onto a new line by itself like this because it looks + broken: + + Some heading text + ⛓ + +3. To achieve this we add an area of padding to the end of the heading and move the link over this + padding using negative margin: + + padding-inline-end creates space at the end of the line + ↓ + Some heading text[ ]⛓ + + margin-inline-start then pulls the anchor link into that space + ↓ + Some heading text[ ⛓ ] + + This ensures that when the anchor link wraps, the final word in the heading will wrap with it. + +*/ +@layer starlight.content { + /* ====================================================== + WRAPPER + ====================================================== */ + .sl-markdown-content .sl-heading-wrapper { + /* The size of the SVG icon. */ + --sl-anchor-icon-size: 0.8275em; + /* The horizontal space between the SVG icon and the end of the heading text. */ + --sl-anchor-icon-gap: 0.25em; + /* The end of line space required to accommodate the anchor link. */ + --sl-anchor-icon-space: calc(var(--sl-anchor-icon-size) + var(--sl-anchor-icon-gap)); + + line-height: var(--sl-line-height-headings); + } + + /* We need to apply the same rule we use for heading spacing to the parent wrapper. */ + .sl-markdown-content + :not(h1, h2, h3, h4, h5, h6, .sl-heading-wrapper) + + :is(.sl-heading-wrapper) { + margin-top: 1.5em; + } + + /* These font sizes are set in `markdown.css` for heading elements, but we need them one level higher on the wrapper. */ + .sl-markdown-content .sl-heading-wrapper.level-h1 { + font-size: var(--sl-text-h1); + } + .sl-markdown-content .sl-heading-wrapper.level-h2 { + font-size: var(--sl-text-h2); + } + .sl-markdown-content .sl-heading-wrapper.level-h3 { + font-size: var(--sl-text-h3); + } + .sl-markdown-content .sl-heading-wrapper.level-h4 { + font-size: var(--sl-text-h4); + } + .sl-markdown-content .sl-heading-wrapper.level-h5 { + font-size: var(--sl-text-h5); + } + .sl-markdown-content .sl-heading-wrapper.level-h6 { + font-size: var(--sl-text-h6); + } + + /* ====================================================== + HEADING + ====================================================== */ + .sl-markdown-content .sl-heading-wrapper > :first-child { + display: inline; + /* Apply end-of-line padding to the heading element. */ + padding-inline-end: var(--sl-anchor-icon-space); + } + + /* ====================================================== + LINK + ====================================================== */ + .sl-markdown-content .sl-anchor-link { + position: relative; + /* Move the anchor link over the heading element’s end-of-line padding. */ + margin-inline-start: calc(-1 * var(--sl-anchor-icon-size)); + } + + /* Increase clickable area for anchor links with a pseudo element that doesn’t impact layout. */ + .sl-markdown-content .sl-anchor-link::after { + content: ''; + position: absolute; + /* While most icon spacing is done with `em` to be relative to the heading font-size, increasing + the touch area is most important for smaller headings like h5/h6, so we use absolute units, + which have a diminishing impact at larger font-sizes. */ + inset: -0.25rem -0.5rem; + } + + /* Size and position the SVG icon inside the link. */ + .sl-markdown-content .sl-anchor-icon > svg { + display: inline; + width: var(--sl-anchor-icon-size); + /* Center the link icon SVG vertically in the line. */ + vertical-align: top; + transform: translateY( + calc((var(--sl-line-height-headings) * 1em - var(--sl-anchor-icon-size)) / 2) + ); + } + + /* On devices with hover capability, hide the anchor link icons and show only show them when focused + or when the heading is hovered. */ + @media (hover: hover) { + .sl-markdown-content .sl-anchor-link { + opacity: 0; + } + .sl-markdown-content .sl-anchor-link:focus, + .sl-markdown-content .sl-heading-wrapper:hover .sl-anchor-link { + opacity: 1; + } + } +} diff --git a/packages/starlight/translations/ar.json b/packages/starlight/translations/ar.json index 5e84413d..5d028817 100644 --- a/packages/starlight/translations/ar.json +++ b/packages/starlight/translations/ar.json @@ -25,5 +25,6 @@ "aside.caution": "تنبيه", "aside.danger": "تحذير", "fileTree.directory": "Directory", - "builtWithStarlight.label": "طوِّر بواسطة Starlight" + "builtWithStarlight.label": "طوِّر بواسطة Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/ca.json b/packages/starlight/translations/ca.json index 2b2d6f67..ebd3f2da 100644 --- a/packages/starlight/translations/ca.json +++ b/packages/starlight/translations/ca.json @@ -38,5 +38,6 @@ "pagefind.one_result": "[COUNT] resultat per a: [SEARCH_TERM]", "pagefind.alt_search": "Cap resultat per a [SEARCH_TERM]. Mostrant resultats per a: [DIFFERENT_TERM]", "pagefind.search_suggestion": "Cap resultat per a [SEARCH_TERM]. Prova alguna d’aquestes cerques:", - "pagefind.searching": "Cercant [SEARCH_TERM]..." + "pagefind.searching": "Cercant [SEARCH_TERM]...", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/cs.json b/packages/starlight/translations/cs.json index 7ec5014c..8b2e3210 100644 --- a/packages/starlight/translations/cs.json +++ b/packages/starlight/translations/cs.json @@ -38,5 +38,6 @@ "pagefind.one_result": "[COUNT] výsledek pro: [SEARCH_TERM]", "pagefind.alt_search": "Žádné výsledky pro [SEARCH_TERM]. Namísto toho zobrazuji výsledky pro: [DIFFERENT_TERM]", "pagefind.search_suggestion": "Žádný výsledek pro [SEARCH_TERM]. Zkus nějaké z těchto hledání:", - "pagefind.searching": "Hledám [SEARCH_TERM]..." + "pagefind.searching": "Hledám [SEARCH_TERM]...", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/da.json b/packages/starlight/translations/da.json index 7aeb4e48..6c8d9af1 100644 --- a/packages/starlight/translations/da.json +++ b/packages/starlight/translations/da.json @@ -25,5 +25,6 @@ "aside.caution": "Caution", "aside.danger": "Danger", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/de.json b/packages/starlight/translations/de.json index 55d60eab..83f57a27 100644 --- a/packages/starlight/translations/de.json +++ b/packages/starlight/translations/de.json @@ -25,5 +25,6 @@ "aside.caution": "Achtung", "aside.danger": "Gefahr", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Erstellt mit Starlight" + "builtWithStarlight.label": "Erstellt mit Starlight", + "heading.anchorLabel": "Abschnitt betitelt „{{title}}“" } diff --git a/packages/starlight/translations/en.json b/packages/starlight/translations/en.json index 186cbe1f..5e968f8c 100644 --- a/packages/starlight/translations/en.json +++ b/packages/starlight/translations/en.json @@ -25,5 +25,6 @@ "aside.caution": "Caution", "aside.danger": "Danger", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/es.json b/packages/starlight/translations/es.json index 0bc851f3..ee807a6a 100644 --- a/packages/starlight/translations/es.json +++ b/packages/starlight/translations/es.json @@ -38,5 +38,6 @@ "pagefind.one_result": "[COUNT] resultado para: [SEARCH_TERM]", "pagefind.alt_search": "Ningún resultado para [SEARCH_TERM]. Mostrando resultados para: [DIFFERENT_TERM]", "pagefind.search_suggestion": "Ningún resultado para [SEARCH_TERM]. Prueba alguna de estas búsquedas:", - "pagefind.searching": "Buscando [SEARCH_TERM]..." + "pagefind.searching": "Buscando [SEARCH_TERM]...", + "heading.anchorLabel": "Sección titulada «{{title}}»" } diff --git a/packages/starlight/translations/fa.json b/packages/starlight/translations/fa.json index 2ce584e8..22db252d 100644 --- a/packages/starlight/translations/fa.json +++ b/packages/starlight/translations/fa.json @@ -25,5 +25,6 @@ "aside.caution": "احتیاط", "aside.danger": "خطر", "fileTree.directory": "فهرست", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/fr.json b/packages/starlight/translations/fr.json index 3ab18641..25e6cea8 100644 --- a/packages/starlight/translations/fr.json +++ b/packages/starlight/translations/fr.json @@ -28,5 +28,6 @@ "expressiveCode.copyButtonTooltip": "Copier dans le presse-papiers", "expressiveCode.terminalWindowFallbackTitle": "Fenêtre de terminal", "fileTree.directory": "Répertoire", - "builtWithStarlight.label": "Créé avec Starlight" + "builtWithStarlight.label": "Créé avec Starlight", + "heading.anchorLabel": "Section intitulée « {{title}} »" } diff --git a/packages/starlight/translations/gl.json b/packages/starlight/translations/gl.json index c61d5990..13c1c861 100644 --- a/packages/starlight/translations/gl.json +++ b/packages/starlight/translations/gl.json @@ -25,5 +25,6 @@ "aside.caution": "Caution", "aside.danger": "Danger", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/he.json b/packages/starlight/translations/he.json index 936ef3be..07281034 100644 --- a/packages/starlight/translations/he.json +++ b/packages/starlight/translations/he.json @@ -25,5 +25,6 @@ "aside.caution": "Caution", "aside.danger": "Danger", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/hi.json b/packages/starlight/translations/hi.json index 7556b350..6cd11a0c 100644 --- a/packages/starlight/translations/hi.json +++ b/packages/starlight/translations/hi.json @@ -25,5 +25,6 @@ "aside.caution": "सावधानी", "aside.danger": "खतरा", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Starlight द्वारा निर्मित" + "builtWithStarlight.label": "Starlight द्वारा निर्मित", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/id.json b/packages/starlight/translations/id.json index 88af6f39..b42d3f3b 100644 --- a/packages/starlight/translations/id.json +++ b/packages/starlight/translations/id.json @@ -25,5 +25,6 @@ "aside.caution": "Perhatian", "aside.danger": "Bahaya", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/it.json b/packages/starlight/translations/it.json index 221c9bb1..65c32f15 100644 --- a/packages/starlight/translations/it.json +++ b/packages/starlight/translations/it.json @@ -25,5 +25,6 @@ "aside.caution": "Attenzione", "aside.danger": "Pericolo", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Sezione intitolata “{{title}}”" } diff --git a/packages/starlight/translations/ja.json b/packages/starlight/translations/ja.json index 17a502b8..2e1fd806 100644 --- a/packages/starlight/translations/ja.json +++ b/packages/starlight/translations/ja.json @@ -25,5 +25,6 @@ "aside.caution": "注意", "aside.danger": "危険", "fileTree.directory": "ディレクトリ", - "builtWithStarlight.label": "Starlightで作成" + "builtWithStarlight.label": "Starlightで作成", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/ko.json b/packages/starlight/translations/ko.json index 7298c8f0..b5ff6bbd 100644 --- a/packages/starlight/translations/ko.json +++ b/packages/starlight/translations/ko.json @@ -25,5 +25,6 @@ "aside.caution": "주의", "aside.danger": "위험", "fileTree.directory": "디렉터리", - "builtWithStarlight.label": "Starlight로 제작됨" + "builtWithStarlight.label": "Starlight로 제작됨", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/lv.json b/packages/starlight/translations/lv.json index 4b8481e3..43190fc9 100644 --- a/packages/starlight/translations/lv.json +++ b/packages/starlight/translations/lv.json @@ -25,5 +25,6 @@ "aside.caution": "Uzmanību", "aside.danger": "Bīstamība", "fileTree.directory": "Direktorija", - "builtWithStarlight.label": "Veidots ar Starlight" + "builtWithStarlight.label": "Veidots ar Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/nb.json b/packages/starlight/translations/nb.json index 14099cda..892fc435 100644 --- a/packages/starlight/translations/nb.json +++ b/packages/starlight/translations/nb.json @@ -25,5 +25,6 @@ "aside.caution": "Advarsel", "aside.danger": "Fare", "fileTree.directory": "Mappe", - "builtWithStarlight.label": "Laget med Starlight" + "builtWithStarlight.label": "Laget med Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/nl.json b/packages/starlight/translations/nl.json index 37393933..e2a6cbe7 100644 --- a/packages/starlight/translations/nl.json +++ b/packages/starlight/translations/nl.json @@ -25,5 +25,6 @@ "aside.caution": "Opgepast", "aside.danger": "Gevaar", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/pl.json b/packages/starlight/translations/pl.json index 157a9610..8a0c4759 100644 --- a/packages/starlight/translations/pl.json +++ b/packages/starlight/translations/pl.json @@ -28,5 +28,6 @@ "expressiveCode.copyButtonCopied": "Skopiowane!", "expressiveCode.copyButtonTooltip": "Skopiuj do schowka", "expressiveCode.terminalWindowFallbackTitle": "Okno terminala", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Dział zatytułowany „{{title}}”" } diff --git a/packages/starlight/translations/pt.json b/packages/starlight/translations/pt.json index 8ce20a92..25b57b2f 100644 --- a/packages/starlight/translations/pt.json +++ b/packages/starlight/translations/pt.json @@ -25,5 +25,6 @@ "aside.caution": "Cuidado", "aside.danger": "Perigo", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Feito com Starlight" + "builtWithStarlight.label": "Feito com Starlight", + "heading.anchorLabel": "Seção intitulada “{{title}}”" } diff --git a/packages/starlight/translations/ro.json b/packages/starlight/translations/ro.json index f9d4b334..e7083649 100644 --- a/packages/starlight/translations/ro.json +++ b/packages/starlight/translations/ro.json @@ -25,5 +25,6 @@ "aside.caution": "Atenție", "aside.danger": "Pericol", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/ru.json b/packages/starlight/translations/ru.json index 967a807f..1d97ae79 100644 --- a/packages/starlight/translations/ru.json +++ b/packages/starlight/translations/ru.json @@ -28,5 +28,6 @@ "expressiveCode.copyButtonCopied": "Скопировано!", "expressiveCode.copyButtonTooltip": "Копировать", "expressiveCode.terminalWindowFallbackTitle": "Окно терминала", - "builtWithStarlight.label": "Сделано с помощью Starlight" + "builtWithStarlight.label": "Сделано с помощью Starlight", + "heading.anchorLabel": "Заголовок раздела «{{title}}»" } diff --git a/packages/starlight/translations/sk.json b/packages/starlight/translations/sk.json index f1f9e008..a54c83fb 100644 --- a/packages/starlight/translations/sk.json +++ b/packages/starlight/translations/sk.json @@ -25,5 +25,6 @@ "aside.caution": "Upozornenie", "aside.danger": "Nebezpečenstvo", "fileTree.directory": "Adresár", - "builtWithStarlight.label": "Postavené so Starlight" + "builtWithStarlight.label": "Postavené so Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/sv.json b/packages/starlight/translations/sv.json index d6b09ea8..a834ea96 100644 --- a/packages/starlight/translations/sv.json +++ b/packages/starlight/translations/sv.json @@ -25,5 +25,6 @@ "aside.caution": "Caution", "aside.danger": "Danger", "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/tr.json b/packages/starlight/translations/tr.json index 9b645a35..9ea48573 100644 --- a/packages/starlight/translations/tr.json +++ b/packages/starlight/translations/tr.json @@ -25,5 +25,6 @@ "aside.caution": "Dikkat", "aside.danger": "Tehlike", "fileTree.directory": "Dizin", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/uk.json b/packages/starlight/translations/uk.json index eba7051d..5bcbd924 100644 --- a/packages/starlight/translations/uk.json +++ b/packages/starlight/translations/uk.json @@ -25,5 +25,6 @@ "aside.caution": "Обережно", "aside.danger": "Небезпечно", "fileTree.directory": "Каталог", - "builtWithStarlight.label": "Створено з Starlight" + "builtWithStarlight.label": "Створено з Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/vi.json b/packages/starlight/translations/vi.json index 3d544ac1..20de8148 100644 --- a/packages/starlight/translations/vi.json +++ b/packages/starlight/translations/vi.json @@ -25,5 +25,6 @@ "aside.caution": "Thận trọng", "aside.danger": "Nguy hiểm", "fileTree.directory": "Danh mục", - "builtWithStarlight.label": "Tạo với Starlight" + "builtWithStarlight.label": "Tạo với Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/zh-CN.json b/packages/starlight/translations/zh-CN.json index 90dd5c13..17c0f4e9 100644 --- a/packages/starlight/translations/zh-CN.json +++ b/packages/starlight/translations/zh-CN.json @@ -25,5 +25,6 @@ "aside.caution": "警告", "aside.danger": "危险", "fileTree.directory": "文件夹", - "builtWithStarlight.label": "基于 Starlight 构建" + "builtWithStarlight.label": "基于 Starlight 构建", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/translations/zh-TW.json b/packages/starlight/translations/zh-TW.json index e57658a6..001e1d77 100644 --- a/packages/starlight/translations/zh-TW.json +++ b/packages/starlight/translations/zh-TW.json @@ -25,5 +25,6 @@ "aside.caution": "警告", "aside.danger": "危險", "fileTree.directory": "目錄", - "builtWithStarlight.label": "Built with Starlight" + "builtWithStarlight.label": "Built with Starlight", + "heading.anchorLabel": "Section titled “{{title}}”" } diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index c1d40749..56955515 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -244,6 +244,20 @@ const UserConfigSchema = z.object({ } }) .describe('Add middleware to process Starlight’s route data for each page.'), + + /** Configure features that impact Starlight’s Markdown processing. */ + markdown: z + .object({ + /** Define whether headings in content should be rendered with clickable anchor links. Default: `true`. */ + headingLinks: z + .boolean() + .default(true) + .describe( + 'Define whether headings in content should be rendered with clickable anchor links. Default: `true`.' + ), + }) + .default({}) + .describe('Configure features that impact Starlight’s Markdown processing.'), }); export const StarlightConfigSchema = UserConfigSchema.strict() diff --git a/packages/starlight/virtual-internal.d.ts b/packages/starlight/virtual-internal.d.ts index 111760e9..3002b451 100644 --- a/packages/starlight/virtual-internal.d.ts +++ b/packages/starlight/virtual-internal.d.ts @@ -4,6 +4,8 @@ declare module 'virtual:starlight/git-info' { declare module 'virtual:starlight/user-css' {} +declare module 'virtual:starlight/optional-css' {} + declare module 'virtual:starlight/user-images' { type ImageMetadata = import('astro').ImageMetadata; export const logos: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1fd5525..671f572a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,9 @@ importers: packages/starlight: dependencies: + '@astrojs/markdown-remark': + specifier: ^6.3.1 + version: 6.3.1 '@astrojs/mdx': specifier: ^4.2.3 version: 4.2.4(astro@5.6.2(@types/node@18.16.19)(jiti@2.4.2)(lightningcss@1.29.3)(rollup@4.36.0)(tsx@4.15.2)(typescript@5.6.3)(yaml@2.6.1)) @@ -230,6 +233,9 @@ importers: remark-directive: specifier: ^3.0.0 version: 3.0.0 + ultrahtml: + specifier: ^1.6.0 + version: 1.6.0 unified: specifier: ^11.0.5 version: 11.0.5 @@ -240,9 +246,6 @@ importers: specifier: ^6.0.2 version: 6.0.3 devDependencies: - '@astrojs/markdown-remark': - specifier: ^6.3.1 - version: 6.3.1 '@playwright/test': specifier: ^1.45.0 version: 1.45.0 |