Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/formats/revealjs/plugins/line-highlight/line-highlight.js
12923 views
1
window.QuartoLineHighlight = function () {
2
function isPrintView() {
3
return /print-pdf/gi.test(window.location.search) || /view=print/gi.test(window.location.search);
4
}
5
6
const delimiters = {
7
step: "|",
8
line: ",",
9
lineRange: "-",
10
};
11
12
const regex = new RegExp(
13
"^[\\d" + Object.values(delimiters).join("") + "]+$"
14
);
15
16
function handleLinesSelector(deck, attr) {
17
// if we are in printview with pdfSeparateFragments: false
18
// then we'll also want to supress
19
if (regex.test(attr)) {
20
if (isPrintView() && deck.getConfig().pdfSeparateFragments !== true) {
21
return false;
22
} else {
23
return true;
24
}
25
} else {
26
return false;
27
}
28
}
29
30
const kCodeLineNumbersAttr = "data-code-line-numbers";
31
const kFragmentIndex = "data-fragment-index";
32
33
function initQuartoLineHighlight(deck) {
34
const divSourceCode = deck
35
.getRevealElement()
36
.querySelectorAll("div.sourceCode");
37
// Process each div created by Pandoc highlighting - numbered line are already included.
38
divSourceCode.forEach((el) => {
39
if (el.hasAttribute(kCodeLineNumbersAttr)) {
40
const codeLineAttr = el.getAttribute(kCodeLineNumbersAttr);
41
el.removeAttribute(kCodeLineNumbersAttr);
42
if (handleLinesSelector(deck, codeLineAttr)) {
43
// Only process if attr is a string to select lines to highlights
44
// e.g "1|3,6|8-11"
45
const codeBlock = el.querySelectorAll("pre code");
46
codeBlock.forEach((code) => {
47
// move attributes on code block
48
code.setAttribute(kCodeLineNumbersAttr, codeLineAttr);
49
50
const scrollState = { currentBlock: code };
51
52
// Check if there are steps and duplicate code block accordingly
53
const highlightSteps = splitLineNumbers(codeLineAttr);
54
if (highlightSteps.length > 1) {
55
// If the original code block has a fragment-index,
56
// each clone should follow in an incremental sequence
57
let fragmentIndex = parseInt(
58
code.getAttribute(kFragmentIndex),
59
10
60
);
61
fragmentIndex =
62
typeof fragmentIndex !== "number" || isNaN(fragmentIndex)
63
? null
64
: fragmentIndex;
65
66
let stepN = 1;
67
highlightSteps.slice(1).forEach(
68
// Generate fragments for all steps except the original block
69
(step) => {
70
var fragmentBlock = code.cloneNode(true);
71
fragmentBlock.setAttribute(
72
"data-code-line-numbers",
73
joinLineNumbers([step])
74
);
75
fragmentBlock.classList.add("fragment");
76
77
// Pandoc sets id on spans we need to keep unique
78
fragmentBlock
79
.querySelectorAll(":scope > span")
80
.forEach((span) => {
81
if (span.hasAttribute("id")) {
82
span.setAttribute(
83
"id",
84
span.getAttribute("id").concat("-" + stepN)
85
);
86
}
87
});
88
stepN = ++stepN;
89
90
// Add duplicated <code> element after existing one
91
code.parentNode.appendChild(fragmentBlock);
92
93
// Each new <code> element is highlighted based on the new attributes value
94
highlightCodeBlock(fragmentBlock);
95
96
if (typeof fragmentIndex === "number") {
97
fragmentBlock.setAttribute(kFragmentIndex, fragmentIndex);
98
fragmentIndex += 1;
99
} else {
100
fragmentBlock.removeAttribute(kFragmentIndex);
101
}
102
103
// Scroll highlights into view as we step through them
104
fragmentBlock.addEventListener(
105
"visible",
106
scrollHighlightedLineIntoView.bind(
107
this,
108
fragmentBlock,
109
scrollState
110
)
111
);
112
fragmentBlock.addEventListener(
113
"hidden",
114
scrollHighlightedLineIntoView.bind(
115
this,
116
fragmentBlock.previousSibling,
117
scrollState
118
)
119
);
120
}
121
);
122
code.removeAttribute(kFragmentIndex);
123
code.setAttribute(
124
kCodeLineNumbersAttr,
125
joinLineNumbers([highlightSteps[0]])
126
);
127
}
128
129
// Scroll the first highlight into view when the slide becomes visible.
130
const slide =
131
typeof code.closest === "function"
132
? code.closest("section:not(.stack)")
133
: null;
134
if (slide) {
135
const scrollFirstHighlightIntoView = function () {
136
scrollHighlightedLineIntoView(code, scrollState, true);
137
slide.removeEventListener(
138
"visible",
139
scrollFirstHighlightIntoView
140
);
141
};
142
slide.addEventListener("visible", scrollFirstHighlightIntoView);
143
}
144
145
highlightCodeBlock(code);
146
});
147
}
148
}
149
});
150
}
151
152
function highlightCodeBlock(codeBlock) {
153
const highlightSteps = splitLineNumbers(
154
codeBlock.getAttribute(kCodeLineNumbersAttr)
155
);
156
157
if (highlightSteps.length) {
158
// If we have at least one step, we generate fragments
159
highlightSteps[0].forEach((highlight) => {
160
// Add expected class on <pre> for reveal CSS
161
codeBlock.parentNode.classList.add("code-wrapper");
162
163
// Select lines to highlight
164
spanToHighlight = [];
165
if (typeof highlight.last === "number") {
166
spanToHighlight = [].slice.call(
167
codeBlock.querySelectorAll(
168
":scope > span:nth-of-type(n+" +
169
highlight.first +
170
"):nth-of-type(-n+" +
171
highlight.last +
172
")"
173
)
174
);
175
} else if (typeof highlight.first === "number") {
176
spanToHighlight = [].slice.call(
177
codeBlock.querySelectorAll(
178
":scope > span:nth-of-type(" + highlight.first + ")"
179
)
180
);
181
}
182
if (spanToHighlight.length) {
183
// Add a class on <code> and <span> to select line to highlight
184
spanToHighlight.forEach((span) =>
185
span.classList.add("highlight-line")
186
);
187
codeBlock.classList.add("has-line-highlights");
188
}
189
});
190
}
191
}
192
193
/**
194
* Animates scrolling to the first highlighted line
195
* in the given code block.
196
*/
197
function scrollHighlightedLineIntoView(block, scrollState, skipAnimation) {
198
window.cancelAnimationFrame(scrollState.animationFrameID);
199
200
// Match the scroll position of the currently visible
201
// code block
202
if (scrollState.currentBlock) {
203
block.scrollTop = scrollState.currentBlock.scrollTop;
204
}
205
206
// Remember the current code block so that we can match
207
// its scroll position when showing/hiding fragments
208
scrollState.currentBlock = block;
209
210
const highlightBounds = getHighlightedLineBounds(block);
211
let viewportHeight = block.offsetHeight;
212
213
// Subtract padding from the viewport height
214
const blockStyles = window.getComputedStyle(block);
215
viewportHeight -=
216
parseInt(blockStyles.paddingTop) + parseInt(blockStyles.paddingBottom);
217
218
// Scroll position which centers all highlights
219
const startTop = block.scrollTop;
220
let targetTop =
221
highlightBounds.top +
222
(Math.min(highlightBounds.bottom - highlightBounds.top, viewportHeight) -
223
viewportHeight) /
224
2;
225
226
// Make sure the scroll target is within bounds
227
targetTop = Math.max(
228
Math.min(targetTop, block.scrollHeight - viewportHeight),
229
0
230
);
231
232
if (skipAnimation === true || startTop === targetTop) {
233
block.scrollTop = targetTop;
234
} else {
235
// Don't attempt to scroll if there is no overflow
236
if (block.scrollHeight <= viewportHeight) return;
237
238
let time = 0;
239
240
const animate = function () {
241
time = Math.min(time + 0.02, 1);
242
243
// Update our eased scroll position
244
block.scrollTop =
245
startTop + (targetTop - startTop) * easeInOutQuart(time);
246
247
// Keep animating unless we've reached the end
248
if (time < 1) {
249
scrollState.animationFrameID = requestAnimationFrame(animate);
250
}
251
};
252
253
animate();
254
}
255
}
256
257
function getHighlightedLineBounds(block) {
258
const highlightedLines = block.querySelectorAll(".highlight-line");
259
if (highlightedLines.length === 0) {
260
return { top: 0, bottom: 0 };
261
} else {
262
const firstHighlight = highlightedLines[0];
263
const lastHighlight = highlightedLines[highlightedLines.length - 1];
264
265
return {
266
top: firstHighlight.offsetTop,
267
bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight,
268
};
269
}
270
}
271
272
/**
273
* The easing function used when scrolling.
274
*/
275
function easeInOutQuart(t) {
276
// easeInOutQuart
277
return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;
278
}
279
280
function splitLineNumbers(lineNumbersAttr) {
281
// remove space
282
lineNumbersAttr = lineNumbersAttr.replace("/s/g", "");
283
// seperate steps (for fragment)
284
lineNumbersAttr = lineNumbersAttr.split(delimiters.step);
285
286
// for each step, calculate first and last line, if any
287
return lineNumbersAttr.map((highlights) => {
288
// detect lines
289
const lines = highlights.split(delimiters.line);
290
return lines.map((range) => {
291
if (/^[\d-]+$/.test(range)) {
292
range = range.split(delimiters.lineRange);
293
const firstLine = parseInt(range[0], 10);
294
const lastLine = range[1] ? parseInt(range[1], 10) : undefined;
295
return {
296
first: firstLine,
297
last: lastLine,
298
};
299
} else {
300
return {};
301
}
302
});
303
});
304
}
305
306
function joinLineNumbers(splittedLineNumbers) {
307
return splittedLineNumbers
308
.map(function (highlights) {
309
return highlights
310
.map(function (highlight) {
311
// Line range
312
if (typeof highlight.last === "number") {
313
return highlight.first + delimiters.lineRange + highlight.last;
314
}
315
// Single line
316
else if (typeof highlight.first === "number") {
317
return highlight.first;
318
}
319
// All lines
320
else {
321
return "";
322
}
323
})
324
.join(delimiters.line);
325
})
326
.join(delimiters.step);
327
}
328
329
return {
330
id: "quarto-line-highlight",
331
init: function (deck) {
332
initQuartoLineHighlight(deck);
333
334
// If we're printing to PDF, scroll the code highlights of
335
// all blocks in the deck into view at once
336
deck.on("pdf-ready", function () {
337
[].slice
338
.call(
339
deck
340
.getRevealElement()
341
.querySelectorAll(
342
"pre code[data-code-line-numbers].current-fragment"
343
)
344
)
345
.forEach(function (block) {
346
scrollHighlightedLineIntoView(block, {}, true);
347
});
348
});
349
},
350
};
351
};
352
353