|
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 (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"); |
|
|
|
divSourceCode.forEach((el) => { |
|
if (el.hasAttribute(kCodeLineNumbersAttr)) { |
|
const codeLineAttr = el.getAttribute(kCodeLineNumbersAttr); |
|
el.removeAttribute("data-code-line-numbers"); |
|
if (handleLinesSelector(deck, codeLineAttr)) { |
|
|
|
|
|
const codeBlock = el.querySelectorAll("pre code"); |
|
codeBlock.forEach((code) => { |
|
|
|
code.setAttribute(kCodeLineNumbersAttr, codeLineAttr); |
|
|
|
const scrollState = { currentBlock: code }; |
|
|
|
|
|
const highlightSteps = splitLineNumbers(codeLineAttr); |
|
if (highlightSteps.length > 1) { |
|
|
|
|
|
let fragmentIndex = parseInt( |
|
code.getAttribute(kFragmentIndex), |
|
10 |
|
); |
|
fragmentIndex = |
|
typeof fragmentIndex !== "number" || isNaN(fragmentIndex) |
|
? null |
|
: fragmentIndex; |
|
|
|
let stepN = 1; |
|
highlightSteps.slice(1).forEach( |
|
|
|
(step) => { |
|
var fragmentBlock = code.cloneNode(true); |
|
fragmentBlock.setAttribute( |
|
"data-code-line-numbers", |
|
joinLineNumbers([step]) |
|
); |
|
fragmentBlock.classList.add("fragment"); |
|
|
|
|
|
fragmentBlock |
|
.querySelectorAll(":scope > span") |
|
.forEach((span) => { |
|
if (span.hasAttribute("id")) { |
|
span.setAttribute( |
|
"id", |
|
span.getAttribute("id").concat("-" + stepN) |
|
); |
|
} |
|
}); |
|
stepN = ++stepN; |
|
|
|
|
|
code.parentNode.appendChild(fragmentBlock); |
|
|
|
|
|
highlightCodeBlock(fragmentBlock); |
|
|
|
if (typeof fragmentIndex === "number") { |
|
fragmentBlock.setAttribute(kFragmentIndex, fragmentIndex); |
|
fragmentIndex += 1; |
|
} else { |
|
fragmentBlock.removeAttribute(kFragmentIndex); |
|
} |
|
|
|
|
|
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]]) |
|
); |
|
} |
|
|
|
|
|
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) { |
|
|
|
highlightSteps[0].forEach((highlight) => { |
|
|
|
codeBlock.parentNode.classList.add("code-wrapper"); |
|
|
|
|
|
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) { |
|
|
|
spanToHighlight.forEach((span) => |
|
span.classList.add("highlight-line") |
|
); |
|
codeBlock.classList.add("has-line-highlights"); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function scrollHighlightedLineIntoView(block, scrollState, skipAnimation) { |
|
window.cancelAnimationFrame(scrollState.animationFrameID); |
|
|
|
|
|
|
|
if (scrollState.currentBlock) { |
|
block.scrollTop = scrollState.currentBlock.scrollTop; |
|
} |
|
|
|
|
|
|
|
scrollState.currentBlock = block; |
|
|
|
const highlightBounds = getHighlightedLineBounds(block); |
|
let viewportHeight = block.offsetHeight; |
|
|
|
|
|
const blockStyles = window.getComputedStyle(block); |
|
viewportHeight -= |
|
parseInt(blockStyles.paddingTop) + parseInt(blockStyles.paddingBottom); |
|
|
|
|
|
const startTop = block.scrollTop; |
|
let targetTop = |
|
highlightBounds.top + |
|
(Math.min(highlightBounds.bottom - highlightBounds.top, viewportHeight) - |
|
viewportHeight) / |
|
2; |
|
|
|
|
|
targetTop = Math.max( |
|
Math.min(targetTop, block.scrollHeight - viewportHeight), |
|
0 |
|
); |
|
|
|
if (skipAnimation === true || startTop === targetTop) { |
|
block.scrollTop = targetTop; |
|
} else { |
|
|
|
if (block.scrollHeight <= viewportHeight) return; |
|
|
|
let time = 0; |
|
|
|
const animate = function () { |
|
time = Math.min(time + 0.02, 1); |
|
|
|
|
|
block.scrollTop = |
|
startTop + (targetTop - startTop) * easeInOutQuart(time); |
|
|
|
|
|
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, |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function easeInOutQuart(t) { |
|
|
|
return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; |
|
} |
|
|
|
function splitLineNumbers(lineNumbersAttr) { |
|
|
|
lineNumbersAttr = lineNumbersAttr.replace("/s/g", ""); |
|
|
|
lineNumbersAttr = lineNumbersAttr.split(delimiters.step); |
|
|
|
|
|
return lineNumbersAttr.map((highlights) => { |
|
|
|
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) { |
|
|
|
if (typeof highlight.last === "number") { |
|
return highlight.first + delimiters.lineRange + highlight.last; |
|
} |
|
|
|
else if (typeof highlight.first === "number") { |
|
return highlight.first; |
|
} |
|
|
|
else { |
|
return ""; |
|
} |
|
}) |
|
.join(delimiters.line); |
|
}) |
|
.join(delimiters.step); |
|
} |
|
|
|
return { |
|
id: "quarto-line-highlight", |
|
init: function (deck) { |
|
initQuartoLineHighlight(deck); |
|
|
|
|
|
|
|
deck.on("pdf-ready", function () { |
|
[].slice |
|
.call( |
|
deck |
|
.getRevealElement() |
|
.querySelectorAll( |
|
"pre code[data-code-line-numbers].current-fragment" |
|
) |
|
) |
|
.forEach(function (block) { |
|
scrollHighlightedLineIntoView(block, {}, true); |
|
}); |
|
}); |
|
}, |
|
}; |
|
}; |
|
|