diff options
-rw-r--r-- | .changeset/large-dolphins-roll.md | 29 | ||||
-rw-r--r-- | packages/astro/src/core/config/schema.ts | 20 | ||||
-rw-r--r-- | packages/astro/src/events/session.ts | 5 | ||||
-rw-r--r-- | packages/astro/src/types/public/config.ts | 68 | ||||
-rw-r--r-- | packages/integrations/mdx/src/plugins.ts | 15 | ||||
-rw-r--r-- | packages/markdown/remark/src/highlight.ts | 14 | ||||
-rw-r--r-- | packages/markdown/remark/src/index.ts | 24 | ||||
-rw-r--r-- | packages/markdown/remark/src/rehype-prism.ts | 18 | ||||
-rw-r--r-- | packages/markdown/remark/src/rehype-shiki.ts | 22 | ||||
-rw-r--r-- | packages/markdown/remark/src/types.ts | 9 | ||||
-rw-r--r-- | packages/markdown/remark/test/highlight.test.js | 52 |
11 files changed, 236 insertions, 40 deletions
diff --git a/.changeset/large-dolphins-roll.md b/.changeset/large-dolphins-roll.md new file mode 100644 index 000000000..c9070f62a --- /dev/null +++ b/.changeset/large-dolphins-roll.md @@ -0,0 +1,29 @@ +--- +'@astrojs/markdown-remark': minor +'astro': minor +--- + +Adds a new configuration option for Markdown syntax highlighting `excludeLangs` + +This option provides better support for diagramming tools that rely on Markdown code blocks, such as Mermaid.js and D2 by allowing you to exclude specific languages from Astro's default syntax highlighting. + +This option allows you to avoid rendering conflicts with tools that depend on the code not being highlighted without forcing you to disable syntax highlighting for other code blocks. + +The default value for `excludeLangs` is `['math']` and remains unchanged by default in this release. +But users can now override it to exclude other languages or exclude no languages. + +The following example configuration will exclude highlighting for `mermaid` and `math` code blocks: + +```js +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + markdown: { + syntaxHighlight: { + type: 'shiki', + excludeLangs: ['mermaid', 'math'], + }, + }, +}); +``` + diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 82609e3b4..a24713ba3 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -7,7 +7,7 @@ import type { RemarkPlugin as _RemarkPlugin, RemarkRehype as _RemarkRehype, } from '@astrojs/markdown-remark'; -import { markdownConfigDefaults } from '@astrojs/markdown-remark'; +import { markdownConfigDefaults, syntaxHighlightDefaults } from '@astrojs/markdown-remark'; import { type BuiltinTheme, bundledThemes } from 'shiki'; import { z } from 'zod'; import type { SvgRenderMode } from '../../assets/utils/svg.js'; @@ -104,6 +104,10 @@ export const ASTRO_CONFIG_DEFAULTS = { }, } satisfies AstroUserConfig & { server: { open: boolean } }; +const highlighterTypesSchema = z + .union([z.literal('shiki'), z.literal('prism')]) + .default(syntaxHighlightDefaults.type); + export const AstroConfigSchema = z.object({ root: z .string() @@ -308,7 +312,19 @@ export const AstroConfigSchema = z.object({ markdown: z .object({ syntaxHighlight: z - .union([z.literal('shiki'), z.literal('prism'), z.literal(false)]) + .union([ + z + .object({ + type: highlighterTypesSchema, + excludeLangs: z + .array(z.string()) + .optional() + .default(syntaxHighlightDefaults.excludeLangs), + }) + .default(syntaxHighlightDefaults), + highlighterTypesSchema, + z.literal(false), + ]) .default(ASTRO_CONFIG_DEFAULTS.markdown.syntaxHighlight), shikiConfig: z .object({ diff --git a/packages/astro/src/events/session.ts b/packages/astro/src/events/session.ts index 6e919f127..f83546b4d 100644 --- a/packages/astro/src/events/session.ts +++ b/packages/astro/src/events/session.ts @@ -107,7 +107,10 @@ function createAnonymousConfigInfo(userConfig: AstroUserConfig) { }; // Measure string literal/enum configuration values configInfo.build.format = measureStringLiteral(userConfig.build?.format); - configInfo.markdown.syntaxHighlight = measureStringLiteral(userConfig.markdown?.syntaxHighlight); + const syntaxHighlight = userConfig.markdown?.syntaxHighlight; + const syntaxHighlightType = + typeof syntaxHighlight === 'object' ? syntaxHighlight.type : syntaxHighlight; + configInfo.markdown.syntaxHighlight = measureStringLiteral(syntaxHighlightType); configInfo.output = measureStringLiteral(userConfig.output); configInfo.scopedStyleStrategy = measureStringLiteral(userConfig.scopedStyleStrategy); configInfo.trailingSlash = measureStringLiteral(userConfig.trailingSlash); diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 477434216..4ff49cc1c 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -5,6 +5,7 @@ import type { RemarkPlugins, RemarkRehype, ShikiConfig, + SyntaxHighlightConfigType, } from '@astrojs/markdown-remark'; import type { BuiltinDriverName, BuiltinDriverOptions, Driver, Storage } from 'unstorage'; import type { UserConfig as OriginalViteUserConfig, SSROptions as ViteSSROptions } from 'vite'; @@ -1289,14 +1290,15 @@ export interface ViteUserConfig extends OriginalViteUserConfig { /** * @docs * @name markdown.syntaxHighlight - * @type {'shiki' | 'prism' | false} - * @default `shiki` + * @type {SyntaxHighlightConfig | SyntaxHighlightConfigType | false} + * @default `{ type: 'shiki', excludeLangs: ['math'] }` * @description * Which syntax highlighter to use for Markdown code blocks (\`\`\`), if any. This determines the CSS classes that Astro will apply to your Markdown code blocks. + * * - `shiki` - use the [Shiki](https://shiki.style) highlighter (`github-dark` theme configured by default) * - `prism` - use the [Prism](https://prismjs.com/) highlighter and [provide your own Prism stylesheet](/en/guides/syntax-highlighting/#add-a-prism-stylesheet) * - `false` - do not apply syntax highlighting. - * + * ```js * { * markdown: { @@ -1305,9 +1307,67 @@ export interface ViteUserConfig extends OriginalViteUserConfig { * } * } * ``` + * + * For more control over syntax highlighting, you can instead specify a configuration object with the properties listed below. */ - syntaxHighlight?: 'shiki' | 'prism' | false; + syntaxHighlight?: + | { + /** + * @docs + * @name markdown.syntaxHighlight.type + * @kind h4 + * @type {'shiki' | 'prism'} + * @default `'shiki'` + * @version 5.5.0 + * @description + * + * The default CSS classes to apply to Markdown code blocks. + * (If no other syntax highlighting configuration is needed, you can instead set `markdown.syntaxHighlight` directly to `shiki`, `prism`, or `false`.) + * + */ + type?: SyntaxHighlightConfigType; + /** + * @docs + * @name markdown.syntaxHighlight.excludeLangs + * @kind h4 + * @type {string[]} + * @default ['math'] + * @version 5.5.0 + * @description + * + * An array of languages to exclude from the default syntax highlighting specified in `markdown.syntaxHighlight.type`. + * This can be useful when using tools that create diagrams from Markdown code blocks, such as Mermaid.js and D2. + * + * ```js title="astro.config.mjs" + * import { defineConfig } from 'astro/config'; + * + * export default defineConfig({ + * markdown: { + * syntaxHighlight: { + * type: 'shiki', + * excludeLangs: ['mermaid', 'math'], + * }, + * }, + * }); + * ``` + * + * ```html + * <!-- Call Mermaid JavaScript Integration in the body --> + * <script> + * import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'; + * mermaid.initialize({ startOnLoad: false }); + * mermaid.run({ + * querySelector: 'data-language="mermaid"', + * }); + * </script> + * ``` + * + * */ + excludeLangs?: string[]; + } + | SyntaxHighlightConfigType + | false; /** * @docs * @name markdown.remarkPlugins diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts index 77c76243c..e1640238f 100644 --- a/packages/integrations/mdx/src/plugins.ts +++ b/packages/integrations/mdx/src/plugins.ts @@ -65,12 +65,17 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList { [rehypeRaw, { passThrough: nodeTypes }], ]; - if (!isPerformanceBenchmark) { + const syntaxHighlight = mdxOptions.syntaxHighlight; + if (syntaxHighlight && !isPerformanceBenchmark) { + const syntaxHighlightType = + typeof syntaxHighlight === 'string' ? syntaxHighlight : syntaxHighlight?.type; + const excludeLangs = + typeof syntaxHighlight === 'object' ? syntaxHighlight?.excludeLangs : undefined; // 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); + if (syntaxHighlightType === 'shiki') { + rehypePlugins.push([rehypeShiki, mdxOptions.shikiConfig, excludeLangs]); + } else if (syntaxHighlightType === 'prism') { + rehypePlugins.push([rehypePrism, excludeLangs]); } } diff --git a/packages/markdown/remark/src/highlight.ts b/packages/markdown/remark/src/highlight.ts index 5cdb2af19..d4457c7d2 100644 --- a/packages/markdown/remark/src/highlight.ts +++ b/packages/markdown/remark/src/highlight.ts @@ -11,6 +11,8 @@ type Highlighter = ( ) => Promise<Root | string>; const languagePattern = /\blanguage-(\S+)\b/; +// Don’t highlight math code blocks by default. +export const defaultExcludeLanguages = ['math']; /** * A hast utility to syntax highlight code blocks with a given syntax highlighter. @@ -21,7 +23,11 @@ const languagePattern = /\blanguage-(\S+)\b/; * A function which receives the code and language, and returns the HTML of a syntax * highlighted `<pre>` element. */ -export async function highlightCodeBlocks(tree: Root, highlighter: Highlighter) { +export async function highlightCodeBlocks( + tree: Root, + highlighter: Highlighter, + excludeLanguages: string[] = [], +) { const nodes: Array<{ node: Element; language: string; @@ -61,14 +67,14 @@ export async function highlightCodeBlocks(tree: Root, highlighter: Highlighter) } } - // Don’t mighlight math code blocks. - if (languageMatch?.[1] === 'math') { + const language = languageMatch?.[1] || 'plaintext'; + if (excludeLanguages.includes(language) || defaultExcludeLanguages.includes(language)) { return; } nodes.push({ node, - language: languageMatch?.[1] || 'plaintext', + language, parent, grandParent: ancestors.at(-2)!, }); diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index d1b6035e4..6d3261496 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -2,6 +2,7 @@ import type { AstroMarkdownOptions, AstroMarkdownProcessorOptions, MarkdownProcessor, + SyntaxHighlightConfig, } from './types.js'; import { loadPlugins } from './load-plugins.js'; @@ -18,8 +19,8 @@ import remarkRehype from 'remark-rehype'; import remarkSmartypants from 'remark-smartypants'; import { unified } from 'unified'; import { VFile } from 'vfile'; +import { defaultExcludeLanguages } from './highlight.js'; import { rehypeImages } from './rehype-images.js'; - export { rehypeHeadingIds } from './rehype-collect-headings.js'; export { remarkCollectImages } from './remark-collect-images.js'; export { rehypePrism } from './rehype-prism.js'; @@ -39,8 +40,13 @@ export { } from './shiki.js'; export * from './types.js'; +export const syntaxHighlightDefaults: Required<SyntaxHighlightConfig> = { + type: 'shiki', + excludeLangs: defaultExcludeLanguages, +}; + export const markdownConfigDefaults: Required<AstroMarkdownOptions> = { - syntaxHighlight: 'shiki', + syntaxHighlight: syntaxHighlightDefaults, shikiConfig: { langs: [], theme: 'github-dark', @@ -107,12 +113,16 @@ export async function createMarkdownProcessor( ...remarkRehypeOptions, }); - if (!isPerformanceBenchmark) { + if (syntaxHighlight && !isPerformanceBenchmark) { + const syntaxHighlightType = + typeof syntaxHighlight === 'string' ? syntaxHighlight : syntaxHighlight?.type; + const excludeLangs = + typeof syntaxHighlight === 'object' ? syntaxHighlight?.excludeLangs : undefined; // Syntax highlighting - if (syntaxHighlight === 'shiki') { - parser.use(rehypeShiki, shikiConfig); - } else if (syntaxHighlight === 'prism') { - parser.use(rehypePrism); + if (syntaxHighlightType === 'shiki') { + parser.use(rehypeShiki, shikiConfig, excludeLangs); + } else if (syntaxHighlightType === 'prism') { + parser.use(rehypePrism, excludeLangs); } } diff --git a/packages/markdown/remark/src/rehype-prism.ts b/packages/markdown/remark/src/rehype-prism.ts index 0dcc5695f..887a0a4b9 100644 --- a/packages/markdown/remark/src/rehype-prism.ts +++ b/packages/markdown/remark/src/rehype-prism.ts @@ -3,14 +3,18 @@ import type { Root } from 'hast'; import type { Plugin } from 'unified'; import { highlightCodeBlocks } from './highlight.js'; -export const rehypePrism: Plugin<[], Root> = () => { +export const rehypePrism: Plugin<[string[]?], Root> = (excludeLangs) => { return async (tree) => { - await highlightCodeBlocks(tree, (code, language) => { - let { html, classLanguage } = runHighlighterWithAstro(language, code); + await highlightCodeBlocks( + tree, + (code, language) => { + let { html, classLanguage } = runHighlighterWithAstro(language, code); - return Promise.resolve( - `<pre class="${classLanguage}" data-language="${language}"><code is:raw class="${classLanguage}">${html}</code></pre>`, - ); - }); + return Promise.resolve( + `<pre class="${classLanguage}" data-language="${language}"><code is:raw class="${classLanguage}">${html}</code></pre>`, + ); + }, + excludeLangs, + ); }; }; diff --git a/packages/markdown/remark/src/rehype-shiki.ts b/packages/markdown/remark/src/rehype-shiki.ts index 43b38f095..c4185eb6a 100644 --- a/packages/markdown/remark/src/rehype-shiki.ts +++ b/packages/markdown/remark/src/rehype-shiki.ts @@ -4,7 +4,7 @@ import { highlightCodeBlocks } from './highlight.js'; import { type ShikiHighlighter, createShikiHighlighter } from './shiki.js'; import type { ShikiConfig } from './types.js'; -export const rehypeShiki: Plugin<[ShikiConfig?], Root> = (config) => { +export const rehypeShiki: Plugin<[ShikiConfig, string[]?], Root> = (config, excludeLangs) => { let highlighterAsync: Promise<ShikiHighlighter> | undefined; return async (tree) => { @@ -16,13 +16,17 @@ export const rehypeShiki: Plugin<[ShikiConfig?], Root> = (config) => { }); const highlighter = await highlighterAsync; - await highlightCodeBlocks(tree, (code, language, options) => { - return highlighter.codeToHast(code, language, { - meta: options?.meta, - wrap: config?.wrap, - defaultColor: config?.defaultColor, - transformers: config?.transformers, - }); - }); + await highlightCodeBlocks( + tree, + (code, language, options) => { + return highlighter.codeToHast(code, language, { + meta: options?.meta, + wrap: config?.wrap, + defaultColor: config?.defaultColor, + transformers: config?.transformers, + }); + }, + excludeLangs, + ); }; }; diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index 24f5f6c4d..a3efae62d 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -37,6 +37,13 @@ export type RemarkRehype = RemarkRehypeOptions; export type ThemePresets = BuiltinTheme | 'css-variables'; +export type SyntaxHighlightConfigType = 'shiki' | 'prism'; + +export interface SyntaxHighlightConfig { + type: SyntaxHighlightConfigType; + excludeLangs?: string[]; +} + export interface ShikiConfig extends Pick<CreateShikiHighlighterOptions, 'langs' | 'theme' | 'themes' | 'langAlias'>, Pick<ShikiHighlighterHighlightOptions, 'defaultColor' | 'wrap' | 'transformers'> {} @@ -45,7 +52,7 @@ export interface ShikiConfig * Configuration options that end up in the markdown section of AstroConfig */ export interface AstroMarkdownOptions { - syntaxHighlight?: 'shiki' | 'prism' | false; + syntaxHighlight?: SyntaxHighlightConfig | SyntaxHighlightConfigType | false; shikiConfig?: ShikiConfig; remarkPlugins?: RemarkPlugins; rehypePlugins?: RehypePlugins; diff --git a/packages/markdown/remark/test/highlight.test.js b/packages/markdown/remark/test/highlight.test.js new file mode 100644 index 000000000..bcd2086b5 --- /dev/null +++ b/packages/markdown/remark/test/highlight.test.js @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { createMarkdownProcessor } from '../dist/index.js'; + +describe('highlight', () => { + it('highlights using shiki by default', async () => { + const processor = await createMarkdownProcessor(); + const { code } = await processor.render('```js\nconsole.log("Hello, world!");\n```'); + assert.match(code, /background-color:/); + }); + + it('does not highlight math code blocks by default', async () => { + const processor = await createMarkdownProcessor(); + const { code } = await processor.render('```math\n\\frac{1}{2}\n```'); + + assert.ok(!code.includes('background-color:')); + }); + + it('highlights using prism', async () => { + const processor = await createMarkdownProcessor({ + syntaxHighlight: { + type: 'prism', + }, + }); + const { code } = await processor.render('```js\nconsole.log("Hello, world!");\n```'); + assert.ok(code.includes('token')); + }); + + it('supports excludeLangs', async () => { + const processor = await createMarkdownProcessor({ + syntaxHighlight: { + type: 'shiki', + excludeLangs: ['mermaid'], + }, + }); + const { code } = await processor.render('```mermaid\ngraph TD\nA --> B\n```'); + + assert.ok(!code.includes('background-color:')); + }); + + it('supports excludeLangs with prism', async () => { + const processor = await createMarkdownProcessor({ + syntaxHighlight: { + type: 'prism', + excludeLangs: ['mermaid'], + }, + }); + const { code } = await processor.render('```mermaid\ngraph TD\nA --> B\n```'); + + assert.ok(!code.includes('token')); + }); +}); |