Skip to content

Commit 50466e6

Browse files
committed
feat: support multiple transformers
1 parent 0132817 commit 50466e6

File tree

7 files changed

+171
-62
lines changed

7 files changed

+171
-62
lines changed

README.md

+14-12
Original file line numberDiff line numberDiff line change
@@ -435,19 +435,21 @@ Since `shikiji` uses `hast` internally, you can use the `transforms` option to c
435435
const code = await codeToHtml('foo\bar', {
436436
lang: 'js',
437437
theme: 'vitesse-light',
438-
transforms: {
439-
code(node) {
440-
node.properties.class = 'language-js'
441-
},
442-
line(node, line) {
443-
node.properties['data-line'] = line
444-
if ([1, 3, 4].includes(line))
445-
node.properties.class += ' highlight'
446-
},
447-
token(node, line, col) {
448-
node.properties.class = `token:${line}:${col}`
438+
transformers: [
439+
{
440+
code(node) {
441+
node.properties.class = 'language-js'
442+
},
443+
line(node, line) {
444+
node.properties['data-line'] = line
445+
if ([1, 3, 4].includes(line))
446+
node.properties.class += ' highlight'
447+
},
448+
token(node, line, col) {
449+
node.properties.class = `token:${line}:${col}`
450+
},
449451
},
450-
},
452+
]
451453
})
452454
```
453455

packages/markdown-it-shikiji/src/index.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function setup(markdownit: MarkdownIt, highlighter: Highlighter, options: Markdo
3232
lang,
3333
}
3434

35-
codeOptions.transforms ||= {}
35+
codeOptions.transformers ||= []
3636

3737
if (highlightLines) {
3838
const lines = parseHighlightLines(attrs)
@@ -41,17 +41,23 @@ function setup(markdownit: MarkdownIt, highlighter: Highlighter, options: Markdo
4141
? 'highlighted'
4242
: highlightLines
4343

44-
codeOptions.transforms.line = (node, line) => {
45-
if (lines.includes(line))
46-
node.properties.class += ` ${className}`
47-
return node
48-
}
44+
codeOptions.transformers.push({
45+
name: 'markdown-it-shikiji:line-class',
46+
line(node, line) {
47+
if (lines.includes(line))
48+
node.properties.class += ` ${className}`
49+
return node
50+
},
51+
})
4952
}
5053
}
5154

52-
codeOptions.transforms.code = (node) => {
53-
node.properties.class = `language-${lang}`
54-
}
55+
codeOptions.transformers.push({
56+
name: 'markdown-it-shikiji:block-class',
57+
code(node) {
58+
node.properties.class = `language-${lang}`
59+
},
60+
})
5561

5662
return highlighter.codeToHtml(
5763
code,

packages/rehype-shikiji/src/index.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,18 @@ const rehypeShikiji: Plugin<[RehypeShikijiOptions], Root> = function (options =
100100
? 'highlighted'
101101
: highlightLines
102102

103-
codeOptions.transforms ||= {}
104-
codeOptions.transforms.line = (node, line) => {
105-
if (lines.includes(line))
106-
node.properties.class += ` ${className}`
107-
return node
108-
}
103+
codeOptions.transformers ||= []
104+
codeOptions.transformers.push({
105+
name: 'rehype-shikiji:line-class',
106+
line(node, line) {
107+
if (lines.includes(line))
108+
node.properties.class += ` ${className}`
109+
return node
110+
},
111+
})
109112
}
110113
}
111-
112114
const fragment = highlighter.codeToHast(code, codeOptions)
113-
114115
parent.children.splice(index, 1, ...fragment.children)
115116
})
116117
}

packages/shikiji-compat/src/index.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,18 @@ export async function getHighlighter(options: HighlighterOptions = {}) {
6565
}
6666

6767
if (options.lineOptions) {
68-
options.transforms = options.transforms || {}
69-
const prev = options.transforms.line
70-
options.transforms.line = (ast, line) => {
71-
const node = prev?.(ast, line) || ast
72-
const lineOption = options.lineOptions?.find(o => o.line === line)
73-
if (lineOption?.classes) {
74-
node.properties ??= {}
75-
node.properties.class = [node.properties.class, ...lineOption.classes].filter(Boolean).join(' ')
76-
}
77-
return node
78-
}
68+
options.transformers ||= []
69+
options.transformers.push({
70+
name: 'shikiji-compat:line-class',
71+
line(node, line) {
72+
const lineOption = options.lineOptions?.find(o => o.line === line)
73+
if (lineOption?.classes) {
74+
node.properties ??= {}
75+
node.properties.class = [node.properties.class, ...lineOption.classes].filter(Boolean).join(' ')
76+
}
77+
return node
78+
},
79+
})
7980
}
8081

8182
return shikiji.codeToHtml(code, options as any)

packages/shikiji/src/core/renderer-hast.ts

+22-5
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,15 @@ export function tokensToHast(
114114
) {
115115
const {
116116
mergeWhitespaces = true,
117+
transformers = [],
117118
} = options
118119

120+
// TODO: remove this in next major version
121+
if (options.transforms) {
122+
transformers.push(options.transforms)
123+
console.warn('[shikiji] `transforms` option is deprecated, use `transformers` instead')
124+
}
125+
119126
if (mergeWhitespaces)
120127
tokens = mergeWhitespaceTokens(tokens)
121128

@@ -169,23 +176,33 @@ export function tokensToHast(
169176
if (style)
170177
tokenNode.properties.style = style
171178

172-
tokenNode = options.transforms?.token?.(tokenNode, idx + 1, col, lineNode) || tokenNode
179+
for (const transformer of transformers)
180+
tokenNode = transformer?.token?.(tokenNode, idx + 1, col, lineNode) || tokenNode
173181

174182
lineNode.children.push(tokenNode)
175183
col += token.content.length
176184
}
177185

178-
lineNode = options.transforms?.line?.(lineNode, idx + 1) || lineNode
186+
for (const transformer of transformers)
187+
lineNode = transformer?.line?.(lineNode, idx + 1) || lineNode
188+
179189
lines.push(lineNode)
180190
})
181191

182-
codeNode = options.transforms?.code?.(codeNode) || codeNode
192+
for (const transformer of transformers)
193+
codeNode = transformer?.code?.(codeNode) || codeNode
183194
preNode.children.push(codeNode)
184195

185-
preNode = options.transforms?.pre?.(preNode) || preNode
196+
for (const transformer of transformers)
197+
preNode = transformer?.pre?.(preNode) || preNode
198+
186199
tree.children.push(preNode)
187200

188-
return options.transforms?.root?.(tree) || tree
201+
let result = tree
202+
for (const transformer of transformers)
203+
result = transformer?.root?.(result) || result
204+
205+
return result
189206
}
190207

191208
function getTokenStyles(token: ThemedToken) {

packages/shikiji/src/types.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,12 @@ export interface CodeToHastOptionsCommon<Languages extends string = string> {
128128
/**
129129
* Transform the generated HAST tree.
130130
*/
131-
transforms?: HastTransformers
131+
transformers?: ShikijiTransformer[]
132+
133+
/**
134+
* @deprecated use `transformers` instead
135+
*/
136+
transforms?: ShikijiTransformer
132137
}
133138

134139
export interface CodeToTokensWithThemesOptions<Languages = string, Themes = string> {
@@ -266,7 +271,11 @@ export interface ThemeRegistration extends ThemeRegistrationRaw {
266271
colors?: Record<string, string>
267272
}
268273

269-
export interface HastTransformers {
274+
export interface ShikijiTransformer {
275+
/**
276+
* Name of the transformer
277+
*/
278+
name?: string
270279
/**
271280
* Transform the entire generated HAST tree. Return a new Node will replace the original one.
272281
*
@@ -291,10 +300,12 @@ export interface HtmlRendererOptionsCommon {
291300
fg?: string
292301
bg?: string
293302

303+
transformers?: ShikijiTransformer[]
304+
294305
/**
295-
* Hast transformers
306+
* @deprecated use `transformers` instead
296307
*/
297-
transforms?: HastTransformers
308+
transforms?: ShikijiTransformer
298309

299310
themeName?: string
300311

packages/shikiji/test/hast.test.ts

+84-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { describe, expect, it } from 'vitest'
1+
/* eslint-disable style/no-tabs */
2+
import { afterEach, describe, expect, it, vi } from 'vitest'
23
import { toHtml } from 'hast-util-to-html'
34
import { codeToHtml, getHighlighter } from '../src'
45

6+
afterEach(() => {
7+
vi.restoreAllMocks
8+
})
9+
510
describe('should', () => {
611
it('works', async () => {
712
const shiki = await getHighlighter({
@@ -19,9 +24,13 @@ describe('should', () => {
1924
})
2025

2126
it('hast transformer', async () => {
27+
const warn = vi.spyOn(console, 'warn')
28+
warn.mockImplementation((() => {}) as any)
29+
2230
const code = await codeToHtml('foo\bar', {
2331
lang: 'js',
2432
theme: 'vitesse-light',
33+
// Use deprecated `transforms` option on purpose to test
2534
transforms: {
2635
line(node, line) {
2736
node.properties['data-line'] = line
@@ -37,6 +46,10 @@ describe('should', () => {
3746

3847
expect(code)
3948
.toMatchInlineSnapshot('"<pre class=\\"shiki vitesse-light\\" style=\\"background-color:#ffffff;color:#393a34\\" tabindex=\\"0\\"><code class=\\"language-js\\"><span class=\\"line\\" data-line=\\"1\\"><span style=\\"color:#B07D48\\" class=\\"token:1:0\\">foo</span><span style=\\"color:#393A34\\" class=\\"token:1:3\\"></span><span style=\\"color:#B07D48\\" class=\\"token:1:4\\">ar</span></span></code></pre>"')
49+
50+
expect(warn).toBeCalledTimes(1)
51+
expect(warn.mock.calls[0][0])
52+
.toMatchInlineSnapshot('"[shikiji] `transforms` option is deprecated, use `transformers` instead"')
4053
})
4154
})
4255

@@ -48,19 +61,21 @@ it('hasfocus support', async () => {
4861
const code = await codeToHtml(snippet, {
4962
lang: 'php',
5063
theme: 'vitesse-light',
51-
transforms: {
52-
code(node) {
53-
node.properties.class = 'language-php'
54-
},
55-
token(node, line, col, parent) {
56-
node.children.forEach((child) => {
57-
if (child.type === 'text' && child.value.includes('[!code focus]')) {
58-
parent.properties['data-has-focus'] = 'true'
59-
node.children.splice(node.children.indexOf(child), 1)
60-
}
61-
})
64+
transformers: [
65+
{
66+
code(node) {
67+
node.properties.class = 'language-php'
68+
},
69+
token(node, line, col, parent) {
70+
node.children.forEach((child) => {
71+
if (child.type === 'text' && child.value.includes('[!code focus]')) {
72+
parent.properties['data-has-focus'] = 'true'
73+
node.children.splice(node.children.indexOf(child), 1)
74+
}
75+
})
76+
},
6277
},
63-
},
78+
],
6479
})
6580

6681
expect(code)
@@ -70,3 +85,59 @@ it('hasfocus support', async () => {
7085
<span class=\\"line\\"><span style=\\"color:#999999\\">$</span><span style=\\"color:#B07D48\\">bar</span><span style=\\"color:#999999\\"> =</span><span style=\\"color:#B5695999\\"> \\"</span><span style=\\"color:#B56959\\">baz</span><span style=\\"color:#B5695999\\">\\"</span><span style=\\"color:#999999\\">;</span></span></code></pre>"
7186
`)
7287
})
88+
89+
it('render whitespace', async () => {
90+
const snippet = [
91+
' space()',
92+
'\t\ttab()',
93+
].join('\n')
94+
95+
const classMap: Record<string, string> = {
96+
' ': 'space',
97+
'\t': 'tab',
98+
}
99+
100+
const code = await codeToHtml(snippet, {
101+
lang: 'js',
102+
theme: 'vitesse-light',
103+
transformers: [
104+
{
105+
line(node) {
106+
const first = node.children[0]
107+
if (!first || first.type !== 'element')
108+
return
109+
const textNode = first.children[0]
110+
if (!textNode || textNode.type !== 'text')
111+
return
112+
node.children = node.children.flatMap((child) => {
113+
if (child.type !== 'element')
114+
return child
115+
const node = child.children[0]
116+
if (node.type !== 'text' || !node.value)
117+
return child
118+
const parts = node.value.split(/([ \t])/).filter(i => i.length)
119+
if (parts.length <= 1)
120+
return child
121+
122+
return parts.map((part) => {
123+
const clone = {
124+
...child,
125+
properties: { ...child.properties },
126+
}
127+
clone.children = [{ type: 'text', value: part }]
128+
if (part in classMap)
129+
clone.properties.class = [clone.properties.class, classMap[part]].filter(Boolean).join(' ')
130+
return clone
131+
})
132+
})
133+
},
134+
},
135+
],
136+
})
137+
138+
expect(code)
139+
.toMatchInlineSnapshot(`
140+
"<pre class=\\"shiki vitesse-light\\" style=\\"background-color:#ffffff;color:#393a34\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color:#59873A\\" class=\\"space\\"> </span><span style=\\"color:#59873A\\" class=\\"space\\"> </span><span style=\\"color:#59873A\\">space</span><span style=\\"color:#999999\\">()</span></span>
141+
<span class=\\"line\\"><span style=\\"color:#59873A\\" class=\\"tab\\"> </span><span style=\\"color:#59873A\\" class=\\"tab\\"> </span><span style=\\"color:#59873A\\">tab</span><span style=\\"color:#999999\\">()</span></span></code></pre>"
142+
`)
143+
})

0 commit comments

Comments
 (0)