Skip to content

Commit d30ad0d

Browse files
committed
Add stylesheet root resolver
1 parent 27e3652 commit d30ad0d

14 files changed

+270
-123
lines changed

gatsby-node.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const logger = require('loglevel');
22
const { getChildNodes } = require('./src/cacheUtils');
33
const highlight = require('./src/graphql/highlight');
4+
const stylesheet = require('./src/graphql/stylesheet');
45

56
exports.createResolvers = ({
67
createResolvers,
@@ -67,6 +68,18 @@ exports.createResolvers = ({
6768
return highlight(args, pluginOptions, { cache, createNodeId });
6869
}
6970
}
71+
},
72+
73+
grvscStylesheet: {
74+
type: 'GRVSCStylesheet',
75+
args: {
76+
defaultTheme: 'String',
77+
additionalThemes: ['GRVSCThemeArgument!'],
78+
injectStyles: 'Boolean'
79+
},
80+
resolve(_, args) {
81+
return stylesheet(args, pluginOptions, { cache, createNodeId });
82+
}
7083
}
7184
});
7285
};

src/createCodeBlockRegistry.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// @ts-check
2+
const { boldDeclarations, italicDeclarations, underlineDeclarations } = require('./factory/css');
23
const { getThemePrefixedTokenClassName, concatConditionalThemes } = require('./themeUtils');
34
const { getTokenDataFromMetadata } = require('../lib/vscode/modes');
45
const { declaration } = require('./renderers/css');
56

67
/**
78
* @template TKey
9+
* @param {CodeBlockRegistryOptions=} options
810
* @returns {CodeBlockRegistry<TKey>}
911
*/
10-
function createCodeBlockRegistry() {
12+
function createCodeBlockRegistry({ prefixAllClassNames } = {}) {
1113
/** @type {Map<TKey, RegisteredCodeBlockData & { index: number }>} */
1214
const nodeMap = new Map();
1315
/** @type {ConditionalTheme[]} */
@@ -86,13 +88,13 @@ function createCodeBlockRegistry() {
8688
if (classNameMap) {
8789
classNameMap.forEach((className, canonicalClassName) => {
8890
if (canonicalClassName === 'mtkb') {
89-
result.unshift({ className, css: [declaration('font-weight', 'bold')] });
91+
result.unshift({ className, css: boldDeclarations });
9092
} else if (canonicalClassName === 'mtki') {
91-
result.unshift({ className, css: [declaration('font-style', 'italic')] });
93+
result.unshift({ className, css: italicDeclarations });
9294
} else if (canonicalClassName === 'mtku') {
9395
result.unshift({
9496
className,
95-
css: [declaration('text-decoration', 'underline'), declaration('text-underline-position', 'under')]
97+
css: underlineDeclarations
9698
});
9799
} else {
98100
result.push({
@@ -131,7 +133,7 @@ function createCodeBlockRegistry() {
131133
canonicalClassNames.forEach(canonicalClassName => {
132134
themeClassNames.set(
133135
canonicalClassName,
134-
tokensAtPosition.length > 1
136+
prefixAllClassNames || tokensAtPosition.length > 1
135137
? getThemePrefixedTokenClassName(canonicalClassName, theme.identifier)
136138
: canonicalClassName
137139
);
@@ -192,8 +194,10 @@ function zipLineTokens(lineTokenSets) {
192194
*/
193195
function getColorFromColorMap(colorMap, canonicalClassName) {
194196
const index = +canonicalClassName.slice('mtk'.length);
195-
if (!Number.isInteger(index) || index < 0) {
196-
throw new Error(`Canonical class name must be in form 'mtk{positive integer}'. Received '${canonicalClassName}'.`);
197+
if (!Number.isInteger(index) || index < 1) {
198+
throw new Error(
199+
`Canonical class name must be in form 'mtk{Integer greater than 0}'. Received '${canonicalClassName}'.`
200+
);
197201
}
198202
return colorMap[index];
199203
}

src/factory/css.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const { groupConditions, getStylesFromThemeSettings, getThemeClassName } = require('../themeUtils');
2+
const { ruleset, declaration, media } = require('../renderers/css');
3+
4+
const boldDeclarations = [declaration('font-weight', 'bold')];
5+
const italicDeclarations = [declaration('font-style', 'italic')];
6+
const underlineDeclarations = [
7+
declaration('text-decoration', 'underline'),
8+
declaration('text-underline-position', 'under')
9+
];
10+
11+
/**
12+
* @param {ConditionalTheme} theme
13+
* @param {Record<string, string>} settings
14+
* @param {{ className: string, css: grvsc.CSSDeclaration[] }[]} tokenClassNames
15+
* @param {(color: string, themeIdentifier: string) => string} replaceColor
16+
*/
17+
function createThemeCSSRules(theme, settings, tokenClassNames, replaceColor) {
18+
const conditions = groupConditions(theme.conditions);
19+
/** @type {grvsc.CSSElement[]} */
20+
const elements = [];
21+
const containerStyles = getStylesFromThemeSettings(settings);
22+
if (conditions.default) {
23+
pushColorRules(elements, '.' + getThemeClassName(theme.identifier, 'default'));
24+
}
25+
for (const condition of conditions.parentSelector) {
26+
pushColorRules(elements, `${condition.value} .${getThemeClassName(theme.identifier, 'parentSelector')}`);
27+
}
28+
for (const condition of conditions.matchMedia) {
29+
/** @type {grvsc.CSSRuleset[]} */
30+
const ruleset = [];
31+
pushColorRules(ruleset, '.' + getThemeClassName(theme.identifier, 'matchMedia'));
32+
elements.push(media(condition.value, ruleset, theme.identifier));
33+
}
34+
return elements;
35+
36+
/**
37+
* @param {grvsc.CSSElement[]} container
38+
* @param {string} selector
39+
* @param {string=} leadingComment
40+
*/
41+
function pushColorRules(container, selector, leadingComment) {
42+
if (containerStyles.length) {
43+
container.push(ruleset(selector, containerStyles, leadingComment));
44+
leadingComment = undefined;
45+
}
46+
for (const { className, css } of tokenClassNames) {
47+
container.push(
48+
ruleset(
49+
`${selector} .${className}`,
50+
css.map(decl =>
51+
decl.property === 'color' ? declaration('color', replaceColor(decl.value, theme.identifier)) : decl
52+
),
53+
leadingComment
54+
)
55+
);
56+
leadingComment = undefined;
57+
}
58+
}
59+
}
60+
61+
module.exports = {
62+
createThemeCSSRules,
63+
boldDeclarations,
64+
italicDeclarations,
65+
underlineDeclarations
66+
};

src/htmlFactory.js renamed to src/factory/html.js

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
const escapeHTML = require('lodash.escape');
2-
const { last, flatMap } = require('./utils');
3-
const { groupConditions, getStylesFromThemeSettings, getThemeClassName } = require('./themeUtils');
4-
const { joinClassNames, ruleset, declaration, media } = require('./renderers/css');
5-
const { pre, code, span, style, TriviaRenderFlags, mergeAttributes } = require('./renderers/html');
2+
const { last, flatMap } = require('../utils');
3+
const { createThemeCSSRules } = require('./css');
4+
const { joinClassNames } = require('../renderers/css');
5+
const { pre, code, span, style, TriviaRenderFlags, mergeAttributes } = require('../renderers/html');
66

77
/**
88
* @param {RegisteredToken} token
@@ -79,48 +79,7 @@ function createCodeBlockElement(preClassName, codeClassName, languageName, index
7979
*/
8080
function createStyleElement(possibleThemes, getTokenStylesForTheme, replaceColor, injectedStyles) {
8181
const rules = flatMap(possibleThemes, ({ theme, settings }) => {
82-
const conditions = groupConditions(theme.conditions);
83-
/** @type {grvsc.CSSElement[]} */
84-
const elements = [];
85-
const tokenClassNames = getTokenStylesForTheme(theme.identifier);
86-
const containerStyles = getStylesFromThemeSettings(settings);
87-
if (conditions.default) {
88-
pushColorRules(elements, '.' + getThemeClassName(theme.identifier, 'default'));
89-
}
90-
for (const condition of conditions.parentSelector) {
91-
pushColorRules(elements, `${condition.value} .${getThemeClassName(theme.identifier, 'parentSelector')}`);
92-
}
93-
for (const condition of conditions.matchMedia) {
94-
/** @type {grvsc.CSSRuleset[]} */
95-
const ruleset = [];
96-
pushColorRules(ruleset, '.' + getThemeClassName(theme.identifier, 'matchMedia'));
97-
elements.push(media(condition.value, ruleset, theme.identifier));
98-
}
99-
return elements;
100-
101-
/**
102-
* @param {grvsc.CSSElement[]} container
103-
* @param {string} selector
104-
* @param {string=} leadingComment
105-
*/
106-
function pushColorRules(container, selector, leadingComment) {
107-
if (containerStyles.length) {
108-
container.push(ruleset(selector, containerStyles, leadingComment));
109-
leadingComment = undefined;
110-
}
111-
for (const { className, css } of tokenClassNames) {
112-
container.push(
113-
ruleset(
114-
`${selector} .${className}`,
115-
css.map(decl =>
116-
decl.property === 'color' ? declaration('color', replaceColor(decl.value, theme.identifier)) : decl
117-
),
118-
leadingComment
119-
)
120-
);
121-
leadingComment = undefined;
122-
}
123-
}
82+
return createThemeCSSRules(theme, settings, getTokenStylesForTheme(theme.identifier), replaceColor);
12483
});
12584

12685
if (rules.length || injectedStyles) {

src/graphql/getCodeBlockDataFromRegistry.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { renderHTML } = require('../renderers/html');
33
const { joinClassNames } = require('../renderers/css');
44
const { flatMap, partitionOne } = require('../utils');
55
const { getThemeClassNames } = require('../themeUtils');
6-
const { createTokenElement, createLineElement, createCodeBlockElement } = require('../htmlFactory');
6+
const { createTokenElement, createLineElement, createCodeBlockElement } = require('../factory/html');
77

88
/**
99
* @template TKey
@@ -12,10 +12,9 @@ const { createTokenElement, createLineElement, createCodeBlockElement } = requir
1212
* @param {RegisteredCodeBlockData} codeBlock
1313
* @param {() => string} getWrapperClassName
1414
* @param {(line: LineData) => string} getLineClassName
15-
* @param {() => string} getNodeId
16-
* @returns {grvsc.gql.GRVSCCodeBlock}
15+
* @returns {Omit<grvsc.gql.GRVSCCodeBlock, 'id'>}
1716
*/
18-
function getCodeBlockDataFromRegistry(registry, key, codeBlock, getWrapperClassName, getLineClassName, getNodeId) {
17+
function getCodeBlockDataFromRegistry(registry, key, codeBlock, getWrapperClassName, getLineClassName) {
1918
const { meta, index, languageName, text, possibleThemes, isTokenized } = codeBlock;
2019
/** @type {grvsc.HTMLElement[]} */
2120
const lineElements = [];
@@ -61,7 +60,6 @@ function getCodeBlockDataFromRegistry(registry, key, codeBlock, getWrapperClassN
6160
);
6261

6362
return {
64-
id: getNodeId(),
6563
index,
6664
text,
6765
html: renderHTML(createCodeBlockElement(preClassName, codeClassName, languageName, index, lineElements)),

src/graphql/getThemes.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const getPossibleThemes = require('../getPossibleThemes');
2+
const { ensureThemeLocation } = require('../storeUtils');
3+
const { createDefaultTheme, concatConditionalThemes } = require('../themeUtils');
4+
const parseThemeCondition = require('./parseThemeCondition');
5+
6+
/**
7+
* @param {grvsc.gql.GRVSCThemeArgument} theme
8+
* @param {any} themeCache
9+
* @returns {Promise<ConditionalTheme>}
10+
*/
11+
async function convertThemeArgument(theme, themeCache) {
12+
return {
13+
identifier: theme.identifier,
14+
path: await ensureThemeLocation(theme.identifier, themeCache, undefined),
15+
conditions: theme.conditions.map(parseThemeCondition)
16+
};
17+
}
18+
19+
/**
20+
* @param {ThemeOption} themeOption
21+
* @param {grvsc.gql.CSSArgs} args
22+
* @param {any} themeCache
23+
* @returns {Promise<ConditionalTheme[]>}
24+
*/
25+
async function getThemes(themeOption, args, themeCache) {
26+
if (args.defaultTheme) {
27+
const defaultTheme = await createDefaultTheme(args.defaultTheme, themeCache);
28+
const additionalThemes = args.additionalThemes
29+
? await Promise.all(args.additionalThemes.map(t => convertThemeArgument(t, themeCache)))
30+
: [];
31+
return concatConditionalThemes([defaultTheme], additionalThemes);
32+
}
33+
if (args.additionalThemes) {
34+
throw new Error(`Must provide a 'defaultTheme' if 'additionalThemes' are provided.`);
35+
}
36+
if (typeof themeOption === 'function') {
37+
throw new Error(
38+
`When plugin option 'theme' is a function, GraphQL resolvers 'grvscHighlight' and ` +
39+
`'grvscStylesheet' must supply a 'defaultTheme' argument. The 'theme' function will not be called while ` +
40+
`evaluating 'grvscHighlight'.`
41+
);
42+
}
43+
return getPossibleThemes(themeOption, themeCache, undefined, undefined, undefined, undefined, undefined);
44+
}
45+
46+
module.exports = getThemes;

src/graphql/highlight.js

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,19 @@
11
const setup = require('../setup');
22
const plugin = require('../../index');
3-
const getPossibleThemes = require('../getPossibleThemes');
43
const registerCodeBlock = require('../registerCodeBlock');
54
const parseCodeFenceHeader = require('../parseCodeFenceHeader');
65
const createCodeBlockRegistry = require('../createCodeBlockRegistry');
76
const getCodeBlockDataFromRegistry = require('./getCodeBlockDataFromRegistry');
8-
const { ensureThemeLocation, getScope } = require('../storeUtils');
9-
const { createDefaultTheme, concatConditionalThemes } = require('../themeUtils');
10-
const parseThemeCondition = require('./parseThemeCondition');
7+
const getThemes = require('./getThemes');
8+
const { createHash } = require('crypto');
9+
const { getScope } = require('../storeUtils');
1110
const registryKey = 0;
1211

13-
/**
14-
* @param {grvsc.gql.GRVSCThemeArgument} theme
15-
* @param {any} themeCache
16-
* @returns {Promise<ConditionalTheme>}
17-
*/
18-
async function convertThemeArgument(theme, themeCache) {
19-
return {
20-
identifier: theme.identifier,
21-
path: await ensureThemeLocation(theme.identifier, themeCache, undefined),
22-
conditions: theme.conditions.map(parseThemeCondition)
23-
};
24-
}
25-
26-
/**
27-
* @param {ThemeOption} themeOption
28-
* @param {grvsc.gql.HighlightArgs} args
29-
* @param {any} themeCache
30-
* @returns {Promise<ConditionalTheme[]>}
31-
*/
32-
async function getThemes(themeOption, args, themeCache) {
33-
if (args.defaultTheme) {
34-
const defaultTheme = await createDefaultTheme(args.defaultTheme, themeCache);
35-
const additionalThemes = args.additionalThemes
36-
? await Promise.all(args.additionalThemes.map(t => convertThemeArgument(t, themeCache)))
37-
: [];
38-
return concatConditionalThemes([defaultTheme], additionalThemes);
39-
}
40-
if (args.additionalThemes) {
41-
throw new Error(`Must provide a 'defaultTheme' if 'additionalThemes' are provided.`);
42-
}
43-
if (typeof themeOption === 'function') {
44-
throw new Error(
45-
`When plugin option 'theme' is a function, GraphQL resolver 'grvscHighlight' must supply a 'defaultTheme' argument. ` +
46-
`The 'theme' function will not be called while evaluating 'grvscHighlight'.`
47-
);
48-
}
49-
return getPossibleThemes(themeOption, themeCache, undefined, undefined, undefined, undefined, undefined);
50-
}
51-
5212
/**
5313
* @param {grvsc.gql.HighlightArgs} args
5414
* @param {PluginOptions} pluginOptions
5515
* @param {{ cache: GatsbyCache, createNodeId: (key: string) => string}} pluginArguments
16+
* @returns {Promise<grvsc.gql.GRVSCCodeBlock>}
5617
*/
5718
async function highlight(args, pluginOptions, { cache, createNodeId }) {
5819
const {
@@ -77,7 +38,7 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
7738
const possibleThemes = await getThemes(theme, args, themeCache);
7839
const scope = getScope(args.language, grammarCache, languageAliases);
7940
/** @type {CodeBlockRegistry<typeof registryKey>} */
80-
const codeBlockRegistry = createCodeBlockRegistry();
41+
const codeBlockRegistry = createCodeBlockRegistry({ prefixAllClassNames: true });
8142
const meta = parseCodeFenceHeader(args.language, args.meta);
8243

8344
await registerCodeBlock(
@@ -93,16 +54,15 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
9354
cache
9455
);
9556

96-
/** @type {grvsc.gql.GRVSCCodeBlock} */
57+
/** @type {Omit<grvsc.gql.GRVSCCodeBlock, 'id'>} */
9758
let result;
9859
codeBlockRegistry.forEachCodeBlock(codeBlock => {
9960
result = getCodeBlockDataFromRegistry(
10061
codeBlockRegistry,
10162
registryKey,
10263
codeBlock,
10364
getWrapperClassName,
104-
getLineClassName,
105-
() => createNodeId(`GRVSCCodeBlock-Highlight`)
65+
getLineClassName
10666
);
10767

10868
function getWrapperClassName() {
@@ -117,7 +77,16 @@ async function highlight(args, pluginOptions, { cache, createNodeId }) {
11777
}
11878
});
11979

120-
return result;
80+
return {
81+
...result,
82+
id: createNodeId('GRVSCCodeBlock-Highlight'),
83+
internal: {
84+
type: 'GRVSCCodeBlock',
85+
contentDigest: createHash('md5')
86+
.update(JSON.stringify(result))
87+
.digest('hex')
88+
}
89+
};
12190
}
12291

12392
module.exports = highlight;

0 commit comments

Comments
 (0)