Skip to content

Commit eddf3ae

Browse files
committed
Start code span highlighting implementation
1 parent cf3d33d commit eddf3ae

12 files changed

+184
-74
lines changed

src/createCodeBlockRegistry.js renamed to src/createCodeNodeRegistry.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ const { declaration } = require('./renderers/css');
66

77
/**
88
* @template TKey
9-
* @param {CodeBlockRegistryOptions=} options
10-
* @returns {CodeBlockRegistry<TKey>}
9+
* @param {CodeNodeRegistryOptions=} options
10+
* @returns {CodeNodeRegistry<TKey>}
1111
*/
12-
function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
12+
function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
1313
/** @type {Map<TKey, RegisteredCodeBlockData & { index: number }>} */
1414
const nodeMap = new Map();
1515
/** @type {ConditionalTheme[]} */
@@ -81,7 +81,7 @@ function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
8181
forEachCodeBlock: nodeMap.forEach.bind(nodeMap),
8282
getAllPossibleThemes: () => themes.map(theme => ({ theme, settings: themeColors.get(theme.identifier).settings })),
8383
getTokenStylesForTheme: themeIdentifier => {
84-
/** @type {ReturnType<CodeBlockRegistry['getTokenStylesForTheme']>} */
84+
/** @type {ReturnType<CodeNodeRegistry['getTokenStylesForTheme']>} */
8585
const result = [];
8686
const colors = themeColors.get(themeIdentifier);
8787
const classNameMap = themeTokenClassNameMap && themeTokenClassNameMap.get(themeIdentifier);
@@ -202,4 +202,4 @@ function getColorFromColorMap(colorMap, canonicalClassName) {
202202
return colorMap[index];
203203
}
204204

205-
module.exports = createCodeBlockRegistry;
205+
module.exports = createCodeNodeRegistry;

src/getPossibleThemes.js

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,25 @@ const {
77
} = require('./themeUtils');
88

99
/**
10-
* @param {ThemeOption} themeOption
10+
* @template {CodeBlockData | CodeSpanData} T
11+
* @param {ThemeOption<T>} themeOption
1112
* @param {ThemeCache} themeCache
1213
* @param {string | undefined} contextDirectory
13-
* @param {MarkdownNode} markdownNode
14-
* @param {MDASTNode} codeFenceNode
15-
* @param {string} languageName
16-
* @param {object} meta
14+
* @param {T} codeNodeData
1715
* @returns {Promise<ConditionalTheme[]>}
1816
*/
1917
async function getPossibleThemes(
2018
themeOption,
2119
themeCache,
2220
contextDirectory,
23-
markdownNode,
24-
codeFenceNode,
25-
languageName,
26-
meta
21+
codeNodeData
2722
) {
2823
if (typeof themeOption === 'function') {
2924
return getPossibleThemes(
30-
themeOption({
31-
markdownNode,
32-
codeFenceNode,
33-
language: languageName,
34-
parsedOptions: meta
35-
}),
25+
themeOption(codeNodeData),
3626
themeCache,
3727
contextDirectory,
38-
markdownNode,
39-
codeFenceNode,
40-
languageName,
41-
meta
28+
codeNodeData
4229
);
4330
}
4431

@@ -53,10 +40,7 @@ async function getPossibleThemes(
5340
themeOption.default,
5441
themeCache,
5542
contextDirectory,
56-
markdownNode,
57-
codeFenceNode,
58-
languageName,
59-
meta
43+
codeNodeData
6044
);
6145
}
6246
if (themeOption.dark) {

src/graphql/getCodeBlockDataFromRegistry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { createTokenElement, createLineElement, createCodeBlockElement } = requir
66

77
/**
88
* @template TKey
9-
* @param {CodeBlockRegistry<TKey>} registry
9+
* @param {CodeNodeRegistry<TKey>} registry
1010
* @param {TKey} key
1111
* @param {RegisteredCodeBlockData} codeBlock
1212
* @param {() => string} getWrapperClassName

src/graphql/highlight.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
4040
const grammarCache = await cache.get('grammars');
4141
const possibleThemes = await getThemes(theme, args, themeCache);
4242
const scope = getScope(args.language, grammarCache, languageAliases);
43-
/** @type {CodeBlockRegistry<typeof registryKey>} */
43+
/** @type {CodeNodeRegistry<typeof registryKey>} */
4444
const codeBlockRegistry = createCodeBlockRegistry({ prefixAllClassNames: true });
4545
const meta = parseCodeFenceHeader(args.language, args.meta);
4646

src/graphql/schema.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ declare namespace grvsc {
4848
additionalThemes: GRVSCTheme[];
4949
tokenizedLines?: GRVSCTokenizedLine[];
5050
}
51+
interface GRVSCCodeSpan extends Node {
52+
index: number;
53+
html: string;
54+
text: string;
55+
className: string;
56+
language?: string;
57+
defaultTheme: GRVSCTheme;
58+
additionalThemes: GRVSCTheme[];
59+
tokens: GRVSCToken[];
60+
}
5161
interface GRVSCStylesheet extends Node {
5262
css: string;
5363
}

src/graphql/schema.graphql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ type GRVSCCodeBlock implements Node {
5151
additionalThemes: [GRVSCTheme!]!
5252
tokenizedLines: [GRVSCTokenizedLine!]
5353
}
54+
type GRVSCCodeSpan implements Node {
55+
index: Int!
56+
html: String!
57+
text: String!
58+
className: String!
59+
language: String
60+
defaultTheme: GRVSCTheme!
61+
additionalThemes: [GRVSCTheme!]!
62+
tokens: [GRVSCToken!]!
63+
}
5464
type GRVSCStylesheet implements Node {
5565
css: String!
5666
}

src/index.js

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ const logger = require('loglevel');
55
const visit = require('unist-util-visit');
66
const setup = require('./setup');
77
const createGetRegistry = require('./createGetRegistry');
8-
const registerCodeBlock = require('./registerCodeBlock');
98
const getPossibleThemes = require('./getPossibleThemes');
10-
const createCodeBlockRegistry = require('./createCodeBlockRegistry');
11-
const parseCodeFenceHeader = require('./parseCodeFenceHeader');
9+
const createCodeNodeRegistry = require('./createCodeNodeRegistry');
10+
const parseCodeFenceInfo = require('./parseCodeFenceInfo');
11+
const parseCodeSpanInfo = require('./parseCodeSpanInfo');
1212
const createSchemaCustomization = require('./graphql/createSchemaCustomization');
1313
const getCodeBlockGraphQLDataFromRegistry = require('./graphql/getCodeBlockDataFromRegistry');
14+
const { registerCodeBlock, registerCodeSpan } = require('./registerCodeNode');
1415
const { createHash } = require('crypto');
1516
const { setChildNodes } = require('./cacheUtils');
1617
const { getScope } = require('./storeUtils');
@@ -38,6 +39,7 @@ function createPlugin() {
3839
replaceColor,
3940
logLevel,
4041
getLineTransformers,
42+
inlineCode,
4143
...rest
4244
} = await once(() => setup(options, cache), 'setup');
4345

@@ -51,16 +53,17 @@ function createPlugin() {
5153
injectStyles,
5254
replaceColor,
5355
logLevel,
56+
inlineCode,
5457
...rest
5558
},
5659
cache
5760
);
5861

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

61-
/** @type {MDASTNode[]} */
64+
/** @type {(MDASTNode<'code'> | MDASTNode<'inlineCode'>)[]} */
6265
const nodes = [];
63-
visit(markdownAST, 'code', node => {
66+
visit(markdownAST, ({ type }) => type === 'code' || type === 'inlineCode', node => {
6467
nodes.push(node);
6568
});
6669

@@ -70,13 +73,20 @@ function createPlugin() {
7073

7174
/** @type {grvsc.gql.GRVSCCodeBlock[]} */
7275
const graphQLNodes = [];
73-
/** @type {CodeBlockRegistry<MDASTNode>} */
74-
const codeBlockRegistry = createCodeBlockRegistry();
76+
/** @type {CodeNodeRegistry<MDASTNode>} */
77+
const codeNodeRegistry = createCodeNodeRegistry();
7578
for (const node of nodes) {
7679
/** @type {string} */
7780
const text = node.value || (node.children && node.children[0] && node.children[0].value);
7881
if (!text) continue;
79-
const { languageName, meta } = parseCodeFenceHeader(node.lang ? node.lang.toLowerCase() : '', node.meta);
82+
const { languageName, meta, text: parsedText = text } = node.type === 'code'
83+
? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta)
84+
: parseCodeSpanInfo(text, inlineCode.marker);
85+
86+
if (node.type === 'inlineCode' && !languageName) {
87+
continue;
88+
}
89+
8090
const grammarCache = await cache.get('grammars');
8191
const scope = getScope(languageName, grammarCache, languageAliases);
8292
if (!scope && languageName) {
@@ -87,39 +97,55 @@ function createPlugin() {
8797
);
8898
}
8999

100+
const nodeData = /** @type {CodeBlockData | CodeSpanData} */ ({
101+
node,
102+
markdownNode,
103+
language: languageName
104+
});
105+
90106
const possibleThemes = await getPossibleThemes(
91-
theme,
107+
node.type === 'inlineCode' ? inlineCode.theme || theme : theme,
92108
await cache.get('themes'),
93109
// Node could be sourced from something other than a File node
94110
markdownNode.fileAbsolutePath ? path.dirname(markdownNode.fileAbsolutePath) : undefined,
95-
markdownNode,
96-
node,
97-
languageName,
98-
meta
111+
nodeData
99112
);
100113

101-
await registerCodeBlock(
102-
codeBlockRegistry,
103-
node,
104-
possibleThemes,
105-
() => getRegistry(cache, scope),
106-
lineTransformers,
107-
scope,
108-
text,
109-
languageName,
110-
meta,
111-
cache
112-
);
114+
if (node.type === 'inlineCode') {
115+
await registerCodeSpan(
116+
codeNodeRegistry,
117+
node,
118+
possibleThemes,
119+
() => getRegistry(cache, scope),
120+
scope,
121+
parsedText,
122+
languageName,
123+
cache
124+
);
125+
} else {
126+
await registerCodeBlock(
127+
codeNodeRegistry,
128+
node,
129+
possibleThemes,
130+
() => getRegistry(cache, scope),
131+
lineTransformers,
132+
scope,
133+
parsedText,
134+
languageName,
135+
meta,
136+
cache
137+
);
138+
}
113139
}
114140

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

120-
codeBlockRegistry.forEachCodeBlock((codeBlock, node) => {
146+
codeNodeRegistry.forEachCodeBlock((codeBlock, node) => {
121147
const graphQLNode = getCodeBlockGraphQLDataFromRegistry(
122-
codeBlockRegistry,
148+
codeNodeRegistry,
123149
node,
124150
codeBlock,
125151
getWrapperClassName,
@@ -148,6 +174,7 @@ function createPlugin() {
148174
? wrapperClassName({
149175
language: codeBlock.languageName,
150176
markdownNode,
177+
node,
151178
codeFenceNode: node,
152179
parsedOptions: codeBlock.meta
153180
})
@@ -159,8 +186,8 @@ function createPlugin() {
159186
// then append that CSS to the Markdown AST in an HTML node.
160187

161188
const styleElement = createStyleElement(
162-
codeBlockRegistry.getAllPossibleThemes(),
163-
codeBlockRegistry.getTokenStylesForTheme,
189+
codeNodeRegistry.getAllPossibleThemes(),
190+
codeNodeRegistry.getTokenStylesForTheme,
164191
replaceColor,
165192
injectStyles ? styles : undefined
166193
);

src/parseCodeFenceHeader.js renamed to src/parseCodeFenceInfo.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function test(input, pattern) {
1717
* @param {string} lang
1818
* @param {string=} metaString
1919
*/
20-
function parseCodeFenceHeader(lang, metaString) {
20+
function parseCodeFenceInfo(lang, metaString) {
2121
let pos = 0;
2222
let meta = {};
2323
let languageName = '';
@@ -170,4 +170,4 @@ function parseCodeFenceHeader(lang, metaString) {
170170
}
171171
}
172172

173-
module.exports = parseCodeFenceHeader;
173+
module.exports = parseCodeFenceInfo;

src/parseCodeSpanInfo.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @param {string} text
3+
* @param {string} delimiter
4+
* @returns {{ languageName: string | undefined, meta: undefined, text: string }}
5+
*/
6+
function parseCodeSpanInfo(text, delimiter) {
7+
const index = text.indexOf(delimiter);
8+
if (index <= 0) return { languageName: undefined, meta: undefined, text };
9+
const languageName = text.slice(0, index).trim().toLowerCase();
10+
if (!languageName) return { languageName: undefined, meta: undefined, text };
11+
return { languageName, meta: undefined, text: text.slice(index + delimiter.length) };
12+
}
13+
14+
module.exports = parseCodeSpanInfo;

src/registerCodeBlock.js renamed to src/registerCodeNode.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { getGrammar } = require('./storeUtils');
44

55
/**
66
* @template TKey
7-
* @param {CodeBlockRegistry<TKey>} codeBlockRegistry
7+
* @param {CodeNodeRegistry<TKey>} codeBlockRegistry
88
* @param {TKey} registryKey
99
* @param {ConditionalTheme[]} possibleThemes
1010
* @param {() => Promise<[import('vscode-textmate').Registry, () => void]>} getTextMateRegistry
@@ -57,4 +57,46 @@ async function registerCodeBlock(
5757
}
5858
}
5959

60-
module.exports = registerCodeBlock;
60+
/**
61+
* @template TKey
62+
* @param {CodeNodeRegistry<TKey>} codeBlockRegistry
63+
* @param {TKey} registryKey
64+
* @param {ConditionalTheme[]} possibleThemes
65+
* @param {() => Promise<[import('vscode-textmate').Registry, () => void]>} getTextMateRegistry
66+
* @param {string} scope
67+
* @param {string} text
68+
* @param {string | undefined} languageName
69+
* @param {GatsbyCache} cache
70+
*/
71+
async function registerCodeSpan(
72+
codeBlockRegistry,
73+
registryKey,
74+
possibleThemes,
75+
getTextMateRegistry,
76+
scope,
77+
text,
78+
languageName,
79+
cache
80+
) {
81+
const grammarCache = await cache.get('grammars');
82+
const [registry, unlockRegistry] = await getTextMateRegistry();
83+
try {
84+
/** @type {Line[]} */
85+
const lines = [{ text, data: {}, attrs: {} }];
86+
const { tokenTypes, languageId } = getGrammar(scope, grammarCache);
87+
const grammar = await registry.loadGrammarWithConfiguration(scope, languageId, { tokenTypes });
88+
codeBlockRegistry.register(registryKey, {
89+
lines,
90+
text,
91+
meta: {},
92+
languageName,
93+
possibleThemes,
94+
isTokenized: true,
95+
tokenizationResults: possibleThemes.map(theme => tokenizeWithTheme(lines, theme, grammar, registry))
96+
});
97+
} finally {
98+
unlockRegistry();
99+
}
100+
}
101+
102+
module.exports = { registerCodeBlock, registerCodeSpan };

0 commit comments

Comments
 (0)