Skip to content

Commit c61b59f

Browse files
committed
Finish main plugin implementation
1 parent eddf3ae commit c61b59f

14 files changed

+170
-61
lines changed

src/createCodeNodeRegistry.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ const { getTokenDataFromMetadata } = require('../lib/vscode/modes');
55
const { declaration } = require('./renderers/css');
66

77
/**
8-
* @template TKey
8+
* @template {Keyable} TKey
99
* @param {CodeNodeRegistryOptions=} options
1010
* @returns {CodeNodeRegistry<TKey>}
1111
*/
1212
function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
13-
/** @type {Map<TKey, RegisteredCodeBlockData & { index: number }>} */
14-
const nodeMap = new Map();
13+
/** @type {Map<TKey, RegisteredCodeNodeData>} */
14+
const blockMap = new Map();
15+
/** @type {Map<TKey, RegisteredCodeNodeData>} */
16+
const spanMap = new Map();
1517
/** @type {ConditionalTheme[]} */
1618
let themes = [];
1719
/** @type {Map<string, { colorMap: string[], settings: Record<string, string> }>} */
@@ -23,16 +25,17 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
2325

2426
return {
2527
register: (key, data) => {
26-
nodeMap.set(key, { ...data, index: nodeMap.size });
28+
const map = key.type === 'code' ? blockMap : spanMap;
29+
map.set(key, { ...data, index: map.size });
2730
themes = concatConditionalThemes(themes, data.possibleThemes);
2831
data.tokenizationResults.forEach(({ theme, colorMap, settings }) =>
2932
themeColors.set(theme.identifier, { colorMap, settings })
3033
);
3134
},
32-
forEachLine: (node, action) => nodeMap.get(node).lines.forEach(action),
35+
forEachLine: (node, action) => blockMap.get(node).lines.forEach(action),
3336
forEachToken: (node, lineIndex, tokenAction) => {
3437
generateClassNames();
35-
const { tokenizationResults, isTokenized, lines } = nodeMap.get(node);
38+
const { tokenizationResults, isTokenized, lines } = blockMap.get(node);
3639
if (!isTokenized) {
3740
return;
3841
}
@@ -78,7 +81,8 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
7881
});
7982
});
8083
},
81-
forEachCodeBlock: nodeMap.forEach.bind(nodeMap),
84+
forEachCodeBlock: blockMap.forEach.bind(blockMap),
85+
forEachCodeSpan: spanMap.forEach.bind(spanMap),
8286
getAllPossibleThemes: () => themes.map(theme => ({ theme, settings: themeColors.get(theme.identifier).settings })),
8387
getTokenStylesForTheme: themeIdentifier => {
8488
/** @type {ReturnType<CodeNodeRegistry['getTokenStylesForTheme']>} */
@@ -113,7 +117,14 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
113117
if (themeTokenClassNameMap) return;
114118
themeTokenClassNameMap = new Map();
115119
zippedLines = new Map();
116-
nodeMap.forEach(({ lines, tokenizationResults, isTokenized }, node) => {
120+
blockMap.forEach(generate);
121+
spanMap.forEach(generate);
122+
123+
/**
124+
* @param {RegisteredCodeNodeData} data
125+
* @param {TKey} node
126+
*/
127+
function generate({ lines, tokenizationResults, isTokenized }, node) {
117128
if (!isTokenized) return;
118129
/** @type {Token[][][]} */
119130
const zippedLinesForNode = [];
@@ -141,7 +152,7 @@ function createCodeNodeRegistry({ prefixAllClassNames } = {}) {
141152
});
142153
});
143154
});
144-
});
155+
}
145156
}
146157
}
147158

src/factory/html.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ function createLineElement(line, meta, index, language, getLineClassName, tokens
3333
return span(attrs, children, { whitespace: TriviaRenderFlags.NoWhitespace });
3434
}
3535

36+
/**
37+
* @param {number} index
38+
* @param {string} language
39+
* @param {string | undefined} className
40+
* @param {grvsc.HTMLElement[]} tokens
41+
*/
42+
function createCodeSpanElement(index, language, className, tokens) {
43+
return code({ class: className, 'data-language': language, 'data-index': index }, tokens, {
44+
whitespace: TriviaRenderFlags.NoWhitespace
45+
});
46+
}
47+
3648
/**
3749
* Returns the token element array with contiguous spans having the same class name
3850
* merged into a single span to minimize the number of elements returned.
@@ -90,5 +102,6 @@ module.exports = {
90102
createTokenElement,
91103
createLineElement,
92104
createCodeBlockElement,
93-
createStyleElement
105+
createStyleElement,
106+
createCodeSpanElement
94107
};

src/getPossibleThemes.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,9 @@ const {
1414
* @param {T} codeNodeData
1515
* @returns {Promise<ConditionalTheme[]>}
1616
*/
17-
async function getPossibleThemes(
18-
themeOption,
19-
themeCache,
20-
contextDirectory,
21-
codeNodeData
22-
) {
17+
async function getPossibleThemes(themeOption, themeCache, contextDirectory, codeNodeData) {
2318
if (typeof themeOption === 'function') {
24-
return getPossibleThemes(
25-
themeOption(codeNodeData),
26-
themeCache,
27-
contextDirectory,
28-
codeNodeData
29-
);
19+
return getPossibleThemes(themeOption(codeNodeData), themeCache, contextDirectory, codeNodeData);
3020
}
3121

3222
if (typeof themeOption === 'string') {
@@ -36,12 +26,7 @@ async function getPossibleThemes(
3626
/** @type {ConditionalTheme[]} */
3727
let themes;
3828
if (themeOption.default) {
39-
themes = await getPossibleThemes(
40-
themeOption.default,
41-
themeCache,
42-
contextDirectory,
43-
codeNodeData
44-
);
29+
themes = await getPossibleThemes(themeOption.default, themeCache, contextDirectory, codeNodeData);
4530
}
4631
if (themeOption.dark) {
4732
themes = concatConditionalThemes(themes, [

src/graphql/getCodeBlockDataFromRegistry.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ const { flatMap, partitionOne, escapeHTML } = require('../utils');
55
const { createTokenElement, createLineElement, createCodeBlockElement } = require('../factory/html');
66

77
/**
8-
* @template TKey
8+
* @template {Keyable} TKey
99
* @param {CodeNodeRegistry<TKey>} registry
1010
* @param {TKey} key
11-
* @param {RegisteredCodeBlockData} codeBlock
11+
* @param {RegisteredCodeNodeData} codeBlock
1212
* @param {() => string} getWrapperClassName
1313
* @param {(line: LineData) => string} getLineClassName
1414
* @returns {Omit<grvsc.gql.GRVSCCodeBlock, 'id'>}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { renderHTML } = require('../renderers/html');
2+
const { partitionOne } = require('../utils');
3+
const { createTokenElement, createCodeSpanElement } = require('../factory/html');
4+
5+
/**
6+
* @template {Keyable} TKey
7+
* @param {CodeNodeRegistry<TKey>} registry
8+
* @param {TKey} key
9+
* @param {RegisteredCodeNodeData} codeSpan
10+
* @param {() => string} getClassName
11+
* @returns {Omit<grvsc.gql.GRVSCCodeSpan, 'id'>}
12+
*/
13+
function getCodeSpanDataFromRegistry(registry, key, codeSpan, getClassName) {
14+
const { index, languageName, text, possibleThemes } = codeSpan;
15+
/** @type {grvsc.HTMLElement[]} */
16+
const tokenElements = [];
17+
/** @type {grvsc.gql.GRVSCToken[]} */
18+
const gqlTokens = [];
19+
20+
registry.forEachToken(key, 0, token => {
21+
const html = createTokenElement(token);
22+
tokenElements.push(html);
23+
gqlTokens.push({
24+
...token,
25+
className: html.attributes.class,
26+
html: renderHTML(html)
27+
});
28+
});
29+
30+
const className = getClassName();
31+
const html = createCodeSpanElement(index, languageName, className, tokenElements);
32+
const [defaultTheme, additionalThemes] = partitionOne(possibleThemes, t =>
33+
t.conditions.some(c => c.condition === 'default')
34+
);
35+
36+
return {
37+
index,
38+
text,
39+
html: renderHTML(html),
40+
language: languageName,
41+
className: getClassName(),
42+
defaultTheme,
43+
additionalThemes,
44+
tokens: gqlTokens
45+
};
46+
}
47+
48+
module.exports = getCodeSpanDataFromRegistry;

src/graphql/getThemes.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async function convertThemeArgument(theme, themeCache) {
1717
}
1818

1919
/**
20-
* @param {ThemeOption} themeOption
20+
* @param {ThemeOption<CodeBlockData | CodeSpanData>} themeOption
2121
* @param {grvsc.gql.CSSArgs} args
2222
* @param {ThemeCache} themeCache
2323
* @returns {Promise<ConditionalTheme[]>}
@@ -40,7 +40,7 @@ async function getThemes(themeOption, args, themeCache) {
4040
`evaluating 'grvscHighlight'.`
4141
);
4242
}
43-
return getPossibleThemes(themeOption, themeCache, undefined, undefined, undefined, undefined, undefined);
43+
return getPossibleThemes(themeOption, themeCache, undefined, undefined);
4444
}
4545

4646
module.exports = getThemes;

src/graphql/highlight.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
const setup = require('../setup');
22
const plugin = require('../../index');
3-
const registerCodeBlock = require('../registerCodeBlock');
4-
const parseCodeFenceHeader = require('../parseCodeFenceHeader');
5-
const createCodeBlockRegistry = require('../createCodeBlockRegistry');
3+
const { registerCodeBlock } = require('../registerCodeNode');
4+
const parseCodeFenceHeader = require('../parseCodeFenceInfo');
5+
const createCodeNodeRegistry = require('../createCodeNodeRegistry');
66
const getCodeBlockDataFromRegistry = require('./getCodeBlockDataFromRegistry');
77
const getThemes = require('./getThemes');
88
const { createHash } = require('crypto');
99
const { getScope } = require('../storeUtils');
10-
const registryKey = 0;
10+
/** @type {{ type: 'code' }} */
11+
const registryKey = { type: 'code' };
1112

1213
/**
1314
* @param {grvsc.gql.HighlightArgs} args
@@ -41,11 +42,11 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
4142
const possibleThemes = await getThemes(theme, args, themeCache);
4243
const scope = getScope(args.language, grammarCache, languageAliases);
4344
/** @type {CodeNodeRegistry<typeof registryKey>} */
44-
const codeBlockRegistry = createCodeBlockRegistry({ prefixAllClassNames: true });
45+
const codeNodeRegistry = createCodeNodeRegistry({ prefixAllClassNames: true });
4546
const meta = parseCodeFenceHeader(args.language, args.meta);
4647

4748
await registerCodeBlock(
48-
codeBlockRegistry,
49+
codeNodeRegistry,
4950
registryKey,
5051
possibleThemes,
5152
() => plugin.getRegistry(cache, scope),
@@ -59,9 +60,9 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
5960

6061
/** @type {Omit<grvsc.gql.GRVSCCodeBlock, 'id'>} */
6162
let result;
62-
codeBlockRegistry.forEachCodeBlock(codeBlock => {
63+
codeNodeRegistry.forEachCodeBlock(codeBlock => {
6364
result = getCodeBlockDataFromRegistry(
64-
codeBlockRegistry,
65+
codeNodeRegistry,
6566
registryKey,
6667
codeBlock,
6768
getWrapperClassName,
@@ -72,6 +73,7 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
7273
return typeof wrapperClassName === 'function'
7374
? wrapperClassName({
7475
language: codeBlock.languageName,
76+
node: undefined,
7577
markdownNode: undefined,
7678
codeFenceNode: undefined,
7779
parsedOptions: codeBlock.meta

src/index.js

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const parseCodeFenceInfo = require('./parseCodeFenceInfo');
1111
const parseCodeSpanInfo = require('./parseCodeSpanInfo');
1212
const createSchemaCustomization = require('./graphql/createSchemaCustomization');
1313
const getCodeBlockGraphQLDataFromRegistry = require('./graphql/getCodeBlockDataFromRegistry');
14+
const getCodeSpanGraphQLDataFromRegistry = require('./graphql/getCodeSpanDataFromRegistry');
1415
const { registerCodeBlock, registerCodeSpan } = require('./registerCodeNode');
1516
const { createHash } = require('crypto');
1617
const { setChildNodes } = require('./cacheUtils');
@@ -63,25 +64,30 @@ function createPlugin() {
6364

6465
/** @type {(MDASTNode<'code'> | MDASTNode<'inlineCode'>)[]} */
6566
const nodes = [];
66-
visit(markdownAST, ({ type }) => type === 'code' || type === 'inlineCode', node => {
67-
nodes.push(node);
68-
});
67+
visit(
68+
markdownAST,
69+
({ type }) => type === 'code' || type === 'inlineCode',
70+
node => {
71+
nodes.push(node);
72+
}
73+
);
6974

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

74-
/** @type {grvsc.gql.GRVSCCodeBlock[]} */
79+
/** @type {(grvsc.gql.GRVSCCodeBlock | grvsc.gql.GRVSCCodeSpan)[]} */
7580
const graphQLNodes = [];
76-
/** @type {CodeNodeRegistry<MDASTNode>} */
81+
/** @type {CodeNodeRegistry<MDASTNode<'code' | 'inlineCode'>>} */
7782
const codeNodeRegistry = createCodeNodeRegistry();
7883
for (const node of nodes) {
7984
/** @type {string} */
8085
const text = node.value || (node.children && node.children[0] && node.children[0].value);
8186
if (!text) continue;
82-
const { languageName, meta, text: parsedText = text } = node.type === 'code'
83-
? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta)
84-
: parseCodeSpanInfo(text, inlineCode.marker);
87+
const { languageName, meta, text: parsedText = text } =
88+
node.type === 'code'
89+
? parseCodeFenceInfo(node.lang ? node.lang.toLowerCase() : '', node.meta)
90+
: parseCodeSpanInfo(text, inlineCode.marker);
8591

8692
if (node.type === 'inlineCode' && !languageName) {
8793
continue;
@@ -138,7 +144,7 @@ function createPlugin() {
138144
}
139145
}
140146

141-
// 3. For each code block registered, convert its tokenization and theme data
147+
// 3. For each code block/span registered, convert its tokenization and theme data
142148
// to a GraphQL-compatible representation, including HTML renderings. At the same
143149
// time, change the original code fence Markdown node to an HTML node and set
144150
// its value to the HTML rendering contained in the GraphQL node.
@@ -153,7 +159,8 @@ function createPlugin() {
153159
);
154160

155161
// Update Markdown node
156-
node.type = 'html';
162+
/** @type {MDASTNode} */
163+
(node).type = 'html';
157164
node.value = graphQLNode.html;
158165

159166
// Push GraphQL node
@@ -182,6 +189,38 @@ function createPlugin() {
182189
}
183190
});
184191

192+
codeNodeRegistry.forEachCodeSpan((codeSpan, node) => {
193+
const graphQLNode = getCodeSpanGraphQLDataFromRegistry(codeNodeRegistry, node, codeSpan, getClassName);
194+
195+
// Update Markdown node
196+
/** @type {MDASTNode} */
197+
(node).type = 'html';
198+
node.value = graphQLNode.html;
199+
200+
// Push GraphQL node
201+
graphQLNodes.push({
202+
...graphQLNode,
203+
id: createNodeId(`GRVSCCodeSpan-${markdownNode.id}-${codeSpan.index}`),
204+
parent: markdownNode.id,
205+
internal: {
206+
type: 'GRVSCCodeSpan',
207+
contentDigest: createHash('md5')
208+
.update(JSON.stringify(graphQLNode))
209+
.digest('hex')
210+
}
211+
});
212+
213+
function getClassName() {
214+
return typeof inlineCode.className === 'function'
215+
? inlineCode.className({
216+
language: codeSpan.languageName,
217+
markdownNode,
218+
node
219+
})
220+
: inlineCode.className;
221+
}
222+
});
223+
185224
// 4. Generate CSS rules for each theme used by one or more code blocks in the registry,
186225
// then append that CSS to the Markdown AST in an HTML node.
187226

src/parseCodeSpanInfo.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
function parseCodeSpanInfo(text, delimiter) {
77
const index = text.indexOf(delimiter);
88
if (index <= 0) return { languageName: undefined, meta: undefined, text };
9-
const languageName = text.slice(0, index).trim().toLowerCase();
9+
const languageName = text
10+
.slice(0, index)
11+
.trim()
12+
.toLowerCase();
1013
if (!languageName) return { languageName: undefined, meta: undefined, text };
1114
return { languageName, meta: undefined, text: text.slice(index + delimiter.length) };
1215
}

0 commit comments

Comments
 (0)