PageSourceSearch

https://soci-269-w12.netlify.app/index_files/libs/revealjs/plugin/quarto-line-highlight/line-highlight.js

js soci-269-w12.netlify.app collected 2026-10-03 11:25:35 UTC 11,549 bytes, 351 lines download raw bytes

1window.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};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.