diff options
author | HiDeoo | 2025-07-16 13:17:26 +0200 |
---|---|---|
committer | GitHub | 2025-07-16 13:17:26 +0200 |
commit | 80ccff7c542794e04a4d2abb17227a076fa57c5d (patch) | |
tree | 412d018bc8961de8d9171bea4f49a5bff12d69fc | |
parent | 8d4d3bf80b474a14953e1768124df6b8d18f9355 (diff) | |
download | IT.starlight-80ccff7c542794e04a4d2abb17227a076fa57c5d.tar.gz IT.starlight-80ccff7c542794e04a4d2abb17227a076fa57c5d.tar.bz2 IT.starlight-80ccff7c542794e04a4d2abb17227a076fa57c5d.zip |
Restrict remark/rehype plugins usage (#3274)
Co-authored-by: Sgal Cheung <zhangsigao@live.com>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
13 files changed, 188 insertions, 22 deletions
diff --git a/.changeset/moody-donkeys-applaud.md b/.changeset/moody-donkeys-applaud.md new file mode 100644 index 00000000..b76c1ed5 --- /dev/null +++ b/.changeset/moody-donkeys-applaud.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': patch +--- + +Prevents Starlight remark and rehype plugins from transforming Markdown and MDX content when using the Astro [`renderMarkdown()`](https://docs.astro.build/en/reference/content-loader-reference/#rendermarkdown) content loader API. diff --git a/.changeset/unlucky-bananas-accept.md b/.changeset/unlucky-bananas-accept.md new file mode 100644 index 00000000..a13dfff5 --- /dev/null +++ b/.changeset/unlucky-bananas-accept.md @@ -0,0 +1,11 @@ +--- +'@astrojs/starlight': minor +--- + +Fixes an issue where some Starlight remark and rehype plugins were transforming Markdown and MDX content in non-Starlight pages. + +⚠️ **BREAKING CHANGE:** + +Previously, some of Starlight’s remark and rehype plugins, most notably the plugin transforming Starlight's custom Markdown syntax for [rendering asides](https://starlight.astro.build/guides/authoring-content/#asides), were applied to all Markdown and MDX content. This included content from [individual Markdown pages](https://docs.astro.build/en/guides/markdown-content/#individual-markdown-pages) and content from [content collections](https://docs.astro.build/en/guides/content-collections/) other than the `docs` collection used by Starlight. + +This change restricts the application of Starlight’s remark and rehype plugins to only Markdown and MDX content loaded using Starlight's [`docsLoader()`](https://starlight.astro.build/reference/configuration/#docsloader). If you were relying on this behavior, please let us know about your use case in the dedicated `#starlight` channel in the [Astro Discord](https://astro.build/chat/) or by [opening an issue](https://github.com/withastro/starlight/issues/new?template=---01-bug-report.yml). diff --git a/packages/starlight/__e2e__/components.test.ts b/packages/starlight/__e2e__/components.test.ts index 85010436..6f217b72 100644 --- a/packages/starlight/__e2e__/components.test.ts +++ b/packages/starlight/__e2e__/components.test.ts @@ -408,6 +408,41 @@ test.describe('anchor headings', () => { }); }); +test.describe('asides', () => { + test('does not render Markdown asides for individual Markdown pages and entries not part of the `docs` collection', async ({ + getProdServer, + page, + }) => { + const starlight = await getProdServer(); + + // Individual Markdown page + await starlight.goto('/markdown-page'); + await expect(page.locator('.starlight-aside')).not.toBeAttached(); + await page.pause(); + + // Content entry from the `reviews` content collection + await starlight.goto('/reviews/alice'); + await expect(page.locator('.starlight-aside')).not.toBeAttached(); + }); +}); + +test.describe('RTL support', () => { + test('does not add RTL support to code and preformatted text elements for individual Markdown pages and entries not part of the `docs` collection', async ({ + getProdServer, + page, + }) => { + const starlight = await getProdServer(); + + // Individual Markdown page + await starlight.goto('/markdown-page'); + await expect(page.locator('code[dir="auto"]')).not.toBeAttached(); + + // Content entry from the `reviews` content collection + await starlight.goto('/reviews/alice'); + await expect(page.locator('code[dir="auto"]')).not.toBeAttached(); + }); +}); + test.describe('head propagation', () => { /** * Due to a head propagation issue in development mode, dynamic routes alphabetically sorted diff --git a/packages/starlight/__e2e__/fixtures/basics/src/content/reviews/alice.mdx b/packages/starlight/__e2e__/fixtures/basics/src/content/reviews/alice.mdx index 8230272c..938357bf 100644 --- a/packages/starlight/__e2e__/fixtures/basics/src/content/reviews/alice.mdx +++ b/packages/starlight/__e2e__/fixtures/basics/src/content/reviews/alice.mdx @@ -9,4 +9,10 @@ This is a review from Alice. ## Description This content collection entry is not part of the Starlight `docs` collection. -It is used to test that anchor links for headings are not generated for non-docs collection entries. +It is used to test that various remark/rehype plugins are not transforming non-docs collection entries. + +:::note +This is a note using Starlight Markdown aside syntax. +::: + +This is an `inline code` example. diff --git a/packages/starlight/__e2e__/fixtures/basics/src/pages/markdown-page.md b/packages/starlight/__e2e__/fixtures/basics/src/pages/markdown-page.md index d3b8d9cf..085dd68c 100644 --- a/packages/starlight/__e2e__/fixtures/basics/src/pages/markdown-page.md +++ b/packages/starlight/__e2e__/fixtures/basics/src/pages/markdown-page.md @@ -7,4 +7,10 @@ title: Individual Markdown Page ## Description This page is an [individual Markdown page](https://docs.astro.build/en/guides/markdown-content/#individual-markdown-pages). -It is used to test that anchor links for headings are not generated for individual Markdown pages. +It is used to test that various remark/rehype plugins are not transforming individual Markdown pages. + +:::note +This is a note using Starlight Markdown aside syntax. +::: + +This is an `inline code` example. diff --git a/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts b/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts index 90b86f36..af5e92de 100644 --- a/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts +++ b/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts @@ -86,3 +86,16 @@ test('localizes accessible label for the current language', async () => { ); expect(res.code).includes('<span class="sr-only">Section intitulée « Some text »</span>'); }); + +test('does not generate anchor links for documents without a file path', async () => { + const res = await processor.render( + ` +## Some text +`, + // Rendering Markdown content using the content loader `renderMarkdown()` API does not provide + // a `fileURL` option. + {} + ); + + expect(res.code).not.includes('Section titled'); +}); diff --git a/packages/starlight/__tests__/remark-rehype/asides.test.ts b/packages/starlight/__tests__/remark-rehype/asides.test.ts index e1486a74..35ad73fb 100644 --- a/packages/starlight/__tests__/remark-rehype/asides.test.ts +++ b/packages/starlight/__tests__/remark-rehype/asides.test.ts @@ -334,3 +334,19 @@ test('does not transform back directive nodes with data', async () => { `"<p>This method is available in the <span class="api">thing</span> API.</p>"` ); }); + +test('does not generate asides for documents without a file path', async () => { + const res = await processor.render( + ` +:::note +Some text +::: +`, + // Rendering Markdown content using the content loader `renderMarkdown()` API does not provide + // a `fileURL` option. + {} + ); + + expect(res.code).not.includes(`aside`); + expect(res.code).not.includes(`</svg>Note</p>`); +}); diff --git a/packages/starlight/__tests__/remark-rehype/code-rtl-support.test.ts b/packages/starlight/__tests__/remark-rehype/code-rtl-support.test.ts index 89435816..b96a7347 100644 --- a/packages/starlight/__tests__/remark-rehype/code-rtl-support.test.ts +++ b/packages/starlight/__tests__/remark-rehype/code-rtl-support.test.ts @@ -1,12 +1,29 @@ import { rehype } from 'rehype'; +import { VFile } from 'vfile'; import { expect, test } from 'vitest'; import { rehypeRtlCodeSupport } from '../../integrations/code-rtl-support'; -const processor = rehype().data('settings', { fragment: true }).use(rehypeRtlCodeSupport()); +const astroConfig = { + root: new URL(import.meta.url), + srcDir: new URL('./_src/', import.meta.url), +}; + +const processor = rehype() + .data('settings', { fragment: true }) + .use(rehypeRtlCodeSupport({ astroConfig })); + +function renderMarkdown(content: string, options: { fileURL?: URL } = {}) { + return processor.process( + new VFile({ + path: options.fileURL ?? new URL(`./_src/content/docs/index.md`, import.meta.url), + value: content, + }) + ); +} test('applies `dir="auto"` to inline code', async () => { const input = `<p>Some text with <code>inline code</code>.</p>`; - const output = String(await processor.process(input)); + const output = String(await renderMarkdown(input)); expect(output).not.toEqual(input); expect(output).includes('dir="auto"'); expect(output).toMatchInlineSnapshot( @@ -16,10 +33,24 @@ test('applies `dir="auto"` to inline code', async () => { test('applies `dir="ltr"` to code blocks', async () => { const input = `<p>Some text in a paragraph:</p><pre><code>console.log('test')</code></pre>`; - const output = String(await processor.process(input)); + const output = String(await renderMarkdown(input)); expect(output).not.toEqual(input); expect(output).includes('dir="ltr"'); expect(output).toMatchInlineSnapshot( `"<p>Some text in a paragraph:</p><pre dir="ltr"><code>console.log('test')</code></pre>"` ); }); + +test('does not transform documents without a file path', async () => { + const input = `<p>Some text with <code>inline code</code>.</p>`; + const output = String( + await processor.process( + new VFile({ + // Rendering Markdown content using the content loader `renderMarkdown()` API does not + // provide a `path` option. + value: input, + }) + ) + ); + expect(output).toEqual(input); +}); diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts index 96cd63de..ea6fd8e1 100644 --- a/packages/starlight/index.ts +++ b/packages/starlight/index.ts @@ -129,7 +129,7 @@ export default function StarlightIntegration( }), ], rehypePlugins: [ - rehypeRtlCodeSupport(), + rehypeRtlCodeSupport({ astroConfig: config }), // Process headings and add anchor links. ...starlightAutolinkHeadings({ starlightConfig, diff --git a/packages/starlight/integrations/asides.ts b/packages/starlight/integrations/asides.ts index 939cfe7c..85334d70 100644 --- a/packages/starlight/integrations/asides.ts +++ b/packages/starlight/integrations/asides.ts @@ -15,6 +15,7 @@ import remarkDirective from 'remark-directive'; import type { Plugin, Transformer } from 'unified'; import { visit } from 'unist-util-visit'; import type { HookParameters, StarlightConfig } from '../types'; +import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils'; interface AsidesOptions { starlightConfig: Pick<StarlightConfig, 'defaultLocale' | 'locales'>; @@ -148,7 +149,11 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> { ], }; + const docsCollectionPath = getRemarkRehypeDocsCollectionPath(options.astroConfig.srcDir); + const transformer: Transformer<Root> = (tree, file) => { + if (!shouldTransformFile(file, docsCollectionPath)) return; + const lang = options.absolutePathToLang(file.path); const t = options.useTranslations(lang); visit(tree, (node, index, parent) => { diff --git a/packages/starlight/integrations/code-rtl-support.ts b/packages/starlight/integrations/code-rtl-support.ts index 781b7afc..be7c9e8d 100644 --- a/packages/starlight/integrations/code-rtl-support.ts +++ b/packages/starlight/integrations/code-rtl-support.ts @@ -1,5 +1,12 @@ +import type { AstroConfig } from 'astro'; import type { Root } from 'hast'; import { CONTINUE, SKIP, visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; +import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils'; + +interface RtlCodeSupportOptions { + astroConfig: Pick<AstroConfig, 'srcDir'>; +} /** * rehype plugin that adds `dir` attributes to `<code>` and `<pre>` @@ -15,8 +22,12 @@ import { CONTINUE, SKIP, visit } from 'unist-util-visit'; * - `<code>` is often LTR, but could also be RTL. `dir="auto"` ensures the bidirectional * algorithm treats the contents of `<code>` in isolation and gives its best guess. */ -export function rehypeRtlCodeSupport() { - return () => (root: Root) => { +export function rehypeRtlCodeSupport({ astroConfig }: RtlCodeSupportOptions) { + const docsCollectionPath = getRemarkRehypeDocsCollectionPath(astroConfig.srcDir); + + return () => (root: Root, file: VFile) => { + if (!shouldTransformFile(file, docsCollectionPath)) return; + visit(root, 'element', (el) => { if (el.tagName === 'pre' || el.tagName === 'code') { el.properties ||= {}; diff --git a/packages/starlight/integrations/heading-links.ts b/packages/starlight/integrations/heading-links.ts index ebb61d4a..9e5086fe 100644 --- a/packages/starlight/integrations/heading-links.ts +++ b/packages/starlight/integrations/heading-links.ts @@ -6,7 +6,7 @@ import { h } from 'hastscript'; import type { Transformer } from 'unified'; import { SKIP, visit } from 'unist-util-visit'; import type { HookParameters, StarlightConfig } from '../types'; -import { resolveCollectionPath } from '../utils/collection'; +import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils'; const AnchorLinkIcon = h( 'span', @@ -30,8 +30,7 @@ export default function rehypeAutolinkHeadings( absolutePathToLang: AutolinkHeadingsOptions['absolutePathToLang'] ) { const transformer: Transformer<Root> = (tree, file) => { - // If the document is not part of the Starlight docs collection, skip it. - if (!normalizePath(file.path).startsWith(docsCollectionPath)) return; + if (!shouldTransformFile(file, docsCollectionPath)) return; const pageLang = absolutePathToLang(file.path); const t = useTranslationsForLang(pageLang); @@ -93,23 +92,13 @@ export const starlightAutolinkHeadings = ({ { experimentalHeadingIdCompat: astroConfig.experimental?.headingIdCompat }, ], rehypeAutolinkHeadings( - normalizePath(resolveCollectionPath('docs', astroConfig.srcDir)), + getRemarkRehypeDocsCollectionPath(astroConfig.srcDir), useTranslations, absolutePathToLang ), ] : []; -/** - * File path separators seems to be inconsistent on Windows when the rehype plugin is used on - * Markdown vs MDX files. - * For the time being, we normalize the path to unix style path. - */ -const backSlashRegex = /\\/g; -function normalizePath(path: string) { - return path.replace(backSlashRegex, '/'); -} - // 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 diff --git a/packages/starlight/integrations/remark-rehype-utils.ts b/packages/starlight/integrations/remark-rehype-utils.ts new file mode 100644 index 00000000..97ff09b0 --- /dev/null +++ b/packages/starlight/integrations/remark-rehype-utils.ts @@ -0,0 +1,38 @@ +import type { AstroConfig } from 'astro'; +import type { VFile } from 'vfile'; +import { resolveCollectionPath } from '../utils/collection'; + +/** + * Returns the path to the Starlight docs collection ready to be used in remark/rehype plugins, + * e.g. with the `shouldTransformFile()` utility to determine if a file should be transformed + * by a plugin or not. + */ +export function getRemarkRehypeDocsCollectionPath(srcDir: AstroConfig['srcDir']) { + return normalizePath(resolveCollectionPath('docs', srcDir)); +} + +/** + * 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 Starlight docs collection should be skipped. + */ +export function shouldTransformFile(file: VFile, docsCollectionPath: 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; + + // If the document is not part of the Starlight docs collection, skip it. + if (!normalizePath(file.path).startsWith(docsCollectionPath)) return false; + + return true; +} + +/** + * 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, '/'); +} |