Skip to content

Instantly share code, notes, and snippets.

@Gerrit0
Created July 3, 2020 20:38
Show Gist options
  • Save Gerrit0/275a4b8ffee4fa133fd075f5edeb3cda to your computer and use it in GitHub Desktop.
Save Gerrit0/275a4b8ffee4fa133fd075f5edeb3cda to your computer and use it in GitHub Desktop.
import { ok as assert } from 'assert'
import { readFile, writeFile } from 'fs/promises'
import { getHighlighter, getTheme } from 'shiki'
// This is bad... but Shiki doesn't export it from the root.
import { Highlighter } from 'shiki/dist/highlighter'
import { TLang } from 'shiki-languages'
import { TTheme } from 'shiki-themes'
import { createElement, JSX } from 'preact'
import renderToString from 'preact-render-to-string'
class DoubleHighlighter {
private schemes = new Map<string, string>();
static async create(lightTheme: TTheme, darkTheme: TTheme) {
const light = getTheme(lightTheme).bg
const dark = getTheme(darkTheme).bg
const [lightHl, darkHl] = await Promise.all([
getHighlighter({ theme: lightTheme }),
getHighlighter({ theme: darkTheme })
])
return new DoubleHighlighter(lightHl, light, darkHl, dark)
}
private constructor(private light: Highlighter, private lightBg: string, private dark: Highlighter, private darkBg: string) {
}
highlight(code: string, lang: TLang) {
const lines = code.split(/\r\n|\r|\n/)
const lightTokens = this.light.codeToThemedTokens(code, lang)
const darkTokens = this.dark.codeToThemedTokens(code, lang)
// If this fails... something went *very* wrong.
assert(lightTokens.length === darkTokens.length)
const docEls: JSX.Element[][] = []
for (let line = 0; line < lightTokens.length; line++) {
const lightLine = lightTokens[line]
const darkLine = darkTokens[line]
const text = lines[line]
// Different themes can have different grammars... so unfortunately we have to deal with different
// sets of tokens.Example: light_plus and dark_plus tokenize " = " differently in the `schemes`
// declaration for this file.
const lineEls: JSX.Element[] = []
while (lightLine.length && darkLine.length) {
// Simple case, same token.
if (lightLine[0].content === darkLine[0].content) {
lineEls.push(<span class={this.getClass(lightLine[0].color, darkLine[0].color)}>
{lightLine[0].content}
</span>)
lightLine.shift()
darkLine.shift()
continue
}
if (lightLine[0].content.length < darkLine[0].content.length) {
lineEls.push(<span class={this.getClass(lightLine[0].color, darkLine[0].color)}>
{lightLine[0].content}
</span>)
darkLine[0].content = darkLine[0].content.substr(lightLine[0].content.length)
lightLine.shift()
continue
}
lineEls.push(<span class={this.getClass(lightLine[0].color, darkLine[0].color)}>
{darkLine[0].content}
</span>)
lightLine[0].content = lightLine[0].content.substr(darkLine[0].content.length)
darkLine.shift()
}
lineEls.push(<br />)
docEls.push(lineEls)
}
return <div class='code' dangerouslySetInnerHTML={{ __html: renderToString(<code>{docEls}</code>) }} />
}
getStyles() {
let styles = Array.from(this.schemes.keys(), (key, i) => {
const [light, dark] = key.split(' | ')
return [
`.hl-${i} { color: ${light}; }`,
`.dark .hl-${i} { color: ${dark}; }`
].join('\n')
}).join('\n')
styles += `body { background: ${this.lightBg}; }`
styles += `body.dark { background: ${this.darkBg}; }`
styles += `code { white-space: pre-wrap; }`
return <style dangerouslySetInnerHTML={{__html: styles }}/>
}
private getClass(lightColor?: string, darkColor?: string): string {
const key = `${lightColor} | ${darkColor}`
let scheme = this.schemes.get(key)
if (scheme == null) {
scheme = `hl-${this.schemes.size}`
this.schemes.set(key, scheme)
}
return scheme
}
}
async function main() {
const code = await readFile('highlight.tsx', 'utf-8')
const highlighter = await DoubleHighlighter.create('light_plus', 'monokai_dimmed')
const highlighted = highlighter.highlight(code, 'tsx');
const rendered = <html>
<head>
{highlighter.getStyles()}
</head>
<body>
<script>
const loads = +localStorage.getItem('loads') || 0;
localStorage.setItem('loads', loads + 1);
if (loads % 2) document.body.classList.add('dark');
</script>
{highlighted}
</body>
</html>
await writeFile('highlight.html', renderToString(rendered, null, { pretty: true }));
}
main().catch(console.error)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment