Built site for gh-pages

This commit is contained in:
Quarto GHA Workflow Runner
2026-03-22 13:17:42 +00:00
parent 61e0653994
commit 3c421e0170
247 changed files with 8025 additions and 10900 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,145 +0,0 @@
class QuartoAxeReporter {
constructor(axeResult, options) {
this.axeResult = axeResult;
this.options = options;
}
report() {
throw new Error("report() is an abstract method");
}
}
class QuartoAxeJsonReporter extends QuartoAxeReporter {
constructor(axeResult, options) {
super(axeResult, options);
}
report() {
console.log(JSON.stringify(this.axeResult, null, 2));
}
}
class QuartoAxeConsoleReporter extends QuartoAxeReporter {
constructor(axeResult, options) {
super(axeResult, options);
}
report() {
for (const violation of this.axeResult.violations) {
console.log(violation.description);
for (const node of violation.nodes) {
for (const target of node.target) {
console.log(target);
console.log(document.querySelector(target));
}
}
}
}
}
class QuartoAxeDocumentReporter extends QuartoAxeReporter {
constructor(axeResult, options) {
super(axeResult, options);
}
createViolationElement(violation) {
const violationElement = document.createElement("div");
const descriptionElement = document.createElement("div");
descriptionElement.className = "quarto-axe-violation-description";
descriptionElement.innerText = `${violation.impact.replace(/^[a-z]/, match => match.toLocaleUpperCase())}: ${violation.description}`;
violationElement.appendChild(descriptionElement);
const helpElement = document.createElement("div");
helpElement.className = "quarto-axe-violation-help";
helpElement.innerText = violation.help;
violationElement.appendChild(helpElement);
const nodesElement = document.createElement("div");
nodesElement.className = "quarto-axe-violation-nodes";
violationElement.appendChild(nodesElement);
const nodeElement = document.createElement("div");
nodeElement.className = "quarto-axe-violation-selector";
for (const node of violation.nodes) {
for (const target of node.target) {
const targetElement = document.createElement("span");
targetElement.className = "quarto-axe-violation-target";
targetElement.innerText = target;
nodeElement.appendChild(targetElement);
nodeElement.addEventListener("mouseenter", () => {
const element = document.querySelector(target);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
element.classList.add("quarto-axe-hover-highlight");
setTimeout(() => {
element.style.border = "";
}, 2000);
}
});
nodeElement.addEventListener("mouseleave", () => {
const element = document.querySelector(target);
if (element) {
element.classList.remove("quarto-axe-hover-highlight");
}
});
nodeElement.addEventListener("click", () => {
console.log(document.querySelector(target));
});
nodeElement.appendChild(targetElement);
}
nodesElement.appendChild(nodeElement);
}
return violationElement;
}
report() {
const violations = this.axeResult.violations;
const reportElement = document.createElement("div");
reportElement.className = "quarto-axe-report";
if (violations.length === 0) {
const noViolationsElement = document.createElement("div");
noViolationsElement.className = "quarto-axe-no-violations";
noViolationsElement.innerText = "No axe-core violations found.";
reportElement.appendChild(noViolationsElement);
}
violations.forEach((violation) => {
reportElement.appendChild(this.createViolationElement(violation));
});
document.querySelector("main").appendChild(reportElement);
}
}
const reporters = {
json: QuartoAxeJsonReporter,
console: QuartoAxeConsoleReporter,
document: QuartoAxeDocumentReporter,
};
class QuartoAxeChecker {
constructor(opts) {
this.options = opts;
}
async init() {
const axe = (await import("https://cdn.skypack.dev/pin/axe-core@v4.10.3-aVOFXWsJaCpVrtv89pCa/mode=imports,min/optimized/axe-core.js")).default;
const result = await axe.run({
exclude: [
// https://github.com/microsoft/tabster/issues/288
// MS has claimed they won't fix this, so we need to add an exclusion to
// all tabster elements
"[data-tabster-dummy]"
],
preload: { assets: ['cssom'], timeout: 50000 }
});
const reporter = this.options === true ? new QuartoAxeConsoleReporter(result) : new reporters[this.options.output](result, this.options);
reporter.report();
}
}
export async function init() {
const opts = document.querySelector("#quarto-axe-checker-options");
if (opts) {
const jsonOptions = JSON.parse(atob(opts.textContent));
const checker = new QuartoAxeChecker(jsonOptions);
await checker.init();
}
}

View File

@@ -216,4 +216,4 @@ code span.wa {
content: "</";
}
/*# sourceMappingURL=86edf53ecdff796acf02dbceed379426.css.map */
/*# sourceMappingURL=378c2a0cc31455d06730fbe37c8f3789.css.map */

View File

@@ -1,5 +1,4 @@
import * as tabsets from "./tabsets/tabsets.js";
import * as axe from "./axe/axe-check.js";
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
detail: {},
@@ -827,7 +826,6 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
});
tabsets.init();
axe.init();
function throttle(func, wait) {
let waiting = false;

View File

@@ -8,6 +8,18 @@ const kResultsArg = "show-results";
// item is a more item (along with the type) and can be handled appropriately
const kItemTypeMoreHref = "0767FDFD-0422-4E5A-BC8A-3BE11E5BBA05";
// Capture search params and clean ?q= from URL at module load time, before
// any DOMContentLoaded handlers run. quarto-nav.js resolves all <a> hrefs
// against window.location during DOMContentLoaded — if ?q= is still present,
// every link on the page gets the query param baked into its href.
const currentUrl = new URL(window.location);
const kQuery = currentUrl.searchParams.get(kQueryArg);
if (kQuery) {
const replacementUrl = new URL(window.location);
replacementUrl.searchParams.delete(kQueryArg);
window.history.replaceState({}, "", replacementUrl);
}
window.document.addEventListener("DOMContentLoaded", function (_event) {
// Ensure that search is available on this page. If it isn't,
// should return early and not do anything
@@ -37,20 +49,30 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
// Used to determine highlighting behavior for this page
// A `q` query param is expected when the user follows a search
// to this page
const currentUrl = new URL(window.location);
const query = currentUrl.searchParams.get(kQueryArg);
const query = kQuery;
const showSearchResults = currentUrl.searchParams.get(kResultsArg);
const mainEl = window.document.querySelector("main");
// highlight matches on the page
if (query && mainEl) {
// perform any highlighting
highlight(escapeRegExp(query), mainEl);
highlight(query, mainEl);
// fix up the URL to remove the q query param
const replacementUrl = new URL(window.location);
replacementUrl.searchParams.delete(kQueryArg);
window.history.replaceState({}, "", replacementUrl);
// Activate tabs on pageshow — after tabsets.js restores localStorage state.
// tabsets.js registers its pageshow handler during module execution (before
// DOMContentLoaded). By registering ours during DOMContentLoaded, listener
// ordering guarantees we run after tabsets.js — so search activation wins.
window.addEventListener("pageshow", function (event) {
if (!event.persisted) {
for (const mark of mainEl.querySelectorAll("mark")) {
openAllTabsetsContainingEl(mark);
}
// Only scroll to first match when there's no hash fragment.
// With a hash, the browser already scrolled to the target section.
if (!currentUrl.hash) {
requestAnimationFrame(() => scrollToFirstVisibleMatch(mainEl));
}
}
}, { once: true });
}
// function to clear highlighting on the page when the search query changes
@@ -63,18 +85,6 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
}
};
// Clear search highlighting when the user scrolls sufficiently
const resetFn = () => {
resetHighlighting("");
window.removeEventListener("quarto-hrChanged", resetFn);
window.removeEventListener("quarto-sectionChanged", resetFn);
};
// Register this event after the initial scrolling and settling of events
// on the page
window.addEventListener("quarto-hrChanged", resetFn);
window.addEventListener("quarto-sectionChanged", resetFn);
// Responsively switch to overlay mode if the search is present on the navbar
// Note that switching the sidebar to overlay mode requires more coordinate (not just
// the media query since we generate different HTML for sidebar overlays than we do
@@ -189,8 +199,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
title: isExpanded
? language["search-hide-matches-text"]
: remainingCount === 1
? `${remainingCount} ${language["search-more-match-text"]}`
: `${remainingCount} ${language["search-more-matches-text"]}`,
? `${remainingCount} ${language["search-more-match-text"]}`
: `${remainingCount} ${language["search-more-matches-text"]}`,
type: kItemTypeMore,
href: kItemTypeMoreHref,
});
@@ -308,9 +318,8 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
return createElement(
"div",
{
class: `quarto-search-no-results${
hasQuery ? "" : " no-query"
}`,
class: `quarto-search-no-results${hasQuery ? "" : " no-query"
}`,
},
language["search-no-results-text"]
);
@@ -356,6 +365,12 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
},
item({ item, createElement }) {
if (item.text && item.href && !item.href.includes('?q=')) {
const [main, hash] = item.href.split('#')
const hashAppend = hash ? '#' + hash : ''
item.href = main + '?q=' + encodeURIComponent(state.query) + hashAppend
}
return renderItem(
item,
createElement,
@@ -466,10 +481,19 @@ function configurePlugins(quartoSearchOptions) {
window.aa &&
window["@algolia/autocomplete-plugin-algolia-insights"]
) {
// Check if cookie consent is enabled from search options
const cookieConsentEnabled = algoliaOptions["cookie-consent-enabled"] || false;
// Generate random session token only when cookies are disabled
const userToken = cookieConsentEnabled ? undefined : Array.from(Array(20), () =>
Math.floor(Math.random() * 36).toString(36)
).join("");
window.aa("init", {
appId,
apiKey,
useCookie: true,
useCookie: cookieConsentEnabled,
userToken: userToken,
});
const { createAlgoliaInsightsPlugin } =
@@ -1095,60 +1119,203 @@ function clearHighlight(searchterm, el) {
}
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
/** Get all html nodes under the given `root` that don't have children. */
function getLeafNodes(root) {
let leaves = [];
function traverse(node) {
if (node.childNodes.length === 0) {
leaves.push(node);
} else {
node.childNodes.forEach(traverse);
}
}
traverse(root);
return leaves;
}
/** create and return `<mark>${txt}</mark>` */
const markEl = txt => {
const el = document.createElement("mark");
el.appendChild(document.createTextNode(txt));
return el
}
/** get all ancestors of an element matching the given css selector */
const matchAncestors = (el, selector) => {
let ancestors = [];
while (el) {
if (el.matches?.(selector)) ancestors.push(el);
el = el.parentNode;
}
return ancestors;
};
const isWhitespace = s => s.trim().length === 0
// =================
// MATCHING CODE
// =================
const initMatch = () => ({
i: 0,
lohisByNode: new Map()
})
/**
* keeps track of the start (lo) and end (hi) index of the match per node (leaf)
* note: mutates the contents of `matchContext`
*/
const advanceMatch = (leaf, leafi, matchContext) => {
matchContext.i++
const curLoHi = matchContext.lohisByNode.get(leaf)
matchContext.lohisByNode.set(leaf, { lo: curLoHi?.lo ?? leafi, hi: leafi })
}
/**
* Finds all non-overlapping matches for a search string in the document.
* The search string may be split between multiple consecutive leaf nodes.
*
* Whitespace in the search string must be present in the document to match, but
* there may be addititional whitespace in the document that is ignored.
*
* e.g. searching for `dogs rock` would match `dogs \n <span> rock</span>`,
* and would contribute the match
* `{ i:9, els: new Map([[textNode, {lo:0, hi:8}],[spanNode,{lo:0,hi:5}]]) }`
*
* @returns {Map<HTMLElement,{lo:number,hi:number}>[]}
*/
function searchMatches(inSearch, el) {
// searchText has all sequences of whitespace replaced by a single space
const searchText = inSearch.toLowerCase().replace(/\s+/g, ' ')
const leafNodes = getLeafNodes(el)
/** @type {Map<HTMLElement,{lo:number,hi:number}>[]} */
const matches = []
/** @type {{i:number; els:Map<HTMLElement,{lo:number,hi:number}>}[]} */
let curMatchContext = initMatch()
for (const leaf of leafNodes) {
const leafStr = leaf.textContent.toLowerCase()
// for each character in this leaf's text:
for (let leafi = 0; leafi < leafStr.length; leafi++) {
if (isWhitespace(leafStr[leafi])) {
// if there is at least one whitespace in the document
// we advance over a search text whitespace.
if (isWhitespace(searchText[curMatchContext.i])) advanceMatch(leaf, leafi, curMatchContext)
// all sequences of whitespace are otherwise ignored.
} else {
if (searchText[curMatchContext.i] === leafStr[leafi]) {
advanceMatch(leaf, leafi, curMatchContext)
} else {
curMatchContext = initMatch()
// if current character in the document did not match at i in the search text,
// reset the search and see if that character matches at 0 in the search text.
if (searchText[curMatchContext.i] === leafStr[leafi]) advanceMatch(leaf, leafi, curMatchContext)
}
}
const isMatchComplete = curMatchContext.i === searchText.length
if (isMatchComplete) {
matches.push(curMatchContext.lohisByNode)
curMatchContext = initMatch()
}
}
}
return matches
}
// highlight matches
function highlight(term, el) {
const termRegex = new RegExp(term, "ig");
const childNodes = el.childNodes;
/**
* e.g. `markMatches(myTextNode, [[0,5],[12,15]])` would wrap the
* character sequences in myTextNode from 0-5 and 12-15 in marks.
* Its important to mark all sequences in a text node at once
* because this function replaces the entire text node; so any
* other references to that text node will no longer be in the DOM.
*/
function markMatches(node, lohis) {
const text = node.nodeValue
// walk back to front avoid mutating elements in front of us
for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i];
const markFragment = document.createDocumentFragment();
if (node.nodeType === Node.TEXT_NODE) {
// Search text nodes for text to highlight
const text = node.nodeValue;
let prevHi = 0
for (const [lo, hi] of lohis) {
markFragment.append(
document.createTextNode(text.slice(prevHi, lo)),
markEl(text.slice(lo, hi + 1))
)
prevHi = hi + 1
}
markFragment.append(
document.createTextNode(text.slice(prevHi, text.length))
)
let startIndex = 0;
let matchIndex = text.search(termRegex);
if (matchIndex > -1) {
const markFragment = document.createDocumentFragment();
while (matchIndex > -1) {
const prefix = text.slice(startIndex, matchIndex);
markFragment.appendChild(document.createTextNode(prefix));
const parent = node.parentElement
parent?.replaceChild(markFragment, node)
return parent
}
const mark = document.createElement("mark");
mark.appendChild(
document.createTextNode(
text.slice(matchIndex, matchIndex + term.length)
)
);
markFragment.appendChild(mark);
// Activate ancestor tabs so a search match inside an inactive pane becomes visible.
// When multiple panes in the same tabset contain matches, avoid switching away from
// the currently active pane — the user already sees a match there.
function openAllTabsetsContainingEl(el) {
for (const pane of matchAncestors(el, '.tab-pane')) {
const tabContent = pane.closest('.tab-content');
if (!tabContent) continue;
const activePane = tabContent.querySelector(':scope > .tab-pane.active');
if (activePane?.querySelector('mark')) continue;
const tabButton = document.querySelector(`[data-bs-target="#${pane.id}"]`);
if (tabButton) new bootstrap.Tab(tabButton).show();
}
}
startIndex = matchIndex + term.length;
matchIndex = text.slice(startIndex).search(new RegExp(term, "ig"));
if (matchIndex > -1) {
matchIndex = startIndex + matchIndex;
}
}
if (startIndex < text.length) {
markFragment.appendChild(
document.createTextNode(text.slice(startIndex, text.length))
);
}
el.replaceChild(markFragment, node);
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// recurse through elements
highlight(term, node);
function scrollToFirstVisibleMatch(mainEl) {
for (const mark of mainEl.querySelectorAll("mark")) {
const isMarkVisible = matchAncestors(mark, '.tab-pane').every(markTabPane =>
markTabPane.classList.contains("active")
)
if (isMarkVisible) {
mark.scrollIntoView({ behavior: "smooth", block: "center" });
return;
}
}
}
/**
* e.g.
* ```js
* const m = new Map()
*
* arrayMapPush(m, 'dog', 'Max')
* console.log(m) // Map { dog->['Max'] }
*
* arrayMapPush(m, 'dog', 'Samba')
* arrayMapPush(m, 'cat', 'Scruffle')
* console.log(m) // Map { dog->['Max', 'Samba'], cat->['Scruffle'] }
* ```
*/
const arrayMapPush = (map, key, item) => {
if (!map.has(key)) map.set(key, [])
map.set(key, [...map.get(key), item])
}
// copy&paste any string from a quarto page and
// this should find that string in the page and highlight it.
// exception: text that starts outside/inside a tabset and ends
// inside/outside that tabset.
function highlight(searchStr, el) {
const matches = searchMatches(searchStr, el);
const matchesGroupedByNode = new Map()
for (const match of matches) {
for (const [mel, { lo, hi }] of match) {
arrayMapPush(matchesGroupedByNode, mel, [lo, hi])
}
}
for (const [node, lohis] of matchesGroupedByNode) {
markMatches(node, lohis)
}
}
/* Link Handling */
// get the offset from this page for a given site root relative url
function offsetURL(url) {