Skip to content

Commit 63d0217

Browse files
hendrikheilclaude
andauthored
fix(render): unwrap a bare shiki class on pre back to a plain fence (#259)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 99cdd28 commit 63d0217

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

packages/comark/src/internal/stringify/attributes.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ export function userBlockAttrs(tag: string, attributes: Record<string, unknown>)
128128
if (remaining) result[key] = remaining
129129
continue
130130
}
131-
if (key === 'class' && tag === 'pre' && typeof value === 'string' && value.startsWith('shiki ')) {
132-
// Shiki injects `shiki [shiki-themes] <themes…> dark:<theme>` and any
133-
// user-supplied class is appended after it. Recover the user portion by
134-
// dropping everything up to and including the first `dark:*` token.
131+
if (key === 'class' && tag === 'pre' && typeof value === 'string' && value.startsWith('shiki')) {
132+
// Shiki injects `shiki [shiki-themes] <themes…>` (or a bare `shiki`) and
133+
// appends any user class after a `.` separator. Recover the user portion
134+
// by dropping everything up to and including that separator.
135135
const tokens = value.split(/\s+/)
136136
let cutoff = tokens.findIndex((t) => t === '.')
137137

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from 'vitest'
2+
import type { ComarkTree } from '../../src/types'
3+
import { renderMarkdown } from '../../src/render'
4+
5+
describe('shiki code block round-trip', () => {
6+
// The highlight plugin's injected attrs have no markdown form, so a
7+
// highlighted block must serialize to a plain fence, never a `::pre{...}`.
8+
function preTree(preClass: string): ComarkTree {
9+
return {
10+
frontmatter: {},
11+
meta: {},
12+
nodes: [['pre', { language: 'bash', class: preClass }, ['code', { class: 'language-bash' }, 'npx install']]],
13+
}
14+
}
15+
16+
it('serializes a bare `shiki` class back to a plain fence', async () => {
17+
// Single-theme shiki emits a bare `class="shiki"`.
18+
const md = await renderMarkdown(preTree('shiki'))
19+
expect(md.trim()).toBe('```bash\nnpx install\n```')
20+
expect(md).not.toContain('::pre')
21+
})
22+
23+
it('serializes a multi-token shiki class back to a plain fence', async () => {
24+
// Dual-theme shiki emits `shiki shiki-themes <theme> dark:<theme>`.
25+
const md = await renderMarkdown(preTree('shiki shiki-themes github-dark dark:github-dark'))
26+
expect(md.trim()).toBe('```bash\nnpx install\n```')
27+
expect(md).not.toContain('::pre')
28+
})
29+
30+
it('keeps a plain fence for a highlighted code block inside a component slot', async () => {
31+
const tree: ComarkTree = {
32+
frontmatter: {},
33+
meta: {},
34+
nodes: [
35+
[
36+
'code-preview',
37+
{},
38+
[
39+
'template',
40+
{ name: 'code' },
41+
['pre', { language: 'bash', class: 'shiki' }, ['code', { class: 'language-bash' }, 'npx install']],
42+
],
43+
],
44+
],
45+
}
46+
const md = await renderMarkdown(tree)
47+
expect(md).not.toContain('::pre')
48+
expect(md).toContain('```bash\nnpx install\n```')
49+
})
50+
})

0 commit comments

Comments
 (0)