Skip to content

Commit 0bf5f92

Browse files
authored
Merge pull request #95 from andrewbranch/gutters
Add gutters, line number option
2 parents bd95106 + d1f1e29 commit 0bf5f92

27 files changed

+1138
-218
lines changed

src/factory/html.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,28 @@ function createLineElement(line, meta, index, language, getLineClassName, tokens
3030
const lineClassName = joinClassNames(getLineClassName(lineData), 'grvsc-line');
3131
const attrs = mergeAttributes({ class: lineClassName }, line.attrs);
3232
const children = typeof tokens === 'string' ? [tokens] : mergeSimilarTokens(tokens);
33-
return span(attrs, children, { whitespace: TriviaRenderFlags.NoWhitespace });
33+
const gutterCells = line.gutterCells.map(createGutterCellElement);
34+
if (gutterCells.length) {
35+
gutterCells.unshift(span({ class: 'grvsc-gutter-pad' }, undefined, { whitespace: TriviaRenderFlags.NoWhitespace }));
36+
}
37+
return span(
38+
attrs,
39+
[...gutterCells, span({ class: 'grvsc-source' }, children, { whitespace: TriviaRenderFlags.NoWhitespace })],
40+
{ whitespace: TriviaRenderFlags.NoWhitespace }
41+
);
42+
}
43+
44+
/** @param {GutterCell | undefined} cell */
45+
function createGutterCellElement(cell) {
46+
return span(
47+
{
48+
class: joinClassNames('grvsc-gutter', cell && cell.className),
49+
'aria-hidden': 'true',
50+
'data-content': cell && cell.text
51+
},
52+
[],
53+
{ whitespace: TriviaRenderFlags.NoWhitespace }
54+
);
3455
}
3556

3657
/**

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: 6 additions & 2 deletions
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();
@@ -82,7 +86,7 @@ async function registerCodeSpan(
8286
const [registry, unlockRegistry] = await getTextMateRegistry();
8387
try {
8488
/** @type {Line[]} */
85-
const lines = [{ text, data: {}, attrs: {} }];
89+
const lines = [{ text, data: {}, attrs: {}, gutterCells: [] }];
8690
const { tokenTypes, languageId } = getGrammar(scope, grammarCache);
8791
const grammar = await registry.loadGrammarWithConfiguration(scope, languageId, { tokenTypes });
8892
codeBlockRegistry.register(registryKey, {

src/renderers/html.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function renderHTML(element) {
6464

6565
const { tagName, attributes, children } = element;
6666
const attrs = Object.keys(attributes)
67+
.filter(attr => attributes[attr] !== undefined)
6768
.map(attr => ` ${attr}="${escapeHTML(attributes[attr])}"`)
6869
.join('');
6970

src/transformers/getTransformedLines.js

Lines changed: 85 additions & 3 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
@@ -9,15 +11,22 @@
911
* @returns {Promise<Line[]>}
1012
*/
1113
async function getTransformedLines(transformers, text, languageName, meta) {
12-
/** @type {Line[]} */
14+
/** @type {Omit<Line, 'gutterCells'>[]} */
1315
const result = [];
1416
const rawLines = text.split(/\r?\n/);
1517
const prevTransformerStates = [];
18+
const gutterCellsPerTransformer = [];
19+
/** @type {GutterCell[][][]} */
20+
const gutterCells = [];
1621

1722
linesLoop: for (let lineIndex = 0; lineIndex < rawLines.length; lineIndex++) {
1823
let line = rawLines[lineIndex];
24+
/** @type {GutterCell[][]} */
25+
const lineGutterCells = [];
1926
const attrs = {};
2027
const graphQLData = {};
28+
/** @type {string[]} */
29+
const addedContainerClassNames = [];
2130
for (let i = 0; i < transformers.length; i++) {
2231
const transformer = transformers[i];
2332
const state = prevTransformerStates[i];
@@ -30,17 +39,90 @@ async function getTransformedLines(transformers, text, languageName, meta) {
3039
});
3140

3241
prevTransformerStates[i] = txResult.state;
42+
if (txResult.setContainerClassName) {
43+
addedContainerClassNames.push(txResult.setContainerClassName);
44+
}
3345
if (!txResult.line) {
3446
continue linesLoop;
3547
}
48+
if (txResult.gutterCells) {
49+
gutterCellsPerTransformer[i] = Math.max(txResult.gutterCells.length, gutterCellsPerTransformer[i] || 0);
50+
lineGutterCells[i] = txResult.gutterCells;
51+
} else {
52+
gutterCellsPerTransformer[i] = Math.max(0, gutterCellsPerTransformer[i] || 0);
53+
}
54+
3655
line = txResult.line.text;
3756
Object.assign(attrs, txResult.line.attrs);
3857
Object.assign(graphQLData, txResult.data);
3958
}
40-
result.push({ text: line, attrs, data: graphQLData });
59+
gutterCells.push(lineGutterCells);
60+
result.push({
61+
text: line,
62+
attrs,
63+
data: graphQLData,
64+
setContainerClassName: joinClassNames(...addedContainerClassNames) || undefined
65+
});
4166
}
4267

43-
return result;
68+
const flattenedGutterCells = flattenGutterCells(gutterCells, gutterCellsPerTransformer);
69+
return result.map((line, i) => ({ ...line, gutterCells: flattenedGutterCells[i] }));
70+
}
71+
72+
/**
73+
* Transforms a 3D array of gutter cells into a 2D array of gutter cells.
74+
* The input is in the form of gutter cells per line transformer per line,
75+
* whereas the output is is gutter cells per line. Each line transformer can
76+
* return more than one gutter cell, and need not return the same number of
77+
* cells for each line, so the flattening must be done in a way that ensures
78+
* that each line transformer has its gutter cells aligned to the same index
79+
* in every line. For example, for the input
80+
*
81+
* ```
82+
* [
83+
* [[t0], [t1a, t1b], [t2]], // Line 1
84+
* [undefined, [t1], [t2]], // Line 2
85+
* [[t0a, t0b], undefined, [t2a, t2b]] // Line 3
86+
* ]
87+
* ```
88+
*
89+
* we would flatten to
90+
*
91+
* ```
92+
* [
93+
* [t0, undefined, t1a, t1b, t2, undefined], // Line 1
94+
* [undefined, undefined, t1, undefined, t2, undefined], // Line 2
95+
* [t0a, t0b, undefined, undefined, t2a, t2b] // Line 3
96+
* ]
97+
* ```
98+
*
99+
* such that each of the three transformers (t0, t1, t2) reserve two gutter
100+
* cells for itself, padding empty spaces in the final array with `undefined`
101+
* to ensure correct vertical alignment.
102+
*
103+
* The parameter `gutterCellsPerTransformer` can be derived from `gutterCells`,
104+
* but as an optimization, we already know it from previously iterating through
105+
* line transformer results.
106+
*
107+
* @param {GutterCell[][][]} gutterCells
108+
* @param {number[]} gutterCellsPerTransformer
109+
* @returns {GutterCell[][]}
110+
*/
111+
function flattenGutterCells(gutterCells, gutterCellsPerTransformer) {
112+
const totalGutterCells = gutterCellsPerTransformer.reduce((a, b) => a + b, 0);
113+
return gutterCells.map(transformerResults => {
114+
/** @type {GutterCell[]} */
115+
const result = Array(totalGutterCells).fill(undefined);
116+
for (let i = 0; i < transformerResults.length; i++) {
117+
const currentTransformerCells = transformerResults[i];
118+
if (currentTransformerCells) {
119+
for (let j = 0; j < currentTransformerCells.length; j++) {
120+
result[(gutterCellsPerTransformer[i - 1] || 0) + j] = currentTransformerCells[j];
121+
}
122+
}
123+
}
124+
return result;
125+
});
44126
}
45127

46128
module.exports = getTransformedLines;

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
/**

0 commit comments

Comments
 (0)