Path: blob/main/src/resources/formats/revealjs/plugins/line-highlight/line-highlight.js
12923 views
window.QuartoLineHighlight = function () {1function isPrintView() {2return /print-pdf/gi.test(window.location.search) || /view=print/gi.test(window.location.search);3}45const delimiters = {6step: "|",7line: ",",8lineRange: "-",9};1011const regex = new RegExp(12"^[\\d" + Object.values(delimiters).join("") + "]+$"13);1415function handleLinesSelector(deck, attr) {16// if we are in printview with pdfSeparateFragments: false17// then we'll also want to supress18if (regex.test(attr)) {19if (isPrintView() && deck.getConfig().pdfSeparateFragments !== true) {20return false;21} else {22return true;23}24} else {25return false;26}27}2829const kCodeLineNumbersAttr = "data-code-line-numbers";30const kFragmentIndex = "data-fragment-index";3132function initQuartoLineHighlight(deck) {33const divSourceCode = deck34.getRevealElement()35.querySelectorAll("div.sourceCode");36// Process each div created by Pandoc highlighting - numbered line are already included.37divSourceCode.forEach((el) => {38if (el.hasAttribute(kCodeLineNumbersAttr)) {39const codeLineAttr = el.getAttribute(kCodeLineNumbersAttr);40el.removeAttribute(kCodeLineNumbersAttr);41if (handleLinesSelector(deck, codeLineAttr)) {42// Only process if attr is a string to select lines to highlights43// e.g "1|3,6|8-11"44const codeBlock = el.querySelectorAll("pre code");45codeBlock.forEach((code) => {46// move attributes on code block47code.setAttribute(kCodeLineNumbersAttr, codeLineAttr);4849const scrollState = { currentBlock: code };5051// Check if there are steps and duplicate code block accordingly52const highlightSteps = splitLineNumbers(codeLineAttr);53if (highlightSteps.length > 1) {54// If the original code block has a fragment-index,55// each clone should follow in an incremental sequence56let fragmentIndex = parseInt(57code.getAttribute(kFragmentIndex),581059);60fragmentIndex =61typeof fragmentIndex !== "number" || isNaN(fragmentIndex)62? null63: fragmentIndex;6465let stepN = 1;66highlightSteps.slice(1).forEach(67// Generate fragments for all steps except the original block68(step) => {69var fragmentBlock = code.cloneNode(true);70fragmentBlock.setAttribute(71"data-code-line-numbers",72joinLineNumbers([step])73);74fragmentBlock.classList.add("fragment");7576// Pandoc sets id on spans we need to keep unique77fragmentBlock78.querySelectorAll(":scope > span")79.forEach((span) => {80if (span.hasAttribute("id")) {81span.setAttribute(82"id",83span.getAttribute("id").concat("-" + stepN)84);85}86});87stepN = ++stepN;8889// Add duplicated <code> element after existing one90code.parentNode.appendChild(fragmentBlock);9192// Each new <code> element is highlighted based on the new attributes value93highlightCodeBlock(fragmentBlock);9495if (typeof fragmentIndex === "number") {96fragmentBlock.setAttribute(kFragmentIndex, fragmentIndex);97fragmentIndex += 1;98} else {99fragmentBlock.removeAttribute(kFragmentIndex);100}101102// Scroll highlights into view as we step through them103fragmentBlock.addEventListener(104"visible",105scrollHighlightedLineIntoView.bind(106this,107fragmentBlock,108scrollState109)110);111fragmentBlock.addEventListener(112"hidden",113scrollHighlightedLineIntoView.bind(114this,115fragmentBlock.previousSibling,116scrollState117)118);119}120);121code.removeAttribute(kFragmentIndex);122code.setAttribute(123kCodeLineNumbersAttr,124joinLineNumbers([highlightSteps[0]])125);126}127128// Scroll the first highlight into view when the slide becomes visible.129const slide =130typeof code.closest === "function"131? code.closest("section:not(.stack)")132: null;133if (slide) {134const scrollFirstHighlightIntoView = function () {135scrollHighlightedLineIntoView(code, scrollState, true);136slide.removeEventListener(137"visible",138scrollFirstHighlightIntoView139);140};141slide.addEventListener("visible", scrollFirstHighlightIntoView);142}143144highlightCodeBlock(code);145});146}147}148});149}150151function highlightCodeBlock(codeBlock) {152const highlightSteps = splitLineNumbers(153codeBlock.getAttribute(kCodeLineNumbersAttr)154);155156if (highlightSteps.length) {157// If we have at least one step, we generate fragments158highlightSteps[0].forEach((highlight) => {159// Add expected class on <pre> for reveal CSS160codeBlock.parentNode.classList.add("code-wrapper");161162// Select lines to highlight163spanToHighlight = [];164if (typeof highlight.last === "number") {165spanToHighlight = [].slice.call(166codeBlock.querySelectorAll(167":scope > span:nth-of-type(n+" +168highlight.first +169"):nth-of-type(-n+" +170highlight.last +171")"172)173);174} else if (typeof highlight.first === "number") {175spanToHighlight = [].slice.call(176codeBlock.querySelectorAll(177":scope > span:nth-of-type(" + highlight.first + ")"178)179);180}181if (spanToHighlight.length) {182// Add a class on <code> and <span> to select line to highlight183spanToHighlight.forEach((span) =>184span.classList.add("highlight-line")185);186codeBlock.classList.add("has-line-highlights");187}188});189}190}191192/**193* Animates scrolling to the first highlighted line194* in the given code block.195*/196function scrollHighlightedLineIntoView(block, scrollState, skipAnimation) {197window.cancelAnimationFrame(scrollState.animationFrameID);198199// Match the scroll position of the currently visible200// code block201if (scrollState.currentBlock) {202block.scrollTop = scrollState.currentBlock.scrollTop;203}204205// Remember the current code block so that we can match206// its scroll position when showing/hiding fragments207scrollState.currentBlock = block;208209const highlightBounds = getHighlightedLineBounds(block);210let viewportHeight = block.offsetHeight;211212// Subtract padding from the viewport height213const blockStyles = window.getComputedStyle(block);214viewportHeight -=215parseInt(blockStyles.paddingTop) + parseInt(blockStyles.paddingBottom);216217// Scroll position which centers all highlights218const startTop = block.scrollTop;219let targetTop =220highlightBounds.top +221(Math.min(highlightBounds.bottom - highlightBounds.top, viewportHeight) -222viewportHeight) /2232;224225// Make sure the scroll target is within bounds226targetTop = Math.max(227Math.min(targetTop, block.scrollHeight - viewportHeight),2280229);230231if (skipAnimation === true || startTop === targetTop) {232block.scrollTop = targetTop;233} else {234// Don't attempt to scroll if there is no overflow235if (block.scrollHeight <= viewportHeight) return;236237let time = 0;238239const animate = function () {240time = Math.min(time + 0.02, 1);241242// Update our eased scroll position243block.scrollTop =244startTop + (targetTop - startTop) * easeInOutQuart(time);245246// Keep animating unless we've reached the end247if (time < 1) {248scrollState.animationFrameID = requestAnimationFrame(animate);249}250};251252animate();253}254}255256function getHighlightedLineBounds(block) {257const highlightedLines = block.querySelectorAll(".highlight-line");258if (highlightedLines.length === 0) {259return { top: 0, bottom: 0 };260} else {261const firstHighlight = highlightedLines[0];262const lastHighlight = highlightedLines[highlightedLines.length - 1];263264return {265top: firstHighlight.offsetTop,266bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight,267};268}269}270271/**272* The easing function used when scrolling.273*/274function easeInOutQuart(t) {275// easeInOutQuart276return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;277}278279function splitLineNumbers(lineNumbersAttr) {280// remove space281lineNumbersAttr = lineNumbersAttr.replace("/s/g", "");282// seperate steps (for fragment)283lineNumbersAttr = lineNumbersAttr.split(delimiters.step);284285// for each step, calculate first and last line, if any286return lineNumbersAttr.map((highlights) => {287// detect lines288const lines = highlights.split(delimiters.line);289return lines.map((range) => {290if (/^[\d-]+$/.test(range)) {291range = range.split(delimiters.lineRange);292const firstLine = parseInt(range[0], 10);293const lastLine = range[1] ? parseInt(range[1], 10) : undefined;294return {295first: firstLine,296last: lastLine,297};298} else {299return {};300}301});302});303}304305function joinLineNumbers(splittedLineNumbers) {306return splittedLineNumbers307.map(function (highlights) {308return highlights309.map(function (highlight) {310// Line range311if (typeof highlight.last === "number") {312return highlight.first + delimiters.lineRange + highlight.last;313}314// Single line315else if (typeof highlight.first === "number") {316return highlight.first;317}318// All lines319else {320return "";321}322})323.join(delimiters.line);324})325.join(delimiters.step);326}327328return {329id: "quarto-line-highlight",330init: function (deck) {331initQuartoLineHighlight(deck);332333// If we're printing to PDF, scroll the code highlights of334// all blocks in the deck into view at once335deck.on("pdf-ready", function () {336[].slice337.call(338deck339.getRevealElement()340.querySelectorAll(341"pre code[data-code-line-numbers].current-fragment"342)343)344.forEach(function (block) {345scrollHighlightedLineIntoView(block, {}, true);346});347});348},349};350};351352353