Skip to content

Commit a926410

Browse files
committed
Finish layout, add test, make it work with line highlighting
1 parent 00abb5a commit a926410

26 files changed

+664
-117
lines changed

src/factory/html.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ function createLineElement(line, meta, index, language, getLineClassName, tokens
4141
);
4242
}
4343

44-
/** @param {GutterCell} cell */
44+
/** @param {GutterCell | undefined} cell */
4545
function createGutterCellElement(cell) {
46-
return span({ class: joinClassNames('grvsc-gutter', cell.className) }, [escapeHTML(cell.text || '')], {
47-
whitespace: TriviaRenderFlags.NoWhitespace
48-
});
46+
return span(
47+
{ class: joinClassNames('grvsc-gutter', cell && cell.className) },
48+
[escapeHTML((cell && cell.text) || '')],
49+
{
50+
whitespace: TriviaRenderFlags.NoWhitespace
51+
}
52+
);
4953
}
5054

5155
/**

src/graphql/getCodeBlockDataFromRegistry.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ function getCodeBlockDataFromRegistry(registry, key, codeBlock, getWrapperClassN
5252

5353
const wrapperClassNameValue = getWrapperClassName();
5454
const themeClassNames = flatMap(possibleThemes, getThemeClassNames);
55-
const preClassName = joinClassNames('grvsc-container', wrapperClassNameValue, ...themeClassNames);
55+
const preClassName = joinClassNames(
56+
'grvsc-container',
57+
wrapperClassNameValue,
58+
codeBlock.className,
59+
...themeClassNames
60+
);
5661
const codeClassName = 'grvsc-code';
5762
const [defaultTheme, additionalThemes] = partitionOne(possibleThemes, t =>
5863
t.conditions.some(c => c.condition === 'default')

src/registerCodeNode.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const tokenizeWithTheme = require('./tokenizeWithTheme');
22
const { getTransformedLines } = require('./transformers');
33
const { getGrammar } = require('./storeUtils');
4+
const { joinClassNames } = require('./renderers/css');
5+
const { uniq } = require('./utils');
46

57
/**
68
* @template {Keyable} TKey
@@ -42,6 +44,7 @@ async function registerCodeBlock(
4244
tokenTypes = grammarData.tokenTypes;
4345
}
4446

47+
const addedClassNames = joinClassNames(...uniq(lines.map(l => l.setContainerClassName)));
4548
const grammar = languageId && (await registry.loadGrammarWithConfiguration(scope, languageId, { tokenTypes }));
4649
codeBlockRegistry.register(registryKey, {
4750
lines,
@@ -50,7 +53,8 @@ async function registerCodeBlock(
5053
languageName,
5154
possibleThemes,
5255
isTokenized: !!grammar,
53-
tokenizationResults: possibleThemes.map(theme => tokenizeWithTheme(lines, theme, grammar, registry))
56+
tokenizationResults: possibleThemes.map(theme => tokenizeWithTheme(lines, theme, grammar, registry)),
57+
className: addedClassNames || undefined
5458
});
5559
} finally {
5660
unlockRegistry();

src/transformers/getTransformedLines.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// @ts-check
22

3+
const { joinClassNames } = require('../renderers/css');
4+
35
/**
46
*
57
* @param {LineTransformer[]} transformers
@@ -23,6 +25,8 @@ async function getTransformedLines(transformers, text, languageName, meta) {
2325
const lineGutterCells = [];
2426
const attrs = {};
2527
const graphQLData = {};
28+
/** @type {string[]} */
29+
const addedContainerClassNames = [];
2630
for (let i = 0; i < transformers.length; i++) {
2731
const transformer = transformers[i];
2832
const state = prevTransformerStates[i];
@@ -35,19 +39,30 @@ async function getTransformedLines(transformers, text, languageName, meta) {
3539
});
3640

3741
prevTransformerStates[i] = txResult.state;
42+
if (txResult.setContainerClassName) {
43+
addedContainerClassNames.push(txResult.setContainerClassName);
44+
}
3845
if (!txResult.line) {
3946
continue linesLoop;
4047
}
4148
if (txResult.gutterCells) {
4249
gutterCellsPerTransformer[i] = Math.max(txResult.gutterCells.length, gutterCellsPerTransformer[i] || 0);
4350
lineGutterCells[i] = txResult.gutterCells;
51+
} else {
52+
gutterCellsPerTransformer[i] = Math.max(0, gutterCellsPerTransformer[i] || 0);
4453
}
54+
4555
line = txResult.line.text;
4656
Object.assign(attrs, txResult.line.attrs);
4757
Object.assign(graphQLData, txResult.data);
4858
}
4959
gutterCells.push(lineGutterCells);
50-
result.push({ text: line, attrs, data: graphQLData });
60+
result.push({
61+
text: line,
62+
attrs,
63+
data: graphQLData,
64+
setContainerClassName: joinClassNames(...addedContainerClassNames) || undefined
65+
});
5166
}
5267

5368
const flattenedGutterCells = flattenGutterCells(gutterCells, gutterCellsPerTransformer);
@@ -94,17 +109,17 @@ async function getTransformedLines(transformers, text, languageName, meta) {
94109
* @returns {GutterCell[][]}
95110
*/
96111
function flattenGutterCells(gutterCells, gutterCellsPerTransformer) {
112+
const totalGutterCells = gutterCellsPerTransformer.reduce((a, b) => a + b, 0);
97113
return gutterCells.map(transformerResults => {
98114
/** @type {GutterCell[]} */
99-
const result = [];
115+
const result = Array(totalGutterCells).fill(undefined);
100116
for (let i = 0; i < transformerResults.length; i++) {
101117
const currentTransformerCells = transformerResults[i];
102-
const length = currentTransformerCells ? currentTransformerCells.length : 0;
103-
const padding = gutterCellsPerTransformer[i] - length;
104118
if (currentTransformerCells) {
105-
result.push(...currentTransformerCells);
119+
for (let j = 0; j < currentTransformerCells.length; j++) {
120+
result[(gutterCellsPerTransformer[i - 1] || 0) + j] = currentTransformerCells[j];
121+
}
106122
}
107-
result.fill(undefined, result.length, result.length + padding);
108123
}
109124
return result;
110125
});

src/transformers/highlightDirectiveLineTransformer.js

Lines changed: 13 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,12 @@
11
// @ts-check
2-
const { highlightLine } = require('./transformerUtils');
2+
const { highlightLine, getCommentContent, getCommentRegExp } = require('./transformerUtils');
33
const { getScope } = require('../storeUtils');
44

55
/**
6-
* @param {string} language
7-
* @param {string} scope
8-
* @param {Record<string, (message: string) => string>} languageCommentMap
9-
* @return {(commentMessage: string) => string} curried function taking a string argument and
10-
* prefixing/wrapping that with a language's comment syntax
11-
*/
12-
const getCommentForLanguage = (language, scope, languageCommentMap) => message => {
13-
// example: languageCommentMap = {js: str => `// ${str}`}
14-
if (languageCommentMap[language]) {
15-
return languageCommentMap[language](message);
16-
}
17-
18-
switch (scope) {
19-
case 'source.python':
20-
case 'source.ruby':
21-
case 'source.shell':
22-
case 'source.perl':
23-
case 'source.coffee':
24-
case 'source.yaml':
25-
return `# ${message}`;
26-
case 'source.css':
27-
case 'source.c':
28-
case 'source.cpp':
29-
case 'source.objc':
30-
case 'source.css.less':
31-
return `/* ${message} */`;
32-
case 'text.html.derivative':
33-
case 'text.xml':
34-
case 'text.html.markdown':
35-
return `<!-- ${message} -->`;
36-
case 'source.clojure':
37-
return `; ${message}`;
38-
case 'source.sql':
39-
return `-- ${message}`;
40-
default:
41-
return `// ${message}`;
42-
}
43-
};
44-
45-
/**
46-
* @param {string} text
47-
* @param {(directive: string) => string} commentWrapper
48-
* @return {(directive: string) => boolean} curried function taking a directive string and checking
49-
* whether it equals the line text
50-
*/
51-
const textIsHighlightDirective = (text, commentWrapper) => directive =>
52-
['// ' + directive, commentWrapper(directive)].includes(text.trim());
53-
54-
/**
55-
* @param {Record<string, (message: string) => string>} languageCommentMap user-defined object mapping language keys to commenting functions
566
* @param {Record<string, string>} languageAliases
577
* @param {GatsbyCache} cache
588
*/
59-
function createHighlightDirectiveLineTransformer(languageCommentMap, languageAliases, cache) {
9+
function createHighlightDirectiveLineTransformer(languageAliases, cache) {
6010
let grammarCache;
6111
/** @type {LineTransformer<HighlightCommentTransfomerState>} */
6212
const transformer = async ({ line, language, state }) => {
@@ -65,30 +15,24 @@ function createHighlightDirectiveLineTransformer(languageCommentMap, languageAli
6515
}
6616

6717
const scope = getScope(language, grammarCache, languageAliases);
68-
const commentWrapper = getCommentForLanguage(language, scope, languageCommentMap);
69-
const isDirective = textIsHighlightDirective(line.text, commentWrapper);
70-
if (isDirective('highlight-start')) {
18+
const commentContent = getCommentContent(line.text, scope, /*trim*/ true);
19+
20+
if (commentContent === 'highlight-start') {
7121
return { state: { inHighlightRange: true } }; // no `line` - drop this line from output
7222
}
73-
if (isDirective('highlight-end')) {
23+
if (commentContent === 'highlight-end') {
7424
return { state: { inHighlightRange: false } }; // again no `line`
7525
}
76-
if (isDirective('highlight-next-line')) {
26+
if (commentContent === 'highlight-next-line') {
7727
return { state: { highlightNextLine: true } }; // again no `line`
7828
}
79-
if (
80-
line.text.endsWith(commentWrapper('highlight-line')) ||
81-
line.text.endsWith('// highlight-line') ||
82-
(state && state.inHighlightRange)
83-
) {
29+
if (commentContent === 'highlight-line' || (state && state.inHighlightRange)) {
8430
// return attrs with added class name, text with comment removed, current state
8531
return {
86-
line: highlightLine(
87-
line,
88-
line.text.replace(commentWrapper('highlight-line'), '').replace('// highlight-line', '')
89-
),
32+
line: highlightLine(line, line.text.replace(getCommentRegExp(scope), '')),
9033
state,
91-
data: { isHighlighted: true }
34+
data: { isHighlighted: true },
35+
setContainerClassName: 'grvsc-has-line-highlighting'
9236
};
9337
}
9438
if (state && state.highlightNextLine) {
@@ -98,7 +42,8 @@ function createHighlightDirectiveLineTransformer(languageCommentMap, languageAli
9842
return {
9943
line: highlightLine(line),
10044
state: { ...state, highlightNextLine: false },
101-
data: { isHighlighted: true }
45+
data: { isHighlighted: true },
46+
setContainerClassName: 'grvsc-has-line-highlighting'
10247
};
10348
}
10449
return { line, state }; // default: don’t change anything, propagate state to next call

src/transformers/highlightMetaTransformer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ const highlightMetaTransformer = ({ meta, line, state = getInitialState(meta) })
4646
state: {
4747
lineNumber: state.lineNumber + 1,
4848
highlightedLines: isHighlighted ? state.highlightedLines.slice(1) : state.highlightedLines
49-
}
49+
},
50+
setContainerClassName: isHighlighted ? 'grvsc-has-line-highlighting' : undefined
5051
};
5152
};
5253

src/transformers/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @ts-check
22
const { highlightMetaTransformer } = require('./highlightMetaTransformer');
33
const { createHighlightDirectiveLineTransformer } = require('./highlightDirectiveLineTransformer');
4+
const { createLineNumberLineTransformer } = require('./lineNumberTransformer');
45
const getTransformedLines = require('./getTransformedLines');
56

67
/**
@@ -9,7 +10,11 @@ const getTransformedLines = require('./getTransformedLines');
910
* @returns {LineTransformer[]}
1011
*/
1112
function getDefaultLineTransformers(pluginOptions, cache) {
12-
return [createHighlightDirectiveLineTransformer({}, pluginOptions.languageAliases, cache), highlightMetaTransformer];
13+
return [
14+
createHighlightDirectiveLineTransformer(pluginOptions.languageAliases, cache),
15+
highlightMetaTransformer,
16+
createLineNumberLineTransformer(pluginOptions.languageAliases, cache)
17+
];
1318
}
1419

1520
/**
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const { getCommentContent, getCommentRegExp } = require('./transformerUtils');
2+
const { getScope } = require('../storeUtils');
3+
const lineNumberRegExp = /L(\d+)$/;
4+
5+
/**
6+
* @param {Record<string, string>} languageAliases
7+
* @param {GatsbyCache} cache
8+
*/
9+
function createLineNumberLineTransformer(languageAliases, cache) {
10+
let grammarCache;
11+
12+
/** @type {LineTransformer<number>} */
13+
const lineNumberTransformer = async ({ meta, state, line, language }) => {
14+
if (!grammarCache) {
15+
grammarCache = await cache.get('grammars');
16+
}
17+
18+
const scope = getScope(language, grammarCache, languageAliases);
19+
const commentContent = getCommentContent(line.text, scope, /*trim*/ true);
20+
if (commentContent) {
21+
const match = lineNumberRegExp.exec(commentContent);
22+
if (match && match[1]) {
23+
const lineNumber = parseInt(match[1], 10);
24+
if (!isNaN(lineNumber)) {
25+
return {
26+
state: lineNumber,
27+
line: {
28+
...line,
29+
text: line.text.replace(getCommentRegExp(scope), '')
30+
},
31+
gutterCells: [
32+
{
33+
className: 'grvsc-line-number',
34+
text: String(lineNumber)
35+
}
36+
],
37+
data: {
38+
lineNumber
39+
}
40+
};
41+
}
42+
}
43+
}
44+
45+
if (state !== undefined) {
46+
return {
47+
state: state + 1,
48+
line,
49+
gutterCells: [
50+
{
51+
className: 'grvsc-line-number',
52+
text: String(state + 1)
53+
}
54+
],
55+
data: {
56+
lineNumber: state + 1
57+
}
58+
};
59+
}
60+
61+
if (meta.numberLines !== undefined) {
62+
const lineNumber = typeof meta.numberLines === 'number' ? meta.numberLines : 1;
63+
return {
64+
state: lineNumber,
65+
line,
66+
gutterCells: [
67+
{
68+
className: 'grvsc-line-number',
69+
text: String(lineNumber)
70+
}
71+
],
72+
data: {
73+
lineNumber
74+
}
75+
};
76+
}
77+
78+
return { state, line };
79+
};
80+
81+
lineNumberTransformer.displayName = 'lineNumber';
82+
lineNumberTransformer.schemaExtension = `
83+
type GRVSCTokenizedLine {
84+
lineNumber: Int
85+
}
86+
`;
87+
88+
return lineNumberTransformer;
89+
}
90+
91+
module.exports = { createLineNumberLineTransformer };

0 commit comments

Comments
 (0)