Skip to content

Commit

Permalink
Merge pull request #89 from andrewbranch/feat/inline
Browse files Browse the repository at this point in the history
Code span highlighting
  • Loading branch information
andrewbranch authored May 4, 2020
2 parents cf3d33d + 87813b1 commit e4a237f
Show file tree
Hide file tree
Showing 28 changed files with 748 additions and 175 deletions.
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

0 comments on commit e4a237f

Please sign in to comment.