From e6846b625ea7475fc7da4ec7f4e4961a46263b08 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Tue, 13 Feb 2024 17:52:05 +0100 Subject: [PATCH 01/10] =?UTF-8?q?fix(markdown):=20don=E2=80=99t=20generate?= =?UTF-8?q?=20mdast=20html=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `html` nodes from mdast are converted to `raw` hast nodes. These nodes are then not processed by proper rehype plugins. Typically if a remark plugin generates `html` nodes, this indicates it should have actually been a rehype plugin. This changes the remark plugins that generate `html` nodes into rehype nodes. These were `remarkPrism` and `remarkShiki`. Closes #9909 --- .changeset/thirty-beds-smoke.md | 6 ++ packages/integrations/mdx/src/plugins.ts | 44 ++++++------- packages/markdown/remark/package.json | 4 ++ packages/markdown/remark/src/highlight.ts | 65 ++++++++++++++++++++ packages/markdown/remark/src/index.ts | 24 ++++---- packages/markdown/remark/src/rehype-prism.ts | 12 ++++ packages/markdown/remark/src/rehype-shiki.ts | 16 +++++ packages/markdown/remark/src/remark-prism.ts | 19 ------ packages/markdown/remark/src/remark-shiki.ts | 21 ------- pnpm-lock.yaml | 14 ++++- 10 files changed, 146 insertions(+), 79 deletions(-) create mode 100644 .changeset/thirty-beds-smoke.md create mode 100644 packages/markdown/remark/src/highlight.ts create mode 100644 packages/markdown/remark/src/rehype-prism.ts create mode 100644 packages/markdown/remark/src/rehype-shiki.ts delete mode 100644 packages/markdown/remark/src/remark-prism.ts delete mode 100644 packages/markdown/remark/src/remark-shiki.ts diff --git a/.changeset/thirty-beds-smoke.md b/.changeset/thirty-beds-smoke.md new file mode 100644 index 000000000000..7b309618eea9 --- /dev/null +++ b/.changeset/thirty-beds-smoke.md @@ -0,0 +1,6 @@ +--- +"@astrojs/mdx": patch +"@astrojs/markdown-remark": patch +--- + +Handle syntax highlighting using rehype plugins instead of remark plugins. diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index d52f303019a3..2804613b5894 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -1,8 +1,8 @@ import { rehypeHeadingIds, + rehypePrism, + rehypeShiki, remarkCollectImages, - remarkPrism, - remarkShiki, } from '@astrojs/markdown-remark'; import { createProcessor, nodeTypes } from '@mdx-js/mdx'; import rehypeRaw from 'rehype-raw'; @@ -54,22 +54,7 @@ function getRemarkPlugins(mdxOptions: MdxOptions): PluggableList { } } - remarkPlugins = [ - ...remarkPlugins, - ...mdxOptions.remarkPlugins, - remarkCollectImages, - remarkImageToComponent, - ]; - - if (!isPerformanceBenchmark) { - // Apply syntax highlighters after user plugins to match `markdown/remark` behavior - if (mdxOptions.syntaxHighlight === 'shiki') { - remarkPlugins.push([remarkShiki, mdxOptions.shikiConfig]); - } - if (mdxOptions.syntaxHighlight === 'prism') { - remarkPlugins.push(remarkPrism); - } - } + remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages, remarkImageToComponent); return remarkPlugins; } @@ -79,18 +64,25 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { // ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters rehypeMetaString, // rehypeRaw allows custom syntax highlighters to work without added config - [rehypeRaw, { passThrough: nodeTypes }] as any, + [rehypeRaw, { passThrough: nodeTypes }], + ...mdxOptions.rehypePlugins, ]; - rehypePlugins = [ - ...rehypePlugins, - ...mdxOptions.rehypePlugins, + if (!isPerformanceBenchmark) { // getHeadings() is guaranteed by TS, so this must be included. // We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins. - ...(isPerformanceBenchmark ? [] : [rehypeHeadingIds, rehypeInjectHeadingsExport]), - // computed from `astro.data.frontmatter` in VFile data - rehypeApplyFrontmatterExport, - ]; + rehypePlugins.push(rehypeHeadingIds, rehypeInjectHeadingsExport); + + // Apply syntax highlighters after user plugins to match `markdown/remark` behavior + if (mdxOptions.syntaxHighlight === 'shiki') { + rehypePlugins.push([rehypeShiki, mdxOptions.shikiConfig]); + } else if (mdxOptions.syntaxHighlight === 'prism') { + rehypePlugins.push(rehypePrism); + } + } + + // computed from `astro.data.frontmatter` in VFile data + rehypePlugins.push(rehypeApplyFrontmatterExport); if (mdxOptions.optimize) { // Convert user `optimize` option to compatible `rehypeOptimizeStatic` option diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json index 965b057cde96..c17c2868a9a8 100644 --- a/packages/markdown/remark/package.json +++ b/packages/markdown/remark/package.json @@ -36,6 +36,8 @@ "dependencies": { "@astrojs/prism": "^3.0.0", "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.0", + "hast-util-to-text": "^4.0.0", "import-meta-resolve": "^4.0.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", @@ -46,7 +48,9 @@ "remark-smartypants": "^2.0.0", "shikiji": "^0.9.19", "unified": "^11.0.4", + "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.1" }, "devDependencies": { diff --git a/packages/markdown/remark/src/highlight.ts b/packages/markdown/remark/src/highlight.ts new file mode 100644 index 000000000000..296d828e2032 --- /dev/null +++ b/packages/markdown/remark/src/highlight.ts @@ -0,0 +1,65 @@ +import type { Element, Root } from 'hast'; +import { fromHtml } from 'hast-util-from-html'; +import { toText } from 'hast-util-to-text'; +import { removePosition } from 'unist-util-remove-position'; +import { visitParents } from 'unist-util-visit-parents'; + +type Highlighter = (code: string, language: string) => string; + +const languagePattern = /\blanguage-(\S+)\b/; + +/** + * A hast utility to syntax highlight code blocks with a given syntax highlighter. + * + * @param tree + * The hast tree in which to syntax highlight code blocks. + * @param highlighter + * A fnction which receives the code and language, and returns the HTML of a syntax + * highlighted `
` element.
+ */
+export function highlightCodeBlocks(tree: Root, highlighter: Highlighter) {
+	// We’re looking for `` elements
+	visitParents(tree, { type: 'element', tagName: 'code' }, (node, ancestors) => {
+		const parent = ancestors.at(-1);
+
+		// Whose parent is a `
`.
+		if (parent?.type !== 'element' || parent.tagName !== 'pre') {
+			return;
+		}
+
+		// Where the `` is the only child.
+		if (parent.children.length !== 1) {
+			return;
+		}
+
+		// And the `` has a class name that starts with `language-`.
+		let languageMatch: RegExpMatchArray | null | undefined;
+		let { className } = node.properties;
+		if (typeof className === 'string') {
+			languageMatch = className.match(languagePattern);
+		} else if (Array.isArray(className)) {
+			for (const cls of className) {
+				if (typeof cls !== 'string') {
+					continue;
+				}
+
+				languageMatch = cls.match(languagePattern);
+				if (languageMatch) {
+					break;
+				}
+			}
+		}
+
+		const code = toText(node, { whitespace: 'pre' });
+		const html = highlighter(code, languageMatch?.[1] || 'plaintext');
+		// The replacement returns a root node with 1 child, the `` element replacement.
+		const replacement = fromHtml(html, { fragment: true }).children[0] as Element;
+		// We just generated this node, so any positional information is invalid.
+		removePosition(replacement);
+
+		// We replace the parent in its parent with the new `
` element.
+		const grandParent = ancestors.at(-2)!;
+		const index = grandParent.children.indexOf(parent);
+		grandParent.children[index] = replacement;
+	});
+}
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
index 7881614e5dbd..b61aa14c9eeb 100644
--- a/packages/markdown/remark/src/index.ts
+++ b/packages/markdown/remark/src/index.ts
@@ -7,9 +7,9 @@ import {
 } from './frontmatter-injection.js';
 import { loadPlugins } from './load-plugins.js';
 import { rehypeHeadingIds } from './rehype-collect-headings.js';
+import { rehypePrism } from './rehype-prism.js';
+import { rehypeShiki } from './rehype-shiki.js';
 import { remarkCollectImages } from './remark-collect-images.js';
-import { remarkPrism } from './remark-prism.js';
-import { remarkShiki } from './remark-shiki.js';
 
 import rehypeRaw from 'rehype-raw';
 import rehypeStringify from 'rehype-stringify';
@@ -24,8 +24,8 @@ import { rehypeImages } from './rehype-images.js';
 export { InvalidAstroDataError, setVfileFrontmatter } from './frontmatter-injection.js';
 export { rehypeHeadingIds } from './rehype-collect-headings.js';
 export { remarkCollectImages } from './remark-collect-images.js';
-export { remarkPrism } from './remark-prism.js';
-export { remarkShiki } from './remark-shiki.js';
+export { rehypePrism } from './rehype-prism.js';
+export { rehypeShiki } from './rehype-shiki.js';
 export { createShikiHighlighter, replaceCssVariables, type ShikiHighlighter } from './shiki.js';
 export * from './types.js';
 
@@ -85,13 +85,6 @@ export async function createMarkdownProcessor(
 	}
 
 	if (!isPerformanceBenchmark) {
-		// Syntax highlighting
-		if (syntaxHighlight === 'shiki') {
-			parser.use(remarkShiki, shikiConfig);
-		} else if (syntaxHighlight === 'prism') {
-			parser.use(remarkPrism);
-		}
-
 		// Apply later in case user plugins resolve relative image paths
 		parser.use(remarkCollectImages);
 	}
@@ -103,6 +96,15 @@ export async function createMarkdownProcessor(
 		...remarkRehypeOptions,
 	});
 
+	if (!isPerformanceBenchmark) {
+		// Syntax highlighting
+		if (syntaxHighlight === 'shiki') {
+			parser.use(rehypeShiki, shikiConfig);
+		} else if (syntaxHighlight === 'prism') {
+			parser.use(rehypePrism);
+		}
+	}
+
 	// User rehype plugins
 	for (const [plugin, pluginOpts] of loadedRehypePlugins) {
 		parser.use(plugin, pluginOpts);
diff --git a/packages/markdown/remark/src/rehype-prism.ts b/packages/markdown/remark/src/rehype-prism.ts
new file mode 100644
index 000000000000..4305a067677f
--- /dev/null
+++ b/packages/markdown/remark/src/rehype-prism.ts
@@ -0,0 +1,12 @@
+import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter';
+import type { Root } from 'hast';
+import type { Plugin } from 'unified';
+import { highlightCodeBlocks } from './highlight.js';
+
+export const rehypePrism: Plugin<[], Root> = () => (tree) => {
+	highlightCodeBlocks(tree, (code, language) => {
+		let { html, classLanguage } = runHighlighterWithAstro(language, code);
+
+		return `
${html}
`; + }); +}; diff --git a/packages/markdown/remark/src/rehype-shiki.ts b/packages/markdown/remark/src/rehype-shiki.ts new file mode 100644 index 000000000000..137cb81411cc --- /dev/null +++ b/packages/markdown/remark/src/rehype-shiki.ts @@ -0,0 +1,16 @@ +import type { Root } from 'hast'; +import type { Plugin } from 'unified'; +import { createShikiHighlighter, type ShikiHighlighter } from './shiki.js'; +import type { ShikiConfig } from './types.js'; +import { highlightCodeBlocks } from './highlight.js'; + +export const rehypeShiki: Plugin<[ShikiConfig?], Root> = (config) => { + let highlighterAsync: Promise | undefined; + + return async (tree) => { + highlighterAsync ??= createShikiHighlighter(config); + const highlighter = await highlighterAsync; + + highlightCodeBlocks(tree, highlighter.highlight); + }; +}; diff --git a/packages/markdown/remark/src/remark-prism.ts b/packages/markdown/remark/src/remark-prism.ts deleted file mode 100644 index a3f476d6e4ab..000000000000 --- a/packages/markdown/remark/src/remark-prism.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter'; -import { visit } from 'unist-util-visit'; -import type { RemarkPlugin } from './types.js'; - -export function remarkPrism(): ReturnType { - return function (tree: any) { - visit(tree, 'code', (node) => { - let { lang, value } = node; - node.type = 'html'; - - let { html, classLanguage } = runHighlighterWithAstro(lang, value); - let classes = [classLanguage]; - node.value = `
${html}
`; - return node; - }); - }; -} diff --git a/packages/markdown/remark/src/remark-shiki.ts b/packages/markdown/remark/src/remark-shiki.ts deleted file mode 100644 index ebf70fbec9b6..000000000000 --- a/packages/markdown/remark/src/remark-shiki.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { visit } from 'unist-util-visit'; -import { createShikiHighlighter, type ShikiHighlighter } from './shiki.js'; -import type { RemarkPlugin, ShikiConfig } from './types.js'; - -export function remarkShiki(config?: ShikiConfig): ReturnType { - let highlighterAsync: Promise | undefined; - - return async (tree: any) => { - highlighterAsync ??= createShikiHighlighter(config); - const highlighter = await highlighterAsync; - - visit(tree, 'code', (node) => { - const lang = typeof node.lang === 'string' ? node.lang : 'plaintext'; - const html = highlighter.highlight(node.value, lang); - - node.type = 'html'; - node.value = html; - node.children = []; - }); - }; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3216e1f2a073..94633dcd1e21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5040,6 +5040,12 @@ importers: github-slugger: specifier: ^2.0.0 version: 2.0.0 + hast-util-from-html: + specifier: ^2.0.0 + version: 2.0.1 + hast-util-to-text: + specifier: ^4.0.0 + version: 4.0.0 import-meta-resolve: specifier: ^4.0.0 version: 4.0.0 @@ -5070,9 +5076,15 @@ importers: unified: specifier: ^11.0.4 version: 11.0.4 + unist-util-remove-position: + specifier: ^5.0.0 + version: 5.0.0 unist-util-visit: specifier: ^5.0.0 version: 5.0.0 + unist-util-visit-parents: + specifier: ^6.0.0 + version: 6.0.1 vfile: specifier: ^6.0.1 version: 6.0.1 @@ -10620,7 +10632,6 @@ packages: '@types/unist': 3.0.2 hast-util-is-element: 3.0.0 unist-util-find-after: 5.0.0 - dev: true /hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -15427,7 +15438,6 @@ packages: dependencies: '@types/unist': 3.0.2 unist-util-is: 6.0.0 - dev: true /unist-util-is@3.0.0: resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} From ed7907471c6e6ea3978eccb8bb5ab5560554ee5d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Feb 2024 11:43:10 +0000 Subject: [PATCH 02/10] Apply suggestions from code review --- .changeset/thirty-beds-smoke.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/thirty-beds-smoke.md b/.changeset/thirty-beds-smoke.md index 7b309618eea9..b75b96386ca7 100644 --- a/.changeset/thirty-beds-smoke.md +++ b/.changeset/thirty-beds-smoke.md @@ -1,6 +1,6 @@ --- -"@astrojs/mdx": patch -"@astrojs/markdown-remark": patch +"@astrojs/mdx": minor +"@astrojs/markdown-remark": minor --- Handle syntax highlighting using rehype plugins instead of remark plugins. From c3302f9c0e736efb4bdc8dee649070a1f6213003 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Thu, 22 Feb 2024 09:39:18 +0100 Subject: [PATCH 03/10] refactor(mdx): move user defined rehype plugins after syntax highlighting --- packages/integrations/mdx/src/plugins.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index 2804613b5894..3cac43711815 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -65,7 +65,6 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { rehypeMetaString, // rehypeRaw allows custom syntax highlighters to work without added config [rehypeRaw, { passThrough: nodeTypes }], - ...mdxOptions.rehypePlugins, ]; if (!isPerformanceBenchmark) { @@ -82,7 +81,7 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { } // computed from `astro.data.frontmatter` in VFile data - rehypePlugins.push(rehypeApplyFrontmatterExport); + rehypePlugins.push(...mdxOptions.rehypePlugins, rehypeApplyFrontmatterExport); if (mdxOptions.optimize) { // Convert user `optimize` option to compatible `rehypeOptimizeStatic` option From 5ab25021fc12b1e9ff7236f3ccba726d18fdcccf Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Thu, 22 Feb 2024 10:18:41 +0100 Subject: [PATCH 04/10] fix(mdx): fix issue in mdx rehype plugin ordering --- packages/integrations/mdx/src/plugins.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index 3cac43711815..b370e5bfda31 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -65,13 +65,19 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { rehypeMetaString, // rehypeRaw allows custom syntax highlighters to work without added config [rehypeRaw, { passThrough: nodeTypes }], + ...mdxOptions.rehypePlugins, ]; if (!isPerformanceBenchmark) { // getHeadings() is guaranteed by TS, so this must be included. // We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins. rehypePlugins.push(rehypeHeadingIds, rehypeInjectHeadingsExport); + } + + // computed from `astro.data.frontmatter` in VFile data + rehypePlugins.push(rehypeApplyFrontmatterExport); + if (!isPerformanceBenchmark) { // Apply syntax highlighters after user plugins to match `markdown/remark` behavior if (mdxOptions.syntaxHighlight === 'shiki') { rehypePlugins.push([rehypeShiki, mdxOptions.shikiConfig]); @@ -80,9 +86,6 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { } } - // computed from `astro.data.frontmatter` in VFile data - rehypePlugins.push(...mdxOptions.rehypePlugins, rehypeApplyFrontmatterExport); - if (mdxOptions.optimize) { // Convert user `optimize` option to compatible `rehypeOptimizeStatic` option const options = mdxOptions.optimize === true ? undefined : mdxOptions.optimize; From fd0b164dbea766c5321239c9ce25d625bbd903da Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Thu, 22 Feb 2024 15:45:02 +0100 Subject: [PATCH 05/10] docs: explain why html/raw nodes are avoided in changeset This also includes some hints on what users could do to upgrade of they rely on these nodes. --- .changeset/thirty-beds-smoke.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/thirty-beds-smoke.md b/.changeset/thirty-beds-smoke.md index b75b96386ca7..789796eaf00e 100644 --- a/.changeset/thirty-beds-smoke.md +++ b/.changeset/thirty-beds-smoke.md @@ -3,4 +3,6 @@ "@astrojs/markdown-remark": minor --- -Handle syntax highlighting using rehype plugins instead of remark plugins. +Handle syntax highlighting using rehype plugins instead of remark plugins. This provides better interoperability with other [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) that deal with code blocks, in particular with third party syntax highlighting plugins and [`rehype-mermaid`](https://github.com/remcohaszing/rehype-mermaid). + +This may break your code if you are using either a remark plugin that relies on nodes of type `html`, or a rehype plugin that depends on nodes of type `raw`. If you are, you should consider using a rehype plugin that deals with the generated `element` nodes instead. You can use [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) to get a string from a `raw` node, but better is to transform the AST instead of raw HTML strings. From d4f54ebfdd7f9e6e67054822a4c963d39dc07294 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 26 Feb 2024 18:06:48 +0100 Subject: [PATCH 06/10] Fix MDX rehype plugin ordering --- packages/integrations/mdx/src/plugins.ts | 21 +++++++++++---------- packages/markdown/remark/src/highlight.ts | 5 +++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index b370e5bfda31..5bc7ca982c6c 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -65,18 +65,8 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { rehypeMetaString, // rehypeRaw allows custom syntax highlighters to work without added config [rehypeRaw, { passThrough: nodeTypes }], - ...mdxOptions.rehypePlugins, ]; - if (!isPerformanceBenchmark) { - // getHeadings() is guaranteed by TS, so this must be included. - // We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins. - rehypePlugins.push(rehypeHeadingIds, rehypeInjectHeadingsExport); - } - - // computed from `astro.data.frontmatter` in VFile data - rehypePlugins.push(rehypeApplyFrontmatterExport); - if (!isPerformanceBenchmark) { // Apply syntax highlighters after user plugins to match `markdown/remark` behavior if (mdxOptions.syntaxHighlight === 'shiki') { @@ -86,6 +76,17 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { } } + rehypePlugins.push(...mdxOptions.rehypePlugins); + + if (!isPerformanceBenchmark) { + // getHeadings() is guaranteed by TS, so this must be included. + // We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins. + rehypePlugins.push(rehypeHeadingIds, rehypeInjectHeadingsExport); + } + + // computed from `astro.data.frontmatter` in VFile data + rehypePlugins.push(rehypeApplyFrontmatterExport); + if (mdxOptions.optimize) { // Convert user `optimize` option to compatible `rehypeOptimizeStatic` option const options = mdxOptions.optimize === true ? undefined : mdxOptions.optimize; diff --git a/packages/markdown/remark/src/highlight.ts b/packages/markdown/remark/src/highlight.ts index 296d828e2032..eaf4c9bdf9b8 100644 --- a/packages/markdown/remark/src/highlight.ts +++ b/packages/markdown/remark/src/highlight.ts @@ -50,6 +50,11 @@ export function highlightCodeBlocks(tree: Root, highlighter: Highlighter) { } } + // Don’t mighlight math code blocks. + if (languageMatch?.[1] === 'math') { + return; + } + const code = toText(node, { whitespace: 'pre' }); const html = highlighter(code, languageMatch?.[1] || 'plaintext'); // The replacement returns a root node with 1 child, the `` element replacement. From 79044e1460c80bd151e1acdd8aa73538a2d4a193 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Mon, 26 Feb 2024 18:09:37 +0100 Subject: [PATCH 07/10] refactor(remark): restore remarkPrism and remarkShiki MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They aren’t used anymore, but removing would be a breaking change. --- packages/markdown/remark/src/index.ts | 2 ++ packages/markdown/remark/src/remark-prism.ts | 19 ++++++++++++++++++ packages/markdown/remark/src/remark-shiki.ts | 21 ++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 packages/markdown/remark/src/remark-prism.ts create mode 100644 packages/markdown/remark/src/remark-shiki.ts diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index b61aa14c9eeb..2e3cc1126190 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -26,6 +26,8 @@ export { rehypeHeadingIds } from './rehype-collect-headings.js'; export { remarkCollectImages } from './remark-collect-images.js'; export { rehypePrism } from './rehype-prism.js'; export { rehypeShiki } from './rehype-shiki.js'; +export { remarkPrism } from './remark-prism.js'; +export { remarkShiki } from './remark-shiki.js'; export { createShikiHighlighter, replaceCssVariables, type ShikiHighlighter } from './shiki.js'; export * from './types.js'; diff --git a/packages/markdown/remark/src/remark-prism.ts b/packages/markdown/remark/src/remark-prism.ts new file mode 100644 index 000000000000..a3f476d6e4ab --- /dev/null +++ b/packages/markdown/remark/src/remark-prism.ts @@ -0,0 +1,19 @@ +import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter'; +import { visit } from 'unist-util-visit'; +import type { RemarkPlugin } from './types.js'; + +export function remarkPrism(): ReturnType { + return function (tree: any) { + visit(tree, 'code', (node) => { + let { lang, value } = node; + node.type = 'html'; + + let { html, classLanguage } = runHighlighterWithAstro(lang, value); + let classes = [classLanguage]; + node.value = `
${html}
`; + return node; + }); + }; +} diff --git a/packages/markdown/remark/src/remark-shiki.ts b/packages/markdown/remark/src/remark-shiki.ts new file mode 100644 index 000000000000..ebf70fbec9b6 --- /dev/null +++ b/packages/markdown/remark/src/remark-shiki.ts @@ -0,0 +1,21 @@ +import { visit } from 'unist-util-visit'; +import { createShikiHighlighter, type ShikiHighlighter } from './shiki.js'; +import type { RemarkPlugin, ShikiConfig } from './types.js'; + +export function remarkShiki(config?: ShikiConfig): ReturnType { + let highlighterAsync: Promise | undefined; + + return async (tree: any) => { + highlighterAsync ??= createShikiHighlighter(config); + const highlighter = await highlighterAsync; + + visit(tree, 'code', (node) => { + const lang = typeof node.lang === 'string' ? node.lang : 'plaintext'; + const html = highlighter.highlight(node.value, lang); + + node.type = 'html'; + node.value = html; + node.children = []; + }); + }; +} From 09a56e78572a8d4e28c4caa2a30b3dab5e254bee Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 27 Feb 2024 14:19:09 +0800 Subject: [PATCH 08/10] chore: mark deprecated --- packages/markdown/remark/src/remark-prism.ts | 3 +++ packages/markdown/remark/src/remark-shiki.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/markdown/remark/src/remark-prism.ts b/packages/markdown/remark/src/remark-prism.ts index a3f476d6e4ab..49e38d73cfd7 100644 --- a/packages/markdown/remark/src/remark-prism.ts +++ b/packages/markdown/remark/src/remark-prism.ts @@ -2,6 +2,9 @@ import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter'; import { visit } from 'unist-util-visit'; import type { RemarkPlugin } from './types.js'; +/** + * @deprecated Use `rehypePrism` instead + */ export function remarkPrism(): ReturnType { return function (tree: any) { visit(tree, 'code', (node) => { diff --git a/packages/markdown/remark/src/remark-shiki.ts b/packages/markdown/remark/src/remark-shiki.ts index ebf70fbec9b6..c72827bbb7f9 100644 --- a/packages/markdown/remark/src/remark-shiki.ts +++ b/packages/markdown/remark/src/remark-shiki.ts @@ -2,6 +2,9 @@ import { visit } from 'unist-util-visit'; import { createShikiHighlighter, type ShikiHighlighter } from './shiki.js'; import type { RemarkPlugin, ShikiConfig } from './types.js'; +/** + * @deprecated Use `rehypeShiki` instead + */ export function remarkShiki(config?: ShikiConfig): ReturnType { let highlighterAsync: Promise | undefined; From 416bfaa5a062358f26a9e0a9d9b3a78a5bcd0274 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Wed, 6 Mar 2024 13:16:23 +0100 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- .changeset/thirty-beds-smoke.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.changeset/thirty-beds-smoke.md b/.changeset/thirty-beds-smoke.md index 789796eaf00e..c53719f59f73 100644 --- a/.changeset/thirty-beds-smoke.md +++ b/.changeset/thirty-beds-smoke.md @@ -3,6 +3,10 @@ "@astrojs/markdown-remark": minor --- -Handle syntax highlighting using rehype plugins instead of remark plugins. This provides better interoperability with other [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) that deal with code blocks, in particular with third party syntax highlighting plugins and [`rehype-mermaid`](https://github.com/remcohaszing/rehype-mermaid). +Changes Astro's internal syntax highlighting to use rehype plugins instead of remark plugins. This provides better interoperability with other [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) that deal with code blocks, in particular with third party syntax highlighting plugins and [`rehype-mermaid`](https://github.com/remcohaszing/rehype-mermaid). -This may break your code if you are using either a remark plugin that relies on nodes of type `html`, or a rehype plugin that depends on nodes of type `raw`. If you are, you should consider using a rehype plugin that deals with the generated `element` nodes instead. You can use [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) to get a string from a `raw` node, but better is to transform the AST instead of raw HTML strings. +This may be a breaking change if your are currently using: +- a remark plugin that relies on nodes of type `html` +- a rehype plugin that depends on nodes of type `raw`. + +Please review your rendered code samples carefully, and if necessary, consider using a rehype plugin that deals with the generated `element` nodes instead. You can transform the AST of raw HTML strings, or alternatively use [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) to get a string from a `raw` node. From 06bd1e3564b61e7f0ad7f1d0b9dae8b1b5466bb0 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Wed, 6 Mar 2024 16:26:50 +0100 Subject: [PATCH 10/10] Update .changeset/thirty-beds-smoke.md Co-authored-by: Sarah Rainsberger --- .changeset/thirty-beds-smoke.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-beds-smoke.md b/.changeset/thirty-beds-smoke.md index c53719f59f73..6d57166c12b9 100644 --- a/.changeset/thirty-beds-smoke.md +++ b/.changeset/thirty-beds-smoke.md @@ -5,7 +5,7 @@ Changes Astro's internal syntax highlighting to use rehype plugins instead of remark plugins. This provides better interoperability with other [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) that deal with code blocks, in particular with third party syntax highlighting plugins and [`rehype-mermaid`](https://github.com/remcohaszing/rehype-mermaid). -This may be a breaking change if your are currently using: +This may be a breaking change if you are currently using: - a remark plugin that relies on nodes of type `html` - a rehype plugin that depends on nodes of type `raw`.