From 84d6efd9f1036fdf3c29e9b786b4a96453a607ed Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 17 Feb 2026 11:01:17 +0000 Subject: [PATCH 1/8] refactor: how Code blocks styles are emitted (#15451) * refactor: how code styles are emitted * fix linting and update more tests * fix another test * fixed it * fix another test * apply styles only when needed for shiki * chore: apply solution only to shiki highlighter, when code blocks are in the document * Update .changeset/happy-frogs-glow.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> * add mdx too --------- Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/happy-frogs-glow.md | 7 + packages/astro/components/Code.astro | 18 +- packages/astro/dev-only.d.ts | 4 + packages/astro/src/core/create-vite.ts | 2 + .../astro/src/vite-plugin-markdown/index.ts | 17 +- .../src/vite-plugin-shiki-styles/index.ts | 59 +++++ .../astro/test/astro-component-code.test.js | 85 ++++--- .../astro-markdown-shiki-conditional.test.js | 106 ++++++++ .../astro/test/astro-markdown-shiki.test.js | 95 ++++--- packages/astro/test/csp.test.js | 236 ++++++++++++++++++ .../excluded-lang/astro.config.mjs | 10 + .../excluded-lang/src/pages/index.md | 11 + .../no-code/astro.config.mjs | 7 + .../no-code/src/pages/index.md | 7 + .../with-code/astro.config.mjs | 7 + .../with-code/src/pages/index.md | 10 + .../test/fixtures/csp/src/pages/markdown.md | 64 +++++ .../test/fixtures/csp/src/pages/shiki-diff.md | 15 ++ .../fixtures/csp/src/pages/shiki-wrap.astro | 24 ++ .../test/fixtures/csp/src/pages/shiki.astro | 51 ++++ .../markdoc/test/render-components.test.js | 7 +- .../test/render-extends-components.test.js | 7 +- .../test/render-indented-components.test.js | 4 +- .../markdoc/test/syntax-highlighting.test.js | 5 +- .../mdx/src/vite-plugin-mdx-postprocess.ts | 13 + .../integrations/mdx/src/vite-plugin-mdx.ts | 9 + .../excluded-lang/astro.config.mjs | 12 + .../excluded-lang/src/pages/index.mdx | 11 + .../no-code/astro.config.mjs | 9 + .../no-code/src/pages/index.mdx | 7 + .../with-code/astro.config.mjs | 9 + .../with-code/src/pages/index.mdx | 10 + ...dx-syntax-highlighting-conditional.test.js | 114 +++++++++ .../mdx/test/mdx-syntax-highlighting.test.js | 19 +- packages/markdown/remark/src/highlight.ts | 5 +- packages/markdown/remark/src/index.ts | 6 + packages/markdown/remark/src/rehype-shiki.ts | 10 +- .../remark/src/shiki-style-collector.ts | 50 ++++ packages/markdown/remark/src/shiki.ts | 30 ++- .../remark/src/transformers/style-to-class.ts | 107 ++++++++ packages/markdown/remark/src/types.ts | 3 + .../markdown/remark/test/highlight.test.js | 5 +- packages/markdown/remark/test/shiki.test.js | 60 ++++- 43 files changed, 1223 insertions(+), 124 deletions(-) create mode 100644 .changeset/happy-frogs-glow.md create mode 100644 packages/astro/src/vite-plugin-shiki-styles/index.ts create mode 100644 packages/astro/test/astro-markdown-shiki-conditional.test.js create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki-conditional/excluded-lang/astro.config.mjs create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki-conditional/excluded-lang/src/pages/index.md create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki-conditional/no-code/astro.config.mjs create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki-conditional/no-code/src/pages/index.md create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki-conditional/with-code/astro.config.mjs create mode 100644 packages/astro/test/fixtures/astro-markdown-shiki-conditional/with-code/src/pages/index.md create mode 100644 packages/astro/test/fixtures/csp/src/pages/markdown.md create mode 100644 packages/astro/test/fixtures/csp/src/pages/shiki-diff.md create mode 100644 packages/astro/test/fixtures/csp/src/pages/shiki-wrap.astro create mode 100644 packages/astro/test/fixtures/csp/src/pages/shiki.astro create mode 100644 packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting-conditional/excluded-lang/astro.config.mjs create mode 100644 packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting-conditional/excluded-lang/src/pages/index.mdx create mode 100644 packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting-conditional/no-code/astro.config.mjs create mode 100644 packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting-conditional/no-code/src/pages/index.mdx create mode 100644 packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting-conditional/with-code/astro.config.mjs create mode 100644 packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting-conditional/with-code/src/pages/index.mdx create mode 100644 packages/integrations/mdx/test/mdx-syntax-highlighting-conditional.test.js create mode 100644 packages/markdown/remark/src/shiki-style-collector.ts create mode 100644 packages/markdown/remark/src/transformers/style-to-class.ts diff --git a/.changeset/happy-frogs-glow.md b/.changeset/happy-frogs-glow.md new file mode 100644 index 000000000000..4dfae39b6f47 --- /dev/null +++ b/.changeset/happy-frogs-glow.md @@ -0,0 +1,7 @@ +--- +'@astrojs/markdown-remark': major +'@astrojs/mdx': major +'astro': major +--- + +Changes how styles applied to code blocks are emitted to support CSP - ([v6 upgrade guidance](https://v6.docs.astro.build/en/guides/upgrade-to/v6/#changed-how-shiki-code-block-styles-are-emitted) diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index a84083cebe8a..860ea9ceea2e 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -1,10 +1,18 @@ --- -import { type ThemePresets, createShikiHighlighter } from '@astrojs/markdown-remark'; +import { + type ThemePresets, + createShikiHighlighter, + globalShikiStyleCollector, + transformerStyleToClass, +} from '@astrojs/markdown-remark'; import type { ShikiTransformer, ThemeRegistration, ThemeRegistrationRaw } from 'shiki'; import { bundledLanguages } from 'shiki/langs'; import type { CodeLanguage } from '../dist/types/public/common.js'; import type { HTMLAttributes } from '../types.js'; +// Code.astro always uses Shiki, so import the virtual CSS module +import 'virtual:astro:shiki-styles.css'; + interface Props extends Omit, 'lang'> { /** The code to highlight. Required. */ code: string; @@ -116,11 +124,17 @@ const highlighter = await createShikiHighlighter({ themes, }); +// Combine style-to-class transformer with user-provided transformers +const allTransformers = [ + globalShikiStyleCollector.register(transformerStyleToClass()), + ...transformers, +]; + const html = await highlighter.codeToHtml(code, typeof lang === 'string' ? lang : lang.name, { defaultColor, wrap, inline, - transformers, + transformers: allTransformers, meta, attributes: rest as any, }); diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index f428b7699af9..e7eb1d2ce652 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -81,3 +81,7 @@ declare module 'virtual:astro:dev-css-all' { declare module 'virtual:astro:app' { export function createApp(): import('./src/core/app/base.js').BaseApp; } + +declare module 'virtual:astro:shiki-styles.css' { + // CSS module - no exports, imported for side effects +} diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index cd05ce7186e6..3a86c66b06e8 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -51,6 +51,7 @@ import { vitePluginEnvironment } from '../vite-plugin-environment/index.js'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from './constants.js'; import { vitePluginChromedevtools } from '../vite-plugin-chromedevtools/index.js'; import { vitePluginAstroServerClient } from '../vite-plugin-overlay/index.js'; +import { vitePluginShikiStyles } from '../vite-plugin-shiki-styles/index.js'; type CreateViteOptions = { settings: AstroSettings; @@ -166,6 +167,7 @@ export async function createVite( astroContainer(), astroHmrReloadPlugin(), vitePluginChromedevtools({ settings }), + vitePluginShikiStyles(), ], publicDir: fileURLToPath(settings.config.publicDir), root: fileURLToPath(settings.config.root), diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts index a3710a36f5c2..3b84418b0ed3 100644 --- a/packages/astro/src/vite-plugin-markdown/index.ts +++ b/packages/astro/src/vite-plugin-markdown/index.ts @@ -127,12 +127,19 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug ); } + // Only inject Shiki styles if using Shiki syntax highlighter (default) AND document contains code blocks + const usesShiki = + settings.config.markdown.syntaxHighlight === 'shiki' || + settings.config.markdown.syntaxHighlight === undefined; + const hasCodeBlocks = renderResult.metadata.hasCodeBlocks ?? false; + const code = ` - import { unescapeHTML, spreadAttributes, createComponent, render, renderComponent, maybeRenderHead } from ${JSON.stringify( - astroServerRuntimeModulePath, - )}; - import { AstroError, AstroErrorData } from ${JSON.stringify(astroErrorModulePath)}; - ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''} + import { unescapeHTML, spreadAttributes, createComponent, render, renderComponent, maybeRenderHead } from ${JSON.stringify( + astroServerRuntimeModulePath, + )}; + import { AstroError, AstroErrorData } from ${JSON.stringify(astroErrorModulePath)}; + ${layout ? `import Layout from ${JSON.stringify(layout)};` : ''} + ${usesShiki && hasCodeBlocks ? `import 'virtual:astro:shiki-styles.css';` : ''} ${ // Only include the code relevant to `astro:assets` if there's images in the file diff --git a/packages/astro/src/vite-plugin-shiki-styles/index.ts b/packages/astro/src/vite-plugin-shiki-styles/index.ts new file mode 100644 index 000000000000..cf8b46f2a8b8 --- /dev/null +++ b/packages/astro/src/vite-plugin-shiki-styles/index.ts @@ -0,0 +1,59 @@ +import type { Plugin } from 'vite'; +import { globalShikiStyleCollector } from '@astrojs/markdown-remark'; + +const VIRTUAL_SHIKI_STYLES_ID = 'virtual:astro:shiki-styles.css'; +const RESOLVED_VIRTUAL_SHIKI_STYLES_ID = '\0virtual:astro:shiki-styles.css'; + +/** + * Vite plugin that provides a virtual CSS module containing all Shiki syntax highlighting styles. + * + * This plugin collects styles from the style-to-class transformer used by both Code.astro + * and Markdown processing, and bundles them into a single CSS file. The .css extension + * ensures Vite processes this through its CSS pipeline (minification, hashing, etc.). + */ +export function vitePluginShikiStyles(): Plugin { + return { + name: 'astro:shiki-styles', + + buildStart() { + // Clear styles at the start of each build to prevent stale data + globalShikiStyleCollector.clear(); + }, + + resolveId: { + filter: { + id: new RegExp(`^${VIRTUAL_SHIKI_STYLES_ID}$`), + }, + handler(id) { + if (id === VIRTUAL_SHIKI_STYLES_ID) { + return RESOLVED_VIRTUAL_SHIKI_STYLES_ID; + } + }, + }, + + load: { + filter: { + id: new RegExp(`^${RESOLVED_VIRTUAL_SHIKI_STYLES_ID}$`), + }, + handler(id) { + if (id === RESOLVED_VIRTUAL_SHIKI_STYLES_ID) { + const css = globalShikiStyleCollector.collectCSS(); + // Return CSS or a comment if no styles generated yet + return css || '/* Shiki styles will be generated during build */'; + } + }, + }, + + // Handle HMR invalidation when markdown/astro files change + handleHotUpdate({ file, server }) { + // If a Markdown or astro file changed, invalidate the virtual CSS module + // so it regenerates with updated styles + if (file.endsWith('.md') || file.endsWith('.mdx') || file.endsWith('.astro')) { + const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SHIKI_STYLES_ID); + if (module) { + server.moduleGraph.invalidateModule(module); + } + } + }, + }; +} diff --git a/packages/astro/test/astro-component-code.test.js b/packages/astro/test/astro-component-code.test.js index 3c7b7738479b..74b60c1f6a3d 100644 --- a/packages/astro/test/astro-component-code.test.js +++ b/packages/astro/test/astro-component-code.test.js @@ -15,11 +15,10 @@ describe('', () => { let html = await fixture.readFile('/no-lang/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal( - $('pre').attr('style'), - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto;', - 'applies default and overflow', - ); + // Styles are now class-based - no inline styles + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); + assert.ok($('pre').attr('class'), 'has classes for styling'); assert.equal($('pre > code').length, 1); // test: contains some generated spans @@ -30,7 +29,10 @@ describe('', () => { let html = await fixture.readFile('/basic/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal($('pre').attr('class'), 'astro-code github-dark'); + // Classes now include token style classes and overflow class + assert.ok($('pre').hasClass('astro-code'), 'has astro-code class'); + assert.ok($('pre').hasClass('github-dark'), 'has github-dark theme class'); + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); assert.equal($('pre > code').length, 1); // test: contains many generated spans assert.equal($('pre > code span').length >= 6, true); @@ -40,12 +42,11 @@ describe('', () => { let html = await fixture.readFile('/custom-theme/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal($('pre').attr('class'), 'astro-code nord'); - assert.equal( - $('pre').attr('style'), - 'background-color:#2e3440ff;color:#d8dee9ff; overflow-x: auto;', - 'applies custom theme', - ); + // Classes now include token style classes and overflow class + assert.ok($('pre').hasClass('astro-code'), 'has astro-code class'); + assert.ok($('pre').hasClass('nord'), 'has nord theme class'); + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); }); it('', async () => { @@ -53,28 +54,28 @@ describe('', () => { let html = await fixture.readFile('/wrap-true/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - // test: applies wrap overflow - assert.equal( - $('pre').attr('style'), - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;', - ); + // Wrap styles are now classes, not inline + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok($('pre').hasClass('astro-code-wrap'), 'has wrap class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); } { let html = await fixture.readFile('/wrap-false/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - // test: applies wrap overflow - assert.equal( - $('pre').attr('style'), - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto;', - ); + // Overflow is now a class, not inline + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('pre').hasClass('astro-code-wrap'), 'should NOT have wrap class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); } { let html = await fixture.readFile('/wrap-null/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - // test: applies wrap overflow - assert.equal($('pre').attr('style'), 'background-color:#24292e;color:#e1e4e8'); + // When wrap is null, no overflow or wrap classes + assert.ok(!$('pre').hasClass('astro-code-overflow'), 'should not have overflow class'); + assert.ok(!$('pre').hasClass('astro-code-wrap'), 'should not have wrap class'); + assert.ok(!$('pre').attr('style'), 'should have no inline style'); } }); @@ -82,20 +83,18 @@ describe('', () => { let html = await fixture.readFile('/css-theme/index.html'); const $ = cheerio.load(html); assert.equal($('pre').length, 1); - assert.equal($('pre').attr('class'), 'astro-code css-variables'); - assert.deepEqual( - $('pre, pre span') - .map((_i, f) => (f.attribs ? f.attribs.style : 'no style found')) - .toArray(), - [ - 'background-color:var(--astro-code-background);color:var(--astro-code-foreground); overflow-x: auto;', - 'color:var(--astro-code-token-constant)', - 'color:var(--astro-code-token-function)', - 'color:var(--astro-code-foreground)', - 'color:var(--astro-code-token-string-expression)', - 'color:var(--astro-code-foreground)', - ], - ); + // Classes now include overflow class + assert.ok($('pre').hasClass('astro-code'), 'has astro-code class'); + assert.ok($('pre').hasClass('css-variables'), 'has css-variables theme class'); + assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class'); + + // CSS variables theme still uses inline styles on spans (not transformed by style-to-class) + // but pre background/color should be in classes, overflow should be a class + assert.ok(!$('pre').attr('style'), 'pre should have no inline style'); + + // Spans should have CSS variable colors as inline styles + const spans = $('pre span'); + assert.ok(spans.length > 0, 'should have spans'); }); it(' with custom theme and lang', async () => { @@ -103,10 +102,9 @@ describe('', () => { const $ = cheerio.load(html); assert.equal($('#theme > pre').length, 1); - assert.equal( - $('#theme > pre').attr('style'), - 'background-color:#FDFDFE;color:#4E5377; overflow-x: auto;', - ); + // Styles are now class-based - no inline styles + assert.ok($('#theme > pre').hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!$('#theme > pre').attr('style'), 'should have no inline style'); assert.equal($('#lang > pre').length, 1); assert.equal($('#lang > pre > code span').length, 3); @@ -118,7 +116,8 @@ describe('', () => { const codeEl = $('.astro-code'); assert.equal(codeEl.prop('tagName'), 'CODE'); - assert.match(codeEl.attr('style'), /background-color:/); + // Inline code now uses classes instead of inline styles + assert.ok(codeEl.attr('class'), 'should have classes for styling'); assert.equal($('pre').length, 0); }); diff --git a/packages/astro/test/astro-markdown-shiki-conditional.test.js b/packages/astro/test/astro-markdown-shiki-conditional.test.js new file mode 100644 index 000000000000..614b99ad4cb1 --- /dev/null +++ b/packages/astro/test/astro-markdown-shiki-conditional.test.js @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('Markdown Shiki CSS conditional injection', () => { + describe('With code blocks', () => { + let fixture; + let $; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-markdown-shiki-conditional/with-code/', + }); + await fixture.build(); + const html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + it('should inject Shiki CSS when code blocks present', () => { + const styles = $('style').text(); + // Check for Shiki class prefix + assert.ok(styles.includes('.__a_'), 'Should have Shiki token class definitions'); + // Check for base utility classes + assert.ok(styles.includes('.astro-code-overflow'), 'Should have overflow class'); + }); + + it('should render code blocks with Shiki classes', () => { + const codeBlock = $('pre.astro-code'); + assert.ok(codeBlock.length > 0, 'Should have code blocks with astro-code class'); + + const classes = codeBlock.attr('class'); + assert.ok(classes && classes.includes('__a_'), 'Code block should have Shiki token class'); + }); + }); + + describe('Without code blocks', () => { + let fixture; + let $; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-markdown-shiki-conditional/no-code/', + }); + await fixture.build(); + const html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + it('should NOT inject Shiki CSS when no code blocks', () => { + const styles = $('style').text(); + // Should not have Shiki classes + assert.ok(!styles.includes('.__a_'), 'Should NOT have Shiki token class definitions'); + assert.ok(!styles.includes('.astro-code-overflow'), 'Should NOT have overflow class'); + }); + + it('should NOT have code blocks', () => { + const codeBlocks = $('pre.astro-code'); + assert.equal(codeBlocks.length, 0, 'Should not have code blocks'); + }); + + it('should still render markdown content', () => { + assert.equal($('h1').text(), 'Hello world'); + assert.ok($('p').length > 0, 'Should have paragraph elements'); + // Inline code should still be present + assert.ok($('code').length > 0, 'Should have inline code elements'); + }); + }); + + describe('With excluded language', () => { + let fixture; + let $; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-markdown-shiki-conditional/excluded-lang/', + }); + await fixture.build(); + const html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + it('should NOT inject CSS for excluded languages', () => { + const styles = $('style').text(); + // Should not have Shiki classes since mermaid is excluded + assert.ok(!styles.includes('.__a_'), 'Should NOT have Shiki token class definitions'); + assert.ok(!styles.includes('.astro-code-overflow'), 'Should NOT have overflow class'); + }); + + it('should still have the code block element (unstyled)', () => { + // The code block should exist but without Shiki styling + const codeElements = $('pre code'); + assert.ok(codeElements.length > 0, 'Should have code block element'); + + // Should have language-mermaid class from markdown processing + const code = codeElements.first(); + const className = code.attr('class') || ''; + assert.ok(className.includes('language-mermaid'), 'Should have language-mermaid class'); + }); + + it('should NOT have astro-code class', () => { + const astroCodeBlocks = $('pre.astro-code'); + assert.equal(astroCodeBlocks.length, 0, 'Should not have astro-code class'); + }); + }); +}); diff --git a/packages/astro/test/astro-markdown-shiki.test.js b/packages/astro/test/astro-markdown-shiki.test.js index 8140b4d5dfee..42417897965c 100644 --- a/packages/astro/test/astro-markdown-shiki.test.js +++ b/packages/astro/test/astro-markdown-shiki.test.js @@ -16,23 +16,24 @@ describe('Astro Markdown Shiki', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - // There should be no HTML from Prism - assert.equal($('.token').length, 0); - + // There are 2 code blocks in index.md (yaml and diff) assert.equal($('pre').length, 2); - assert.ok($('pre').hasClass('astro-code')); - assert.equal( - $('pre').attr().style, - 'background-color:#24292e;color:#e1e4e8; overflow-x: auto;', - ); + const yamlBlock = $('pre').first(); + assert.ok(yamlBlock.hasClass('astro-code')); + // Styles are now class-based - no inline styles + assert.ok(yamlBlock.hasClass('astro-code-overflow'), 'has overflow class'); + assert.ok(!yamlBlock.attr('style'), 'should have no inline style attribute'); }); it('Can render diff syntax with "user-select: none"', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - const diffBlockHtml = $('pre').last().html(); - assert.ok(diffBlockHtml.includes(`+`)); - assert.ok(diffBlockHtml.includes(`-`)); + // The diff block is the second
 in index.html
+			const diffBlock = $('pre').eq(1);
+			const diffHtml = $.html(diffBlock);
+			// user-select: none is now a class, not inline style
+			assert.ok(diffHtml.includes(`+`));
+			assert.ok(diffHtml.includes(`-`));
 		});
 	});
 
@@ -51,10 +52,9 @@ describe('Astro Markdown Shiki', () => {
 
 				assert.equal($('pre').length, 1);
 				assert.ok($('pre').hasClass('astro-code'));
-				assert.equal(
-					$('pre').attr().style,
-					'background-color:#fff;color:#24292e; overflow-x: auto;',
-				);
+				// Styles are now class-based - no inline styles
+				assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class');
+				assert.ok(!$('pre').attr('style'), 'should have no inline style attribute');
 			});
 		});
 
@@ -70,12 +70,15 @@ describe('Astro Markdown Shiki', () => {
 				const html = await fixture.readFile('/index.html');
 				const $ = cheerio.load(html);
 
-				assert.equal($('pre').length, 1);
-				assert.ok($('pre').hasClass('astro-code'));
-				assert.equal(
-					$('pre').attr().style,
-					'background-color:#FDFDFE;color:#4E5377; overflow-x: auto;',
-				);
+				// With class-based styles, ALL styles are in CSS classes (no inline styles)
+				assert.ok(!$('pre').attr('style'), 'should have no inline style attribute');
+				assert.ok($('pre').hasClass('astro-code-overflow'), 'has overflow class');
+
+				// Verify styles are in the