Spaces:
Running
Running
| window.QuartoLineHighlight = function () { | |
| function isPrintView() { | |
| return /print-pdf/gi.test(window.location.search); | |
| } | |
| const delimiters = { | |
| step: "|", | |
| line: ",", | |
| lineRange: "-", | |
| }; | |
| const regex = new RegExp( | |
| "^[\\d" + Object.values(delimiters).join("") + "]+$" | |
| ); | |
| function handleLinesSelector(deck, attr) { | |
| // if we are in printview with pdfSeparateFragments: false | |
| // then we'll also want to supress | |
| if (regex.test(attr)) { | |
| if (isPrintView() && deck.getConfig().pdfSeparateFragments !== true) { | |
| return false; | |
| } else { | |
| return true; | |
| } | |
| } else { | |
| return false; | |
| } | |
| } | |
| const kCodeLineNumbersAttr = "data-code-line-numbers"; | |
| const kFragmentIndex = "data-fragment-index"; | |
| function initQuartoLineHighlight(deck) { | |
| const divSourceCode = deck | |
| .getRevealElement() | |
| .querySelectorAll("div.sourceCode"); | |
| // Process each div created by Pandoc highlighting - numbered line are already included. | |
| divSourceCode.forEach((el) => { | |
| if (el.hasAttribute(kCodeLineNumbersAttr)) { | |
| const codeLineAttr = el.getAttribute(kCodeLineNumbersAttr); | |
| el.removeAttribute("data-code-line-numbers"); | |
| if (handleLinesSelector(deck, codeLineAttr)) { | |
| // Only process if attr is a string to select lines to highlights | |
| // e.g "1|3,6|8-11" | |
| const codeBlock = el.querySelectorAll("pre code"); | |
| codeBlock.forEach((code) => { | |
| // move attributes on code block | |
| code.setAttribute(kCodeLineNumbersAttr, codeLineAttr); | |
| const scrollState = { currentBlock: code }; | |
| // Check if there are steps and duplicate code block accordingly | |
| const highlightSteps = splitLineNumbers(codeLineAttr); | |
| if (highlightSteps.length > 1) { | |
| // If the original code block has a fragment-index, | |
| // each clone should follow in an incremental sequence | |
| let fragmentIndex = parseInt( | |
| code.getAttribute(kFragmentIndex), | |
| 10 | |
| ); | |
| fragmentIndex = | |
| typeof fragmentIndex !== "number" || isNaN(fragmentIndex) | |
| ? null | |
| : fragmentIndex; | |
| let stepN = 1; | |
| highlightSteps.slice(1).forEach( | |
| // Generate fragments for all steps except the original block | |
| (step) => { | |
| var fragmentBlock = code.cloneNode(true); | |
| fragmentBlock.setAttribute( | |
| "data-code-line-numbers", | |
| joinLineNumbers([step]) | |
| ); | |
| fragmentBlock.classList.add("fragment"); | |
| // Pandoc sets id on spans we need to keep unique | |
| fragmentBlock | |
| .querySelectorAll(":scope > span") | |
| .forEach((span) => { | |
| if (span.hasAttribute("id")) { | |
| span.setAttribute( | |
| "id", | |
| span.getAttribute("id").concat("-" + stepN) | |
| ); | |
| } | |
| }); | |
| stepN = ++stepN; | |
| // Add duplicated <code> element after existing one | |
| code.parentNode.appendChild(fragmentBlock); | |
| // Each new <code> element is highlighted based on the new attributes value | |
| highlightCodeBlock(fragmentBlock); | |
| if (typeof fragmentIndex === "number") { | |
| fragmentBlock.setAttribute(kFragmentIndex, fragmentIndex); | |
| fragmentIndex += 1; | |
| } else { | |
| fragmentBlock.removeAttribute(kFragmentIndex); | |
| } | |
| // Scroll highlights into view as we step through them | |
| fragmentBlock.addEventListener( | |
| "visible", | |
| scrollHighlightedLineIntoView.bind( | |
| this, | |
| fragmentBlock, | |
| scrollState | |
| ) | |
| ); | |
| fragmentBlock.addEventListener( | |
| "hidden", | |
| scrollHighlightedLineIntoView.bind( | |
| this, | |
| fragmentBlock.previousSibling, | |
| scrollState | |
| ) | |
| ); | |
| } | |
| ); | |
| code.removeAttribute(kFragmentIndex); | |
| code.setAttribute( | |
| kCodeLineNumbersAttr, | |
| joinLineNumbers([highlightSteps[0]]) | |
| ); | |
| } | |
| // Scroll the first highlight into view when the slide becomes visible. | |
| const slide = | |
| typeof code.closest === "function" | |
| ? code.closest("section:not(.stack)") | |
| : null; | |
| if (slide) { | |
| const scrollFirstHighlightIntoView = function () { | |
| scrollHighlightedLineIntoView(code, scrollState, true); | |
| slide.removeEventListener( | |
| "visible", | |
| scrollFirstHighlightIntoView | |
| ); | |
| }; | |
| slide.addEventListener("visible", scrollFirstHighlightIntoView); | |
| } | |
| highlightCodeBlock(code); | |
| }); | |
| } | |
| } | |
| }); | |
| } | |
| function highlightCodeBlock(codeBlock) { | |
| const highlightSteps = splitLineNumbers( | |
| codeBlock.getAttribute(kCodeLineNumbersAttr) | |
| ); | |
| if (highlightSteps.length) { | |
| // If we have at least one step, we generate fragments | |
| highlightSteps[0].forEach((highlight) => { | |
| // Add expected class on <pre> for reveal CSS | |
| codeBlock.parentNode.classList.add("code-wrapper"); | |
| // Select lines to highlight | |
| spanToHighlight = []; | |
| if (typeof highlight.last === "number") { | |
| spanToHighlight = [].slice.call( | |
| codeBlock.querySelectorAll( | |
| ":scope > span:nth-child(n+" + | |
| highlight.first + | |
| "):nth-child(-n+" + | |
| highlight.last + | |
| ")" | |
| ) | |
| ); | |
| } else if (typeof highlight.first === "number") { | |
| spanToHighlight = [].slice.call( | |
| codeBlock.querySelectorAll( | |
| ":scope > span:nth-child(" + highlight.first + ")" | |
| ) | |
| ); | |
| } | |
| if (spanToHighlight.length) { | |
| // Add a class on <code> and <span> to select line to highlight | |
| spanToHighlight.forEach((span) => | |
| span.classList.add("highlight-line") | |
| ); | |
| codeBlock.classList.add("has-line-highlights"); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * Animates scrolling to the first highlighted line | |
| * in the given code block. | |
| */ | |
| function scrollHighlightedLineIntoView(block, scrollState, skipAnimation) { | |
| window.cancelAnimationFrame(scrollState.animationFrameID); | |
| // Match the scroll position of the currently visible | |
| // code block | |
| if (scrollState.currentBlock) { | |
| block.scrollTop = scrollState.currentBlock.scrollTop; | |
| } | |
| // Remember the current code block so that we can match | |
| // its scroll position when showing/hiding fragments | |
| scrollState.currentBlock = block; | |
| const highlightBounds = getHighlightedLineBounds(block); | |
| let viewportHeight = block.offsetHeight; | |
| // Subtract padding from the viewport height | |
| const blockStyles = window.getComputedStyle(block); | |
| viewportHeight -= | |
| parseInt(blockStyles.paddingTop) + parseInt(blockStyles.paddingBottom); | |
| // Scroll position which centers all highlights | |
| const startTop = block.scrollTop; | |
| let targetTop = | |
| highlightBounds.top + | |
| (Math.min(highlightBounds.bottom - highlightBounds.top, viewportHeight) - | |
| viewportHeight) / | |
| 2; | |
| // Make sure the scroll target is within bounds | |
| targetTop = Math.max( | |
| Math.min(targetTop, block.scrollHeight - viewportHeight), | |
| 0 | |
| ); | |
| if (skipAnimation === true || startTop === targetTop) { | |
| block.scrollTop = targetTop; | |
| } else { | |
| // Don't attempt to scroll if there is no overflow | |
| if (block.scrollHeight <= viewportHeight) return; | |
| let time = 0; | |
| const animate = function () { | |
| time = Math.min(time + 0.02, 1); | |
| // Update our eased scroll position | |
| block.scrollTop = | |
| startTop + (targetTop - startTop) * easeInOutQuart(time); | |
| // Keep animating unless we've reached the end | |
| if (time < 1) { | |
| scrollState.animationFrameID = requestAnimationFrame(animate); | |
| } | |
| }; | |
| animate(); | |
| } | |
| } | |
| function getHighlightedLineBounds(block) { | |
| const highlightedLines = block.querySelectorAll(".highlight-line"); | |
| if (highlightedLines.length === 0) { | |
| return { top: 0, bottom: 0 }; | |
| } else { | |
| const firstHighlight = highlightedLines[0]; | |
| const lastHighlight = highlightedLines[highlightedLines.length - 1]; | |
| return { | |
| top: firstHighlight.offsetTop, | |
| bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight, | |
| }; | |
| } | |
| } | |
| /** | |
| * The easing function used when scrolling. | |
| */ | |
| function easeInOutQuart(t) { | |
| // easeInOutQuart | |
| return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; | |
| } | |
| function splitLineNumbers(lineNumbersAttr) { | |
| // remove space | |
| lineNumbersAttr = lineNumbersAttr.replace("/s/g", ""); | |
| // seperate steps (for fragment) | |
| lineNumbersAttr = lineNumbersAttr.split(delimiters.step); | |
| // for each step, calculate first and last line, if any | |
| return lineNumbersAttr.map((highlights) => { | |
| // detect lines | |
| const lines = highlights.split(delimiters.line); | |
| return lines.map((range) => { | |
| if (/^[\d-]+$/.test(range)) { | |
| range = range.split(delimiters.lineRange); | |
| const firstLine = parseInt(range[0], 10); | |
| const lastLine = range[1] ? parseInt(range[1], 10) : undefined; | |
| return { | |
| first: firstLine, | |
| last: lastLine, | |
| }; | |
| } else { | |
| return {}; | |
| } | |
| }); | |
| }); | |
| } | |
| function joinLineNumbers(splittedLineNumbers) { | |
| return splittedLineNumbers | |
| .map(function (highlights) { | |
| return highlights | |
| .map(function (highlight) { | |
| // Line range | |
| if (typeof highlight.last === "number") { | |
| return highlight.first + delimiters.lineRange + highlight.last; | |
| } | |
| // Single line | |
| else if (typeof highlight.first === "number") { | |
| return highlight.first; | |
| } | |
| // All lines | |
| else { | |
| return ""; | |
| } | |
| }) | |
| .join(delimiters.line); | |
| }) | |
| .join(delimiters.step); | |
| } | |
| return { | |
| id: "quarto-line-highlight", | |
| init: function (deck) { | |
| initQuartoLineHighlight(deck); | |
| // If we're printing to PDF, scroll the code highlights of | |
| // all blocks in the deck into view at once | |
| deck.on("pdf-ready", function () { | |
| [].slice | |
| .call( | |
| deck | |
| .getRevealElement() | |
| .querySelectorAll( | |
| "pre code[data-code-line-numbers].current-fragment" | |
| ) | |
| ) | |
| .forEach(function (block) { | |
| scrollHighlightedLineIntoView(block, {}, true); | |
| }); | |
| }); | |
| }, | |
| }; | |
| }; | |