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

Code span highlighting #89

Merged
merged 5 commits into from
May 4, 2020
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
146 changes: 131 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ If you’re updating from v1.x.x to v2.x.x, see [MIGRATING.md](./MIGRATING.md).
- [Variables](#variables)
- [Tweaking or replacing theme colors](#tweaking-or-replacing-theme-colors)
- [Extra stuff](#extra-stuff)
- [Inline code highlighting](#inline-code-highlighting)
- [Line highlighting](#line-highlighting)
- [Using different themes for different code fences](#using-different-themes-for-different-code-fences)
- [Arbitrary code fence options](#arbitrary-code-fence-options)
- [Options reference](#options-reference)
- [Contributing](#contributing)

## Why gatsby-remark-vscode?
Expand All @@ -48,7 +50,7 @@ Install the package:
npm install --save gatsby-remark-vscode
```

Add to your `gatsby-config.js` (all options are optional; defaults shown here):
Add to your `gatsby-config.js`:

```js
{
Expand All @@ -58,21 +60,8 @@ Add to your `gatsby-config.js` (all options are optional; defaults shown here):
options: {
plugins: [{
resolve: `gatsby-remark-vscode`,
// All options are optional. Defaults shown here.
options: {
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: [], // Third-party extensions providing additional themes and languages
languageAliases: {}, // Map of custom/unknown language codes to standard/known language codes
replaceColor: x => x, // Function allowing replacement of a theme color with another. Useful for replacing hex colors with CSS variables.
getLineClassName: ({ // Function allowing dynamic setting of additional class names on individual lines
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)
}) => '',
logLevel: 'warn' // Set to 'info' to debug if something looks wrong
theme: 'Abyss' // Or install your favorite theme from GitHub
}
}]
}
Expand Down Expand Up @@ -329,6 +318,41 @@ Since the CSS for token colors is auto-generated, it’s fragile and inconvenien

## Extra stuff

### Inline code highlighting

To highlight inline code spans, add an `inlineCode` key to the plugin options and choose a `marker` string:

```js
{
inlineCode: {
marker: '•'
}
}
```

Then, in your Markdown, you can prefix code spans by the language name followed by the `marker` string to opt into highlighting that span:

```md
Now you can highlight inline code: `js•Array.prototype.concat.apply([], array)`.
```

The syntax theme defaults to the one selected for code blocks, but you can control the inline code theme independently:

```js
{
theme: 'Default Dark+',
inlineCode: {
marker: '•',
theme: {
default: 'Default Light+',
dark: 'Default Dark+'
}
}
}
```

See [`inlineCode`](#inlinecode) in the options reference for more API details.

### Line highlighting

`gatsby-remark-vscode` offers the same line-range-after-language-name strategy of highlighting or emphasizing lines as [gatsby-remark-prismjs](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-remark-prismjs):
Expand Down Expand Up @@ -416,6 +440,98 @@ Line numbers and ranges aren’t the only things you can pass as options on your
}
```

## Options reference

### `theme`

The syntax theme used for code blocks.

- **Default:** `'Default Dark+'`
- **Accepted types**:
- **`string`:** The name or id of a theme. (See [Built-in themes](#themes) and [Using languages and themes from an extension](#using-languages-and-themes-from-an-extension).)
- **`ThemeSettings`:** An object that selects different themes to use in different contexts. (See [Multi-theme support](#multi-theme-support).)
- **`(data: CodeBlockData) => string | ThemeSettings`:** A function returning the theme selection for a given code block. `CodeBlockData` is an object with properties:
- **`language`:** The language of the code block, if one was specified.
- **`markdownNode`:** The MarkdownRemark GraphQL node.
- **`node`:** The Remark AST node of the code block.
- **`parsedOptions`:** The object form of of any code fence info supplied. (See [Arbitrary code fence options](#arbitrary-code-fence-options).)

### `wrapperClassName`

A custom class name to be set on the `pre` tag.

- **Default:** None, but the class `grvsc-container` will always be on the tag.
- **Accepted types:**
- **`string`:** The class name to add.
- **`(data: CodeBlockData) => string`:** A function returning the class name to add for a given code block. (See the [`theme`](#theme) option above for the details of `CodeBlockData`.)

### `languageAliases`

An object that allows additional language names to be mapped to recognized languages so they can be used on opening code fences:

```js
{
languageAliases: {
fish: 'sh'
}
}
```

````md
Then you can use code fences like this:

```fish
ls -la
```

And they’ll be parsed as shell script (`sh`).
````

- **Default:** None, but many built-in languages are already recognized by a variety of names.
- **Accepted type:** `Record<string, string>`; that is, an object with string keys and string values.

### `extensions`

A list of third party extensions to search for additional langauges and themes. (See [Using languages and themes from an extension](#using-languages-and-themes-from-an-extension).)

- **Default:** None
- **Accepted type:** `string[]`; that is, an array of strings, where the strings are the package names of the extensions.

### `inlineCode`

Enables syntax highlighting for inline code spans. (See [Inline code highlighting](#inline-code-highlighting).)

- **Default:** None
- **Accepted type:** An object with properties:
- **`theme`:** A string or `ThemeSettings` object selecting the theme, or a function returning a string or `ThemeSettings` object for a given code span. The type is the same as the one documented in the top-level [theme option](#theme). Defaults to the value of the top-level [theme option](#theme).
- **`marker`:** A string used as a separator between the language name and the content of a code span. For example, with a `marker` of value `'•'`, you can highlight a code span as JavaScript by writing the Markdown code span as `` `js•Code.to.highlight("inline")` ``.
- **`className`:** A string, or function returning a string for a given code span, that sets a custom class name on the wrapper `code` HTML tag. If the function form is used, it is passed an object parameter describing the code span with properties:
- **`language`:** The language of the code span (the bit before the `marker` character).
- **`markdownNode`:** The MarkdownRemark GraphQL node.
- **`node`:** The Remark AST node of the code span.

### `injectStyles`

Whether to add supporting CSS to the end of the Markdown document. (See [Styles](#styles).)

- **Default:** `true`
- **Accepted type:** `boolean`

### `replaceColor`

A function allowing individual color values to be replaced in the generated CSS. (See [Tweaking or replacing theme colors](#tweaking-or-replacing-theme-colors).)

- **Default:** None
- **Accepted type:** `(colorValue: string, theme: string) => string`; that is, a function that takes the original color and the identifier of the theme it came from and returns a new color value.

### `logLevel`

The verbosity of logging. Useful for diagnosing unexpected behavior.

- **Default**: `'warn'`
- **Accepted values:** From most verbose to least verbose, `'trace'`, `'debug'`, `'info'`, `'warn'`, or `'error'`.


## Contributing

Please note that this project is released with a Contributor [Code of Conduct](./CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
Expand Down
88 changes: 50 additions & 38 deletions gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,15 @@ exports.createResolvers = ({
grvscCodeBlocks: {
type: ['GRVSCCodeBlock'],
resolve(source, _, context) {
return getFromCache();

/** @param {boolean=} stop */
async function getFromCache(stop) {
const childNodes = await getChildNodes(cache, source.id, source.internal.contentDigest);
// Hack alert: ensure plugin has been run by querying htmlAst,
// which is set via `setFieldsOnGraphQLNodeType` by gatsby-transformer-remark,
// therefore might not have been run before this resolver runs.
if (!childNodes && !stop) {
await context.nodeModel.runQuery({
query: {
filter: {
id: { eq: source.id },
htmlAst: { ne: null },
},
},
type: 'MarkdownRemark',
firstOnly: true,
});
return getFromCache(true);
}
if (!childNodes) {
logger.error(
'gatsby-remark-vscode couldn’t retrieve up-to-date GRVSCCodeBlock GraphQL nodes. ' +
'The `GRVSCCodeBlocks` field may be missing, empty or stale. ' +
'The Gatsby cache is probably in a weird state. Try running `gatsby clean`, and file an ' +
'issue at https://github.com/andrewbranch/gatsby-remark-vscode/issues/new if the problem persists.'
);

return context.nodeModel.runQuery({
query: { parent: { id: { eq: source.id } } },
type: 'GRVSCCodeBlock',
firstOnly: false
});
}
return childNodes || [];
}
},
return getFromCache('GRVSCCodeBlock', cache, source, context);
}
},
grvscCodeSpans: {
type: ['GRVSCCodeSpan'],
resolve(source, _, context) {
return getFromCache('GRVSCCodeSpan', cache, source, context);
}
}
},

Query: {
Expand Down Expand Up @@ -83,3 +53,45 @@ exports.createResolvers = ({
}
});
};

/**
* @param {string} type
* @param {any} cache
* @param {any} source
* @param {any} context
* @param {boolean=} stop
*/
async function getFromCache(type, cache, source, context, stop) {
const childNodes = await getChildNodes(cache, source.id, source.internal.contentDigest);
// Hack alert: ensure plugin has been run by querying htmlAst,
// which is set via `setFieldsOnGraphQLNodeType` by gatsby-transformer-remark,
// therefore might not have been run before this resolver runs.
if (!childNodes && !stop) {
await context.nodeModel.runQuery({
query: {
filter: {
id: { eq: source.id },
htmlAst: { ne: null },
},
},
type: 'MarkdownRemark',
firstOnly: true,
});
return getFromCache(cache, source, context, true);
}
if (!childNodes) {
logger.error(
'gatsby-remark-vscode couldn’t retrieve up-to-date GRVSCCodeBlock GraphQL nodes. ' +
'The `GRVSCCodeBlocks` field may be missing, empty or stale. ' +
'The Gatsby cache is probably in a weird state. Try running `gatsby clean`, and file an ' +
'issue at https://github.com/andrewbranch/gatsby-remark-vscode/issues/new if the problem persists.'
);

return context.nodeModel.runQuery({
query: { parent: { id: { eq: source.id } } },
type,
firstOnly: false
});
}
return childNodes || [];
}
40 changes: 26 additions & 14 deletions src/createCodeBlockRegistry.js → src/createCodeNodeRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ const { getTokenDataFromMetadata } = require('../lib/vscode/modes');
const { declaration } = require('./renderers/css');

/**
* @template TKey
* @param {CodeBlockRegistryOptions=} options
* @returns {CodeBlockRegistry<TKey>}
* @template {Keyable} TKey
* @param {CodeNodeRegistryOptions=} options
* @returns {CodeNodeRegistry<TKey>}
*/
function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
/** @type {Map<TKey, RegisteredCodeBlockData & { index: number }>} */
const nodeMap = new Map();
function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
/** @type {Map<TKey, RegisteredCodeNodeData>} */
const blockMap = new Map();
/** @type {Map<TKey, RegisteredCodeNodeData>} */
const spanMap = new Map();
/** @type {ConditionalTheme[]} */
let themes = [];
/** @type {Map<string, { colorMap: string[], settings: Record<string, string> }>} */
Expand All @@ -23,16 +25,18 @@ function createCodeBlockRegistry({ prefixAllClassNames } = {}) {

return {
register: (key, data) => {
nodeMap.set(key, { ...data, index: nodeMap.size });
const map = key.type === 'code' ? blockMap : spanMap;
map.set(key, { ...data, index: map.size });
themes = concatConditionalThemes(themes, data.possibleThemes);
data.tokenizationResults.forEach(({ theme, colorMap, settings }) =>
themeColors.set(theme.identifier, { colorMap, settings })
);
},
forEachLine: (node, action) => nodeMap.get(node).lines.forEach(action),
forEachLine: (node, action) => blockMap.get(node).lines.forEach(action),
forEachToken: (node, lineIndex, tokenAction) => {
generateClassNames();
const { tokenizationResults, isTokenized, lines } = nodeMap.get(node);
const map = node.type === 'code' ? blockMap : spanMap;
const { tokenizationResults, isTokenized, lines } = map.get(node);
if (!isTokenized) {
return;
}
Expand Down Expand Up @@ -78,10 +82,11 @@ function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
});
});
},
forEachCodeBlock: nodeMap.forEach.bind(nodeMap),
forEachCodeBlock: blockMap.forEach.bind(blockMap),
forEachCodeSpan: spanMap.forEach.bind(spanMap),
getAllPossibleThemes: () => themes.map(theme => ({ theme, settings: themeColors.get(theme.identifier).settings })),
getTokenStylesForTheme: themeIdentifier => {
/** @type {ReturnType<CodeBlockRegistry['getTokenStylesForTheme']>} */
/** @type {ReturnType<CodeNodeRegistry['getTokenStylesForTheme']>} */
const result = [];
const colors = themeColors.get(themeIdentifier);
const classNameMap = themeTokenClassNameMap && themeTokenClassNameMap.get(themeIdentifier);
Expand Down Expand Up @@ -113,7 +118,14 @@ function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
if (themeTokenClassNameMap) return;
themeTokenClassNameMap = new Map();
zippedLines = new Map();
nodeMap.forEach(({ lines, tokenizationResults, isTokenized }, node) => {
blockMap.forEach(generate);
spanMap.forEach(generate);

/**
* @param {RegisteredCodeNodeData} data
* @param {TKey} node
*/
function generate({ lines, tokenizationResults, isTokenized }, node) {
if (!isTokenized) return;
/** @type {Token[][][]} */
const zippedLinesForNode = [];
Expand Down Expand Up @@ -141,7 +153,7 @@ function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
});
});
});
});
}
}
}

Expand Down Expand Up @@ -202,4 +214,4 @@ function getColorFromColorMap(colorMap, canonicalClassName) {
return colorMap[index];
}

module.exports = createCodeBlockRegistry;
module.exports = createCodeNodeRegistry;
Loading