Skip to content

Usage without gatsby #157

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

Merged
merged 5 commits into from
Jul 6, 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
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ If you’re updating from v2.x.x (or v1), see [MIGRATING.md](./MIGRATING.md). Ne
- [Diff highlighting](#diff-highlighting)
- [Using different themes for different code fences](#using-different-themes-for-different-code-fences)
- [Arbitrary code fence options](#arbitrary-code-fence-options)
- [Usage as a remark plugin without Gatsby](#usage-as-a-remark-plugin-without-gatsby)
- [Options reference](#options-reference)
- [Contributing](#contributing)

Expand Down Expand Up @@ -554,6 +555,51 @@ Line numbers and ranges aren’t the only things you can pass as options on your
}
```

### Usage as a remark plugin without Gatsby

This package exports a `remarkPlugin` property that accepts the same [options](#options-reference) as the main Gatsby plugin and is usable as a [remark](https://github.com/remarkjs/remark) plugin in any [unifiedjs](https://github.com/unifiedjs/unified) processing pipeline:

````js
const unified = require('unified');
const remarkParse = require('remarkParse');
const remarkVscode = require('gatsby-remark-vscode');
const remarkToRehype = require('remark-rehype');
const rehypeRaw = require('rehype-raw');
const rehypeStringify = require('rehype-stringify');

const markdownSource = `
# Code example with awesome syntax highlighting

```ts {numberLines}
export function sum(a: number, b: number): number {
return a + b;
}
```

This is a paragraph after the code example.
`

const processor = unified()
// parse markdown to remark AST
.use(remarkParse)
// apply syntax highlighting using `remarkPlugin`
// with your preferred options
.use(remarkVscode.remarkPlugin, {
theme: 'Default Light+',
})
// convert remark AST to rehype AST
.use(remarkToRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
// stringify
.use(rehypeStringify, {
allowDangerousHtml: true,
closeSelfClosing: true,
});

const vfile = await processor.process(markdownSource);
console.log(vfile.contents); // logs resulting HTML
````

## Options reference

### `theme`
Expand All @@ -567,7 +613,7 @@ The syntax theme used for code blocks.
- **`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.
- **`markdownNode`:** The MarkdownRemark GraphQL node (*not available if used as `remarkPlugin`*)
- **`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).)

Expand Down Expand Up @@ -625,7 +671,7 @@ Enables syntax highlighting for inline code spans. (See [Inline code highlightin
- **`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.
- **`markdownNode`:** The MarkdownRemark GraphQL node. (*not available if used as `remarkPlugin`*)
- **`node`:** The Remark AST node of the code span.

### `injectStyles`
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const { getScope } = require('./storeUtils');
const { createStyleElement } = require('./factory/html');
const { renderHTML } = require('./renderers/html');
const { createOnce } = require('./utils');
const remarkPlugin = require('./remarkPlugin');
const styles = fs.readFileSync(path.resolve(__dirname, '../styles.css'), 'utf8');

function createPlugin() {
Expand Down Expand Up @@ -258,6 +259,7 @@ function createPlugin() {
textmateHighlight.getRegistry = getRegistry;
textmateHighlight.once = once;
textmateHighlight.createSchemaCustomization = createSchemaCustomization;
textmateHighlight.remarkPlugin = remarkPlugin;
return textmateHighlight;
}

Expand Down
5 changes: 3 additions & 2 deletions src/processExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const {
} = require('./utils');
const { getHighestBuiltinLanguageId } = require('./storeUtils');
const unzipDir = path.resolve(__dirname, '../lib/extensions');
const requireMain = createRequire(require.main.filename);
/** @type NodeRequire */
const requireMain = require.main ? createRequire(require.main.filename) : undefined;
const requireCwd = createRequire(path.join(process.cwd(), 'index.js'));

/**
Expand Down Expand Up @@ -151,7 +152,7 @@ async function getExtensionPackageJsonPath(specifier, host) {
function requireResolveExtension(specifier) {
return (
tryResolve(require) ||
tryResolve(requireMain) ||
(requireMain && tryResolve(requireMain)) ||
tryResolve(requireCwd) ||
require.resolve(path.join(specifier, 'package.json'))
); // If none work, throw the best error stack
Expand Down
218 changes: 218 additions & 0 deletions src/remarkPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// @ts-check
const fs = require('fs');
const path = require('path');
const logger = require('loglevel');
const visit = require('unist-util-visit');
const setup = require('./setup');
const getPossibleThemes = require('./getPossibleThemes');
const createCodeNodeRegistry = require('./createCodeNodeRegistry');
const parseCodeFenceInfo = require('./parseCodeFenceInfo');
const parseCodeSpanInfo = require('./parseCodeSpanInfo');
const getCodeBlockGraphQLDataFromRegistry = require('./graphql/getCodeBlockDataFromRegistry');
const getCodeSpanGraphQLDataFromRegistry = require('./graphql/getCodeSpanDataFromRegistry');
const { registerCodeBlock, registerCodeSpan } = require('./registerCodeNode');
const { getScope } = require('./storeUtils');
const { createStyleElement } = require('./factory/html');
const { renderHTML } = require('./renderers/html');
const { createOnce } = require('./utils');
const createGetRegistry = require('./createGetRegistry');
const styles = fs.readFileSync(path.resolve(__dirname, '../styles.css'), 'utf8');

class Cache {
constructor() {
this.cache = new Map();
}
async set(key, value) {
this.cache.set(key, value);
}
async get(key) {
return this.cache.get(key);
}
}

const once = createOnce();
const cache = new Cache();
const getRegistry = createGetRegistry();

/**
* @param {PluginOptions=} options
*/
function remarkPlugin(options = {}) {
return async function(tree) {
const {
theme,
wrapperClassName,
languageAliases,
extensions,
getLineClassName,
injectStyles,
replaceColor,
logLevel,
getLineTransformers,
inlineCode,
...rest
} = await setup(options, undefined, cache, once);

const lineTransformers = getLineTransformers(
{
theme,
wrapperClassName,
languageAliases,
extensions,
getLineClassName,
injectStyles,
replaceColor,
logLevel,
inlineCode,
...rest
},
cache
);

// 1. Gather all code fence nodes from Markdown AST.

/** @type {(MDASTNode<'code'> | MDASTNode<'inlineCode'>)[]} */
const nodes = [];
visit(
tree,
({ type }) => type === 'code' || (inlineCode && type === 'inlineCode'),
node => {
nodes.push(node);
}
);

// 2. For each code fence found, parse its header, determine what themes it will use,
// and register its contents with a central code block registry, performing tokenization
// along the way.

/** @type {CodeNodeRegistry<MDASTNode<'code' | 'inlineCode'>>} */
const codeNodeRegistry = createCodeNodeRegistry();
for (const node of nodes) {
/** @type {string} */
const text = node.value || (node.children && node.children[0] && node.children[0].value);
if (!text) continue;
const { languageName, meta, text: parsedText = text } =
node.type === 'code'
? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta)
: parseCodeSpanInfo(text, inlineCode.marker);

if (node.type === 'inlineCode' && !languageName) {
continue;
}

const grammarCache = await cache.get('grammars');
const scope = getScope(languageName, grammarCache, languageAliases);
if (!scope && languageName) {
logger.warn(
`Encountered unknown language '${languageName}'. ` +
`If '${languageName}' is an alias for a supported language, ` +
`use the 'languageAliases' plugin option to map it to the canonical language name.`
);
}

const nodeData = /** @type {CodeBlockData | CodeSpanData} */ ({
node,
language: languageName,
parsedOptions: meta
});

const possibleThemes = await getPossibleThemes(
node.type === 'inlineCode' ? inlineCode.theme || theme : theme,
await cache.get('themes'),
undefined,
nodeData
);

if (node.type === 'inlineCode') {
await registerCodeSpan(
codeNodeRegistry,
node,
possibleThemes,
() => getRegistry(cache, scope),
scope,
parsedText,
languageName,
cache
);
} else {
await registerCodeBlock(
codeNodeRegistry,
node,
possibleThemes,
() => getRegistry(cache, scope),
lineTransformers,
scope,
parsedText,
languageName,
meta,
cache
);
}
}

// 3. For each code block/span registered, convert its tokenization and theme data
// to a GraphQL-compatible representation, including HTML renderings. At the same
// time, change the original code fence Markdown node to an HTML node and set
// its value to the HTML rendering contained in the GraphQL node.

codeNodeRegistry.forEachCodeBlock((codeBlock, node) => {
const graphQLNode = getCodeBlockGraphQLDataFromRegistry(
codeNodeRegistry,
node,
codeBlock,
getWrapperClassName,
getLineClassName
);

// Update Markdown node
/** @type {MDASTNode} */
(node).type = 'html';
node.value = graphQLNode.html;

function getWrapperClassName() {
return typeof wrapperClassName === 'function'
? wrapperClassName({
language: codeBlock.languageName,
node,
codeFenceNode: node,
parsedOptions: codeBlock.meta
})
: wrapperClassName;
}
});

codeNodeRegistry.forEachCodeSpan((codeSpan, node) => {
const graphQLNode = getCodeSpanGraphQLDataFromRegistry(codeNodeRegistry, node, codeSpan, getClassName);

// Update Markdown node
/** @type {MDASTNode} */
(node).type = 'html';
node.value = graphQLNode.html;

function getClassName() {
return typeof inlineCode.className === 'function'
? inlineCode.className({
language: codeSpan.languageName,
node
})
: inlineCode.className;
}
});

// 4. Generate CSS rules for each theme used by one or more code blocks in the registry,
// then append that CSS to the Markdown AST in an HTML node.

const styleElement = createStyleElement(
codeNodeRegistry.getAllPossibleThemes(),
codeNodeRegistry.getTokenStylesForTheme,
replaceColor,
injectStyles ? styles : undefined
);

if (styleElement) {
tree.children.unshift({ type: 'html', value: renderHTML(styleElement) });
}
};
}

module.exports = remarkPlugin;
2 changes: 1 addition & 1 deletion src/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { processExtensions } = require('./processExtension');
* @returns {Promise<PluginOptions>}
*/
async function setup(options, markdownAbsolutePath, cache, once) {
if (options['__getOptions__']) {
if (markdownAbsolutePath && options['__getOptions__']) {
return setupOptions(options['__getOptions__'](markdownAbsolutePath), cache);
} else {
return once(() => setupOptions(options, cache), 'setup');
Expand Down
4 changes: 2 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface RemarkPluginArguments {

interface CodeBlockData {
language?: string;
markdownNode: MarkdownNode;
markdownNode?: MarkdownNode;
/** @deprecated Use `node` instead. */
codeFenceNode: MDASTNode<'code'>;
node: MDASTNode<'code'>;
Expand All @@ -20,7 +20,7 @@ interface CodeBlockData {

interface CodeSpanData {
language?: string;
markdownNode: MarkdownNode;
markdownNode?: MarkdownNode;
node: MDASTNode<'inlineCode'>;
}

Expand Down