From 55ce40ba92869208beb09f755300fb07ffbc152d Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Thu, 26 Aug 2021 14:05:43 -0700 Subject: [PATCH 1/3] add css variables theme --- docs/themes.md | 58 +++++++ packages/shiki/themes/css-variables.json | 200 +++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 packages/shiki/themes/css-variables.json diff --git a/docs/themes.md b/docs/themes.md index 6472f34ab..aaa487378 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -26,7 +26,65 @@ 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(--code-block-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 + +``` ## All Themes ```ts diff --git a/packages/shiki/themes/css-variables.json b/packages/shiki/themes/css-variables.json new file mode 100644 index 000000000..93d2918e6 --- /dev/null +++ b/packages/shiki/themes/css-variables.json @@ -0,0 +1,200 @@ +{ + "name": "css-variables", + "type": "light", + "colors": { + "editor.foreground": "var(--code-foreground)", + "editor.background": "var(--code-background)" + }, + "tokenColors": [ + { + "settings": { + "foreground": "var(--code-token-default)" + } + }, + { + "scope": [ + "keyword.operator.accessor", + "meta.group.braces.round.function.arguments", + "meta.template.expression", + "markup.fenced_code meta.embedded.block" + ], + "settings": { + "foreground": "var(--code-token-default)" + } + }, + { + "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": "var(--code-token-constant)" + } + }, + { + "scope": ["string", "markup.fenced_code", "markup.inline"], + "settings": { + "foreground": "var(--code-token-string)" + } + }, + { + "scope": ["comment", "string.quoted.docstring.multi"], + "settings": { + "foreground": "var(--code-token-comment)" + } + }, + { + "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": "var(--code-token-constant)" + } + }, + { + "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": "var(--code-token-keyword)" + } + }, + { + "scope": "variable.parameter.function", + "settings": { + "foreground": "var(--code-token-parameter)" + } + }, + { + "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": "var(--code-token-function)" + } + }, + { + "scope": [ + "entity.name.tag", + "string.quoted", + "string.regexp", + "string.interpolated", + "string.template", + "string.unquoted.plain.out.yaml", + "keyword.other.template" + ], + "settings": { + "foreground": "var(--code-token-string-expression)" + } + }, + { + "scope": "token.info-token", + "settings": { + "foreground": "var(--code-token-info)" + } + }, + { + "scope": "token.warn-token", + "settings": { + "foreground": "var(--code-token-warn)" + } + }, + { + "scope": "token.error-token", + "settings": { + "foreground": "var(--code-token-warn)" + } + }, + { + "scope": "token.debug-token", + "settings": { + "foreground": "var(--code-token-debug)" + } + }, + { + "scope": ["strong", "markup.heading.markdown", "markup.bold.markdown"], + "settings": { + "foreground": "var(--code-token-strong)" + } + }, + { + "scope": [ + "punctuation.definition.arguments", + "punctuation.definition.dict", + "punctuation.separator", + "meta.function-call.arguments" + ], + "settings": { + "foreground": "var(--code-token-punctuation)" + } + }, + { + "name": "[Custom] Markdown links", + "scope": ["markup.underline.link", "punctuation.definition.metadata.markdown"], + "settings": { + "foreground": "var(--code-token-link)" + } + }, + { + "name": "[Custom] Markdown list", + "scope": ["beginning.punctuation.definition.list.markdown"], + "settings": { + "foreground": "var(--code-token-string)" + } + }, + { + "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": "var(--code-token-keyword)" + } + } + ] +} From c2d564c8872e117cc747f87e781881be3a89e415 Mon Sep 17 00:00:00 2001 From: Orta Date: Thu, 26 Aug 2021 22:25:46 +0100 Subject: [PATCH 2/3] Ship a test --- .../__snapshots__/cssVars.test.ts.snap | 15 +++++++++++++ packages/shiki/src/__tests__/cssVars.test.ts | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap create mode 100644 packages/shiki/src/__tests__/cssVars.test.ts diff --git a/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap b/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap new file mode 100644 index 000000000..8a417c2a2 --- /dev/null +++ b/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`The theme with css-variables renders correctly 1`] = ` +"

+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()
+})
+
" +`; diff --git a/packages/shiki/src/__tests__/cssVars.test.ts b/packages/shiki/src/__tests__/cssVars.test.ts new file mode 100644 index 000000000..d97d8162f --- /dev/null +++ b/packages/shiki/src/__tests__/cssVars.test.ts @@ -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() +}) From 61103c2c7a91e761cb95518545a058a485bc32da Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 29 Aug 2021 22:05:48 -0700 Subject: [PATCH 3/3] get css variables theme working without vscode support --- docs/themes.md | 31 ++++------ .../__snapshots__/cssVars.test.ts.snap | 18 +++--- packages/shiki/src/highlighter.ts | 30 +++++++++ packages/shiki/themes/css-variables.json | 62 +++++-------------- 4 files changed, 68 insertions(+), 73 deletions(-) diff --git a/docs/themes.md b/docs/themes.md index aaa487378..ec4c5163b 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -52,7 +52,7 @@ This gives you access to CSS variable styling, which you can control across Dark 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(--code-block-XXX)"` which you can use to style your code blocks using CSS. +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 @@ -65,23 +65,17 @@ Note that this client-side theme is less granular than most other supported VSCo ```html ``` @@ -89,6 +83,7 @@ Note that this client-side theme is less granular than most other supported VSCo ```ts export type Theme = + | 'css-variables' | 'dark-plus' | 'dracula-soft' | 'dracula' diff --git a/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap b/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap index 8a417c2a2..f9f9568a2 100644 --- a/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap +++ b/packages/shiki/src/__tests__/__snapshots__/cssVars.test.ts.snap @@ -1,15 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`The theme with css-variables renders correctly 1`] = ` -"

-import { getHighlighter } from '../index'
+"

+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()
-})
+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()
+})
 
" `; diff --git a/packages/shiki/src/highlighter.ts b/packages/shiki/src/highlighter.ts index fc989a2bf..41739e4a4 100644 --- a/packages/shiki/src/highlighter.ts +++ b/packages/shiki/src/highlighter.ts @@ -56,6 +56,33 @@ export async function getHighlighter(options: HighlighterOptions): Promise = { + '#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) { @@ -66,6 +93,9 @@ export async function getHighlighter(options: HighlighterOptions): Promise