Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-beans-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rehype-pretty-code": patch
---

fix: mirror the `--shiki-*` theme variables onto `<code>` in multi-theme configurations, so the documented `code[data-theme*=' ']` selector can apply the theme background without relying on inheritance from `<pre>` (#222)
24 changes: 24 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getInlineCodeLang,
isInlineCode,
getThemeNames,
getShikiCustomProperties,
replaceLineClass,
getLineId,
} from './utils';
Expand Down Expand Up @@ -109,6 +110,29 @@ function apply(
return code;
}

// Shiki emits the dual-theme keys (e.g. `--shiki-light-bg`) as inline
// styles on <pre> only. When multiple themes are active, mirror them onto
// <code> as well, so the documented `code[data-theme*=' ']` selector can
// read the theme background variables directly instead of relying on them
// inheriting from <pre>. See rehype-pretty/rehype-pretty-code#222.
//
// Only those custom properties are copied: <pre> also carries
// declarations owned by someone else, such as a transformer's own
// styling, which must not be duplicated onto <code>.
if (
keepBackground &&
themeNames.length > 1 &&
typeof pre.properties.style === 'string'
) {
const themeProperties = getShikiCustomProperties(pre.properties.style);
if (themeProperties.length > 0) {
const declarations = `${themeProperties.join(';')};`;
code.properties.style = code.properties.style
? `${declarations}${code.properties.style}`
: declarations;
}
}

if (grid) {
if (code.properties.style) {
code.properties.style += 'display: grid;';
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ export function getThemeNames(theme: Theme | Record<string, Theme>) {
);
}

// Declarations for the `--shiki-*` custom properties Shiki writes for the
// non-default themes of a multi-theme config.
const shikiCustomPropertyRegex = /(?:^|;)\s*(--shiki-[\w-]+\s*:[^;]*)/g;

/**
* Extracts the `--shiki-*` custom-property declarations from an inline
* style string, dropping everything else.
*
* `<pre>` also carries declarations that are none of our business — most
* notably whatever user transformers put there — so copying its whole
* style elsewhere would move unrelated layout with it.
*/
export function getShikiCustomProperties(style: string) {
return Array.from(
style.matchAll(shikiCustomPropertyRegex),
([, declaration]) => declaration.trim(),
);
}

export function replaceLineClass(element: Element) {
if (
Array.isArray(element.properties?.className) &&
Expand Down
24 changes: 24 additions & 0 deletions packages/core/test/fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,30 @@ describe('Multiple theme', () => {
});
});

it('only mirrors the theme variables from <pre> onto <code>', async () => {
const html = await getHtml('```js\nconst answer = 42;\n```', {
theme: getTheme(true),
transformers: [
{
name: 'test-pre-padding',
pre(node) {
node.properties.style = `${node.properties.style ?? ''};padding:123px`;
},
},
],
});

const preAttributes = html.match(/<pre([^>]*)>/)?.[1] ?? '';
const codeAttributes = html.match(/<code([^>]*)>/)?.[1] ?? '';

// The theme variables are mirrored so `code[data-theme*=' ']` can read them
expect(codeAttributes).toContain('--shiki-light-bg');
expect(codeAttributes).toContain('--shiki-dark-bg');
// ...but a transformer's own styling stays where the transformer put it
expect(preAttributes).toContain('padding:123px');
expect(codeAttributes).not.toContain('padding');
});

it("highlighter caches don't overwrite each other", async () => {
const [html1, html2] = await Promise.all([
getHtml('`[1, 2, 3]{:js}`', { theme: 'github-light' }),
Expand Down
5 changes: 5 additions & 0 deletions packages/core/test/fixtures/codeBackgroundMultipleTheme.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions packages/core/test/results/codeBackgroundMultipleTheme.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading