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 new theme option with parentSelector API #66

Merged
merged 6 commits into from Dec 24, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions MIGRATING.md
@@ -0,0 +1,31 @@
# Migration Guide

## v1 → v2

### The `colorTheme` plugin option has been deprecated and replaced by the `theme` option.

- If you did not supply `colorTheme`, no change is necessary.
- If your `colorTheme` is a string, rename the `colorTheme` key to `theme`.
- If your `colorTheme` is an object,
- Rename the `defaultTheme` key to `default`.
- If present, rename the `prefersDarkTheme` to `dark`.
- If you have a `prefersLightTheme`, replace it with an entry in the new `media` array. For example:

```diff
{
- defaultTheme: 'Default Dark+',
- prefersLightTheme: 'Solarized Light'
+ default: 'Default Dark+',
+ media: [{
+ match: '(prefers-color-scheme: light)',
+ theme: 'Solarized Light'
+ }]
}
```

### CSS variables and class names have changed

- If you wrote custom CSS targeting the class `.vscode-highlight`, replace that selector with `.grvsc-container`.
- If you wrote custom CSS targeting any other class beginning with `.vscode-highlight`, replace the `.vscode-highlight` prefix with `.grvsc`. For example, `.vscode-highlight-line` is now `.grvsc-line`.
- If you set any CSS variables, replace the `--vscode-highlight` prefix with `--grvsc`. For example, `--vscode-highlight-border-radius` is now `--grvsc-border-radius`.
- If you wrote custom CSS targeting a token class name beginning with `.mtk`, that was never intended to be supported! Consider using `replaceColor` instead, or [file an issue](https://github.com/andrewbranch/gatsby-remark-vscode/issues/new) if you think you have a compelling use case for writing custom token CSS.
72 changes: 54 additions & 18 deletions README.md
@@ -1,6 +1,6 @@
# gatsby-remark-vscode

[![Greenkeeper badge](https://badges.greenkeeper.io/andrewbranch/gatsby-remark-vscode.svg)](https://greenkeeper.io/) [![npm](https://img.shields.io/npm/v/gatsby-remark-vscode.svg)](https://www.npmjs.com/package/gatsby-remark-vscode)
[![npm](https://img.shields.io/npm/v/gatsby-remark-vscode.svg)](https://www.npmjs.com/package/gatsby-remark-vscode)

A syntax highlighting plugin for [Gatsby](https://www.gatsbyjs.org/) that uses VS Code’s extensions, themes, and highlighting engine. Any language and theme VS Code supports, whether built-in or via a [Marketplace extension](https://marketplace.visualstudio.com/vscode), can be rendered on your Gatsby site.

Expand Down Expand Up @@ -56,7 +56,7 @@ Add to your `gatsby-config.js` (all options are optional; defaults shown here):
resolve: `gatsby-remark-vscode`,
// All options are optional. Defaults shown here.
options: {
colorTheme: 'Dark+ (default dark)', // Read on for list of included themes. Also accepts object and function forms.
theme: 'Dark+ (default dark)', // Read on for list of included themes. Also accepts object and function forms.
wrapperClassName: '', // Additional class put on 'pre' tag. Also accepts function to set the class dynamically.
injectStyles: true, // Injects (minimal) additional CSS for layout and scrolling
extensions: [], // Extensions to download from the marketplace to provide more languages and themes
Expand All @@ -68,13 +68,14 @@ Add to your `gatsby-config.js` (all options are optional; defaults shown here):
content, // - the string content of the line
index, // - the zero-based index of the line within the code fence
language, // - the language specified for the code fence
meta // - any options set on the code fence alongside the language (more on this later)
meta // - any options set on the code fence alongside the language (more on this later)
}) => '',
logLevel: 'error' // Set to 'warn' to debug if something looks wrong
logLevel: 'warn' // Set to 'info' to debug if something looks wrong
}
}]
}
}
}]
}
```

Write code examples in your markdown file as usual:
Expand All @@ -85,25 +86,60 @@ this.willBe(highlighted);
```
````

## Dark mode support via `prefers-color-scheme`
## Multi-theme support

You can select different themes to be activated by media query or by parent selector (e.g. a class or data attribute on the `html` or `body` element).

### Reacting to OS dark mode with `prefers-color-scheme`

```js
{
theme: {
default: 'Solarized Light',
dark: 'Monokai Dimmed'
}
}
```

Instead of passing a string for `colorTheme`, you can pass an object specifying which theme to use for different values of a user’s operating system color scheme preference.
### Reacting to a parent selector

```js
// Note: you probably don’t actually want to provide all three options,
// this example just aims to show all possible options.
{
colorTheme: {
defaultTheme: 'Solarized Light', // Required
prefersDarkTheme: 'Monokai Dimmed', // Optional: used with `prefers-color-scheme: dark`
prefersLightTheme: 'Quiet Light' // Optional: used with `prefers-color-scheme: light`
theme: {
default: 'Solarized Light',
parentSelector: {
// Any CSS selector will work!
'html[data-theme=dark]': 'Monokai Dimed',
'html[data-theme=hc]': 'My Cool Custom High Contrast Theme'
}
}
}
```

This places CSS for each theme inside a corresponding [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media query. [See browser support.](https://caniuse.com/#feat=prefers-color-scheme)
### Reacting to other media queries

Generally, you probably don’t need or want to set all three `colorTheme` options—typical usage would be to set `defaultTheme` to a light theme and `prefersDarkTheme` to a dark theme.
The `dark` option is shorthand for a general-purpose `media` option that can be used to match any media query:

```js
{
theme: {
default: 'Solarized Light',
media: [{
// Longhand for `dark` option.
// Don’t forget the parentheses!
match: '(prefers-color-scheme: dark)',
theme: 'Monokai Dimmed'
}, {
// Proposed in Media Queries Level 5 Draft
match: '(prefers-contrast: high)',
theme: 'My Cool Custom High Contrast Theme'
}, {
match: 'print',
theme: 'My Printer Friendly Theme???'
}]
}
}
```

## Built-in languages and themes

Expand Down Expand Up @@ -334,7 +370,7 @@ or by setting custom styles on the lines:

### Using different themes for different code fences

The `colorTheme` option can take a function instead of a constant value. The function is called once per code fence with information about that code fence, and should return either a string or [an object](#dark-mode-support-via-prefers-color-scheme). See the [following section](#arbitrary-code-fence-options) for an example.
The `theme` option can take a function instead of a constant value. The function is called once per code fence with information about that code fence, and should return either a string or [an object](#dark-mode-support-via-prefers-color-scheme). See the [following section](#arbitrary-code-fence-options) for an example.

### Arbitrary code fence options

Expand All @@ -346,11 +382,11 @@ Line numbers and ranges aren’t the only things you can pass as options on your
```
````

`gatsby-remark-vscode` doesn’t inherently understand these things, but it parses the input and allows you to access it in the `colorTheme`, `wrapperClassName` and `getLineClassName` functions:
`gatsby-remark-vscode` doesn’t inherently understand these things, but it parses the input and allows you to access it in the `theme`, `wrapperClassName` and `getLineClassName` functions:

```js
{
colorTheme: ({ parsedOptions, language, markdownNode, codeFenceNode }) => {
theme: ({ parsedOptions, language, markdownNode, codeFenceNode }) => {
// 'language' is 'jsx', in this case
// 'markdownNode' is the gatsby-transformer-remark GraphQL node
// 'codeFenceNode' is the Markdown AST node of the current code fence
Expand Down
2 changes: 1 addition & 1 deletion src/createGetRegistry.js
Expand Up @@ -42,7 +42,7 @@ const getLock = (() => {
* @param {string} rootScopeName
*/
function warnMissingLanguageFile(missingScopeName, rootScopeName) {
logger.warn(`No language file was loaded for scope '${missingScopeName}' (requested by '${rootScopeName}').`);
logger.info(`No language file was loaded for scope '${missingScopeName}' (requested by '${rootScopeName}').`);
}

function createGetRegistry() {
Expand Down
50 changes: 29 additions & 21 deletions src/getPossibleThemes.js
Expand Up @@ -3,7 +3,7 @@ const { concatConditionalThemes } = require('./utils');
const { ensureThemeLocation } = require('./storeUtils');

/**
* @param {ColorThemeOption} themeOption
* @param {ThemeOption} themeOption
* @param {object} themeCache
* @param {object} markdownNode
* @param {object} codeFenceNode
Expand Down Expand Up @@ -41,33 +41,41 @@ async function getPossibleThemes(themeOption, themeCache, markdownNode, codeFenc

/** @type {ConditionalTheme[]} */
let themes;
if (themeOption.defaultTheme) {
themes = await getPossibleThemes(
themeOption.defaultTheme,
themeCache,
markdownNode,
codeFenceNode,
languageName,
meta
);
if (themeOption.default) {
themes = await getPossibleThemes(themeOption.default, themeCache, markdownNode, codeFenceNode, languageName, meta);
}
if (themeOption.prefersDarkTheme) {
if (themeOption.dark) {
themes = concatConditionalThemes(themes, [
{
identifier: themeOption.prefersDarkTheme,
path: await ensureThemeLocation(themeOption.prefersDarkTheme, themeCache, markdownNode.fileAbsolutePath),
identifier: themeOption.dark,
path: await ensureThemeLocation(themeOption.dark, themeCache, markdownNode.fileAbsolutePath),
conditions: [{ condition: 'matchMedia', value: '(prefers-color-scheme: dark)' }]
}
]);
}
if (themeOption.prefersLightTheme) {
themes = concatConditionalThemes(themes, [
{
identifier: themeOption.prefersDarkTheme,
path: await ensureThemeLocation(themeOption.prefersDarkTheme, themeCache, markdownNode.fileAbsolutePath),
conditions: [{ condition: 'matchMedia', value: '(prefers-color-scheme: dark)' }]
}
]);
if (themeOption.media) {
themes = concatConditionalThemes(
themes,
await Promise.all(
themeOption.media.map(async setting => ({
identifier: setting.theme,
path: await ensureThemeLocation(setting.theme, themeCache, markdownNode.fileAbsolutePath),
conditions: [{ condition: /** @type {'matchMedia'} */ ('matchMedia'), value: setting.match }]
}))
)
);
}
if (themeOption.parentSelector) {
themes = concatConditionalThemes(
themes,
await Promise.all(
Object.keys(themeOption.parentSelector).map(async key => ({
identifier: themeOption.parentSelector[key],
path: await ensureThemeLocation(themeOption.parentSelector[key], themeCache, markdownNode.fileAbsolutePath),
conditions: [{ condition: /** @type {'parentSelector'} */ ('parentSelector'), value: key }]
}))
)
);
}

return themes;
Expand Down
39 changes: 27 additions & 12 deletions src/index.js
Expand Up @@ -16,11 +16,13 @@ const { getGrammar, getScope } = require('./storeUtils');
const { renderHTML, span, code, pre, style, mergeAttributes, TriviaRenderFlags } = require('./renderers/html');
const { joinClassNames, ruleset, media, declaration } = require('./renderers/css');
const {
deprecationNotice,
getThemeClassName,
getThemeClassNames,
getStylesFromThemeSettings,
flatMap,
groupConditions
groupConditions,
convertLegacyThemeOption
} = require('./utils');
const styles = fs.readFileSync(path.resolve(__dirname, '../styles.css'), 'utf8');

Expand All @@ -34,23 +36,33 @@ function createPlugin() {
async function textmateHighlight(
{ markdownAST, markdownNode, cache },
{
colorTheme = 'Default Dark+',
theme = 'Default Dark+',
colorTheme: legacyTheme,
wrapperClassName = '',
languageAliases = {},
extensions = [],
getLineClassName = () => '',
injectStyles = true,
replaceColor = x => x,
extensionDataDirectory = path.resolve(__dirname, '../lib/extensions'),
logLevel = 'error',
logLevel = 'warn',
host = defaultHost,
getLineTransformers = getDefaultLineTransformers,
...rest
} = {}
) {
logger.setLevel(logLevel);
if (legacyTheme) {
deprecationNotice(
`The 'colorTheme' option has been replaced by 'theme' and will be removed in a future version. ` +
`See https://github.com/andrewbranch/gatsby-remark-vscode/blob/master/MIGRATING.md for details.`,
'colorThemeWarning'
);
theme = convertLegacyThemeOption(legacyTheme);
}

const lineTransformers = getLineTransformers({
colorTheme,
theme,
wrapperClassName,
languageAliases,
extensions,
Expand Down Expand Up @@ -91,7 +103,7 @@ function createPlugin() {
}

const possibleThemes = await getPossibleThemes(
colorTheme,
theme,
await cache.get('themes'),
markdownNode,
node,
Expand Down Expand Up @@ -140,7 +152,7 @@ function createPlugin() {
lineIndex,
/** @returns {grvsc.HTMLElement | string} */
(tokenText, classNamesByTheme) =>
span({ class: classNamesByTheme.map(name => name.value).join(' ') }, [escapeHTML(tokenText)], {
span({ class: flatMap(classNamesByTheme, name => name.value).join(' ') }, [escapeHTML(tokenText)], {
whitespace: TriviaRenderFlags.NoWhitespace
}),
lineText => lineText
Expand Down Expand Up @@ -182,30 +194,33 @@ function createPlugin() {
const tokenClassNames = nodeRegistry.getTokenStylesForTheme(theme.identifier);
const containerStyles = getStylesFromThemeSettings(settings);
if (conditions.default) {
pushColorRules(elements, getThemeClassName(theme.identifier, 'default'));
pushColorRules(elements, '.' + getThemeClassName(theme.identifier, 'default'));
}
for (const condition of conditions.parentSelector) {
pushColorRules(elements, `${condition.value} .${getThemeClassName(theme.identifier, 'parentSelector')}`);
}
for (const condition of conditions.matchMedia) {
/** @type {grvsc.CSSRuleset[]} */
const ruleset = [];
pushColorRules(ruleset, getThemeClassName(theme.identifier, 'matchMedia'));
pushColorRules(ruleset, '.' + getThemeClassName(theme.identifier, 'matchMedia'));
elements.push(media(condition.value, ruleset, theme.identifier));
}
return elements;

/**
* @param {grvsc.CSSElement[]} container
* @param {string} parentClassName
* @param {string} selector
* @param {string=} leadingComment
*/
function pushColorRules(container, parentClassName, leadingComment) {
function pushColorRules(container, selector, leadingComment) {
if (containerStyles.length) {
container.push(ruleset(`.${parentClassName}`, containerStyles, leadingComment));
container.push(ruleset(selector, containerStyles, leadingComment));
leadingComment = undefined;
}
for (const { className, css } of tokenClassNames) {
container.push(
ruleset(
`.${parentClassName} .${className}`,
`${selector} .${className}`,
css.map(decl =>
decl.property === 'color' ? declaration('color', replaceColor(decl.value, theme.identifier)) : decl
),
Expand Down