From 80ccff7c542794e04a4d2abb17227a076fa57c5d Mon Sep 17 00:00:00 2001 From: HiDeoo Date: Wed, 16 Jul 2025 13:17:26 +0200 Subject: Restrict remark/rehype plugins usage (#3274) Co-authored-by: Sgal Cheung Co-authored-by: Chris Swithinbank --- .changeset/moody-donkeys-applaud.md | 5 +++ .changeset/unlucky-bananas-accept.md | 11 +++++++ packages/starlight/__e2e__/components.test.ts | 35 ++++++++++++++++++++ .../fixtures/basics/src/content/reviews/alice.mdx | 8 ++++- .../fixtures/basics/src/pages/markdown-page.md | 8 ++++- .../__tests__/remark-rehype/anchor-links.test.ts | 13 ++++++++ .../__tests__/remark-rehype/asides.test.ts | 16 +++++++++ .../remark-rehype/code-rtl-support.test.ts | 37 +++++++++++++++++++-- packages/starlight/index.ts | 2 +- packages/starlight/integrations/asides.ts | 5 +++ .../starlight/integrations/code-rtl-support.ts | 15 +++++++-- packages/starlight/integrations/heading-links.ts | 17 ++-------- .../starlight/integrations/remark-rehype-utils.ts | 38 ++++++++++++++++++++++ 13 files changed, 188 insertions(+), 22 deletions(-) create mode 100644 .changeset/moody-donkeys-applaud.md create mode 100644 .changeset/unlucky-bananas-accept.md create mode 100644 packages/starlight/integrations/remark-rehype-utils.ts 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('Section intitulée « Some text »'); }); + +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 () => { `"

This method is available in the thing API.

"` ); }); + +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(`Note

`); +}); 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 = `

Some text with inline code.

`; - 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 = `

Some text in a paragraph:

console.log('test')
`; - 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( `"

Some text in a paragraph:

console.log('test')
"` ); }); + +test('does not transform documents without a file path', async () => { + const input = `

Some text with inline code.

`; + 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; @@ -148,7 +149,11 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> { ], }; + const docsCollectionPath = getRemarkRehypeDocsCollectionPath(options.astroConfig.srcDir); + const transformer: Transformer = (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; +} /** * rehype plugin that adds `dir` attributes to `` and `
`
@@ -15,8 +22,12 @@ import { CONTINUE, SKIP, visit } from 'unist-util-visit';
  * - `` is often LTR, but could also be RTL. `dir="auto"` ensures the bidirectional
  *   algorithm treats the contents of `` 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 = (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 
 // 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, '/');
+}
-- 
cgit