Skip to content

Commit 3d24591

Browse files
committed
feat!: rework hastTransform to transforms
1 parent c68be10 commit 3d24591

File tree

4 files changed

+99
-67
lines changed

4 files changed

+99
-67
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,30 @@ console.log(root)
415415
}
416416
```
417417

418+
### Hast transformers
419+
420+
Since `shikiji` uses `hast` internally, you can use the `transforms` option to customize the generated HTML by manipulating the hast tree. You can pass custom functions to modify the tree for different types of nodes. For example:
421+
422+
```js
423+
const code = await codeToHtml('foo\bar', {
424+
lang: 'js',
425+
theme: 'vitesse-light',
426+
transforms: {
427+
code(node) {
428+
node.properties.class = 'language-js'
429+
},
430+
line(node, line) {
431+
node.properties['data-line'] = line
432+
if ([1, 3, 4].includes(line))
433+
node.properties.class += ' highlight'
434+
},
435+
token(node, line, col) {
436+
node.properties.class = `token:${line}:${col}`
437+
},
438+
},
439+
})
440+
```
441+
418442
## Breaking Changes from Shiki
419443

420444
As of [`shiki@0.4.3`](https://github.com/shikijs/shiki/releases/tag/v0.14.3):
@@ -425,7 +449,7 @@ As of [`shiki@0.4.3`](https://github.com/shikijs/shiki/releases/tag/v0.14.3):
425449
- Highlighter does not maintain an internal default theme context. `theme` option is required for `codeToHtml` and `codeToThemedTokens`.
426450
- `.ansiToHtml` is merged into `.codeToHtml` as a special language `ansi`. Use `.codeToHtml(code, { lang: 'ansi' })` instead.
427451
- `codeToHtml` uses [`hast`](https://github.com/syntax-tree/hast) internally. The generated HTML will be a bit different but should behavior the same.
428-
- `lineOptions` is dropped in favor of the fully customizable `hastTransform` option.
452+
- `lineOptions` is dropped in favor of the fully customizable `transforms` option.
429453
- CJS and IIFE builds are dropped. See [CJS Usage](#cjs-usage) and [CDN Usage](#cdn-usage) for more details.
430454
- `LanguageRegistration`'s `grammar` field is flattened to `LanguageRegistration` itself (refer to the types for more details).
431455

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

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,38 +99,39 @@ export function tokensToHast(
9999
const lines: (Element | Text)[] = []
100100
const tree: Root = {
101101
type: 'root',
102-
children: [
103-
{
104-
type: 'element',
105-
tagName: 'pre',
106-
properties: {
107-
class: `shiki ${options.themeName || ''}`,
108-
style: options.rootStyle || `background-color:${options.bg};color:${options.fg}`,
109-
tabindex: '0',
110-
},
111-
children: [
112-
{
113-
type: 'element',
114-
tagName: 'code',
115-
properties: {},
116-
children: lines,
117-
},
118-
],
119-
},
120-
],
102+
children: [],
103+
}
104+
105+
let preNode: Element = {
106+
type: 'element',
107+
tagName: 'pre',
108+
properties: {
109+
class: `shiki ${options.themeName || ''}`,
110+
style: options.rootStyle || `background-color:${options.bg};color:${options.fg}`,
111+
tabindex: '0',
112+
},
113+
children: [],
114+
}
115+
116+
let codeNode: Element = {
117+
type: 'element',
118+
tagName: 'code',
119+
properties: {},
120+
children: lines,
121121
}
122122

123123
tokens.forEach((line, idx) => {
124124
if (idx)
125125
lines.push({ type: 'text', value: '\n' })
126126

127-
const lineNode: Element = {
127+
let lineNode: Element = {
128128
type: 'element',
129129
tagName: 'span',
130130
properties: { class: 'line' },
131131
children: [],
132132
}
133-
lines.push(lineNode)
133+
134+
let col = 0
134135

135136
for (const token of line) {
136137
const styles = [token.htmlStyle || `color:${token.color}`]
@@ -143,7 +144,7 @@ export function tokensToHast(
143144
styles.push('text-decoration:underline')
144145
}
145146

146-
const tokenNode: Element = {
147+
let tokenNode: Element = {
147148
type: 'element',
148149
tagName: 'span',
149150
properties: {
@@ -152,11 +153,23 @@ export function tokensToHast(
152153
children: [{ type: 'text', value: token.content }],
153154
}
154155

156+
tokenNode = options.transforms?.token?.(tokenNode, idx + 1, col) || tokenNode
157+
155158
lineNode.children.push(tokenNode)
159+
col += token.content.length
156160
}
161+
162+
lineNode = options.transforms?.line?.(lineNode, idx + 1) || lineNode
163+
lines.push(lineNode)
157164
})
158165

159-
return options.hastTransform?.(tree) || tree
166+
codeNode = options.transforms?.code?.(codeNode) || codeNode
167+
preNode.children.push(codeNode)
168+
169+
preNode = options.transforms?.pre?.(preNode) || preNode
170+
tree.children.push(preNode)
171+
172+
return options.transforms?.root?.(tree) || tree
160173
}
161174

162175
function mergeWhitespaceTokens(tokens: ThemedToken[][]) {

packages/shikiji/src/types.ts

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IGrammar, IRawGrammar, IRawTheme } from 'vscode-textmate'
2-
import type { Root } from 'hast'
2+
import type { Element, Root } from 'hast'
33
import type { bundledThemes } from './themes'
44
import type { bundledLanguages } from './assets/langs'
55
import type { FontStyle } from './core/stackElementMetadata'
@@ -121,9 +121,9 @@ export interface CodeToThemedTokensOptions<Languages = string, Themes = string>
121121
export interface CodeToHastOptionsCommon<Languages = string> {
122122
lang: Languages | SpecialLanguage
123123
/**
124-
* TODO
124+
* Transform the generated HAST tree.
125125
*/
126-
hastTransform?: (hast: Root) => Root | void
126+
transforms?: HastTransformers
127127
}
128128

129129
export interface CodeToTokensWithThemesOptions<Languages = string, Themes = string> {
@@ -254,14 +254,34 @@ export interface ThemeRegistration extends ThemeRegistrationRaw {
254254
colors?: Record<string, string>
255255
}
256256

257+
export interface HastTransformers {
258+
/**
259+
* Transform the entire generated HAST tree. Return a new Node will replace the original one.
260+
*
261+
* @param hast
262+
*/
263+
root?: (hast: Root) => Root | void
264+
pre?: (hast: Element) => Element | void
265+
code?: (hast: Element) => Element | void
266+
/**
267+
* Transform each line element.
268+
*
269+
* @param hast
270+
* @param line 1-based line number
271+
*/
272+
line?: (hast: Element, line: number) => Element | void
273+
token?: (hast: Element, line: number, col: number) => Element | void
274+
}
275+
257276
export interface HtmlRendererOptions {
258277
langId?: string
259278
fg?: string
260279
bg?: string
261280

262-
hastTransform?: (hast: Root) => Root | void
263-
264-
elements?: ElementsOptions
281+
/**
282+
* Hast transformers
283+
*/
284+
transforms?: HastTransformers
265285

266286
themeName?: string
267287

@@ -270,6 +290,7 @@ export interface HtmlRendererOptions {
270290
* When specified, `fg` and `bg` will be ignored.
271291
*/
272292
rootStyle?: string
293+
273294
/**
274295
* Merge token with only whitespace to the next token,
275296
* Saving a few extra `<span>`
@@ -279,39 +300,6 @@ export interface HtmlRendererOptions {
279300
mergeWhitespaces?: boolean
280301
}
281302

282-
export interface ElementsOptions {
283-
pre?: (props: PreElementProps) => string
284-
code?: (props: CodeElementProps) => string
285-
line?: (props: LineElementProps) => string
286-
token?: (props: TokenElementProps) => string
287-
}
288-
289-
interface ElementProps {
290-
children: string
291-
[key: string]: unknown
292-
}
293-
294-
interface PreElementProps extends ElementProps {
295-
className: string
296-
style: string
297-
}
298-
299-
interface CodeElementProps extends ElementProps {}
300-
301-
interface LineElementProps extends ElementProps {
302-
className: string
303-
lines: ThemedToken[][]
304-
line: ThemedToken[]
305-
index: number
306-
}
307-
308-
interface TokenElementProps extends ElementProps {
309-
style: string
310-
tokens: ThemedToken[]
311-
token: ThemedToken
312-
index: number
313-
}
314-
315303
export interface ThemedTokenScopeExplanation {
316304
scopeName: string
317305
themeMatches: any[]

packages/shikiji/test/hast.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, expect, it } from 'vitest'
22
import { toHtml } from 'hast-util-to-html'
3-
import type { Element } from 'hast'
43
import { codeToHtml, getHighlighter } from '../src'
54

65
describe('should', () => {
@@ -23,12 +22,20 @@ describe('should', () => {
2322
const code = await codeToHtml('foo\bar', {
2423
lang: 'js',
2524
theme: 'vitesse-light',
26-
hastTransform(root) {
27-
(root.children[0] as Element).properties.class = 'foo'
25+
transforms: {
26+
line(node, line) {
27+
node.properties['data-line'] = line
28+
},
29+
code(node) {
30+
node.properties.class = 'language-js'
31+
},
32+
token(node, line, col) {
33+
node.properties.class = `token:${line}:${col}`
34+
},
2835
},
2936
})
3037

3138
expect(code)
32-
.toMatchInlineSnapshot('"<pre class=\\"foo\\" style=\\"background-color:#ffffff;color:#393a34\\" tabindex=\\"0\\"><code><span class=\\"line\\"><span style=\\"color:#B07D48\\">foo</span><span style=\\"color:#393A34\\"></span><span style=\\"color:#B07D48\\">ar</span></span></code></pre>"')
39+
.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>"')
3340
})
3441
})

0 commit comments

Comments
 (0)