Skip to content

Commit d09a748

Browse files
authored
🧮 Improve emphasize-lines to allow for ranges (#2048)
See jupyter-book/jupyter-book#2361
1 parent 7908bac commit d09a748

File tree

3 files changed

+94
-8
lines changed

3 files changed

+94
-8
lines changed

.changeset/thick-geese-sing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"myst-directives": patch
3+
---
4+
5+
Enable ranges in emphasize lines (e.g. "5,7-9") for code blocks in addition to comma-separated line numbers.

packages/myst-directives/src/code.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,44 @@ describe('Code block options', () => {
4545
expect(opts).toEqual({ emphasizeLines: [3, 5] });
4646
expect(vfile.messages.length).toEqual(0);
4747
});
48+
test.each([
49+
['3', [3]],
50+
['3,5', [3, 5]],
51+
['3-6, 1', [1, 3, 4, 5, 6]],
52+
['1-3,5', [1, 2, 3, 5]],
53+
['2,4-6', [2, 4, 5, 6]],
54+
[' 1 - 2 , 4 ', [1, 2, 4]],
55+
['3,5,4', [3, 4, 5]], // Out of order is fine
56+
['3,,5', [3, 5]], // tolerant to extra comma
57+
['3, ', [3]], // tolerant to trailing comma
58+
['7-7', [7]], // valid single range
59+
['3,3,3-3', [3]], // duplicates removed
60+
['1-3,2', [1, 2, 3]], // duplicates removed
61+
])('parses emphasize-lines="%s" into %j', (input, expected) => {
62+
const vfile = new VFile();
63+
const opts = getCodeBlockOptionsWrap({ 'emphasize-lines': input }, vfile);
64+
expect(opts).toEqual({ emphasizeLines: expected });
65+
expect(vfile.messages.length).toEqual(0);
66+
});
67+
test.each([
68+
['abc'],
69+
['one,two'],
70+
['1-'],
71+
['4--5'],
72+
['1-3-5'],
73+
['-2'], // Must be positive
74+
['6-2'], // must be ascending
75+
['5,6-2,7, 2-4', [2, 3, 4, 5, 7]], // Good numbers are still parsed
76+
])(
77+
'invalid emphasize-lines="%s" logs warning and returns empty',
78+
(input, expected = undefined) => {
79+
const vfile = new VFile();
80+
const opts = getCodeBlockOptionsWrap({ 'emphasize-lines': input }, vfile);
81+
expect(opts).toEqual({ emphasizeLines: expected });
82+
expect(vfile.messages.length).toBeGreaterThan(0);
83+
expect(vfile.messages[0].message).toMatch(/Invalid emphasize-lines/i);
84+
},
85+
);
4886
// See https://github.com/jupyter-book/jupyterlab-myst/issues/174
4987
test(':lineno-start: 10, :emphasize-lines: 12,13', () => {
5088
const vfile = new VFile();

packages/myst-directives/src/code.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,52 @@ import type { VFile } from 'vfile';
88
import { select } from 'unist-util-select';
99
import { addCommonDirectiveOptions, commonDirectiveOptions } from './utils.js';
1010

11-
function parseEmphasizeLines(emphasizeLinesString?: string | undefined): number[] | undefined {
11+
function parseEmphasizeLines(
12+
data: DirectiveData,
13+
vfile: VFile,
14+
emphasizeLinesString?: string,
15+
): number[] | undefined {
16+
const { node } = data;
1217
if (!emphasizeLinesString) return undefined;
13-
const emphasizeLines = emphasizeLinesString
14-
?.split(',')
15-
.map((val) => Number(val.trim()))
16-
.filter((val) => Number.isInteger(val));
17-
return emphasizeLines;
18+
19+
const result = new Set<number>();
20+
21+
for (const part of emphasizeLinesString.split(',')) {
22+
const trimmed = part.trim();
23+
24+
const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
25+
if (rangeMatch) {
26+
const start = Number(rangeMatch[1]);
27+
const end = Number(rangeMatch[2]);
28+
if (start <= end) {
29+
for (let i = start; i <= end; i++) {
30+
result.add(i);
31+
}
32+
continue;
33+
} else {
34+
fileWarn(vfile, `Invalid emphasize-lines range "${trimmed}" (start > end)`, {
35+
node,
36+
source: 'code-block:options',
37+
ruleId: RuleId.directiveOptionsCorrect,
38+
});
39+
continue;
40+
}
41+
}
42+
43+
if (/^\d+$/.test(trimmed)) {
44+
result.add(Number(trimmed));
45+
} else if (trimmed !== '') {
46+
fileWarn(vfile, `Invalid emphasize-lines value "${trimmed}"`, {
47+
node,
48+
source: 'code-block:options',
49+
ruleId: RuleId.directiveOptionsCorrect,
50+
});
51+
}
52+
}
53+
54+
if (result.size === 0) return undefined;
55+
56+
return Array.from(result).sort((a, b) => a - b);
1857
}
1958

2059
/** This function parses both sphinx and RST code-block options */
@@ -31,7 +70,11 @@ export function getCodeBlockOptions(
3170
ruleId: RuleId.directiveOptionsCorrect,
3271
});
3372
}
34-
const emphasizeLines = parseEmphasizeLines(options?.['emphasize-lines'] as string | undefined);
73+
const emphasizeLines = parseEmphasizeLines(
74+
data,
75+
vfile,
76+
options?.['emphasize-lines'] as string | undefined,
77+
);
3578
const numberLines = options?.['number-lines'] as number | undefined;
3679
// Only include this in mdast if it is `true`
3780
const showLineNumbers =
@@ -78,7 +121,7 @@ export const CODE_DIRECTIVE_OPTIONS: DirectiveSpec['options'] = {
78121
},
79122
'emphasize-lines': {
80123
type: String,
81-
doc: 'Emphasize particular lines (comma-separated numbers), e.g. "3,5"',
124+
doc: 'Emphasize particular lines (comma-separated numbers which can include ranges), e.g. "3,5,7-9"',
82125
},
83126
filename: {
84127
type: String,

0 commit comments

Comments
 (0)