const kQueryArg = "q"; const kResultsArg = "show-results"; // If items don't provide a URL, then both the navigator and the onSelect // function aren't called (and therefore, the default implementation is used) // // We're using this sentinel URL to signal to those handlers that this // item is a more item (along with the type) and can be handled appropriately const kItemTypeMoreHref = "0767FDFD-0422-4E5A-BC8A-3BE11E5BBA05"; 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 var searchEl = window.document.getElementById("quarto-search"); if (!searchEl) return; const { autocomplete } = window["@algolia/autocomplete-js"]; let quartoSearchOptions = {}; let language = {}; const searchOptionEl = window.document.getElementById( "quarto-search-options" ); if (searchOptionEl) { const jsonStr = searchOptionEl.textContent; quartoSearchOptions = JSON.parse(jsonStr); language = quartoSearchOptions.language; } // note the search mode if (quartoSearchOptions.type === "overlay") { searchEl.classList.add("type-overlay"); } else { searchEl.classList.add("type-textbox"); } // 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 showSearchResults = currentUrl.searchParams.get(kResultsArg); const mainEl = window.document.querySelector("main"); // highlight matches on the page if (query !== null && mainEl) { // perform any highlighting highlight(escapeRegExp(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); } // function to clear highlighting on the page when the search query changes // (e.g. if the user edits the query or clears it) let highlighting = true; const resetHighlighting = (searchTerm) => { if (mainEl && highlighting && query !== null && searchTerm !== query) { clearHighlight(query, mainEl); highlighting = false; } }; // 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 // for sidebar input UI) const detachedMediaQuery = quartoSearchOptions.type === "overlay" ? "all" : "(max-width: 991px)"; // If configured, include the analytics client to send insights const plugins = configurePlugins(quartoSearchOptions); let lastState = null; const { setIsOpen, setQuery, setCollections } = autocomplete({ container: searchEl, detachedMediaQuery: detachedMediaQuery, defaultActiveItemId: 0, panelContainer: "#quarto-search-results", panelPlacement: quartoSearchOptions["panel-placement"], debug: false, openOnFocus: true, plugins, classNames: { form: "d-flex", }, translations: { clearButtonTitle: language["search-clear-button-title"], detachedCancelButtonText: language["search-detached-cancel-button-title"], submitButtonTitle: language["search-submit-button-title"], }, initialState: { query, }, getItemUrl({ item }) { return item.href; }, onStateChange({ state }) { // Perhaps reset highlighting resetHighlighting(state.query); // If the panel just opened, ensure the panel is positioned properly if (state.isOpen) { if (lastState && !lastState.isOpen) { setTimeout(() => { positionPanel(quartoSearchOptions["panel-placement"]); }, 150); } } // Perhaps show the copy link showCopyLink(state.query, quartoSearchOptions); lastState = state; }, reshape({ sources, state }) { return sources.map((source) => { try { const items = source.getItems(); // Validate the items validateItems(items); // group the items by document const groupedItems = new Map(); items.forEach((item) => { const hrefParts = item.href.split("#"); const baseHref = hrefParts[0]; const isDocumentItem = hrefParts.length === 1; const items = groupedItems.get(baseHref); if (!items) { groupedItems.set(baseHref, [item]); } else { // If the href for this item matches the document // exactly, place this item first as it is the item that represents // the document itself if (isDocumentItem) { items.unshift(item); } else { items.push(item); } groupedItems.set(baseHref, items); } }); const reshapedItems = []; let count = 1; for (const [_key, value] of groupedItems) { const firstItem = value[0]; reshapedItems.push({ ...firstItem, type: kItemTypeDoc, }); const collapseMatches = quartoSearchOptions["collapse-after"]; const collapseCount = typeof collapseMatches === "number" ? collapseMatches : 1; if (value.length > 1) { const target = `search-more-${count}`; const isExpanded = state.context.expanded && state.context.expanded.includes(target); const remainingCount = value.length - collapseCount; for (let i = 1; i < value.length; i++) { if (collapseMatches && i === collapseCount) { reshapedItems.push({ target, title: isExpanded ? language["search-hide-matches-text"] : remainingCount === 1 ? `${remainingCount} ${language["search-more-match-text"]}` : `${remainingCount} ${language["search-more-matches-text"]}`, type: kItemTypeMore, href: kItemTypeMoreHref, }); } if (isExpanded || !collapseMatches || i < collapseCount) { reshapedItems.push({ ...value[i], type: kItemTypeItem, target, }); } } } count += 1; } return { ...source, getItems() { return reshapedItems; }, }; } catch (error) { // Some form of error occurred return { ...source, getItems() { return [ { title: error.name || "An Error Occurred While Searching", text: error.message || "An unknown error occurred while attempting to perform the requested search.", type: kItemTypeError, }, ]; }, }; } }); }, navigator: { navigate({ itemUrl }) { if (itemUrl !== offsetURL(kItemTypeMoreHref)) { window.location.assign(itemUrl); } }, navigateNewTab({ itemUrl }) { if (itemUrl !== offsetURL(kItemTypeMoreHref)) { const windowReference = window.open(itemUrl, "_blank", "noopener"); if (windowReference) { windowReference.focus(); } } }, navigateNewWindow({ itemUrl }) { if (itemUrl !== offsetURL(kItemTypeMoreHref)) { window.open(itemUrl, "_blank", "noopener"); } }, }, getSources({ state, setContext, setActiveItemId, refresh }) { return [ { sourceId: "documents", getItemUrl({ item }) { if (item.href) { return offsetURL(item.href); } else { return undefined; } }, onSelect({ item, state, setContext, setIsOpen, setActiveItemId, refresh, }) { if (item.type === kItemTypeMore) { toggleExpanded(item, state, setContext, setActiveItemId, refresh); // Toggle more setIsOpen(true); } }, getItems({ query }) { if (query === null || query === "") { return []; } const limit = quartoSearchOptions.limit; if (quartoSearchOptions.algolia) { return algoliaSearch(query, limit, quartoSearchOptions.algolia); } else { // Fuse search options const fuseSearchOptions = { isCaseSensitive: false, shouldSort: true, minMatchCharLength: 2, limit: limit, }; return readSearchData().then(function (fuse) { return fuseSearch(query, fuse, fuseSearchOptions); }); } }, templates: { noResults({ createElement }) { const hasQuery = lastState.query; return createElement( "div", { class: `quarto-search-no-results${ hasQuery ? "" : " no-query" }`, }, language["search-no-results-text"] ); }, header({ items, createElement }) { // count the documents const count = items.filter((item) => { return item.type === kItemTypeDoc; }).length; if (count > 0) { return createElement( "div", { class: "search-result-header" }, `${count} ${language["search-matching-documents-text"]}` ); } else { return createElement( "div", { class: "search-result-header-no-results" }, `` ); } }, footer({ _items, createElement }) { if ( quartoSearchOptions.algolia && quartoSearchOptions.algolia["show-logo"] ) { const libDir = quartoSearchOptions.algolia["libDir"]; const logo = createElement("img", { src: offsetURL( `${libDir}/quarto-search/search-by-algolia.svg` ), class: "algolia-search-logo", }); return createElement( "a", { href: "http://www.algolia.com/" }, logo ); } }, item({ item, createElement }) { return renderItem( item, createElement, state, setActiveItemId, setContext, refresh ); }, }, }, ]; }, }); window.quartoOpenSearch = () => { setIsOpen(false); setIsOpen(true); focusSearchInput(); }; // Remove the labeleledby attribute since it is pointing // to a non-existent label if (quartoSearchOptions.type === "overlay") { const inputEl = window.document.querySelector( "#quarto-search .aa-Autocomplete" ); if (inputEl) { inputEl.removeAttribute("aria-labelledby"); } } // If the main document scrolls dismiss the search results // (otherwise, since they're floating in the document they can scroll with the document) window.document.body.onscroll = () => { setIsOpen(false); }; if (showSearchResults) { setIsOpen(true); focusSearchInput(); } }); function configurePlugins(quartoSearchOptions) { const autocompletePlugins = []; const algoliaOptions = quartoSearchOptions.algolia; if ( algoliaOptions && algoliaOptions["analytics-events"] && algoliaOptions["search-only-api-key"] && algoliaOptions["application-id"] ) { const apiKey = algoliaOptions["search-only-api-key"]; const appId = algoliaOptions["application-id"]; // Aloglia insights may not be loaded because they require cookie consent // Use deferred loading so events will start being recorded when/if consent // is granted. const algoliaInsightsDeferredPlugin = deferredLoadPlugin(() => { if ( window.aa && window["@algolia/autocomplete-plugin-algolia-insights"] ) { window.aa("init", { appId, apiKey, useCookie: true, }); const { createAlgoliaInsightsPlugin } = window["@algolia/autocomplete-plugin-algolia-insights"]; // Register the insights client const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({ insightsClient: window.aa, onItemsChange({ insights, insightsEvents }) { const events = insightsEvents.map((event) => { const maxEvents = event.objectIDs.slice(0, 20); return { ...event, objectIDs: maxEvents, }; }); insights.viewedObjectIDs(...events); }, }); return algoliaInsightsPlugin; } }); // Add the plugin autocompletePlugins.push(algoliaInsightsDeferredPlugin); return autocompletePlugins; } } // For plugins that may not load immediately, create a wrapper // plugin and forward events and plugin data once the plugin // is initialized. This is useful for cases like cookie consent // which may prevent the analytics insights event plugin from initializing // immediately. function deferredLoadPlugin(createPlugin) { let plugin = undefined; let subscribeObj = undefined; const wrappedPlugin = () => { if (!plugin && subscribeObj) { plugin = createPlugin(); if (plugin && plugin.subscribe) { plugin.subscribe(subscribeObj); } } return plugin; }; return { subscribe: (obj) => { subscribeObj = obj; }, onStateChange: (obj) => { const plugin = wrappedPlugin(); if (plugin && plugin.onStateChange) { plugin.onStateChange(obj); } }, onSubmit: (obj) => { const plugin = wrappedPlugin(); if (plugin && plugin.onSubmit) { plugin.onSubmit(obj); } }, onReset: (obj) => { const plugin = wrappedPlugin(); if (plugin && plugin.onReset) { plugin.onReset(obj); } }, getSources: (obj) => { const plugin = wrappedPlugin(); if (plugin && plugin.getSources) { return plugin.getSources(obj); } else { return Promise.resolve([]); } }, data: (obj) => { const plugin = wrappedPlugin(); if (plugin && plugin.data) { plugin.data(obj); } }, }; } function validateItems(items) { // Validate the first item if (items.length > 0) { const item = items[0]; const missingFields = []; if (item.href == undefined) { missingFields.push("href"); } if (!item.title == undefined) { missingFields.push("title"); } if (!item.text == undefined) { missingFields.push("text"); } if (missingFields.length === 1) { throw { name: `Error: Search index is missing the ${missingFields[0]} field.`, message: `The items being returned for this search do not include all the required fields. Please ensure that your index items include the ${missingFields[0]} field or use index-fields in your _quarto.yml file to specify the field names.`, }; } else if (missingFields.length > 1) { const missingFieldList = missingFields .map((field) => { return `${field}`; }) .join(", "); throw { name: `Error: Search index is missing the following fields: ${missingFieldList}.`, message: `The items being returned for this search do not include all the required fields. Please ensure that your index items includes the following fields: ${missingFieldList}, or use index-fields in your _quarto.yml file to specify the field names.`, }; } } } let lastQuery = null; function showCopyLink(query, options) { const language = options.language; lastQuery = query; // Insert share icon const inputSuffixEl = window.document.body.querySelector( ".aa-Form .aa-InputWrapperSuffix" ); if (inputSuffixEl) { let copyButtonEl = window.document.body.querySelector( ".aa-Form .aa-InputWrapperSuffix .aa-CopyButton" ); if (copyButtonEl === null) { copyButtonEl = window.document.createElement("button"); copyButtonEl.setAttribute("class", "aa-CopyButton"); copyButtonEl.setAttribute("type", "button"); copyButtonEl.setAttribute("title", language["search-copy-link-title"]); copyButtonEl.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); }; const linkIcon = "bi-clipboard"; const checkIcon = "bi-check2"; const shareIconEl = window.document.createElement("i"); shareIconEl.setAttribute("class", `bi ${linkIcon}`); copyButtonEl.appendChild(shareIconEl); inputSuffixEl.prepend(copyButtonEl); const clipboard = new window.ClipboardJS(".aa-CopyButton", { text: function (_trigger) { const copyUrl = new URL(window.location); copyUrl.searchParams.set(kQueryArg, lastQuery); copyUrl.searchParams.set(kResultsArg, "1"); return copyUrl.toString(); }, }); clipboard.on("success", function (e) { // Focus the input // button target const button = e.trigger; const icon = button.querySelector("i.bi"); // flash "checked" icon.classList.add(checkIcon); icon.classList.remove(linkIcon); setTimeout(function () { icon.classList.remove(checkIcon); icon.classList.add(linkIcon); }, 1000); }); } // If there is a query, show the link icon if (copyButtonEl) { if (lastQuery && options["copy-button"]) { copyButtonEl.style.display = "flex"; } else { copyButtonEl.style.display = "none"; } } } } /* Search Index Handling */ // create the index var fuseIndex = undefined; async function readSearchData() { // Initialize the search index on demand if (fuseIndex === undefined) { // create fuse index const options = { keys: [ { name: "title", weight: 20 }, { name: "section", weight: 20 }, { name: "text", weight: 10 }, ], ignoreLocation: true, threshold: 0.1, }; const fuse = new window.Fuse([], options); // fetch the main search.json const response = await fetch(offsetURL("search.json")); if (response.status == 200) { return response.json().then(function (searchDocs) { searchDocs.forEach(function (searchDoc) { fuse.add(searchDoc); }); fuseIndex = fuse; return fuseIndex; }); } else { return Promise.reject( new Error( "Unexpected status from search index request: " + response.status ) ); } } return fuseIndex; } function inputElement() { return window.document.body.querySelector(".aa-Form .aa-Input"); } function focusSearchInput() { setTimeout(() => { const inputEl = inputElement(); if (inputEl) { inputEl.focus(); } }, 50); } /* Panels */ const kItemTypeDoc = "document"; const kItemTypeMore = "document-more"; const kItemTypeItem = "document-item"; const kItemTypeError = "error"; function renderItem( item, createElement, state, setActiveItemId, setContext, refresh ) { switch (item.type) { case kItemTypeDoc: return createDocumentCard( createElement, "file-richtext", item.title, item.section, item.text, item.href ); case kItemTypeMore: return createMoreCard( createElement, item, state, setActiveItemId, setContext, refresh ); case kItemTypeItem: return createSectionCard( createElement, item.section, item.text, item.href ); case kItemTypeError: return createErrorCard(createElement, item.title, item.text); default: return undefined; } } function createDocumentCard(createElement, icon, title, section, text, href) { const iconEl = createElement("i", { class: `bi bi-${icon} search-result-icon`, }); const titleEl = createElement("p", { class: "search-result-title" }, title); const titleContainerEl = createElement( "div", { class: "search-result-title-container" }, [iconEl, titleEl] ); const textEls = []; if (section) { const sectionEl = createElement( "p", { class: "search-result-section" }, section ); textEls.push(sectionEl); } const descEl = createElement("p", { class: "search-result-text", dangerouslySetInnerHTML: { __html: text, }, }); textEls.push(descEl); const textContainerEl = createElement( "div", { class: "search-result-text-container" }, textEls ); const containerEl = createElement( "div", { class: "search-result-container", }, [titleContainerEl, textContainerEl] ); const linkEl = createElement( "a", { href: offsetURL(href), class: "search-result-link", }, containerEl ); const classes = ["search-result-doc", "search-item"]; if (!section) { classes.push("document-selectable"); } return createElement( "div", { class: classes.join(" "), }, linkEl ); } function createMoreCard( createElement, item, state, setActiveItemId, setContext, refresh ) { const moreCardEl = createElement( "div", { class: "search-result-more search-item", onClick: (e) => { // Handle expanding the sections by adding the expanded // section to the list of expanded sections toggleExpanded(item, state, setContext, setActiveItemId, refresh); e.stopPropagation(); }, }, item.title ); return moreCardEl; } function toggleExpanded(item, state, setContext, setActiveItemId, refresh) { const expanded = state.context.expanded || []; if (expanded.includes(item.target)) { setContext({ expanded: expanded.filter((target) => target !== item.target), }); } else { setContext({ expanded: [...expanded, item.target] }); } refresh(); setActiveItemId(item.__autocomplete_id); } function createSectionCard(createElement, section, text, href) { const sectionEl = createSection(createElement, section, text, href); return createElement( "div", { class: "search-result-doc-section search-item", }, sectionEl ); } function createSection(createElement, title, text, href) { const descEl = createElement("p", { class: "search-result-text", dangerouslySetInnerHTML: { __html: text, }, }); const titleEl = createElement("p", { class: "search-result-section" }, title); const linkEl = createElement( "a", { href: offsetURL(href), class: "search-result-link", }, [titleEl, descEl] ); return linkEl; } function createErrorCard(createElement, title, text) { const descEl = createElement("p", { class: "search-error-text", dangerouslySetInnerHTML: { __html: text, }, }); const titleEl = createElement("p", { class: "search-error-title", dangerouslySetInnerHTML: { __html: ` ${title}`, }, }); const errorEl = createElement("div", { class: "search-error" }, [ titleEl, descEl, ]); return errorEl; } function positionPanel(pos) { const panelEl = window.document.querySelector( "#quarto-search-results .aa-Panel" ); const inputEl = window.document.querySelector( "#quarto-search .aa-Autocomplete" ); if (panelEl && inputEl) { panelEl.style.top = `${Math.round(panelEl.offsetTop)}px`; if (pos === "start") { panelEl.style.left = `${Math.round(inputEl.left)}px`; } else { panelEl.style.right = `${Math.round(inputEl.offsetRight)}px`; } } } /* Highlighting */ // highlighting functions function highlightMatch(query, text) { if (text) { const start = text.toLowerCase().indexOf(query.toLowerCase()); if (start !== -1) { const startMark = ""; const endMark = ""; const end = start + query.length; text = text.slice(0, start) + startMark + text.slice(start, end) + endMark + text.slice(end); const startInfo = clipStart(text, start); const endInfo = clipEnd( text, startInfo.position + startMark.length + endMark.length ); text = startInfo.prefix + text.slice(startInfo.position, endInfo.position) + endInfo.suffix; return text; } else { return text; } } else { return text; } } function clipStart(text, pos) { const clipStart = pos - 50; if (clipStart < 0) { // This will just return the start of the string return { position: 0, prefix: "", }; } else { // We're clipping before the start of the string, walk backwards to the first space. const spacePos = findSpace(text, pos, -1); return { position: spacePos.position, prefix: "", }; } } function clipEnd(text, pos) { const clipEnd = pos + 200; if (clipEnd > text.length) { return { position: text.length, suffix: "", }; } else { const spacePos = findSpace(text, clipEnd, 1); return { position: spacePos.position, suffix: spacePos.clipped ? "…" : "", }; } } function findSpace(text, start, step) { let stepPos = start; while (stepPos > -1 && stepPos < text.length) { const char = text[stepPos]; if (char === " " || char === "," || char === ":") { return { position: step === 1 ? stepPos : stepPos - step, clipped: stepPos > 1 && stepPos < text.length, }; } stepPos = stepPos + step; } return { position: stepPos - step, clipped: false, }; } // removes highlighting as implemented by the mark tag function clearHighlight(searchterm, el) { const childNodes = el.childNodes; for (let i = childNodes.length - 1; i >= 0; i--) { const node = childNodes[i]; if (node.nodeType === Node.ELEMENT_NODE) { if ( node.tagName === "MARK" && node.innerText.toLowerCase() === searchterm.toLowerCase() ) { el.replaceChild(document.createTextNode(node.innerText), node); } else { clearHighlight(searchterm, node); } } } } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string } // highlight matches function highlight(term, el) { const termRegex = new RegExp(term, "ig"); const childNodes = el.childNodes; // walk back to front avoid mutating elements in front of us for (let i = childNodes.length - 1; i >= 0; i--) { const node = childNodes[i]; if (node.nodeType === Node.TEXT_NODE) { // Search text nodes for text to highlight const text = node.nodeValue; 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 mark = document.createElement("mark"); mark.appendChild( document.createTextNode( text.slice(matchIndex, matchIndex + term.length) ) ); markFragment.appendChild(mark); 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); } } } /* Link Handling */ // get the offset from this page for a given site root relative url function offsetURL(url) { var offset = getMeta("quarto:offset"); return offset ? offset + url : url; } // read a meta tag value function getMeta(metaName) { var metas = window.document.getElementsByTagName("meta"); for (let i = 0; i < metas.length; i++) { if (metas[i].getAttribute("name") === metaName) { return metas[i].getAttribute("content"); } } return ""; } function algoliaSearch(query, limit, algoliaOptions) { const { getAlgoliaResults } = window["@algolia/autocomplete-preset-algolia"]; const applicationId = algoliaOptions["application-id"]; const searchOnlyApiKey = algoliaOptions["search-only-api-key"]; const indexName = algoliaOptions["index-name"]; const indexFields = algoliaOptions["index-fields"]; const searchClient = window.algoliasearch(applicationId, searchOnlyApiKey); const searchParams = algoliaOptions["params"]; const searchAnalytics = !!algoliaOptions["analytics-events"]; return getAlgoliaResults({ searchClient, queries: [ { indexName: indexName, query, params: { hitsPerPage: limit, clickAnalytics: searchAnalytics, ...searchParams, }, }, ], transformResponse: (response) => { if (!indexFields) { return response.hits.map((hit) => { return hit.map((item) => { return { ...item, text: highlightMatch(query, item.text), }; }); }); } else { const remappedHits = response.hits.map((hit) => { return hit.map((item) => { const newItem = { ...item }; ["href", "section", "title", "text"].forEach((keyName) => { const mappedName = indexFields[keyName]; if ( mappedName && item[mappedName] !== undefined && mappedName !== keyName ) { newItem[keyName] = item[mappedName]; delete newItem[mappedName]; } }); newItem.text = highlightMatch(query, newItem.text); return newItem; }); }); return remappedHits; } }, }); } function fuseSearch(query, fuse, fuseOptions) { return fuse.search(query, fuseOptions).map((result) => { const addParam = (url, name, value) => { const anchorParts = url.split("#"); const baseUrl = anchorParts[0]; const sep = baseUrl.search("\\?") > 0 ? "&" : "?"; anchorParts[0] = baseUrl + sep + name + "=" + value; return anchorParts.join("#"); }; return { title: result.item.title, section: result.item.section, href: addParam(result.item.href, kQueryArg, query), text: highlightMatch(query, result.item.text), }; }); }