Created
July 3, 2020 20:38
-
-
Save Gerrit0/275a4b8ffee4fa133fd075f5edeb3cda to your computer and use it in GitHub Desktop.
This file contains hidden or 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
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