Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "css-variables" theme, support dark mode #212

Merged
merged 3 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,64 @@ shiki.getHighlighter({
theme: t
})
```
## Dark Mode Support

Because Shiki generates themes at build time, client-side theme switching support is not built in. There are two popular two options for supporting something like Dark Mode with Shiki.

#### 1. Use the "css-variables" theme.

This gives you access to CSS variable styling, which you can control across Dark and Light mode. See the [Theming with CSS Variables](#theming-with-css-variables) section below for more details.
#### 2. Generate two Shiki code blocks, one for each theme.

```css
@media (prefers-color-scheme: light) {
.shiki.dark-plus {
display: none;
}
}
@media (prefers-color-scheme: dark) {
.shiki.light-plus {
display: none;
}
}
```

## Theming with CSS Variables

Shiki handles all theme logic at build-time, so that the browser only ever sees already-computed `style="color: #XXXXXX"` attributes. This allows more granular theme support in a way that doesn't require any additional steps to add global CSS to your page.

In some cases, a user may require custom client-side theming via CSS. To support this, you may use the `css-variables` theme with Shiki. This is a special theme that uses CSS variables for colors instead of hardcoded values. Each token in your code block is given an attribute of `style="color: var(--shiki-XXX)"` which you can use to style your code blocks using CSS.


```js
const shiki = require('shiki')
shiki.getHighlighter({theme: 'css-variables'})
```

Note that this client-side theme is less granular than most other supported VSCode themes. Also, be aware that this will generate unstyled code if you do not define these CSS variables somewhere else on your page:

```html
<style>
:root {
--shiki-color-text: #EEEEEE;
--shiki-color-background: #333333;
--shiki-token-constant: #660000;
--shiki-token-string: #770000;
--shiki-token-comment: #880000;
--shiki-token-keyword: #990000;
--shiki-token-parameter: #AA0000;
--shiki-token-function: #BB0000;
--shiki-token-string-expression: #CC0000;
--shiki-token-punctuation: #DD0000;
--shiki-token-link: #EE0000;
}
</style>
```
## All Themes

```ts
export type Theme =
| 'css-variables'
| 'dark-plus'
| 'dracula-soft'
| 'dracula'
Expand Down
15 changes: 15 additions & 0 deletions packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The theme with css-variables renders correctly 1`] = `
"<pre class=\\"shiki\\" style=\\"background-color: #000002\\"><code><span class=\\"line\\"></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-token-keyword)\\">import</span><span style=\\"color: var(--shiki-color-text)\\"> { getHighlighter } </span><span style=\\"color: var(--shiki-token-keyword)\\">from</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-string-expression)\\">&#39;../index&#39;</span></span>
<span class=\\"line\\"></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-token-function)\\">test</span><span style=\\"color: var(--shiki-color-text)\\">(</span><span style=\\"color: var(--shiki-token-string-expression)\\">&#39;The theme with css-variables renders correctly&#39;</span><span style=\\"color: var(--shiki-token-punctuation)\\">,</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-keyword)\\">async</span><span style=\\"color: var(--shiki-color-text)\\"> () </span><span style=\\"color: var(--shiki-token-keyword)\\">=&gt;</span><span style=\\"color: var(--shiki-color-text)\\"> {</span></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-keyword)\\">const</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-constant)\\">highlighter</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-keyword)\\">=</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-keyword)\\">await</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-function)\\">getHighlighter</span><span style=\\"color: var(--shiki-color-text)\\">({</span></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-color-text)\\"> theme</span><span style=\\"color: var(--shiki-token-keyword)\\">:</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-string-expression)\\">&#39;css-variables&#39;</span></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-color-text)\\"> })</span></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-keyword)\\">const</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-constant)\\">out</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-keyword)\\">=</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-constant)\\">highlighter</span><span style=\\"color: var(--shiki-token-function)\\">.codeToHtml</span><span style=\\"color: var(--shiki-color-text)\\">(</span><span style=\\"color: var(--shiki-token-string-expression)\\">&quot;console.log(&#39;shiki&#39;);&quot;</span><span style=\\"color: var(--shiki-token-punctuation)\\">,</span><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-string-expression)\\">&#39;js&#39;</span><span style=\\"color: var(--shiki-color-text)\\">)</span></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-color-text)\\"> </span><span style=\\"color: var(--shiki-token-function)\\">expect</span><span style=\\"color: var(--shiki-color-text)\\">(out)</span><span style=\\"color: var(--shiki-token-function)\\">.toMatchSnapshot</span><span style=\\"color: var(--shiki-color-text)\\">()</span></span>
<span class=\\"line\\"><span style=\\"color: var(--shiki-color-text)\\">})</span></span>
<span class=\\"line\\"></span></code></pre>"
`;
22 changes: 22 additions & 0 deletions packages/shiki/src/__tests__/cssVars.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getHighlighter } from '../index'

test('The theme with css-variables renders correctly', async () => {
const highlighter = await getHighlighter({
theme: 'css-variables'
})

const kindOfAQuine = `
import { getHighlighter } from '../index'

test('The theme with css-variables renders correctly', async () => {
const highlighter = await getHighlighter({
theme: 'css-variables'
})
const out = highlighter.codeToHtml("console.log('shiki');", 'js')
expect(out).toMatchSnapshot()
})
`

const out = highlighter.codeToHtml(kindOfAQuine, 'js')
expect(out).toMatchSnapshot()
})
30 changes: 30 additions & 0 deletions packages/shiki/src/highlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ export async function getHighlighter(options: HighlighterOptions): Promise<Highl
let _currentTheme: IShikiTheme | undefined
await _registry.loadLanguages(_languages)

/**
* Shiki was designed for VSCode, so CSS variables are not currently supported.
* See: https://github.com/shikijs/shiki/pull/212#issuecomment-906924986
*
* Instead, we work around this by using valid hex color codes as lookups in a
* final "repair" step which translates those codes to the correct CSS variables.
*/
const COLOR_REPLACEMENTS: Record<string, string> = {
'#000001': 'var(--shiki-color-text)',
'#000002': 'var(--shiki-color-background)',
'#000004': 'var(--shiki-token-constant)',
'#000005': 'var(--shiki-token-string)',
'#000006': 'var(--shiki-token-comment)',
'#000007': 'var(--shiki-token-keyword)',
'#000008': 'var(--shiki-token-parameter)',
'#000009': 'var(--shiki-token-function)',
'#000010': 'var(--shiki-token-string-expression)',
'#000011': 'var(--shiki-token-punctuation)',
'#000012': 'var(--shiki-token-link)'
}

function fixCssVariablesColorMap(colorMap: string[]) {
colorMap.forEach((val, i) => {
colorMap[i] = COLOR_REPLACEMENTS[val] || val
})
}

function getTheme(theme?: IThemeRegistration) {
const _theme = theme ? _registry.getTheme(theme) : _defaultTheme
if (!_theme) {
Expand All @@ -66,6 +93,9 @@ export async function getHighlighter(options: HighlighterOptions): Promise<Highl
_currentTheme = _theme
}
const _colorMap = _registry.getColorMap()
if (_theme.name === 'css-variables') {
fixCssVariablesColorMap(_colorMap)
}
return { _theme, _colorMap }
}

Expand Down
170 changes: 170 additions & 0 deletions packages/shiki/themes/css-variables.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{
"name": "css-variables",
"type": "light",
"colors": {
"editor.foreground": "#000001",
"editor.background": "#000002"
},
"tokenColors": [
{
"settings": {
"foreground": "#000001"
}
},
{
"scope": [
"keyword.operator.accessor",
"meta.group.braces.round.function.arguments",
"meta.template.expression",
"markup.fenced_code meta.embedded.block"
],
"settings": {
"foreground": "#000001"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": ["strong", "markup.heading.markdown", "markup.bold.markdown"],
"settings": {
"fontStyle": "bold"
}
},
{
"scope": ["markup.italic.markdown"],
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "meta.link.inline.markdown",
"settings": {
"fontStyle": "underline",
"foreground": "#000004"
}
},
{
"scope": ["string", "markup.fenced_code", "markup.inline"],
"settings": {
"foreground": "#000005"
}
},
{
"scope": ["comment", "string.quoted.docstring.multi"],
"settings": {
"foreground": "#000006"
}
},
{
"scope": [
"constant.numeric",
"constant.language",
"constant.other.placeholder",
"constant.character.format.placeholder",
"variable.language.this",
"variable.other.object",
"variable.other.class",
"variable.other.constant",
"meta.property-name",
"meta.property-value",
"support"
],
"settings": {
"foreground": "#000004"
}
},
{
"scope": [
"keyword",
"storage.modifier",
"storage.type",
"storage.control.clojure",
"entity.name.function.clojure",
"entity.name.tag.yaml",
"support.function.node",
"support.type.property-name.json",
"punctuation.separator.key-value",
"punctuation.definition.template-expression"
],
"settings": {
"foreground": "#000007"
}
},
{
"scope": "variable.parameter.function",
"settings": {
"foreground": "#000008"
}
},
{
"scope": [
"support.function",
"entity.name.type",
"entity.other.inherited-class",
"meta.function-call",
"meta.instance.constructor",
"entity.other.attribute-name",
"entity.name.function",
"constant.keyword.clojure"
],
"settings": {
"foreground": "#000009"
}
},
{
"scope": [
"entity.name.tag",
"string.quoted",
"string.regexp",
"string.interpolated",
"string.template",
"string.unquoted.plain.out.yaml",
"keyword.other.template"
],
"settings": {
"foreground": "#000010"
}
},
{
"scope": [
"punctuation.definition.arguments",
"punctuation.definition.dict",
"punctuation.separator",
"meta.function-call.arguments"
],
"settings": {
"foreground": "#000011"
}
},
{
"name": "[Custom] Markdown links",
"scope": ["markup.underline.link", "punctuation.definition.metadata.markdown"],
"settings": {
"foreground": "#000012"
}
},
{
"name": "[Custom] Markdown list",
"scope": ["beginning.punctuation.definition.list.markdown"],
"settings": {
"foreground": "#000005"
}
},
{
"name": "[Custom] Markdown punctuation definition brackets",
"scope": [
"punctuation.definition.string.begin.markdown",
"punctuation.definition.string.end.markdown",
"string.other.link.title.markdown",
"string.other.link.description.markdown"
],
"settings": {
"foreground": "#000007"
}
}
]
}