-
-
Notifications
You must be signed in to change notification settings - Fork 382
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transformers): add Style to Class transformer (#826)
- Loading branch information
Showing
4 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
packages/transformers/src/transformers/style-to-class.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import type { ShikiTransformer } from 'shiki' | ||
|
||
export interface TransformerStyleToClassOptions { | ||
/** | ||
* Prefix for class names. | ||
* @default '__shiki_' | ||
*/ | ||
classPrefix?: string | ||
/** | ||
* Suffix for class names. | ||
* @default '' | ||
*/ | ||
classSuffix?: string | ||
/** | ||
* Callback to replace class names. | ||
* @default (className) => className | ||
*/ | ||
classReplacer?: (className: string) => string | ||
} | ||
|
||
export interface ShikiTransformerStyleToClass extends ShikiTransformer { | ||
getClassRegistry: () => Map<string, Record<string, string> | string> | ||
getCSS: () => string | ||
clearRegistry: () => void | ||
} | ||
|
||
/** | ||
* Remove line breaks between lines. | ||
* Useful when you override `display: block` to `.line` in CSS. | ||
*/ | ||
export function transformerStyleToClass(options: TransformerStyleToClassOptions = {}): ShikiTransformerStyleToClass { | ||
const { | ||
classPrefix = '__shiki_', | ||
classSuffix = '', | ||
classReplacer = (className: string) => className, | ||
} = options | ||
|
||
const classToStyle = new Map<string, Record<string, string> | string>() | ||
|
||
function stringifyStyle(style: Record<string, string>): string { | ||
return Object.entries(style) | ||
.map(([key, value]) => `${key}:${value}`) | ||
.join(';') | ||
} | ||
|
||
function registerStyle(style: Record<string, string> | string): string { | ||
const str = typeof style === 'string' | ||
? style | ||
: stringifyStyle(style) | ||
let className = classPrefix + cyrb53(str) + classSuffix | ||
className = classReplacer(className) | ||
if (!classToStyle.has(className)) { | ||
classToStyle.set( | ||
className, | ||
typeof style === 'string' | ||
? style | ||
: { ...style }, | ||
) | ||
} | ||
return className | ||
} | ||
|
||
return { | ||
name: '@shikijs/transformers:style-to-class', | ||
pre(t) { | ||
if (!t.properties.style) | ||
return | ||
const className = registerStyle(t.properties.style as string) | ||
delete t.properties.style | ||
this.addClassToHast(t, className) | ||
}, | ||
tokens(lines) { | ||
for (const line of lines) { | ||
for (const token of line) { | ||
if (!token.htmlStyle) | ||
continue | ||
|
||
const className = registerStyle(token.htmlStyle) | ||
token.htmlStyle = {} | ||
token.htmlAttrs ||= {} | ||
if (!token.htmlAttrs.class) | ||
token.htmlAttrs.class = className | ||
else | ||
token.htmlAttrs.class += ` ${className}` | ||
} | ||
} | ||
}, | ||
getClassRegistry() { | ||
return classToStyle | ||
}, | ||
getCSS() { | ||
let css = '' | ||
for (const [className, style] of classToStyle.entries()) { | ||
css += `.${className}{${typeof style === 'string' ? style : stringifyStyle(style)}}` | ||
} | ||
return css | ||
}, | ||
clearRegistry() { | ||
classToStyle.clear() | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* A simple hash function. | ||
* | ||
* @see https://stackoverflow.com/a/52171480 | ||
*/ | ||
function cyrb53(str: string, seed = 0): string { | ||
let h1 = 0xDEADBEEF ^ seed | ||
let h2 = 0x41C6CE57 ^ seed | ||
for (let i = 0, ch; i < str.length; i++) { | ||
ch = str.charCodeAt(i) | ||
h1 = Math.imul(h1 ^ ch, 2654435761) | ||
h2 = Math.imul(h2 ^ ch, 1597334677) | ||
} | ||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) | ||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909) | ||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) | ||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909) | ||
|
||
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).slice(0, 6) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { createHighlighter } from 'shiki' | ||
import { expect, it } from 'vitest' | ||
import { transformerStyleToClass } from '../src/transformers/style-to-class' | ||
|
||
it('transformerStyleToClass', async () => { | ||
const shiki = await createHighlighter({ | ||
themes: ['vitesse-dark', 'vitesse-light', 'nord'], | ||
langs: ['typescript'], | ||
}) | ||
|
||
const transformer = transformerStyleToClass() | ||
|
||
const code = ` | ||
const a = Math.random() > 0.5 ? 1 : \`foo\` | ||
`.trim() | ||
|
||
const result = shiki.codeToHtml(code, { | ||
lang: 'typescript', | ||
themes: { | ||
dark: 'vitesse-dark', | ||
light: 'vitesse-light', | ||
nord: 'nord', | ||
}, | ||
defaultColor: false, | ||
transformers: [transformer], | ||
}) | ||
|
||
expect(result.replace(/<span/g, '\n<span')) | ||
.toMatchInlineSnapshot(` | ||
"<pre class="shiki shiki-themes vitesse-dark vitesse-light nord __shiki_uywmyh" tabindex="0"><code> | ||
<span class="line"> | ||
<span class="__shiki_223nhr">const</span> | ||
<span class="__shiki_u5wfov"> a</span> | ||
<span class="__shiki_26darv"> =</span> | ||
<span class="__shiki_u5wfov"> Math</span> | ||
<span class="__shiki_17lqoe">.</span> | ||
<span class="__shiki_6u0ar0">random</span> | ||
<span class="__shiki_k92bfk">()</span> | ||
<span class="__shiki_26darv"> ></span> | ||
<span class="__shiki_1328cg"> 0.5</span> | ||
<span class="__shiki_223nhr"> ?</span> | ||
<span class="__shiki_1328cg"> 1</span> | ||
<span class="__shiki_223nhr"> :</span> | ||
<span class="__shiki_ga6n9x"> \`</span> | ||
<span class="__shiki_23isjw">foo</span> | ||
<span class="__shiki_ga6n9x">\`</span></span></code></pre>" | ||
`) | ||
|
||
expect(transformer.getCSS()).toMatchInlineSnapshot(`".__shiki_223nhr{--shiki-dark:#CB7676;--shiki-light:#AB5959;--shiki-nord:#81A1C1}.__shiki_u5wfov{--shiki-dark:#BD976A;--shiki-light:#B07D48;--shiki-nord:#D8DEE9}.__shiki_26darv{--shiki-dark:#666666;--shiki-light:#999999;--shiki-nord:#81A1C1}.__shiki_17lqoe{--shiki-dark:#666666;--shiki-light:#999999;--shiki-nord:#ECEFF4}.__shiki_6u0ar0{--shiki-dark:#80A665;--shiki-light:#59873A;--shiki-nord:#88C0D0}.__shiki_k92bfk{--shiki-dark:#666666;--shiki-light:#999999;--shiki-nord:#D8DEE9FF}.__shiki_1328cg{--shiki-dark:#4C9A91;--shiki-light:#2F798A;--shiki-nord:#B48EAD}.__shiki_ga6n9x{--shiki-dark:#C98A7D77;--shiki-light:#B5695977;--shiki-nord:#ECEFF4}.__shiki_23isjw{--shiki-dark:#C98A7D;--shiki-light:#B56959;--shiki-nord:#A3BE8C}.__shiki_uywmyh{--shiki-dark:#dbd7caee;--shiki-light:#393a34;--shiki-nord:#d8dee9ff;--shiki-dark-bg:#121212;--shiki-light-bg:#ffffff;--shiki-nord-bg:#2e3440ff}"`) | ||
}) |