Skip to content

Commit c48dd09

Browse files
authored
Usage without gatsby (#157)
* feat: remarkPlugin * only requireMain when require.main exists * refactor: extract remarkPlugin to own file * chore: prettier formatting * docs: document usage as remark plugin w/o gatsby
1 parent b848e19 commit c48dd09

File tree

6 files changed

+274
-7
lines changed

6 files changed

+274
-7
lines changed

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ If you’re updating from v2.x.x (or v1), see [MIGRATING.md](./MIGRATING.md). Ne
3030
- [Diff highlighting](#diff-highlighting)
3131
- [Using different themes for different code fences](#using-different-themes-for-different-code-fences)
3232
- [Arbitrary code fence options](#arbitrary-code-fence-options)
33+
- [Usage as a remark plugin without Gatsby](#usage-as-a-remark-plugin-without-gatsby)
3334
- [Options reference](#options-reference)
3435
- [Contributing](#contributing)
3536

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

558+
### Usage as a remark plugin without Gatsby
559+
560+
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:
561+
562+
````js
563+
const unified = require('unified');
564+
const remarkParse = require('remarkParse');
565+
const remarkVscode = require('gatsby-remark-vscode');
566+
const remarkToRehype = require('remark-rehype');
567+
const rehypeRaw = require('rehype-raw');
568+
const rehypeStringify = require('rehype-stringify');
569+
570+
const markdownSource = `
571+
# Code example with awesome syntax highlighting
572+
573+
```ts {numberLines}
574+
export function sum(a: number, b: number): number {
575+
return a + b;
576+
}
577+
```
578+
579+
This is a paragraph after the code example.
580+
`
581+
582+
const processor = unified()
583+
// parse markdown to remark AST
584+
.use(remarkParse)
585+
// apply syntax highlighting using `remarkPlugin`
586+
// with your preferred options
587+
.use(remarkVscode.remarkPlugin, {
588+
theme: 'Default Light+',
589+
})
590+
// convert remark AST to rehype AST
591+
.use(remarkToRehype, { allowDangerousHtml: true })
592+
.use(rehypeRaw)
593+
// stringify
594+
.use(rehypeStringify, {
595+
allowDangerousHtml: true,
596+
closeSelfClosing: true,
597+
});
598+
599+
const vfile = await processor.process(markdownSource);
600+
console.log(vfile.contents); // logs resulting HTML
601+
````
602+
557603
## Options reference
558604
559605
### `theme`
@@ -567,7 +613,7 @@ The syntax theme used for code blocks.
567613
- **`ThemeSettings`:** An object that selects different themes to use in different contexts. (See [Multi-theme support](#multi-theme-support).)
568614
- **`(data: CodeBlockData) => string | ThemeSettings`:** A function returning the theme selection for a given code block. `CodeBlockData` is an object with properties:
569615
- **`language`:** The language of the code block, if one was specified.
570-
- **`markdownNode`:** The MarkdownRemark GraphQL node.
616+
- **`markdownNode`:** The MarkdownRemark GraphQL node (*not available if used as `remarkPlugin`*)
571617
- **`node`:** The Remark AST node of the code block.
572618
- **`parsedOptions`:** The object form of of any code fence info supplied. (See [Arbitrary code fence options](#arbitrary-code-fence-options).)
573619
@@ -625,7 +671,7 @@ Enables syntax highlighting for inline code spans. (See [Inline code highlightin
625671
- **`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")` ``.
626672
- **`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:
627673
- **`language`:** The language of the code span (the bit before the `marker` character).
628-
- **`markdownNode`:** The MarkdownRemark GraphQL node.
674+
- **`markdownNode`:** The MarkdownRemark GraphQL node. (*not available if used as `remarkPlugin`*)
629675
- **`node`:** The Remark AST node of the code span.
630676

631677
### `injectStyles`

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const { getScope } = require('./storeUtils');
1919
const { createStyleElement } = require('./factory/html');
2020
const { renderHTML } = require('./renderers/html');
2121
const { createOnce } = require('./utils');
22+
const remarkPlugin = require('./remarkPlugin');
2223
const styles = fs.readFileSync(path.resolve(__dirname, '../styles.css'), 'utf8');
2324

2425
function createPlugin() {
@@ -258,6 +259,7 @@ function createPlugin() {
258259
textmateHighlight.getRegistry = getRegistry;
259260
textmateHighlight.once = once;
260261
textmateHighlight.createSchemaCustomization = createSchemaCustomization;
262+
textmateHighlight.remarkPlugin = remarkPlugin;
261263
return textmateHighlight;
262264
}
263265

src/processExtension.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const {
1313
} = require('./utils');
1414
const { getHighestBuiltinLanguageId } = require('./storeUtils');
1515
const unzipDir = path.resolve(__dirname, '../lib/extensions');
16-
const requireMain = createRequire(require.main.filename);
16+
/** @type NodeRequire */
17+
const requireMain = require.main ? createRequire(require.main.filename) : undefined;
1718
const requireCwd = createRequire(path.join(process.cwd(), 'index.js'));
1819

1920
/**
@@ -151,7 +152,7 @@ async function getExtensionPackageJsonPath(specifier, host) {
151152
function requireResolveExtension(specifier) {
152153
return (
153154
tryResolve(require) ||
154-
tryResolve(requireMain) ||
155+
(requireMain && tryResolve(requireMain)) ||
155156
tryResolve(requireCwd) ||
156157
require.resolve(path.join(specifier, 'package.json'))
157158
); // If none work, throw the best error stack

src/remarkPlugin.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// @ts-check
2+
const fs = require('fs');
3+
const path = require('path');
4+
const logger = require('loglevel');
5+
const visit = require('unist-util-visit');
6+
const setup = require('./setup');
7+
const getPossibleThemes = require('./getPossibleThemes');
8+
const createCodeNodeRegistry = require('./createCodeNodeRegistry');
9+
const parseCodeFenceInfo = require('./parseCodeFenceInfo');
10+
const parseCodeSpanInfo = require('./parseCodeSpanInfo');
11+
const getCodeBlockGraphQLDataFromRegistry = require('./graphql/getCodeBlockDataFromRegistry');
12+
const getCodeSpanGraphQLDataFromRegistry = require('./graphql/getCodeSpanDataFromRegistry');
13+
const { registerCodeBlock, registerCodeSpan } = require('./registerCodeNode');
14+
const { getScope } = require('./storeUtils');
15+
const { createStyleElement } = require('./factory/html');
16+
const { renderHTML } = require('./renderers/html');
17+
const { createOnce } = require('./utils');
18+
const createGetRegistry = require('./createGetRegistry');
19+
const styles = fs.readFileSync(path.resolve(__dirname, '../styles.css'), 'utf8');
20+
21+
class Cache {
22+
constructor() {
23+
this.cache = new Map();
24+
}
25+
async set(key, value) {
26+
this.cache.set(key, value);
27+
}
28+
async get(key) {
29+
return this.cache.get(key);
30+
}
31+
}
32+
33+
const once = createOnce();
34+
const cache = new Cache();
35+
const getRegistry = createGetRegistry();
36+
37+
/**
38+
* @param {PluginOptions=} options
39+
*/
40+
function remarkPlugin(options = {}) {
41+
return async function(tree) {
42+
const {
43+
theme,
44+
wrapperClassName,
45+
languageAliases,
46+
extensions,
47+
getLineClassName,
48+
injectStyles,
49+
replaceColor,
50+
logLevel,
51+
getLineTransformers,
52+
inlineCode,
53+
...rest
54+
} = await setup(options, undefined, cache, once);
55+
56+
const lineTransformers = getLineTransformers(
57+
{
58+
theme,
59+
wrapperClassName,
60+
languageAliases,
61+
extensions,
62+
getLineClassName,
63+
injectStyles,
64+
replaceColor,
65+
logLevel,
66+
inlineCode,
67+
...rest
68+
},
69+
cache
70+
);
71+
72+
// 1. Gather all code fence nodes from Markdown AST.
73+
74+
/** @type {(MDASTNode<'code'> | MDASTNode<'inlineCode'>)[]} */
75+
const nodes = [];
76+
visit(
77+
tree,
78+
({ type }) => type === 'code' || (inlineCode && type === 'inlineCode'),
79+
node => {
80+
nodes.push(node);
81+
}
82+
);
83+
84+
// 2. For each code fence found, parse its header, determine what themes it will use,
85+
// and register its contents with a central code block registry, performing tokenization
86+
// along the way.
87+
88+
/** @type {CodeNodeRegistry<MDASTNode<'code' | 'inlineCode'>>} */
89+
const codeNodeRegistry = createCodeNodeRegistry();
90+
for (const node of nodes) {
91+
/** @type {string} */
92+
const text = node.value || (node.children && node.children[0] && node.children[0].value);
93+
if (!text) continue;
94+
const { languageName, meta, text: parsedText = text } =
95+
node.type === 'code'
96+
? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta)
97+
: parseCodeSpanInfo(text, inlineCode.marker);
98+
99+
if (node.type === 'inlineCode' && !languageName) {
100+
continue;
101+
}
102+
103+
const grammarCache = await cache.get('grammars');
104+
const scope = getScope(languageName, grammarCache, languageAliases);
105+
if (!scope && languageName) {
106+
logger.warn(
107+
`Encountered unknown language '${languageName}'. ` +
108+
`If '${languageName}' is an alias for a supported language, ` +
109+
`use the 'languageAliases' plugin option to map it to the canonical language name.`
110+
);
111+
}
112+
113+
const nodeData = /** @type {CodeBlockData | CodeSpanData} */ ({
114+
node,
115+
language: languageName,
116+
parsedOptions: meta
117+
});
118+
119+
const possibleThemes = await getPossibleThemes(
120+
node.type === 'inlineCode' ? inlineCode.theme || theme : theme,
121+
await cache.get('themes'),
122+
undefined,
123+
nodeData
124+
);
125+
126+
if (node.type === 'inlineCode') {
127+
await registerCodeSpan(
128+
codeNodeRegistry,
129+
node,
130+
possibleThemes,
131+
() => getRegistry(cache, scope),
132+
scope,
133+
parsedText,
134+
languageName,
135+
cache
136+
);
137+
} else {
138+
await registerCodeBlock(
139+
codeNodeRegistry,
140+
node,
141+
possibleThemes,
142+
() => getRegistry(cache, scope),
143+
lineTransformers,
144+
scope,
145+
parsedText,
146+
languageName,
147+
meta,
148+
cache
149+
);
150+
}
151+
}
152+
153+
// 3. For each code block/span registered, convert its tokenization and theme data
154+
// to a GraphQL-compatible representation, including HTML renderings. At the same
155+
// time, change the original code fence Markdown node to an HTML node and set
156+
// its value to the HTML rendering contained in the GraphQL node.
157+
158+
codeNodeRegistry.forEachCodeBlock((codeBlock, node) => {
159+
const graphQLNode = getCodeBlockGraphQLDataFromRegistry(
160+
codeNodeRegistry,
161+
node,
162+
codeBlock,
163+
getWrapperClassName,
164+
getLineClassName
165+
);
166+
167+
// Update Markdown node
168+
/** @type {MDASTNode} */
169+
(node).type = 'html';
170+
node.value = graphQLNode.html;
171+
172+
function getWrapperClassName() {
173+
return typeof wrapperClassName === 'function'
174+
? wrapperClassName({
175+
language: codeBlock.languageName,
176+
node,
177+
codeFenceNode: node,
178+
parsedOptions: codeBlock.meta
179+
})
180+
: wrapperClassName;
181+
}
182+
});
183+
184+
codeNodeRegistry.forEachCodeSpan((codeSpan, node) => {
185+
const graphQLNode = getCodeSpanGraphQLDataFromRegistry(codeNodeRegistry, node, codeSpan, getClassName);
186+
187+
// Update Markdown node
188+
/** @type {MDASTNode} */
189+
(node).type = 'html';
190+
node.value = graphQLNode.html;
191+
192+
function getClassName() {
193+
return typeof inlineCode.className === 'function'
194+
? inlineCode.className({
195+
language: codeSpan.languageName,
196+
node
197+
})
198+
: inlineCode.className;
199+
}
200+
});
201+
202+
// 4. Generate CSS rules for each theme used by one or more code blocks in the registry,
203+
// then append that CSS to the Markdown AST in an HTML node.
204+
205+
const styleElement = createStyleElement(
206+
codeNodeRegistry.getAllPossibleThemes(),
207+
codeNodeRegistry.getTokenStylesForTheme,
208+
replaceColor,
209+
injectStyles ? styles : undefined
210+
);
211+
212+
if (styleElement) {
213+
tree.children.unshift({ type: 'html', value: renderHTML(styleElement) });
214+
}
215+
};
216+
}
217+
218+
module.exports = remarkPlugin;

src/setup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const { processExtensions } = require('./processExtension');
1212
* @returns {Promise<PluginOptions>}
1313
*/
1414
async function setup(options, markdownAbsolutePath, cache, once) {
15-
if (options['__getOptions__']) {
15+
if (markdownAbsolutePath && options['__getOptions__']) {
1616
return setupOptions(options['__getOptions__'](markdownAbsolutePath), cache);
1717
} else {
1818
return once(() => setupOptions(options, cache), 'setup');

src/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface RemarkPluginArguments {
1111

1212
interface CodeBlockData {
1313
language?: string;
14-
markdownNode: MarkdownNode;
14+
markdownNode?: MarkdownNode;
1515
/** @deprecated Use `node` instead. */
1616
codeFenceNode: MDASTNode<'code'>;
1717
node: MDASTNode<'code'>;
@@ -20,7 +20,7 @@ interface CodeBlockData {
2020

2121
interface CodeSpanData {
2222
language?: string;
23-
markdownNode: MarkdownNode;
23+
markdownNode?: MarkdownNode;
2424
node: MDASTNode<'inlineCode'>;
2525
}
2626

0 commit comments

Comments
 (0)