diff options
Diffstat (limited to 'src/librustdoc/html/static/js')
-rw-r--r-- | src/librustdoc/html/static/js/README.md | 15 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/externs.js | 142 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/main.js | 974 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/scrape-examples.js | 106 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/search.js | 2297 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/settings.js | 272 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/source-script.js | 241 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/storage.js | 268 |
8 files changed, 4315 insertions, 0 deletions
diff --git a/src/librustdoc/html/static/js/README.md b/src/librustdoc/html/static/js/README.md new file mode 100644 index 000000000..1fd859ad7 --- /dev/null +++ b/src/librustdoc/html/static/js/README.md @@ -0,0 +1,15 @@ +# Rustdoc JS + +These JavaScript files are incorporated into the rustdoc binary at build time, +and are minified and written to the filesystem as part of the doc build process. + +We use the [Closure Compiler](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler) +dialect of JSDoc to comment our code and annotate params and return types. +To run a check: + + ./x.py doc library/std + npm i -g google-closure-compiler + google-closure-compiler -W VERBOSE \ + build/<YOUR PLATFORM>/doc/{search-index*.js,crates*.js} \ + src/librustdoc/html/static/js/{search.js,main.js,storage.js} \ + --externs src/librustdoc/html/static/js/externs.js >/dev/null diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js new file mode 100644 index 000000000..ecbe15a59 --- /dev/null +++ b/src/librustdoc/html/static/js/externs.js @@ -0,0 +1,142 @@ +// This file contains type definitions that are processed by the Closure Compiler but are +// not put into the JavaScript we include as part of the documentation. It is used for +// type checking. See README.md in this directory for more info. + +/* eslint-disable */ +let searchState; +function initSearch(searchIndex){} + +/** + * @typedef {{ + * name: string, + * fullPath: Array<string>, + * pathWithoutLast: Array<string>, + * pathLast: string, + * generics: Array<QueryElement>, + * }} + */ +let QueryElement; + +/** + * @typedef {{ + * pos: number, + * totalElems: number, + * typeFilter: (null|string), + * userQuery: string, + * }} + */ +let ParserState; + +/** + * @typedef {{ + * original: string, + * userQuery: string, + * typeFilter: number, + * elems: Array<QueryElement>, + * args: Array<QueryElement>, + * returned: Array<QueryElement>, + * foundElems: number, + * }} + */ +let ParsedQuery; + +/** + * @typedef {{ + * crate: string, + * desc: string, + * id: number, + * name: string, + * normalizedName: string, + * parent: (Object|null|undefined), + * path: string, + * ty: (Number|null|number), + * type: (Array<?>|null) + * }} + */ +let Row; + +/** + * @typedef {{ + * in_args: Array<Object>, + * returned: Array<Object>, + * others: Array<Object>, + * query: ParsedQuery, + * }} + */ +let ResultsTable; + +/** + * @typedef {{ + * desc: string, + * displayPath: string, + * fullPath: string, + * href: string, + * id: number, + * lev: number, + * name: string, + * normalizedName: string, + * parent: (Object|undefined), + * path: string, + * ty: number, + * }} + */ +let Results; + +/** + * A pair of [inputs, outputs], or 0 for null. This is stored in the search index. + * The JavaScript deserializes this into FunctionSearchType. + * + * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null` + * because `null` is four bytes while `0` is one byte. + * + * An input or output can be encoded as just a number if there is only one of them, AND + * it has no generics. The no generics rule exists to avoid ambiguity: imagine if you had + * a function with a single output, and that output had a single generic: + * + * fn something() -> Result<usize, usize> + * + * If output was allowed to be any RawFunctionType, it would look like this + * + * [[], [50, [3, 3]]] + * + * The problem is that the above output could be interpreted as either a type with ID 50 and two + * generics, or it could be interpreted as a pair of types, the first one with ID 50 and the second + * with ID 3 and a single generic parameter that is also ID 3. We avoid this ambiguity by choosing + * in favor of the pair of types interpretation. This is why the `(number|Array<RawFunctionType>)` + * is used instead of `(RawFunctionType|Array<RawFunctionType>)`. + * + * @typedef {( + * 0 | + * [(number|Array<RawFunctionType>)] | + * [(number|Array<RawFunctionType>), (number|Array<RawFunctionType>)] + * )} + */ +let RawFunctionSearchType; + +/** + * A single function input or output type. This is either a single path ID, or a pair of + * [path ID, generics]. + * + * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null` + * because `null` is four bytes while `0` is one byte. + * + * @typedef {number | [number, Array<RawFunctionType>]} + */ +let RawFunctionType; + +/** + * @typedef {{ + * inputs: Array<FunctionType>, + * outputs: Array<FunctionType>, + * }} + */ +let FunctionSearchType; + +/** + * @typedef {{ + * name: (null|string), + * ty: (null|number), + * generics: Array<FunctionType>, + * }} + */ +let FunctionType; diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js new file mode 100644 index 000000000..0702b2b0b --- /dev/null +++ b/src/librustdoc/html/static/js/main.js @@ -0,0 +1,974 @@ +// Local js definitions: +/* global addClass, getSettingValue, hasClass, searchState */ +/* global onEach, onEachLazy, removeClass */ + +"use strict"; + +// Get a value from the rustdoc-vars div, which is used to convey data from +// Rust to the JS. If there is no such element, return null. +function getVar(name) { + const el = document.getElementById("rustdoc-vars"); + if (el) { + return el.attributes["data-" + name].value; + } else { + return null; + } +} + +// Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL +// for a resource under the root-path, with the resource-suffix. +function resourcePath(basename, extension) { + return getVar("root-path") + basename + getVar("resource-suffix") + extension; +} + +function hideMain() { + addClass(document.getElementById(MAIN_ID), "hidden"); +} + +function showMain() { + removeClass(document.getElementById(MAIN_ID), "hidden"); +} + +function elemIsInParent(elem, parent) { + while (elem && elem !== document.body) { + if (elem === parent) { + return true; + } + elem = elem.parentElement; + } + return false; +} + +function blurHandler(event, parentElem, hideCallback) { + if (!elemIsInParent(document.activeElement, parentElem) && + !elemIsInParent(event.relatedTarget, parentElem) + ) { + hideCallback(); + } +} + +(function() { + window.rootPath = getVar("root-path"); + window.currentCrate = getVar("current-crate"); +}()); + +function setMobileTopbar() { + // FIXME: It would be nicer to generate this text content directly in HTML, + // but with the current code it's hard to get the right information in the right place. + const mobileLocationTitle = document.querySelector(".mobile-topbar h2.location"); + const locationTitle = document.querySelector(".sidebar h2.location"); + if (mobileLocationTitle && locationTitle) { + mobileLocationTitle.innerHTML = locationTitle.innerHTML; + } +} + +// Gets the human-readable string for the virtual-key code of the +// given KeyboardEvent, ev. +// +// This function is meant as a polyfill for KeyboardEvent#key, +// since it is not supported in IE 11 or Chrome for Android. We also test for +// KeyboardEvent#keyCode because the handleShortcut handler is +// also registered for the keydown event, because Blink doesn't fire +// keypress on hitting the Escape key. +// +// So I guess you could say things are getting pretty interoperable. +function getVirtualKey(ev) { + if ("key" in ev && typeof ev.key !== "undefined") { + return ev.key; + } + + const c = ev.charCode || ev.keyCode; + if (c === 27) { + return "Escape"; + } + return String.fromCharCode(c); +} + +const MAIN_ID = "main-content"; +const SETTINGS_BUTTON_ID = "settings-menu"; +const ALTERNATIVE_DISPLAY_ID = "alternative-display"; +const NOT_DISPLAYED_ID = "not-displayed"; +const HELP_BUTTON_ID = "help-button"; + +function getSettingsButton() { + return document.getElementById(SETTINGS_BUTTON_ID); +} + +function getHelpButton() { + return document.getElementById(HELP_BUTTON_ID); +} + +// Returns the current URL without any query parameter or hash. +function getNakedUrl() { + return window.location.href.split("?")[0].split("#")[0]; +} + +/** + * This function inserts `newNode` after `referenceNode`. It doesn't work if `referenceNode` + * doesn't have a parent node. + * + * @param {HTMLElement} newNode + * @param {HTMLElement} referenceNode + */ +function insertAfter(newNode, referenceNode) { + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); +} + +/** + * This function creates a new `<section>` with the given `id` and `classes` if it doesn't already + * exist. + * + * More information about this in `switchDisplayedElement` documentation. + * + * @param {string} id + * @param {string} classes + */ +function getOrCreateSection(id, classes) { + let el = document.getElementById(id); + + if (!el) { + el = document.createElement("section"); + el.id = id; + el.className = classes; + insertAfter(el, document.getElementById(MAIN_ID)); + } + return el; +} + +/** + * Returns the `<section>` element which contains the displayed element. + * + * @return {HTMLElement} + */ +function getAlternativeDisplayElem() { + return getOrCreateSection(ALTERNATIVE_DISPLAY_ID, "content hidden"); +} + +/** + * Returns the `<section>` element which contains the not-displayed elements. + * + * @return {HTMLElement} + */ +function getNotDisplayedElem() { + return getOrCreateSection(NOT_DISPLAYED_ID, "hidden"); +} + +/** + * To nicely switch between displayed "extra" elements (such as search results or settings menu) + * and to alternate between the displayed and not displayed elements, we hold them in two different + * `<section>` elements. They work in pair: one holds the hidden elements while the other + * contains the displayed element (there can be only one at the same time!). So basically, we switch + * elements between the two `<section>` elements. + * + * @param {HTMLElement} elemToDisplay + */ +function switchDisplayedElement(elemToDisplay) { + const el = getAlternativeDisplayElem(); + + if (el.children.length > 0) { + getNotDisplayedElem().appendChild(el.firstElementChild); + } + if (elemToDisplay === null) { + addClass(el, "hidden"); + showMain(); + return; + } + el.appendChild(elemToDisplay); + hideMain(); + removeClass(el, "hidden"); +} + +function browserSupportsHistoryApi() { + return window.history && typeof window.history.pushState === "function"; +} + +// eslint-disable-next-line no-unused-vars +function loadCss(cssFileName) { + const link = document.createElement("link"); + link.href = resourcePath(cssFileName, ".css"); + link.type = "text/css"; + link.rel = "stylesheet"; + document.getElementsByTagName("head")[0].appendChild(link); +} + +(function() { + function loadScript(url) { + const script = document.createElement("script"); + script.src = url; + document.head.append(script); + } + + getSettingsButton().onclick = event => { + addClass(getSettingsButton(), "rotate"); + event.preventDefault(); + // Sending request for the CSS and the JS files at the same time so it will + // hopefully be loaded when the JS will generate the settings content. + loadCss("settings"); + loadScript(resourcePath("settings", ".js")); + }; + + window.searchState = { + loadingText: "Loading search results...", + input: document.getElementsByClassName("search-input")[0], + outputElement: () => { + let el = document.getElementById("search"); + if (!el) { + el = document.createElement("section"); + el.id = "search"; + getNotDisplayedElem().appendChild(el); + } + return el; + }, + title: document.title, + titleBeforeSearch: document.title, + timeout: null, + // On the search screen, so you remain on the last tab you opened. + // + // 0 for "In Names" + // 1 for "In Parameters" + // 2 for "In Return Types" + currentTab: 0, + // tab and back preserves the element that was focused. + focusedByTab: [null, null, null], + clearInputTimeout: () => { + if (searchState.timeout !== null) { + clearTimeout(searchState.timeout); + searchState.timeout = null; + } + }, + isDisplayed: () => searchState.outputElement().parentElement.id === ALTERNATIVE_DISPLAY_ID, + // Sets the focus on the search bar at the top of the page + focus: () => { + searchState.input.focus(); + }, + // Removes the focus from the search bar. + defocus: () => { + searchState.input.blur(); + }, + showResults: search => { + if (search === null || typeof search === "undefined") { + search = searchState.outputElement(); + } + switchDisplayedElement(search); + searchState.mouseMovedAfterSearch = false; + document.title = searchState.title; + }, + hideResults: () => { + switchDisplayedElement(null); + document.title = searchState.titleBeforeSearch; + // We also remove the query parameter from the URL. + if (browserSupportsHistoryApi()) { + history.replaceState(null, window.currentCrate + " - Rust", + getNakedUrl() + window.location.hash); + } + }, + getQueryStringParams: () => { + const params = {}; + window.location.search.substring(1).split("&"). + map(s => { + const pair = s.split("="); + params[decodeURIComponent(pair[0])] = + typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]); + }); + return params; + }, + setup: () => { + const search_input = searchState.input; + if (!searchState.input) { + return; + } + let searchLoaded = false; + function loadSearch() { + if (!searchLoaded) { + searchLoaded = true; + loadScript(resourcePath("search", ".js")); + loadScript(resourcePath("search-index", ".js")); + } + } + + search_input.addEventListener("focus", () => { + search_input.origPlaceholder = search_input.placeholder; + search_input.placeholder = "Type your search here."; + loadSearch(); + }); + + if (search_input.value !== "") { + loadSearch(); + } + + const params = searchState.getQueryStringParams(); + if (params.search !== undefined) { + const search = searchState.outputElement(); + search.innerHTML = "<h3 class=\"search-loading\">" + + searchState.loadingText + "</h3>"; + searchState.showResults(search); + loadSearch(); + } + }, + }; + + function getPageId() { + if (window.location.hash) { + const tmp = window.location.hash.replace(/^#/, ""); + if (tmp.length > 0) { + return tmp; + } + } + return null; + } + + const toggleAllDocsId = "toggle-all-docs"; + let savedHash = ""; + + function handleHashes(ev) { + if (ev !== null && searchState.isDisplayed() && ev.newURL) { + // This block occurs when clicking on an element in the navbar while + // in a search. + switchDisplayedElement(null); + const hash = ev.newURL.slice(ev.newURL.indexOf("#") + 1); + if (browserSupportsHistoryApi()) { + // `window.location.search`` contains all the query parameters, not just `search`. + history.replaceState(null, "", + getNakedUrl() + window.location.search + "#" + hash); + } + const elem = document.getElementById(hash); + if (elem) { + elem.scrollIntoView(); + } + } + // This part is used in case an element is not visible. + if (savedHash !== window.location.hash) { + savedHash = window.location.hash; + if (savedHash.length === 0) { + return; + } + expandSection(savedHash.slice(1)); // we remove the '#' + } + } + + function onHashChange(ev) { + // If we're in mobile mode, we should hide the sidebar in any case. + const sidebar = document.getElementsByClassName("sidebar")[0]; + removeClass(sidebar, "shown"); + handleHashes(ev); + } + + function openParentDetails(elem) { + while (elem) { + if (elem.tagName === "DETAILS") { + elem.open = true; + } + elem = elem.parentNode; + } + } + + function expandSection(id) { + openParentDetails(document.getElementById(id)); + } + + function handleEscape(ev) { + searchState.clearInputTimeout(); + switchDisplayedElement(null); + if (browserSupportsHistoryApi()) { + history.replaceState(null, window.currentCrate + " - Rust", + getNakedUrl() + window.location.hash); + } + ev.preventDefault(); + searchState.defocus(); + window.hidePopoverMenus(); + } + + function handleShortcut(ev) { + // Don't interfere with browser shortcuts + const disableShortcuts = getSettingValue("disable-shortcuts") === "true"; + if (ev.ctrlKey || ev.altKey || ev.metaKey || disableShortcuts) { + return; + } + + if (document.activeElement.tagName === "INPUT" && + document.activeElement.type !== "checkbox") { + switch (getVirtualKey(ev)) { + case "Escape": + handleEscape(ev); + break; + } + } else { + switch (getVirtualKey(ev)) { + case "Escape": + handleEscape(ev); + break; + + case "s": + case "S": + ev.preventDefault(); + searchState.focus(); + break; + + case "+": + case "-": + ev.preventDefault(); + toggleAllDocs(); + break; + + case "?": + showHelp(); + break; + + default: + break; + } + } + } + + document.addEventListener("keypress", handleShortcut); + document.addEventListener("keydown", handleShortcut); + + function addSidebarItems() { + if (!window.SIDEBAR_ITEMS) { + return; + } + const sidebar = document.getElementsByClassName("sidebar-elems")[0]; + + /** + * Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items. + * + * @param {string} shortty - A short type name, like "primitive", "mod", or "macro" + * @param {string} id - The HTML id of the corresponding section on the module page. + * @param {string} longty - A long, capitalized, plural name, like "Primitive Types", + * "Modules", or "Macros". + */ + function block(shortty, id, longty) { + const filtered = window.SIDEBAR_ITEMS[shortty]; + if (!filtered) { + return; + } + + const div = document.createElement("div"); + div.className = "block " + shortty; + const h3 = document.createElement("h3"); + h3.innerHTML = `<a href="index.html#${id}">${longty}</a>`; + div.appendChild(h3); + const ul = document.createElement("ul"); + + for (const item of filtered) { + const name = item[0]; + const desc = item[1]; // can be null + + let klass = shortty; + let path; + if (shortty === "mod") { + path = name + "/index.html"; + } else { + path = shortty + "." + name + ".html"; + } + const current_page = document.location.href.split("/").pop(); + if (path === current_page) { + klass += " current"; + } + const link = document.createElement("a"); + link.href = path; + link.title = desc; + link.className = klass; + link.textContent = name; + const li = document.createElement("li"); + li.appendChild(link); + ul.appendChild(li); + } + div.appendChild(ul); + sidebar.appendChild(div); + } + + if (sidebar) { + block("primitive", "primitives", "Primitive Types"); + block("mod", "modules", "Modules"); + block("macro", "macros", "Macros"); + block("struct", "structs", "Structs"); + block("enum", "enums", "Enums"); + block("union", "unions", "Unions"); + block("constant", "constants", "Constants"); + block("static", "static", "Statics"); + block("trait", "traits", "Traits"); + block("fn", "functions", "Functions"); + block("type", "types", "Type Definitions"); + block("foreigntype", "foreign-types", "Foreign Types"); + block("keyword", "keywords", "Keywords"); + block("traitalias", "trait-aliases", "Trait Aliases"); + } + } + + window.register_implementors = imp => { + const implementors = document.getElementById("implementors-list"); + const synthetic_implementors = document.getElementById("synthetic-implementors-list"); + const inlined_types = new Set(); + + if (synthetic_implementors) { + // This `inlined_types` variable is used to avoid having the same implementation + // showing up twice. For example "String" in the "Sync" doc page. + // + // By the way, this is only used by and useful for traits implemented automatically + // (like "Send" and "Sync"). + onEachLazy(synthetic_implementors.getElementsByClassName("impl"), el => { + const aliases = el.getAttribute("data-aliases"); + if (!aliases) { + return; + } + aliases.split(",").forEach(alias => { + inlined_types.add(alias); + }); + }); + } + + let currentNbImpls = implementors.getElementsByClassName("impl").length; + const traitName = document.querySelector("h1.fqn > .in-band > .trait").textContent; + const baseIdName = "impl-" + traitName + "-"; + const libs = Object.getOwnPropertyNames(imp); + // We don't want to include impls from this JS file, when the HTML already has them. + // The current crate should always be ignored. Other crates that should also be + // ignored are included in the attribute `data-ignore-extern-crates`. + const ignoreExternCrates = document + .querySelector("script[data-ignore-extern-crates]") + .getAttribute("data-ignore-extern-crates"); + for (const lib of libs) { + if (lib === window.currentCrate || ignoreExternCrates.indexOf(lib) !== -1) { + continue; + } + const structs = imp[lib]; + + struct_loop: + for (const struct of structs) { + const list = struct.synthetic ? synthetic_implementors : implementors; + + if (struct.synthetic) { + for (const struct_type of struct.types) { + if (inlined_types.has(struct_type)) { + continue struct_loop; + } + inlined_types.add(struct_type); + } + } + + const code = document.createElement("h3"); + code.innerHTML = struct.text; + addClass(code, "code-header"); + addClass(code, "in-band"); + + onEachLazy(code.getElementsByTagName("a"), elem => { + const href = elem.getAttribute("href"); + + if (href && href.indexOf("http") !== 0) { + elem.setAttribute("href", window.rootPath + href); + } + }); + + const currentId = baseIdName + currentNbImpls; + const anchor = document.createElement("a"); + anchor.href = "#" + currentId; + addClass(anchor, "anchor"); + + const display = document.createElement("div"); + display.id = currentId; + addClass(display, "impl"); + display.appendChild(anchor); + display.appendChild(code); + list.appendChild(display); + currentNbImpls += 1; + } + } + }; + if (window.pending_implementors) { + window.register_implementors(window.pending_implementors); + } + + function addSidebarCrates() { + if (!window.ALL_CRATES) { + return; + } + const sidebarElems = document.getElementsByClassName("sidebar-elems")[0]; + if (!sidebarElems) { + return; + } + // Draw a convenient sidebar of known crates if we have a listing + const div = document.createElement("div"); + div.className = "block crate"; + div.innerHTML = "<h3>Crates</h3>"; + const ul = document.createElement("ul"); + div.appendChild(ul); + + for (const crate of window.ALL_CRATES) { + let klass = "crate"; + if (window.rootPath !== "./" && crate === window.currentCrate) { + klass += " current"; + } + const link = document.createElement("a"); + link.href = window.rootPath + crate + "/index.html"; + link.className = klass; + link.textContent = crate; + + const li = document.createElement("li"); + li.appendChild(link); + ul.appendChild(li); + } + sidebarElems.appendChild(div); + } + + + function labelForToggleButton(sectionIsCollapsed) { + if (sectionIsCollapsed) { + // button will expand the section + return "+"; + } + // button will collapse the section + // note that this text is also set in the HTML template in ../render/mod.rs + return "\u2212"; // "\u2212" is "−" minus sign + } + + function toggleAllDocs() { + const innerToggle = document.getElementById(toggleAllDocsId); + if (!innerToggle) { + return; + } + let sectionIsCollapsed = false; + if (hasClass(innerToggle, "will-expand")) { + removeClass(innerToggle, "will-expand"); + onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => { + if (!hasClass(e, "type-contents-toggle")) { + e.open = true; + } + }); + innerToggle.title = "collapse all docs"; + } else { + addClass(innerToggle, "will-expand"); + onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => { + if (e.parentNode.id !== "implementations-list" || + (!hasClass(e, "implementors-toggle") && + !hasClass(e, "type-contents-toggle")) + ) { + e.open = false; + } + }); + sectionIsCollapsed = true; + innerToggle.title = "expand all docs"; + } + innerToggle.children[0].innerText = labelForToggleButton(sectionIsCollapsed); + } + + (function() { + const toggles = document.getElementById(toggleAllDocsId); + if (toggles) { + toggles.onclick = toggleAllDocs; + } + + const hideMethodDocs = getSettingValue("auto-hide-method-docs") === "true"; + const hideImplementations = getSettingValue("auto-hide-trait-implementations") === "true"; + const hideLargeItemContents = getSettingValue("auto-hide-large-items") !== "false"; + + function setImplementorsTogglesOpen(id, open) { + const list = document.getElementById(id); + if (list !== null) { + onEachLazy(list.getElementsByClassName("implementors-toggle"), e => { + e.open = open; + }); + } + } + + if (hideImplementations) { + setImplementorsTogglesOpen("trait-implementations-list", false); + setImplementorsTogglesOpen("blanket-implementations-list", false); + } + + onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => { + if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) { + e.open = true; + } + if (hideMethodDocs && hasClass(e, "method-toggle")) { + e.open = false; + } + + }); + + const pageId = getPageId(); + if (pageId !== null) { + expandSection(pageId); + } + }()); + + (function() { + // To avoid checking on "rustdoc-line-numbers" value on every loop... + let lineNumbersFunc = () => {}; + if (getSettingValue("line-numbers") === "true") { + lineNumbersFunc = x => { + const count = x.textContent.split("\n").length; + const elems = []; + for (let i = 0; i < count; ++i) { + elems.push(i + 1); + } + const node = document.createElement("pre"); + addClass(node, "line-number"); + node.innerHTML = elems.join("\n"); + x.parentNode.insertBefore(node, x); + }; + } + onEachLazy(document.getElementsByClassName("rust-example-rendered"), e => { + if (hasClass(e, "compile_fail")) { + e.addEventListener("mouseover", function() { + this.parentElement.previousElementSibling.childNodes[0].style.color = "#f00"; + }); + e.addEventListener("mouseout", function() { + this.parentElement.previousElementSibling.childNodes[0].style.color = ""; + }); + } else if (hasClass(e, "ignore")) { + e.addEventListener("mouseover", function() { + this.parentElement.previousElementSibling.childNodes[0].style.color = "#ff9200"; + }); + e.addEventListener("mouseout", function() { + this.parentElement.previousElementSibling.childNodes[0].style.color = ""; + }); + } + lineNumbersFunc(e); + }); + }()); + + function hideSidebar() { + const sidebar = document.getElementsByClassName("sidebar")[0]; + removeClass(sidebar, "shown"); + } + + function handleClick(id, f) { + const elem = document.getElementById(id); + if (elem) { + elem.addEventListener("click", f); + } + } + handleClick(MAIN_ID, () => { + hideSidebar(); + }); + + onEachLazy(document.getElementsByTagName("a"), el => { + // For clicks on internal links (<A> tags with a hash property), we expand the section we're + // jumping to *before* jumping there. We can't do this in onHashChange, because it changes + // the height of the document so we wind up scrolled to the wrong place. + if (el.hash) { + el.addEventListener("click", () => { + expandSection(el.hash.slice(1)); + hideSidebar(); + }); + } + }); + + onEachLazy(document.querySelectorAll(".rustdoc-toggle > summary:not(.hideme)"), el => { + el.addEventListener("click", e => { + if (e.target.tagName !== "SUMMARY" && e.target.tagName !== "A") { + e.preventDefault(); + } + }); + }); + + onEachLazy(document.getElementsByClassName("notable-traits"), e => { + e.onclick = function() { + this.getElementsByClassName("notable-traits-tooltiptext")[0] + .classList.toggle("force-tooltip"); + }; + }); + + const sidebar_menu_toggle = document.getElementsByClassName("sidebar-menu-toggle")[0]; + if (sidebar_menu_toggle) { + sidebar_menu_toggle.addEventListener("click", () => { + const sidebar = document.getElementsByClassName("sidebar")[0]; + if (!hasClass(sidebar, "shown")) { + addClass(sidebar, "shown"); + } else { + removeClass(sidebar, "shown"); + } + }); + } + + function helpBlurHandler(event) { + blurHandler(event, getHelpButton(), window.hidePopoverMenus); + } + + function buildHelpMenu() { + const book_info = document.createElement("span"); + book_info.className = "top"; + book_info.innerHTML = "You can find more information in \ + <a href=\"https://doc.rust-lang.org/rustdoc/\">the rustdoc book</a>."; + + const shortcuts = [ + ["?", "Show this help dialog"], + ["S", "Focus the search field"], + ["↑", "Move up in search results"], + ["↓", "Move down in search results"], + ["← / →", "Switch result tab (when results focused)"], + ["⏎", "Go to active search result"], + ["+", "Expand all sections"], + ["-", "Collapse all sections"], + ].map(x => "<dt>" + + x[0].split(" ") + .map((y, index) => ((index & 1) === 0 ? "<kbd>" + y + "</kbd>" : " " + y + " ")) + .join("") + "</dt><dd>" + x[1] + "</dd>").join(""); + const div_shortcuts = document.createElement("div"); + addClass(div_shortcuts, "shortcuts"); + div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>"; + + const infos = [ + "Prefix searches with a type followed by a colon (e.g., <code>fn:</code>) to \ + restrict the search to a given item kind.", + "Accepted kinds are: <code>fn</code>, <code>mod</code>, <code>struct</code>, \ + <code>enum</code>, <code>trait</code>, <code>type</code>, <code>macro</code>, \ + and <code>const</code>.", + "Search functions by type signature (e.g., <code>vec -> usize</code> or \ + <code>-> vec</code>)", + "Search multiple things at once by splitting your query with comma (e.g., \ + <code>str,u8</code> or <code>String,struct:Vec,test</code>)", + "You can look for items with an exact name by putting double quotes around \ + your request: <code>\"string\"</code>", + "Look for items inside another one by searching for a path: <code>vec::Vec</code>", + ].map(x => "<p>" + x + "</p>").join(""); + const div_infos = document.createElement("div"); + addClass(div_infos, "infos"); + div_infos.innerHTML = "<h2>Search Tricks</h2>" + infos; + + const rustdoc_version = document.createElement("span"); + rustdoc_version.className = "bottom"; + const rustdoc_version_code = document.createElement("code"); + rustdoc_version_code.innerText = "rustdoc " + getVar("rustdoc-version"); + rustdoc_version.appendChild(rustdoc_version_code); + + const container = document.createElement("div"); + container.className = "popover"; + container.style.display = "none"; + + const side_by_side = document.createElement("div"); + side_by_side.className = "side-by-side"; + side_by_side.appendChild(div_shortcuts); + side_by_side.appendChild(div_infos); + + container.appendChild(book_info); + container.appendChild(side_by_side); + container.appendChild(rustdoc_version); + + const help_button = getHelpButton(); + help_button.appendChild(container); + + container.onblur = helpBlurHandler; + container.onclick = event => { + event.preventDefault(); + }; + help_button.onblur = helpBlurHandler; + help_button.children[0].onblur = helpBlurHandler; + + return container; + } + + /** + * Hide all the popover menus. + */ + window.hidePopoverMenus = function() { + onEachLazy(document.querySelectorAll(".search-container .popover"), elem => { + elem.style.display = "none"; + }); + }; + + /** + * Returns the help menu element (not the button). + * + * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be + * built if it doesn't exist. + * + * @return {HTMLElement} + */ + function getHelpMenu(buildNeeded) { + let menu = getHelpButton().querySelector(".popover"); + if (!menu && buildNeeded) { + menu = buildHelpMenu(); + } + return menu; + } + + /** + * Show the help popup menu. + */ + function showHelp() { + const menu = getHelpMenu(true); + if (menu.style.display === "none") { + window.hidePopoverMenus(); + menu.style.display = ""; + } + } + + document.querySelector(`#${HELP_BUTTON_ID} > button`).addEventListener("click", event => { + const target = event.target; + if (target.tagName !== "BUTTON" || target.parentElement.id !== HELP_BUTTON_ID) { + return; + } + const menu = getHelpMenu(true); + const shouldShowHelp = menu.style.display === "none"; + if (shouldShowHelp) { + showHelp(); + } else { + window.hidePopoverMenus(); + } + }); + + setMobileTopbar(); + addSidebarItems(); + addSidebarCrates(); + onHashChange(null); + window.addEventListener("hashchange", onHashChange); + searchState.setup(); +}()); + +(function() { + let reset_button_timeout = null; + + window.copy_path = but => { + const parent = but.parentElement; + const path = []; + + onEach(parent.childNodes, child => { + if (child.tagName === "A") { + path.push(child.textContent); + } + }); + + const el = document.createElement("textarea"); + el.value = path.join("::"); + el.setAttribute("readonly", ""); + // To not make it appear on the screen. + el.style.position = "absolute"; + el.style.left = "-9999px"; + + document.body.appendChild(el); + el.select(); + document.execCommand("copy"); + document.body.removeChild(el); + + // There is always one children, but multiple childNodes. + but.children[0].style.display = "none"; + + let tmp; + if (but.childNodes.length < 2) { + tmp = document.createTextNode("✓"); + but.appendChild(tmp); + } else { + onEachLazy(but.childNodes, e => { + if (e.nodeType === Node.TEXT_NODE) { + tmp = e; + return true; + } + }); + tmp.textContent = "✓"; + } + + if (reset_button_timeout !== null) { + window.clearTimeout(reset_button_timeout); + } + + function reset_button() { + tmp.textContent = ""; + reset_button_timeout = null; + but.children[0].style.display = ""; + } + + reset_button_timeout = window.setTimeout(reset_button, 1000); + }; +}()); diff --git a/src/librustdoc/html/static/js/scrape-examples.js b/src/librustdoc/html/static/js/scrape-examples.js new file mode 100644 index 000000000..fd7a14497 --- /dev/null +++ b/src/librustdoc/html/static/js/scrape-examples.js @@ -0,0 +1,106 @@ +/* global addClass, hasClass, removeClass, onEachLazy */ + +"use strict"; + +(function() { + // Number of lines shown when code viewer is not expanded + const MAX_LINES = 10; + + // Scroll code block to the given code location + function scrollToLoc(elt, loc) { + const lines = elt.querySelector(".line-numbers"); + let scrollOffset; + + // If the block is greater than the size of the viewer, + // then scroll to the top of the block. Otherwise scroll + // to the middle of the block. + if (loc[1] - loc[0] > MAX_LINES) { + const line = Math.max(0, loc[0] - 1); + scrollOffset = lines.children[line].offsetTop; + } else { + const wrapper = elt.querySelector(".code-wrapper"); + const halfHeight = wrapper.offsetHeight / 2; + const offsetMid = (lines.children[loc[0]].offsetTop + + lines.children[loc[1]].offsetTop) / 2; + scrollOffset = offsetMid - halfHeight; + } + + lines.scrollTo(0, scrollOffset); + elt.querySelector(".rust").scrollTo(0, scrollOffset); + } + + function updateScrapedExample(example) { + const locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent); + let locIndex = 0; + const highlights = Array.prototype.slice.call(example.querySelectorAll(".highlight")); + const link = example.querySelector(".scraped-example-title a"); + + if (locs.length > 1) { + // Toggle through list of examples in a given file + const onChangeLoc = changeIndex => { + removeClass(highlights[locIndex], "focus"); + changeIndex(); + scrollToLoc(example, locs[locIndex][0]); + addClass(highlights[locIndex], "focus"); + + const url = locs[locIndex][1]; + const title = locs[locIndex][2]; + + link.href = url; + link.innerHTML = title; + }; + + example.querySelector(".prev") + .addEventListener("click", () => { + onChangeLoc(() => { + locIndex = (locIndex - 1 + locs.length) % locs.length; + }); + }); + + example.querySelector("next") + .addEventListener("click", () => { + onChangeLoc(() => { + locIndex = (locIndex + 1) % locs.length; + }); + }); + } + + const expandButton = example.querySelector(".expand"); + if (expandButton) { + expandButton.addEventListener("click", () => { + if (hasClass(example, "expanded")) { + removeClass(example, "expanded"); + scrollToLoc(example, locs[0][0]); + } else { + addClass(example, "expanded"); + } + }); + } + + // Start with the first example in view + scrollToLoc(example, locs[0][0]); + } + + const firstExamples = document.querySelectorAll(".scraped-example-list > .scraped-example"); + onEachLazy(firstExamples, updateScrapedExample); + onEachLazy(document.querySelectorAll(".more-examples-toggle"), toggle => { + // Allow users to click the left border of the <details> section to close it, + // since the section can be large and finding the [+] button is annoying. + onEachLazy(toggle.querySelectorAll(".toggle-line, .hide-more"), button => { + button.addEventListener("click", () => { + toggle.open = false; + }); + }); + + const moreExamples = toggle.querySelectorAll(".scraped-example"); + toggle.querySelector("summary").addEventListener("click", () => { + // Wrapping in setTimeout ensures the update happens after the elements are actually + // visible. This is necessary since updateScrapedExample calls scrollToLoc which + // depends on offsetHeight, a property that requires an element to be visible to + // compute correctly. + setTimeout(() => { + onEachLazy(moreExamples, updateScrapedExample); + }); + }, {once: true}); + }); +})(); diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js new file mode 100644 index 000000000..75c7bd45a --- /dev/null +++ b/src/librustdoc/html/static/js/search.js @@ -0,0 +1,2297 @@ +/* global addClass, getNakedUrl, getSettingValue */ +/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */ + +"use strict"; + +(function() { +// This mapping table should match the discriminants of +// `rustdoc::formats::item_type::ItemType` type in Rust. +const itemTypes = [ + "mod", + "externcrate", + "import", + "struct", + "enum", + "fn", + "type", + "static", + "trait", + "impl", + "tymethod", + "method", + "structfield", + "variant", + "macro", + "primitive", + "associatedtype", + "constant", + "associatedconstant", + "union", + "foreigntype", + "keyword", + "existential", + "attr", + "derive", + "traitalias", +]; + +// used for special search precedence +const TY_PRIMITIVE = itemTypes.indexOf("primitive"); +const TY_KEYWORD = itemTypes.indexOf("keyword"); +const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../"; + +function hasOwnPropertyRustdoc(obj, property) { + return Object.prototype.hasOwnProperty.call(obj, property); +} + +// In the search display, allows to switch between tabs. +function printTab(nb) { + let iter = 0; + let foundCurrentTab = false; + let foundCurrentResultSet = false; + onEachLazy(document.getElementById("titles").childNodes, elem => { + if (nb === iter) { + addClass(elem, "selected"); + foundCurrentTab = true; + } else { + removeClass(elem, "selected"); + } + iter += 1; + }); + iter = 0; + onEachLazy(document.getElementById("results").childNodes, elem => { + if (nb === iter) { + addClass(elem, "active"); + foundCurrentResultSet = true; + } else { + removeClass(elem, "active"); + } + iter += 1; + }); + if (foundCurrentTab && foundCurrentResultSet) { + searchState.currentTab = nb; + } else if (nb !== 0) { + printTab(0); + } +} + +/** + * A function to compute the Levenshtein distance between two strings + * Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported + * Full License can be found at http://creativecommons.org/licenses/by-sa/3.0/legalcode + * This code is an unmodified version of the code written by Marco de Wit + * and was found at https://stackoverflow.com/a/18514751/745719 + */ +const levenshtein_row2 = []; +function levenshtein(s1, s2) { + if (s1 === s2) { + return 0; + } + const s1_len = s1.length, s2_len = s2.length; + if (s1_len && s2_len) { + let i1 = 0, i2 = 0, a, b, c, c2; + const row = levenshtein_row2; + while (i1 < s1_len) { + row[i1] = ++i1; + } + while (i2 < s2_len) { + c2 = s2.charCodeAt(i2); + a = i2; + ++i2; + b = i2; + for (i1 = 0; i1 < s1_len; ++i1) { + c = a + (s1.charCodeAt(i1) !== c2 ? 1 : 0); + a = row[i1]; + b = b < a ? (b < c ? b + 1 : c) : (a < c ? a + 1 : c); + row[i1] = b; + } + } + return b; + } + return s1_len + s2_len; +} + +function initSearch(rawSearchIndex) { + const MAX_LEV_DISTANCE = 3; + const MAX_RESULTS = 200; + const NO_TYPE_FILTER = -1; + /** + * @type {Array<Row>} + */ + let searchIndex; + let currentResults; + const ALIASES = Object.create(null); + + function isWhitespace(c) { + return " \t\n\r".indexOf(c) !== -1; + } + + function isSpecialStartCharacter(c) { + return "<\"".indexOf(c) !== -1; + } + + function isEndCharacter(c) { + return ",>-".indexOf(c) !== -1; + } + + function isStopCharacter(c) { + return isWhitespace(c) || isEndCharacter(c); + } + + function isErrorCharacter(c) { + return "()".indexOf(c) !== -1; + } + + function itemTypeFromName(typename) { + for (let i = 0, len = itemTypes.length; i < len; ++i) { + if (itemTypes[i] === typename) { + return i; + } + } + + throw new Error("Unknown type filter `" + typename + "`"); + } + + /** + * If we encounter a `"`, then we try to extract the string from it until we find another `"`. + * + * This function will throw an error in the following cases: + * * There is already another string element. + * * We are parsing a generic argument. + * * There is more than one element. + * * There is no closing `"`. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {boolean} isInGenerics + */ + function getStringElem(query, parserState, isInGenerics) { + if (isInGenerics) { + throw new Error("`\"` cannot be used in generics"); + } else if (query.literalSearch) { + throw new Error("Cannot have more than one literal search element"); + } else if (parserState.totalElems - parserState.genericsElems > 0) { + throw new Error("Cannot use literal search when there is more than one element"); + } + parserState.pos += 1; + const start = parserState.pos; + const end = getIdentEndPosition(parserState); + if (parserState.pos >= parserState.length) { + throw new Error("Unclosed `\"`"); + } else if (parserState.userQuery[end] !== "\"") { + throw new Error(`Unexpected \`${parserState.userQuery[end]}\` in a string element`); + } else if (start === end) { + throw new Error("Cannot have empty string element"); + } + // To skip the quote at the end. + parserState.pos += 1; + query.literalSearch = true; + } + + /** + * Returns `true` if the current parser position is starting with "::". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isPathStart(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "::"; + } + + /** + * Returns `true` if the current parser position is starting with "->". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isReturnArrow(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "->"; + } + + /** + * Returns `true` if the given `c` character is valid for an ident. + * + * @param {string} c + * + * @return {boolean} + */ + function isIdentCharacter(c) { + return ( + c === "_" || + (c >= "0" && c <= "9") || + (c >= "a" && c <= "z") || + (c >= "A" && c <= "Z")); + } + + /** + * Returns `true` if the given `c` character is a separator. + * + * @param {string} c + * + * @return {boolean} + */ + function isSeparatorCharacter(c) { + return c === "," || isWhitespaceCharacter(c); + } + + /** + * Returns `true` if the given `c` character is a whitespace. + * + * @param {string} c + * + * @return {boolean} + */ + function isWhitespaceCharacter(c) { + return c === " " || c === "\t"; + } + + /** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {string} name - Name of the query element. + * @param {Array<QueryElement>} generics - List of generics of this query element. + * + * @return {QueryElement} - The newly created `QueryElement`. + */ + function createQueryElement(query, parserState, name, generics, isInGenerics) { + if (name === "*" || (name.length === 0 && generics.length === 0)) { + return; + } + if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { + throw new Error("You cannot have more than one element if you use quotes"); + } + const pathSegments = name.split("::"); + if (pathSegments.length > 1) { + for (let i = 0, len = pathSegments.length; i < len; ++i) { + const pathSegment = pathSegments[i]; + + if (pathSegment.length === 0) { + if (i === 0) { + throw new Error("Paths cannot start with `::`"); + } else if (i + 1 === len) { + throw new Error("Paths cannot end with `::`"); + } + throw new Error("Unexpected `::::`"); + } + } + } + // In case we only have something like `<p>`, there is no name. + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) { + throw new Error("Found generics without a path"); + } + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; + } + return { + name: name, + fullPath: pathSegments, + pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), + pathLast: pathSegments[pathSegments.length - 1], + generics: generics, + }; + } + + /** + * This function goes through all characters until it reaches an invalid ident character or the + * end of the query. It returns the position of the last character of the ident. + * + * @param {ParserState} parserState + * + * @return {integer} + */ + function getIdentEndPosition(parserState) { + let end = parserState.pos; + let foundExclamation = false; + while (parserState.pos < parserState.length) { + const c = parserState.userQuery[parserState.pos]; + if (!isIdentCharacter(c)) { + if (c === "!") { + if (foundExclamation) { + throw new Error("Cannot have more than one `!` in an ident"); + } else if (parserState.pos + 1 < parserState.length && + isIdentCharacter(parserState.userQuery[parserState.pos + 1]) + ) { + throw new Error("`!` can only be at the end of an ident"); + } + foundExclamation = true; + } else if (isErrorCharacter(c)) { + throw new Error(`Unexpected \`${c}\``); + } else if ( + isStopCharacter(c) || + isSpecialStartCharacter(c) || + isSeparatorCharacter(c) + ) { + break; + } else if (c === ":") { // If we allow paths ("str::string" for example). + if (!isPathStart(parserState)) { + break; + } + // Skip current ":". + parserState.pos += 1; + foundExclamation = false; + } else { + throw new Error(`Unexpected \`${c}\``); + } + } + parserState.pos += 1; + end = parserState.pos; + } + return end; + } + + /** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added. + * @param {boolean} isInGenerics + */ + function getNextElem(query, parserState, elems, isInGenerics) { + const generics = []; + + let start = parserState.pos; + let end; + // We handle the strings on their own mostly to make code easier to follow. + if (parserState.userQuery[parserState.pos] === "\"") { + start += 1; + getStringElem(query, parserState, isInGenerics); + end = parserState.pos - 1; + } else { + end = getIdentEndPosition(parserState); + } + if (parserState.pos < parserState.length && + parserState.userQuery[parserState.pos] === "<" + ) { + if (isInGenerics) { + throw new Error("Unexpected `<` after `<`"); + } else if (start >= end) { + throw new Error("Found generics without a path"); + } + parserState.pos += 1; + getItemsBefore(query, parserState, generics, ">"); + } + if (start >= end && generics.length === 0) { + return; + } + elems.push( + createQueryElement( + query, + parserState, + parserState.userQuery.slice(start, end), + generics, + isInGenerics + ) + ); + } + + /** + * This function parses the next query element until it finds `endChar`, calling `getNextElem` + * to collect each element. + * + * If there is no `endChar`, this function will implicitly stop at the end without raising an + * error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added. + * @param {string} endChar - This function will stop when it'll encounter this + * character. + */ + function getItemsBefore(query, parserState, elems, endChar) { + let foundStopChar = true; + + while (parserState.pos < parserState.length) { + const c = parserState.userQuery[parserState.pos]; + if (c === endChar) { + break; + } else if (isSeparatorCharacter(c)) { + parserState.pos += 1; + foundStopChar = true; + continue; + } else if (c === ":" && isPathStart(parserState)) { + throw new Error("Unexpected `::`: paths cannot start with `::`"); + } else if (c === ":" || isEndCharacter(c)) { + let extra = ""; + if (endChar === ">") { + extra = "`<`"; + } else if (endChar === "") { + extra = "`->`"; + } + throw new Error("Unexpected `" + c + "` after " + extra); + } + if (!foundStopChar) { + if (endChar !== "") { + throw new Error(`Expected \`,\`, \` \` or \`${endChar}\`, found \`${c}\``); + } + throw new Error(`Expected \`,\` or \` \`, found \`${c}\``); + } + const posBefore = parserState.pos; + getNextElem(query, parserState, elems, endChar === ">"); + // This case can be encountered if `getNextElem` encounted a "stop character" right from + // the start. For example if you have `,,` or `<>`. In this case, we simply move up the + // current position to continue the parsing. + if (posBefore === parserState.pos) { + parserState.pos += 1; + } + foundStopChar = false; + } + // We are either at the end of the string or on the `endChar`` character, let's move forward + // in any case. + parserState.pos += 1; + } + + /** + * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored + * if empty). + * + * @param {ParserState} parserState + */ + function checkExtraTypeFilterCharacters(parserState) { + const query = parserState.userQuery; + + for (let pos = 0; pos < parserState.pos; ++pos) { + if (!isIdentCharacter(query[pos]) && !isWhitespaceCharacter(query[pos])) { + throw new Error(`Unexpected \`${query[pos]}\` in type filter`); + } + } + } + + /** + * Parses the provided `query` input to fill `parserState`. If it encounters an error while + * parsing `query`, it'll throw an error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + */ + function parseInput(query, parserState) { + let c, before; + let foundStopChar = true; + + while (parserState.pos < parserState.length) { + c = parserState.userQuery[parserState.pos]; + if (isStopCharacter(c)) { + foundStopChar = true; + if (isSeparatorCharacter(c)) { + parserState.pos += 1; + continue; + } else if (c === "-" || c === ">") { + if (isReturnArrow(parserState)) { + break; + } + throw new Error(`Unexpected \`${c}\` (did you mean \`->\`?)`); + } + throw new Error(`Unexpected \`${c}\``); + } else if (c === ":" && !isPathStart(parserState)) { + if (parserState.typeFilter !== null) { + throw new Error("Unexpected `:`"); + } + if (query.elems.length === 0) { + throw new Error("Expected type filter before `:`"); + } else if (query.elems.length !== 1 || parserState.totalElems !== 1) { + throw new Error("Unexpected `:`"); + } else if (query.literalSearch) { + throw new Error("You cannot use quotes on type filter"); + } + checkExtraTypeFilterCharacters(parserState); + // The type filter doesn't count as an element since it's a modifier. + parserState.typeFilter = query.elems.pop().name; + parserState.pos += 1; + parserState.totalElems = 0; + query.literalSearch = false; + foundStopChar = true; + continue; + } + if (!foundStopChar) { + if (parserState.typeFilter !== null) { + throw new Error(`Expected \`,\`, \` \` or \`->\`, found \`${c}\``); + } + throw new Error(`Expected \`,\`, \` \`, \`:\` or \`->\`, found \`${c}\``); + } + before = query.elems.length; + getNextElem(query, parserState, query.elems, false); + if (query.elems.length === before) { + // Nothing was added, weird... Let's increase the position to not remain stuck. + parserState.pos += 1; + } + foundStopChar = false; + } + while (parserState.pos < parserState.length) { + c = parserState.userQuery[parserState.pos]; + if (isReturnArrow(parserState)) { + parserState.pos += 2; + // Get returned elements. + getItemsBefore(query, parserState, query.returned, ""); + // Nothing can come afterward! + if (query.returned.length === 0) { + throw new Error("Expected at least one item after `->`"); + } + break; + } else { + parserState.pos += 1; + } + } + } + + /** + * Takes the user search input and returns an empty `ParsedQuery`. + * + * @param {string} userQuery + * + * @return {ParsedQuery} + */ + function newParsedQuery(userQuery) { + return { + original: userQuery, + userQuery: userQuery.toLowerCase(), + typeFilter: NO_TYPE_FILTER, + elems: [], + returned: [], + // Total number of "top" elements (does not include generics). + foundElems: 0, + literalSearch: false, + error: null, + }; + } + + /** + * Build an URL with search parameters. + * + * @param {string} search - The current search being performed. + * @param {string|null} filterCrates - The current filtering crate (if any). + * + * @return {string} + */ + function buildUrl(search, filterCrates) { + let extra = "?search=" + encodeURIComponent(search); + + if (filterCrates !== null) { + extra += "&filter-crate=" + encodeURIComponent(filterCrates); + } + return getNakedUrl() + extra + window.location.hash; + } + + /** + * Return the filtering crate or `null` if there is none. + * + * @return {string|null} + */ + function getFilterCrates() { + const elem = document.getElementById("crate-search"); + + if (elem && + elem.value !== "All crates" && + hasOwnPropertyRustdoc(rawSearchIndex, elem.value) + ) { + return elem.value; + } + return null; + } + + /** + * Parses the query. + * + * The supported syntax by this parser is as follow: + * + * ident = *(ALPHA / DIGIT / "_") [!] + * path = ident *(DOUBLE-COLON ident) + * arg = path [generics] + * arg-without-generic = path + * type-sep = COMMA/WS *(COMMA/WS) + * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) + * nonempty-arg-list-without-generics = *(type-sep) arg-without-generic + * *(type-sep arg-without-generic) *(type-sep) + * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list-without-generics ] *(type-sep) + * CLOSE-ANGLE-BRACKET/EOF + * return-args = RETURN-ARROW *(type-sep) nonempty-arg-list + * + * exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ] + * type-search = [type-filter *WS COLON] [ nonempty-arg-list ] [ return-args ] + * + * query = *WS (exact-search / type-search) *WS + * + * type-filter = ( + * "mod" / + * "externcrate" / + * "import" / + * "struct" / + * "enum" / + * "fn" / + * "type" / + * "static" / + * "trait" / + * "impl" / + * "tymethod" / + * "method" / + * "structfield" / + * "variant" / + * "macro" / + * "primitive" / + * "associatedtype" / + * "constant" / + * "associatedconstant" / + * "union" / + * "foreigntype" / + * "keyword" / + * "existential" / + * "attr" / + * "derive" / + * "traitalias") + * + * OPEN-ANGLE-BRACKET = "<" + * CLOSE-ANGLE-BRACKET = ">" + * COLON = ":" + * DOUBLE-COLON = "::" + * QUOTE = %x22 + * COMMA = "," + * RETURN-ARROW = "->" + * + * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + * DIGIT = %x30-39 + * WS = %x09 / " " + * + * @param {string} val - The user query + * + * @return {ParsedQuery} - The parsed query + */ + function parseQuery(userQuery) { + userQuery = userQuery.trim(); + const parserState = { + length: userQuery.length, + pos: 0, + // Total number of elements (includes generics). + totalElems: 0, + genericsElems: 0, + typeFilter: null, + userQuery: userQuery.toLowerCase(), + }; + let query = newParsedQuery(userQuery); + + try { + parseInput(query, parserState); + if (parserState.typeFilter !== null) { + let typeFilter = parserState.typeFilter; + if (typeFilter === "const") { + typeFilter = "constant"; + } + query.typeFilter = itemTypeFromName(typeFilter); + } + } catch (err) { + query = newParsedQuery(userQuery); + query.error = err.message; + query.typeFilter = -1; + return query; + } + + if (!query.literalSearch) { + // If there is more than one element in the query, we switch to literalSearch in any + // case. + query.literalSearch = parserState.totalElems > 1; + } + query.foundElems = query.elems.length + query.returned.length; + return query; + } + + /** + * Creates the query results. + * + * @param {Array<Result>} results_in_args + * @param {Array<Result>} results_returned + * @param {Array<Result>} results_in_args + * @param {ParsedQuery} parsedQuery + * + * @return {ResultsTable} + */ + function createQueryResults(results_in_args, results_returned, results_others, parsedQuery) { + return { + "in_args": results_in_args, + "returned": results_returned, + "others": results_others, + "query": parsedQuery, + }; + } + + /** + * Executes the parsed query and builds a {ResultsTable}. + * + * @param {ParsedQuery} parsedQuery - The parsed user query + * @param {Object} searchWords - The list of search words to query against + * @param {Object} [filterCrates] - Crate to search in if defined + * @param {Object} [currentCrate] - Current crate, to rank results from this crate higher + * + * @return {ResultsTable} + */ + function execQuery(parsedQuery, searchWords, filterCrates, currentCrate) { + const results_others = {}, results_in_args = {}, results_returned = {}; + + function transformResults(results) { + const duplicates = {}; + const out = []; + + for (const result of results) { + if (result.id > -1) { + const obj = searchIndex[result.id]; + obj.lev = result.lev; + const res = buildHrefAndPath(obj); + obj.displayPath = pathSplitter(res[0]); + obj.fullPath = obj.displayPath + obj.name; + // To be sure than it some items aren't considered as duplicate. + obj.fullPath += "|" + obj.ty; + + if (duplicates[obj.fullPath]) { + continue; + } + duplicates[obj.fullPath] = true; + + obj.href = res[1]; + out.push(obj); + if (out.length >= MAX_RESULTS) { + break; + } + } + } + return out; + } + + function sortResults(results, isType, preferredCrate) { + const userQuery = parsedQuery.userQuery; + const ar = []; + for (const entry in results) { + if (hasOwnPropertyRustdoc(results, entry)) { + const result = results[entry]; + result.word = searchWords[result.id]; + result.item = searchIndex[result.id] || {}; + ar.push(result); + } + } + results = ar; + // if there are no results then return to default and fail + if (results.length === 0) { + return []; + } + + results.sort((aaa, bbb) => { + let a, b; + + // sort by exact match with regard to the last word (mismatch goes later) + a = (aaa.word !== userQuery); + b = (bbb.word !== userQuery); + if (a !== b) { + return a - b; + } + + // Sort by non levenshtein results and then levenshtein results by the distance + // (less changes required to match means higher rankings) + a = (aaa.lev); + b = (bbb.lev); + if (a !== b) { + return a - b; + } + + // sort by crate (current crate comes first) + a = (aaa.item.crate !== preferredCrate); + b = (bbb.item.crate !== preferredCrate); + if (a !== b) { + return a - b; + } + + // sort by item name length (longer goes later) + a = aaa.word.length; + b = bbb.word.length; + if (a !== b) { + return a - b; + } + + // sort by item name (lexicographically larger goes later) + a = aaa.word; + b = bbb.word; + if (a !== b) { + return (a > b ? +1 : -1); + } + + // sort by index of keyword in item name (no literal occurrence goes later) + a = (aaa.index < 0); + b = (bbb.index < 0); + if (a !== b) { + return a - b; + } + // (later literal occurrence, if any, goes later) + a = aaa.index; + b = bbb.index; + if (a !== b) { + return a - b; + } + + // special precedence for primitive and keyword pages + if ((aaa.item.ty === TY_PRIMITIVE && bbb.item.ty !== TY_KEYWORD) || + (aaa.item.ty === TY_KEYWORD && bbb.item.ty !== TY_PRIMITIVE)) { + return -1; + } + if ((bbb.item.ty === TY_PRIMITIVE && aaa.item.ty !== TY_PRIMITIVE) || + (bbb.item.ty === TY_KEYWORD && aaa.item.ty !== TY_KEYWORD)) { + return 1; + } + + // sort by description (no description goes later) + a = (aaa.item.desc === ""); + b = (bbb.item.desc === ""); + if (a !== b) { + return a - b; + } + + // sort by type (later occurrence in `itemTypes` goes later) + a = aaa.item.ty; + b = bbb.item.ty; + if (a !== b) { + return a - b; + } + + // sort by path (lexicographically larger goes later) + a = aaa.item.path; + b = bbb.item.path; + if (a !== b) { + return (a > b ? +1 : -1); + } + + // que sera, sera + return 0; + }); + + let nameSplit = null; + if (parsedQuery.elems.length === 1) { + const hasPath = typeof parsedQuery.elems[0].path === "undefined"; + nameSplit = hasPath ? null : parsedQuery.elems[0].path; + } + + for (const result of results) { + // this validation does not make sense when searching by types + if (result.dontValidate) { + continue; + } + const name = result.item.name.toLowerCase(), + path = result.item.path.toLowerCase(), + parent = result.item.parent; + + if (!isType && !validateResult(name, path, nameSplit, parent)) { + result.id = -1; + } + } + return transformResults(results); + } + + /** + * This function checks if the object (`row`) generics match the given type (`elem`) + * generics. If there are no generics on `row`, `defaultLev` is returned. + * + * @param {Row} row - The object to check. + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} defaultLev - This is the value to return in case there are no generics. + * + * @return {integer} - Returns the best match (if any) or `MAX_LEV_DISTANCE + 1`. + */ + function checkGenerics(row, elem, defaultLev) { + if (row.generics.length === 0) { + return elem.generics.length === 0 ? defaultLev : MAX_LEV_DISTANCE + 1; + } else if (row.generics.length > 0 && row.generics[0].name === null) { + return checkGenerics(row.generics[0], elem, defaultLev); + } + // The names match, but we need to be sure that all generics kinda + // match as well. + let elem_name; + if (elem.generics.length > 0 && row.generics.length >= elem.generics.length) { + const elems = Object.create(null); + for (const entry of row.generics) { + elem_name = entry.name; + if (elem_name === "") { + // Pure generic, needs to check into it. + if (checkGenerics(entry, elem, MAX_LEV_DISTANCE + 1) !== 0) { + return MAX_LEV_DISTANCE + 1; + } + continue; + } + if (elems[elem_name] === undefined) { + elems[elem_name] = 0; + } + elems[elem_name] += 1; + } + // We need to find the type that matches the most to remove it in order + // to move forward. + for (const generic of elem.generics) { + let match = null; + if (elems[generic.name]) { + match = generic.name; + } else { + for (elem_name in elems) { + if (!hasOwnPropertyRustdoc(elems, elem_name)) { + continue; + } + if (elem_name === generic) { + match = elem_name; + break; + } + } + } + if (match === null) { + return MAX_LEV_DISTANCE + 1; + } + elems[match] -= 1; + if (elems[match] === 0) { + delete elems[match]; + } + } + return 0; + } + return MAX_LEV_DISTANCE + 1; + } + + /** + * This function checks if the object (`row`) matches the given type (`elem`) and its + * generics (if any). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * + * @return {integer} - Returns a Levenshtein distance to the best match. + */ + function checkIfInGenerics(row, elem) { + let lev = MAX_LEV_DISTANCE + 1; + for (const entry of row.generics) { + lev = Math.min(checkType(entry, elem, true), lev); + if (lev === 0) { + break; + } + } + return lev; + } + + /** + * This function checks if the object (`row`) matches the given type (`elem`) and its + * generics (if any). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * @param {boolean} literalSearch + * + * @return {integer} - Returns a Levenshtein distance to the best match. If there is + * no match, returns `MAX_LEV_DISTANCE + 1`. + */ + function checkType(row, elem, literalSearch) { + if (row.name === null) { + // This is a pure "generic" search, no need to run other checks. + if (row.generics.length > 0) { + return checkIfInGenerics(row, elem); + } + return MAX_LEV_DISTANCE + 1; + } + + let lev = levenshtein(row.name, elem.name); + if (literalSearch) { + if (lev !== 0) { + // The name didn't match, let's try to check if the generics do. + if (elem.generics.length === 0) { + const checkGeneric = row.generics.length > 0; + if (checkGeneric && row.generics + .findIndex(tmp_elem => tmp_elem.name === elem.name) !== -1) { + return 0; + } + } + return MAX_LEV_DISTANCE + 1; + } else if (elem.generics.length > 0) { + return checkGenerics(row, elem, MAX_LEV_DISTANCE + 1); + } + return 0; + } else if (row.generics.length > 0) { + if (elem.generics.length === 0) { + if (lev === 0) { + return 0; + } + // The name didn't match so we now check if the type we're looking for is inside + // the generics! + lev = checkIfInGenerics(row, elem); + // Now whatever happens, the returned distance is "less good" so we should mark + // it as such, and so we add 0.5 to the distance to make it "less good". + return lev + 0.5; + } else if (lev > MAX_LEV_DISTANCE) { + // So our item's name doesn't match at all and has generics. + // + // Maybe it's present in a sub generic? For example "f<A<B<C>>>()", if we're + // looking for "B<C>", we'll need to go down. + return checkIfInGenerics(row, elem); + } else { + // At this point, the name kinda match and we have generics to check, so + // let's go! + const tmp_lev = checkGenerics(row, elem, lev); + if (tmp_lev > MAX_LEV_DISTANCE) { + return MAX_LEV_DISTANCE + 1; + } + // We compute the median value of both checks and return it. + return (tmp_lev + lev) / 2; + } + } else if (elem.generics.length > 0) { + // In this case, we were expecting generics but there isn't so we simply reject this + // one. + return MAX_LEV_DISTANCE + 1; + } + // No generics on our query or on the target type so we can return without doing + // anything else. + return lev; + } + + /** + * This function checks if the object (`row`) has an argument with the given type (`elem`). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} typeFilter + * + * @return {integer} - Returns a Levenshtein distance to the best match. If there is no + * match, returns `MAX_LEV_DISTANCE + 1`. + */ + function findArg(row, elem, typeFilter) { + let lev = MAX_LEV_DISTANCE + 1; + + if (row && row.type && row.type.inputs && row.type.inputs.length > 0) { + for (const input of row.type.inputs) { + if (!typePassesFilter(typeFilter, input.ty)) { + continue; + } + lev = Math.min(lev, checkType(input, elem, parsedQuery.literalSearch)); + if (lev === 0) { + return 0; + } + } + } + return parsedQuery.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; + } + + /** + * This function checks if the object (`row`) returns the given type (`elem`). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} typeFilter + * + * @return {integer} - Returns a Levenshtein distance to the best match. If there is no + * match, returns `MAX_LEV_DISTANCE + 1`. + */ + function checkReturned(row, elem, typeFilter) { + let lev = MAX_LEV_DISTANCE + 1; + + if (row && row.type && row.type.output.length > 0) { + const ret = row.type.output; + for (const ret_ty of ret) { + if (!typePassesFilter(typeFilter, ret_ty.ty)) { + continue; + } + lev = Math.min(lev, checkType(ret_ty, elem, parsedQuery.literalSearch)); + if (lev === 0) { + return 0; + } + } + } + return parsedQuery.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; + } + + function checkPath(contains, ty) { + if (contains.length === 0) { + return 0; + } + let ret_lev = MAX_LEV_DISTANCE + 1; + const path = ty.path.split("::"); + + if (ty.parent && ty.parent.name) { + path.push(ty.parent.name.toLowerCase()); + } + + const length = path.length; + const clength = contains.length; + if (clength > length) { + return MAX_LEV_DISTANCE + 1; + } + for (let i = 0; i < length; ++i) { + if (i + clength > length) { + break; + } + let lev_total = 0; + let aborted = false; + for (let x = 0; x < clength; ++x) { + const lev = levenshtein(path[i + x], contains[x]); + if (lev > MAX_LEV_DISTANCE) { + aborted = true; + break; + } + lev_total += lev; + } + if (!aborted) { + ret_lev = Math.min(ret_lev, Math.round(lev_total / clength)); + } + } + return ret_lev; + } + + function typePassesFilter(filter, type) { + // No filter or Exact mach + if (filter <= NO_TYPE_FILTER || filter === type) return true; + + // Match related items + const name = itemTypes[type]; + switch (itemTypes[filter]) { + case "constant": + return name === "associatedconstant"; + case "fn": + return name === "method" || name === "tymethod"; + case "type": + return name === "primitive" || name === "associatedtype"; + case "trait": + return name === "traitalias"; + } + + // No match + return false; + } + + function createAliasFromItem(item) { + return { + crate: item.crate, + name: item.name, + path: item.path, + desc: item.desc, + ty: item.ty, + parent: item.parent, + type: item.type, + is_alias: true, + }; + } + + function handleAliases(ret, query, filterCrates, currentCrate) { + const lowerQuery = query.toLowerCase(); + // We separate aliases and crate aliases because we want to have current crate + // aliases to be before the others in the displayed results. + const aliases = []; + const crateAliases = []; + if (filterCrates !== null) { + if (ALIASES[filterCrates] && ALIASES[filterCrates][lowerQuery]) { + const query_aliases = ALIASES[filterCrates][lowerQuery]; + for (const alias of query_aliases) { + aliases.push(createAliasFromItem(searchIndex[alias])); + } + } + } else { + Object.keys(ALIASES).forEach(crate => { + if (ALIASES[crate][lowerQuery]) { + const pushTo = crate === currentCrate ? crateAliases : aliases; + const query_aliases = ALIASES[crate][lowerQuery]; + for (const alias of query_aliases) { + pushTo.push(createAliasFromItem(searchIndex[alias])); + } + } + }); + } + + const sortFunc = (aaa, bbb) => { + if (aaa.path < bbb.path) { + return 1; + } else if (aaa.path === bbb.path) { + return 0; + } + return -1; + }; + crateAliases.sort(sortFunc); + aliases.sort(sortFunc); + + const pushFunc = alias => { + alias.alias = query; + const res = buildHrefAndPath(alias); + alias.displayPath = pathSplitter(res[0]); + alias.fullPath = alias.displayPath + alias.name; + alias.href = res[1]; + + ret.others.unshift(alias); + if (ret.others.length > MAX_RESULTS) { + ret.others.pop(); + } + }; + + aliases.forEach(pushFunc); + crateAliases.forEach(pushFunc); + } + + /** + * This function adds the given result into the provided `results` map if it matches the + * following condition: + * + * * If it is a "literal search" (`parsedQuery.literalSearch`), then `lev` must be 0. + * * If it is not a "literal search", `lev` must be <= `MAX_LEV_DISTANCE`. + * + * The `results` map contains information which will be used to sort the search results: + * + * * `fullId` is a `string`` used as the key of the object we use for the `results` map. + * * `id` is the index in both `searchWords` and `searchIndex` arrays for this element. + * * `index` is an `integer`` used to sort by the position of the word in the item's name. + * * `lev` is the main metric used to sort the search results. + * + * @param {Results} results + * @param {string} fullId + * @param {integer} id + * @param {integer} index + * @param {integer} lev + */ + function addIntoResults(results, fullId, id, index, lev) { + if (lev === 0 || (!parsedQuery.literalSearch && lev <= MAX_LEV_DISTANCE)) { + if (results[fullId] !== undefined) { + const result = results[fullId]; + if (result.dontValidate || result.lev <= lev) { + return; + } + } + results[fullId] = { + id: id, + index: index, + dontValidate: parsedQuery.literalSearch, + lev: lev, + }; + } + } + + /** + * This function is called in case the query is only one element (with or without generics). + * This element will be compared to arguments' and returned values' items and also to items. + * + * Other important thing to note: since there is only one element, we use levenshtein + * distance for name comparisons. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {QueryElement} elem - The element from the parsed query. + * @param {Results} results_others - Unqualified results (not in arguments nor in + * returned values). + * @param {Results} results_in_args - Matching arguments results. + * @param {Results} results_returned - Matching returned arguments results. + */ + function handleSingleArg( + row, + pos, + elem, + results_others, + results_in_args, + results_returned + ) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; + } + let lev, lev_add = 0, index = -1; + const fullId = row.id; + + const in_args = findArg(row, elem, parsedQuery.typeFilter); + const returned = checkReturned(row, elem, parsedQuery.typeFilter); + + addIntoResults(results_in_args, fullId, pos, index, in_args); + addIntoResults(results_returned, fullId, pos, index, returned); + + if (!typePassesFilter(parsedQuery.typeFilter, row.ty)) { + return; + } + const searchWord = searchWords[pos]; + + if (parsedQuery.literalSearch) { + if (searchWord === elem.name) { + addIntoResults(results_others, fullId, pos, -1, 0); + } + return; + } + + // No need to check anything else if it's a "pure" generics search. + if (elem.name.length === 0) { + if (row.type !== null) { + lev = checkGenerics(row.type, elem, MAX_LEV_DISTANCE + 1); + addIntoResults(results_others, fullId, pos, index, lev); + } + return; + } + + if (elem.fullPath.length > 1) { + lev = checkPath(elem.pathWithoutLast, row); + if (lev > MAX_LEV_DISTANCE || (parsedQuery.literalSearch && lev !== 0)) { + return; + } else if (lev > 0) { + lev_add = lev / 10; + } + } + + if (searchWord.indexOf(elem.pathLast) > -1 || + row.normalizedName.indexOf(elem.pathLast) > -1 + ) { + index = row.normalizedName.indexOf(elem.pathLast); + } + lev = levenshtein(searchWord, elem.pathLast); + if (lev > 0 && elem.pathLast.length > 2 && searchWord.indexOf(elem.pathLast) > -1) { + if (elem.pathLast.length < 6) { + lev = 1; + } else { + lev = 0; + } + } + lev += lev_add; + if (lev > MAX_LEV_DISTANCE) { + return; + } else if (index !== -1 && elem.fullPath.length < 2) { + lev -= 1; + } + if (lev < 0) { + lev = 0; + } + addIntoResults(results_others, fullId, pos, index, lev); + } + + /** + * This function is called in case the query has more than one element. In this case, it'll + * try to match the items which validates all the elements. For `aa -> bb` will look for + * functions which have a parameter `aa` and has `bb` in its returned values. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {Object} results + */ + function handleArgs(row, pos, results) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; + } + + let totalLev = 0; + let nbLev = 0; + + // If the result is too "bad", we return false and it ends this search. + function checkArgs(elems, callback) { + for (const elem of elems) { + // There is more than one parameter to the query so all checks should be "exact" + const lev = callback(row, elem, NO_TYPE_FILTER); + if (lev <= 1) { + nbLev += 1; + totalLev += lev; + } else { + return false; + } + } + return true; + } + if (!checkArgs(parsedQuery.elems, findArg)) { + return; + } + if (!checkArgs(parsedQuery.returned, checkReturned)) { + return; + } + + if (nbLev === 0) { + return; + } + const lev = Math.round(totalLev / nbLev); + addIntoResults(results, row.id, pos, 0, lev); + } + + function innerRunQuery() { + let elem, i, nSearchWords, in_returned, row; + + if (parsedQuery.foundElems === 1) { + if (parsedQuery.elems.length === 1) { + elem = parsedQuery.elems[0]; + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + // It means we want to check for this element everywhere (in names, args and + // returned). + handleSingleArg( + searchIndex[i], + i, + elem, + results_others, + results_in_args, + results_returned + ); + } + } else if (parsedQuery.returned.length === 1) { + // We received one returned argument to check, so looking into returned values. + elem = parsedQuery.returned[0]; + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + row = searchIndex[i]; + in_returned = checkReturned(row, elem, parsedQuery.typeFilter); + addIntoResults(results_others, row.id, i, -1, in_returned); + } + } + } else if (parsedQuery.foundElems > 0) { + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + handleArgs(searchIndex[i], i, results_others); + } + } + } + + if (parsedQuery.error === null) { + innerRunQuery(); + } + + const ret = createQueryResults( + sortResults(results_in_args, true, currentCrate), + sortResults(results_returned, true, currentCrate), + sortResults(results_others, false, currentCrate), + parsedQuery); + handleAliases(ret, parsedQuery.original.replace(/"/g, ""), filterCrates, currentCrate); + if (parsedQuery.error !== null && ret.others.length !== 0) { + // It means some doc aliases were found so let's "remove" the error! + ret.query.error = null; + } + return ret; + } + + /** + * Validate performs the following boolean logic. For example: + * "File::open" will give IF A PARENT EXISTS => ("file" && "open") + * exists in (name || path || parent) OR => ("file" && "open") exists in + * (name || path ) + * + * This could be written functionally, but I wanted to minimise + * functions on stack. + * + * @param {string} name - The name of the result + * @param {string} path - The path of the result + * @param {string} keys - The keys to be used (["file", "open"]) + * @param {Object} parent - The parent of the result + * + * @return {boolean} - Whether the result is valid or not + */ + function validateResult(name, path, keys, parent) { + if (!keys || !keys.length) { + return true; + } + for (const key of keys) { + // each check is for validation so we negate the conditions and invalidate + if (!( + // check for an exact name match + name.indexOf(key) > -1 || + // then an exact path match + path.indexOf(key) > -1 || + // next if there is a parent, check for exact parent match + (parent !== undefined && parent.name !== undefined && + parent.name.toLowerCase().indexOf(key) > -1) || + // lastly check to see if the name was a levenshtein match + levenshtein(name, key) <= MAX_LEV_DISTANCE)) { + return false; + } + } + return true; + } + + function nextTab(direction) { + const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; + searchState.focusedByTab[searchState.currentTab] = document.activeElement; + printTab(next); + focusSearchResult(); + } + + // Focus the first search result on the active tab, or the result that + // was focused last time this tab was active. + function focusSearchResult() { + const target = searchState.focusedByTab[searchState.currentTab] || + document.querySelectorAll(".search-results.active a").item(0) || + document.querySelectorAll("#titles > button").item(searchState.currentTab); + if (target) { + target.focus(); + } + } + + function buildHrefAndPath(item) { + let displayPath; + let href; + const type = itemTypes[item.ty]; + const name = item.name; + let path = item.path; + + if (type === "mod") { + displayPath = path + "::"; + href = ROOT_PATH + path.replace(/::/g, "/") + "/" + + name + "/index.html"; + } else if (type === "import") { + displayPath = item.path + "::"; + href = ROOT_PATH + item.path.replace(/::/g, "/") + "/index.html#reexport." + name; + } else if (type === "primitive" || type === "keyword") { + displayPath = ""; + href = ROOT_PATH + path.replace(/::/g, "/") + + "/" + type + "." + name + ".html"; + } else if (type === "externcrate") { + displayPath = ""; + href = ROOT_PATH + name + "/index.html"; + } else if (item.parent !== undefined) { + const myparent = item.parent; + let anchor = "#" + type + "." + name; + const parentType = itemTypes[myparent.ty]; + let pageType = parentType; + let pageName = myparent.name; + + if (parentType === "primitive") { + displayPath = myparent.name + "::"; + } else if (type === "structfield" && parentType === "variant") { + // Structfields belonging to variants are special: the + // final path element is the enum name. + const enumNameIdx = item.path.lastIndexOf("::"); + const enumName = item.path.substr(enumNameIdx + 2); + path = item.path.substr(0, enumNameIdx); + displayPath = path + "::" + enumName + "::" + myparent.name + "::"; + anchor = "#variant." + myparent.name + ".field." + name; + pageType = "enum"; + pageName = enumName; + } else { + displayPath = path + "::" + myparent.name + "::"; + } + href = ROOT_PATH + path.replace(/::/g, "/") + + "/" + pageType + + "." + pageName + + ".html" + anchor; + } else { + displayPath = item.path + "::"; + href = ROOT_PATH + item.path.replace(/::/g, "/") + + "/" + type + "." + name + ".html"; + } + return [displayPath, href]; + } + + function escape(content) { + const h1 = document.createElement("h1"); + h1.textContent = content; + return h1.innerHTML; + } + + function pathSplitter(path) { + const tmp = "<span>" + path.replace(/::/g, "::</span><span>"); + if (tmp.endsWith("<span>")) { + return tmp.slice(0, tmp.length - 6); + } + return tmp; + } + + /** + * Render a set of search results for a single tab. + * @param {Array<?>} array - The search results for this tab + * @param {ParsedQuery} query + * @param {boolean} display - True if this is the active tab + */ + function addTab(array, query, display) { + let extraClass = ""; + if (display === true) { + extraClass = " active"; + } + + const output = document.createElement("div"); + let length = 0; + if (array.length > 0) { + output.className = "search-results " + extraClass; + + array.forEach(item => { + const name = item.name; + const type = itemTypes[item.ty]; + + length += 1; + + let extra = ""; + if (type === "primitive") { + extra = " <i>(primitive type)</i>"; + } else if (type === "keyword") { + extra = " <i>(keyword)</i>"; + } + + const link = document.createElement("a"); + link.className = "result-" + type; + link.href = item.href; + + const wrapper = document.createElement("div"); + const resultName = document.createElement("div"); + resultName.className = "result-name"; + + if (item.is_alias) { + const alias = document.createElement("span"); + alias.className = "alias"; + + const bold = document.createElement("b"); + bold.innerText = item.alias; + alias.appendChild(bold); + + alias.insertAdjacentHTML( + "beforeend", + "<span class=\"grey\"><i> - see </i></span>"); + + resultName.appendChild(alias); + } + resultName.insertAdjacentHTML( + "beforeend", + item.displayPath + "<span class=\"" + type + "\">" + name + extra + "</span>"); + wrapper.appendChild(resultName); + + const description = document.createElement("div"); + description.className = "desc"; + const spanDesc = document.createElement("span"); + spanDesc.insertAdjacentHTML("beforeend", item.desc); + + description.appendChild(spanDesc); + wrapper.appendChild(description); + link.appendChild(wrapper); + output.appendChild(link); + }); + } else if (query.error === null) { + output.className = "search-failed" + extraClass; + output.innerHTML = "No results :(<br/>" + + "Try on <a href=\"https://duckduckgo.com/?q=" + + encodeURIComponent("rust " + query.userQuery) + + "\">DuckDuckGo</a>?<br/><br/>" + + "Or try looking in one of these:<ul><li>The <a " + + "href=\"https://doc.rust-lang.org/reference/index.html\">Rust Reference</a> " + + " for technical details about the language.</li><li><a " + + "href=\"https://doc.rust-lang.org/rust-by-example/index.html\">Rust By " + + "Example</a> for expository code examples.</a></li><li>The <a " + + "href=\"https://doc.rust-lang.org/book/index.html\">Rust Book</a> for " + + "introductions to language features and the language itself.</li><li><a " + + "href=\"https://docs.rs\">Docs.rs</a> for documentation of crates released on" + + " <a href=\"https://crates.io/\">crates.io</a>.</li></ul>"; + } + return [output, length]; + } + + function makeTabHeader(tabNb, text, nbElems) { + if (searchState.currentTab === tabNb) { + return "<button class=\"selected\">" + text + + " <div class=\"count\">(" + nbElems + ")</div></button>"; + } + return "<button>" + text + " <div class=\"count\">(" + nbElems + ")</div></button>"; + } + + /** + * @param {ResultsTable} results + * @param {boolean} go_to_first + * @param {string} filterCrates + */ + function showResults(results, go_to_first, filterCrates) { + const search = searchState.outputElement(); + if (go_to_first || (results.others.length === 1 + && getSettingValue("go-to-only-result") === "true" + // By default, the search DOM element is "empty" (meaning it has no children not + // text content). Once a search has been run, it won't be empty, even if you press + // ESC or empty the search input (which also "cancels" the search). + && (!search.firstChild || search.firstChild.innerText !== searchState.loadingText)) + ) { + const elem = document.createElement("a"); + elem.href = results.others[0].href; + removeClass(elem, "active"); + // For firefox, we need the element to be in the DOM so it can be clicked. + document.body.appendChild(elem); + elem.click(); + return; + } + if (results.query === undefined) { + results.query = parseQuery(searchState.input.value); + } + + currentResults = results.query.userQuery; + + const ret_others = addTab(results.others, results.query, true); + const ret_in_args = addTab(results.in_args, results.query, false); + const ret_returned = addTab(results.returned, results.query, false); + + // Navigate to the relevant tab if the current tab is empty, like in case users search + // for "-> String". If they had selected another tab previously, they have to click on + // it again. + let currentTab = searchState.currentTab; + if ((currentTab === 0 && ret_others[1] === 0) || + (currentTab === 1 && ret_in_args[1] === 0) || + (currentTab === 2 && ret_returned[1] === 0)) { + if (ret_others[1] !== 0) { + currentTab = 0; + } else if (ret_in_args[1] !== 0) { + currentTab = 1; + } else if (ret_returned[1] !== 0) { + currentTab = 2; + } + } + + let crates = ""; + const crates_list = Object.keys(rawSearchIndex); + if (crates_list.length > 1) { + crates = " in <select id=\"crate-search\"><option value=\"All crates\">" + + "All crates</option>"; + for (const c of crates_list) { + crates += `<option value="${c}" ${c === filterCrates && "selected"}>${c}</option>`; + } + crates += "</select>"; + } + + let typeFilter = ""; + if (results.query.typeFilter !== NO_TYPE_FILTER) { + typeFilter = " (type: " + escape(itemTypes[results.query.typeFilter]) + ")"; + } + + let output = "<div id=\"search-settings\">" + + `<h1 class="search-results-title">Results for ${escape(results.query.userQuery)}` + + `${typeFilter}</h1>${crates}</div>`; + if (results.query.error !== null) { + output += `<h3>Query parser error: "${results.query.error}".</h3>`; + output += "<div id=\"titles\">" + + makeTabHeader(0, "In Names", ret_others[1]) + + "</div>"; + currentTab = 0; + } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) { + output += "<div id=\"titles\">" + + makeTabHeader(0, "In Names", ret_others[1]) + + makeTabHeader(1, "In Parameters", ret_in_args[1]) + + makeTabHeader(2, "In Return Types", ret_returned[1]) + + "</div>"; + } else { + const signatureTabTitle = + results.query.elems.length === 0 ? "In Function Return Types" : + results.query.returned.length === 0 ? "In Function Parameters" : + "In Function Signatures"; + output += "<div id=\"titles\">" + + makeTabHeader(0, signatureTabTitle, ret_others[1]) + + "</div>"; + currentTab = 0; + } + + const resultsElem = document.createElement("div"); + resultsElem.id = "results"; + resultsElem.appendChild(ret_others[0]); + resultsElem.appendChild(ret_in_args[0]); + resultsElem.appendChild(ret_returned[0]); + + search.innerHTML = output; + const crateSearch = document.getElementById("crate-search"); + if (crateSearch) { + crateSearch.addEventListener("input", updateCrate); + } + search.appendChild(resultsElem); + // Reset focused elements. + searchState.showResults(search); + const elems = document.getElementById("titles").childNodes; + searchState.focusedByTab = []; + let i = 0; + for (const elem of elems) { + const j = i; + elem.onclick = () => printTab(j); + searchState.focusedByTab.push(null); + i += 1; + } + printTab(currentTab); + } + + /** + * Perform a search based on the current state of the search input element + * and display the results. + * @param {Event} [e] - The event that triggered this search, if any + * @param {boolean} [forced] + */ + function search(e, forced) { + const params = searchState.getQueryStringParams(); + const query = parseQuery(searchState.input.value.trim()); + + if (e) { + e.preventDefault(); + } + + if (!forced && query.userQuery === currentResults) { + if (query.userQuery.length > 0) { + putBackSearch(); + } + return; + } + + let filterCrates = getFilterCrates(); + + // In case we have no information about the saved crate and there is a URL query parameter, + // we override it with the URL query parameter. + if (filterCrates === null && params["filter-crate"] !== undefined) { + filterCrates = params["filter-crate"]; + } + + // Update document title to maintain a meaningful browser history + searchState.title = "Results for " + query.original + " - Rust"; + + // Because searching is incremental by character, only the most + // recent search query is added to the browser history. + if (browserSupportsHistoryApi()) { + const newURL = buildUrl(query.original, filterCrates); + + if (!history.state && !params.search) { + history.pushState(null, "", newURL); + } else { + history.replaceState(null, "", newURL); + } + } + + showResults( + execQuery(query, searchWords, filterCrates, window.currentCrate), + params.go_to_first, + filterCrates); + } + + /** + * Convert a list of RawFunctionType / ID to object-based FunctionType. + * + * Crates often have lots of functions in them, and it's common to have a large number of + * functions that operate on a small set of data types, so the search index compresses them + * by encoding function parameter and return types as indexes into an array of names. + * + * Even when a general-purpose compression algorithm is used, this is still a win. I checked. + * https://github.com/rust-lang/rust/pull/98475#issue-1284395985 + * + * The format for individual function types is encoded in + * librustdoc/html/render/mod.rs: impl Serialize for RenderType + * + * @param {null|Array<RawFunctionType>} types + * @param {Array<{name: string, ty: number}>} lowercasePaths + * + * @return {Array<FunctionSearchType>} + */ + function buildItemSearchTypeAll(types, lowercasePaths) { + const PATH_INDEX_DATA = 0; + const GENERICS_DATA = 1; + return types.map(type => { + let pathIndex, generics; + if (typeof type === "number") { + pathIndex = type; + generics = []; + } else { + pathIndex = type[PATH_INDEX_DATA]; + generics = buildItemSearchTypeAll(type[GENERICS_DATA], lowercasePaths); + } + return { + // `0` is used as a sentinel because it's fewer bytes than `null` + name: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].name, + ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, + generics: generics, + }; + }); + } + + /** + * Convert from RawFunctionSearchType to FunctionSearchType. + * + * Crates often have lots of functions in them, and function signatures are sometimes complex, + * so rustdoc uses a pretty tight encoding for them. This function converts it to a simpler, + * object-based encoding so that the actual search code is more readable and easier to debug. + * + * The raw function search type format is generated using serde in + * librustdoc/html/render/mod.rs: impl Serialize for IndexItemFunctionType + * + * @param {RawFunctionSearchType} functionSearchType + * @param {Array<{name: string, ty: number}>} lowercasePaths + * + * @return {null|FunctionSearchType} + */ + function buildFunctionSearchType(functionSearchType, lowercasePaths) { + const INPUTS_DATA = 0; + const OUTPUT_DATA = 1; + // `0` is used as a sentinel because it's fewer bytes than `null` + if (functionSearchType === 0) { + return null; + } + let inputs, output; + if (typeof functionSearchType[INPUTS_DATA] === "number") { + const pathIndex = functionSearchType[INPUTS_DATA]; + inputs = [{ + name: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].name, + ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, + generics: [], + }]; + } else { + inputs = buildItemSearchTypeAll(functionSearchType[INPUTS_DATA], lowercasePaths); + } + if (functionSearchType.length > 1) { + if (typeof functionSearchType[OUTPUT_DATA] === "number") { + const pathIndex = functionSearchType[OUTPUT_DATA]; + output = [{ + name: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].name, + ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, + generics: [], + }]; + } else { + output = buildItemSearchTypeAll(functionSearchType[OUTPUT_DATA], lowercasePaths); + } + } else { + output = []; + } + return { + inputs, output, + }; + } + + function buildIndex(rawSearchIndex) { + searchIndex = []; + /** + * @type {Array<string>} + */ + const searchWords = []; + let i, word; + let currentIndex = 0; + let id = 0; + + for (const crate in rawSearchIndex) { + if (!hasOwnPropertyRustdoc(rawSearchIndex, crate)) { + continue; + } + + let crateSize = 0; + + /** + * The raw search data for a given crate. `n`, `t`, `d`, and `q`, `i`, and `f` + * are arrays with the same length. n[i] contains the name of an item. + * t[i] contains the type of that item (as a small integer that represents an + * offset in `itemTypes`). d[i] contains the description of that item. + * + * q[i] contains the full path of the item, or an empty string indicating + * "same as q[i-1]". + * + * i[i] contains an item's parent, usually a module. For compactness, + * it is a set of indexes into the `p` array. + * + * f[i] contains function signatures, or `0` if the item isn't a function. + * Functions are themselves encoded as arrays. The first item is a list of + * types representing the function's inputs, and the second list item is a list + * of types representing the function's output. Tuples are flattened. + * Types are also represented as arrays; the first item is an index into the `p` + * array, while the second is a list of types representing any generic parameters. + * + * `a` defines aliases with an Array of pairs: [name, offset], where `offset` + * points into the n/t/d/q/i/f arrays. + * + * `doc` contains the description of the crate. + * + * `p` is a list of path/type pairs. It is used for parents and function parameters. + * + * @type {{ + * doc: string, + * a: Object, + * n: Array<string>, + * t: Array<Number>, + * d: Array<string>, + * q: Array<string>, + * i: Array<Number>, + * f: Array<RawFunctionSearchType>, + * p: Array<Object>, + * }} + */ + const crateCorpus = rawSearchIndex[crate]; + + searchWords.push(crate); + // This object should have exactly the same set of fields as the "row" + // object defined below. Your JavaScript runtime will thank you. + // https://mathiasbynens.be/notes/shapes-ics + const crateRow = { + crate: crate, + ty: 1, // == ExternCrate + name: crate, + path: "", + desc: crateCorpus.doc, + parent: undefined, + type: null, + id: id, + normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""), + }; + id += 1; + searchIndex.push(crateRow); + currentIndex += 1; + + // an array of (Number) item types + const itemTypes = crateCorpus.t; + // an array of (String) item names + const itemNames = crateCorpus.n; + // an array of (String) full paths (or empty string for previous path) + const itemPaths = crateCorpus.q; + // an array of (String) descriptions + const itemDescs = crateCorpus.d; + // an array of (Number) the parent path index + 1 to `paths`, or 0 if none + const itemParentIdxs = crateCorpus.i; + // an array of (Object | null) the type of the function, if any + const itemFunctionSearchTypes = crateCorpus.f; + // an array of [(Number) item type, + // (String) name] + const paths = crateCorpus.p; + // an array of [(String) alias name + // [Number] index to items] + const aliases = crateCorpus.a; + + // an array of [{name: String, ty: Number}] + const lowercasePaths = []; + + // convert `rawPaths` entries into object form + // generate normalizedPaths for function search mode + let len = paths.length; + for (i = 0; i < len; ++i) { + lowercasePaths.push({ty: paths[i][0], name: paths[i][1].toLowerCase()}); + paths[i] = {ty: paths[i][0], name: paths[i][1]}; + } + + // convert `item*` into an object form, and construct word indices. + // + // before any analysis is performed lets gather the search terms to + // search against apart from the rest of the data. This is a quick + // operation that is cached for the life of the page state so that + // all other search operations have access to this cached data for + // faster analysis operations + len = itemTypes.length; + let lastPath = ""; + for (i = 0; i < len; ++i) { + // This object should have exactly the same set of fields as the "crateRow" + // object defined above. + if (typeof itemNames[i] === "string") { + word = itemNames[i].toLowerCase(); + searchWords.push(word); + } else { + word = ""; + searchWords.push(""); + } + const row = { + crate: crate, + ty: itemTypes[i], + name: itemNames[i], + path: itemPaths[i] ? itemPaths[i] : lastPath, + desc: itemDescs[i], + parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined, + type: buildFunctionSearchType(itemFunctionSearchTypes[i], lowercasePaths), + id: id, + normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), + }; + id += 1; + searchIndex.push(row); + lastPath = row.path; + crateSize += 1; + } + + if (aliases) { + ALIASES[crate] = Object.create(null); + for (const alias_name in aliases) { + if (!hasOwnPropertyRustdoc(aliases, alias_name)) { + continue; + } + + if (!hasOwnPropertyRustdoc(ALIASES[crate], alias_name)) { + ALIASES[crate][alias_name] = []; + } + for (const local_alias of aliases[alias_name]) { + ALIASES[crate][alias_name].push(local_alias + currentIndex); + } + } + } + currentIndex += crateSize; + } + return searchWords; + } + + /** + * Callback for when the search form is submitted. + * @param {Event} [e] - The event that triggered this call, if any + */ + function onSearchSubmit(e) { + e.preventDefault(); + searchState.clearInputTimeout(); + search(); + } + + function putBackSearch() { + const search_input = searchState.input; + if (!searchState.input) { + return; + } + if (search_input.value !== "" && !searchState.isDisplayed()) { + searchState.showResults(); + if (browserSupportsHistoryApi()) { + history.replaceState(null, "", + buildUrl(search_input.value, getFilterCrates())); + } + document.title = searchState.title; + } + } + + function registerSearchEvents() { + const params = searchState.getQueryStringParams(); + + // Populate search bar with query string search term when provided, + // but only if the input bar is empty. This avoid the obnoxious issue + // where you start trying to do a search, and the index loads, and + // suddenly your search is gone! + if (searchState.input.value === "") { + searchState.input.value = params.search || ""; + } + + const searchAfter500ms = () => { + searchState.clearInputTimeout(); + if (searchState.input.value.length === 0) { + if (browserSupportsHistoryApi()) { + history.replaceState(null, window.currentCrate + " - Rust", + getNakedUrl() + window.location.hash); + } + searchState.hideResults(); + } else { + searchState.timeout = setTimeout(search, 500); + } + }; + searchState.input.onkeyup = searchAfter500ms; + searchState.input.oninput = searchAfter500ms; + document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit; + searchState.input.onchange = e => { + if (e.target !== document.activeElement) { + // To prevent doing anything when it's from a blur event. + return; + } + // Do NOT e.preventDefault() here. It will prevent pasting. + searchState.clearInputTimeout(); + // zero-timeout necessary here because at the time of event handler execution the + // pasted content is not in the input field yet. Shouldn’t make any difference for + // change, though. + setTimeout(search, 0); + }; + searchState.input.onpaste = searchState.input.onchange; + + searchState.outputElement().addEventListener("keydown", e => { + // We only handle unmodified keystrokes here. We don't want to interfere with, + // for instance, alt-left and alt-right for history navigation. + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + // up and down arrow select next/previous search result, or the + // search box if we're already at the top. + if (e.which === 38) { // up + const previous = document.activeElement.previousElementSibling; + if (previous) { + previous.focus(); + } else { + searchState.focus(); + } + e.preventDefault(); + } else if (e.which === 40) { // down + const next = document.activeElement.nextElementSibling; + if (next) { + next.focus(); + } + const rect = document.activeElement.getBoundingClientRect(); + if (window.innerHeight - rect.bottom < rect.height) { + window.scrollBy(0, rect.height); + } + e.preventDefault(); + } else if (e.which === 37) { // left + nextTab(-1); + e.preventDefault(); + } else if (e.which === 39) { // right + nextTab(1); + e.preventDefault(); + } + }); + + searchState.input.addEventListener("keydown", e => { + if (e.which === 40) { // down + focusSearchResult(); + e.preventDefault(); + } + }); + + searchState.input.addEventListener("focus", () => { + putBackSearch(); + }); + + searchState.input.addEventListener("blur", () => { + searchState.input.placeholder = searchState.input.origPlaceholder; + }); + + // Push and pop states are used to add search results to the browser + // history. + if (browserSupportsHistoryApi()) { + // Store the previous <title> so we can revert back to it later. + const previousTitle = document.title; + + window.addEventListener("popstate", e => { + const params = searchState.getQueryStringParams(); + // Revert to the previous title manually since the History + // API ignores the title parameter. + document.title = previousTitle; + // When browsing forward to search results the previous + // search will be repeated, so the currentResults are + // cleared to ensure the search is successful. + currentResults = null; + // Synchronize search bar with query string state and + // perform the search. This will empty the bar if there's + // nothing there, which lets you really go back to a + // previous state with nothing in the bar. + if (params.search && params.search.length > 0) { + searchState.input.value = params.search; + // Some browsers fire "onpopstate" for every page load + // (Chrome), while others fire the event only when actually + // popping a state (Firefox), which is why search() is + // called both here and at the end of the startSearch() + // function. + search(e); + } else { + searchState.input.value = ""; + // When browsing back from search results the main page + // visibility must be reset. + searchState.hideResults(); + } + }); + } + + // This is required in firefox to avoid this problem: Navigating to a search result + // with the keyboard, hitting enter, and then hitting back would take you back to + // the doc page, rather than the search that should overlay it. + // This was an interaction between the back-forward cache and our handlers + // that try to sync state between the URL and the search input. To work around it, + // do a small amount of re-init on page show. + window.onpageshow = () => { + const qSearch = searchState.getQueryStringParams().search; + if (searchState.input.value === "" && qSearch) { + searchState.input.value = qSearch; + } + search(); + }; + } + + function updateCrate(ev) { + if (ev.target.value === "All crates") { + // If we don't remove it from the URL, it'll be picked up again by the search. + const params = searchState.getQueryStringParams(); + const query = searchState.input.value.trim(); + if (!history.state && !params.search) { + history.pushState(null, "", buildUrl(query, null)); + } else { + history.replaceState(null, "", buildUrl(query, null)); + } + } + // In case you "cut" the entry from the search input, then change the crate filter + // before paste back the previous search, you get the old search results without + // the filter. To prevent this, we need to remove the previous results. + currentResults = null; + search(undefined, true); + } + + /** + * @type {Array<string>} + */ + const searchWords = buildIndex(rawSearchIndex); + if (typeof window !== "undefined") { + registerSearchEvents(); + // If there's a search term in the URL, execute the search now. + if (window.searchState.getQueryStringParams().search) { + search(); + } + } + + if (typeof exports !== "undefined") { + exports.initSearch = initSearch; + exports.execQuery = execQuery; + exports.parseQuery = parseQuery; + } + return searchWords; +} + +if (typeof window !== "undefined") { + window.initSearch = initSearch; + if (window.searchIndex !== undefined) { + initSearch(window.searchIndex); + } +} else { + // Running in Node, not a browser. Run initSearch just to produce the + // exports. + initSearch({}); +} + + +})(); diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js new file mode 100644 index 000000000..797b931af --- /dev/null +++ b/src/librustdoc/html/static/js/settings.js @@ -0,0 +1,272 @@ +// Local js definitions: +/* global getSettingValue, getVirtualKey, updateLocalStorage, updateSystemTheme */ +/* global addClass, removeClass, onEach, onEachLazy, blurHandler, elemIsInParent */ +/* global MAIN_ID, getVar, getSettingsButton */ + +"use strict"; + +(function() { + const isSettingsPage = window.location.pathname.endsWith("/settings.html"); + + function changeSetting(settingName, value) { + updateLocalStorage(settingName, value); + + switch (settingName) { + case "theme": + case "preferred-dark-theme": + case "preferred-light-theme": + case "use-system-theme": + updateSystemTheme(); + updateLightAndDark(); + break; + } + } + + function handleKey(ev) { + // Don't interfere with browser shortcuts + if (ev.ctrlKey || ev.altKey || ev.metaKey) { + return; + } + switch (getVirtualKey(ev)) { + case "Enter": + case "Return": + case "Space": + ev.target.checked = !ev.target.checked; + ev.preventDefault(); + break; + } + } + + function showLightAndDark() { + addClass(document.getElementById("theme").parentElement, "hidden"); + removeClass(document.getElementById("preferred-light-theme").parentElement, "hidden"); + removeClass(document.getElementById("preferred-dark-theme").parentElement, "hidden"); + } + + function hideLightAndDark() { + addClass(document.getElementById("preferred-light-theme").parentElement, "hidden"); + addClass(document.getElementById("preferred-dark-theme").parentElement, "hidden"); + removeClass(document.getElementById("theme").parentElement, "hidden"); + } + + function updateLightAndDark() { + if (getSettingValue("use-system-theme") !== "false") { + showLightAndDark(); + } else { + hideLightAndDark(); + } + } + + function setEvents(settingsElement) { + updateLightAndDark(); + onEachLazy(settingsElement.getElementsByClassName("slider"), elem => { + const toggle = elem.previousElementSibling; + const settingId = toggle.id; + const settingValue = getSettingValue(settingId); + if (settingValue !== null) { + toggle.checked = settingValue === "true"; + } + toggle.onchange = function() { + changeSetting(this.id, this.checked); + }; + toggle.onkeyup = handleKey; + toggle.onkeyrelease = handleKey; + }); + onEachLazy(settingsElement.getElementsByClassName("select-wrapper"), elem => { + const select = elem.getElementsByTagName("select")[0]; + const settingId = select.id; + const settingValue = getSettingValue(settingId); + if (settingValue !== null) { + select.value = settingValue; + } + select.onchange = function() { + changeSetting(this.id, this.value); + }; + }); + onEachLazy(settingsElement.querySelectorAll("input[type=\"radio\"]"), elem => { + const settingId = elem.name; + const settingValue = getSettingValue(settingId); + if (settingValue !== null && settingValue !== "null") { + elem.checked = settingValue === elem.value; + } + elem.addEventListener("change", ev => { + changeSetting(ev.target.name, ev.target.value); + }); + }); + } + + /** + * This function builds the sections inside the "settings page". It takes a `settings` list + * as argument which describes each setting and how to render it. It returns a string + * representing the raw HTML. + * + * @param {Array<Object>} settings + * + * @return {string} + */ + function buildSettingsPageSections(settings) { + let output = ""; + + for (const setting of settings) { + output += "<div class=\"setting-line\">"; + const js_data_name = setting["js_name"]; + const setting_name = setting["name"]; + + if (setting["options"] !== undefined) { + // This is a select setting. + output += `<div class="radio-line" id="${js_data_name}">\ + <span class="setting-name">${setting_name}</span>\ + <div class="choices">`; + onEach(setting["options"], option => { + const checked = option === setting["default"] ? " checked" : ""; + + output += `<label for="${js_data_name}-${option}" class="choice">\ + <input type="radio" name="${js_data_name}" \ + id="${js_data_name}-${option}" value="${option}"${checked}>\ + <span>${option}</span>\ + </label>`; + }); + output += "</div></div>"; + } else { + // This is a toggle. + const checked = setting["default"] === true ? " checked" : ""; + output += `<label class="toggle">\ + <input type="checkbox" id="${js_data_name}"${checked}>\ + <span class="slider"></span>\ + <span class="label">${setting_name}</span>\ + </label>`; + } + output += "</div>"; + } + return output; + } + + /** + * This function builds the "settings page" and returns the generated HTML element. + * + * @return {HTMLElement} + */ + function buildSettingsPage() { + const themes = getVar("themes").split(","); + const settings = [ + { + "name": "Use system theme", + "js_name": "use-system-theme", + "default": true, + }, + { + "name": "Theme", + "js_name": "theme", + "default": "light", + "options": themes, + }, + { + "name": "Preferred light theme", + "js_name": "preferred-light-theme", + "default": "light", + "options": themes, + }, + { + "name": "Preferred dark theme", + "js_name": "preferred-dark-theme", + "default": "dark", + "options": themes, + }, + { + "name": "Auto-hide item contents for large items", + "js_name": "auto-hide-large-items", + "default": true, + }, + { + "name": "Auto-hide item methods' documentation", + "js_name": "auto-hide-method-docs", + "default": false, + }, + { + "name": "Auto-hide trait implementation documentation", + "js_name": "auto-hide-trait-implementations", + "default": false, + }, + { + "name": "Directly go to item in search if there is only one result", + "js_name": "go-to-only-result", + "default": false, + }, + { + "name": "Show line numbers on code examples", + "js_name": "line-numbers", + "default": false, + }, + { + "name": "Disable keyboard shortcuts", + "js_name": "disable-shortcuts", + "default": false, + }, + ]; + + // Then we build the DOM. + const elementKind = isSettingsPage ? "section" : "div"; + const innerHTML = `<div class="settings">${buildSettingsPageSections(settings)}</div>`; + const el = document.createElement(elementKind); + el.id = "settings"; + el.className = "popover"; + el.innerHTML = innerHTML; + + if (isSettingsPage) { + document.getElementById(MAIN_ID).appendChild(el); + } else { + el.setAttribute("tabindex", "-1"); + getSettingsButton().appendChild(el); + } + return el; + } + + const settingsMenu = buildSettingsPage(); + + function displaySettings() { + settingsMenu.style.display = ""; + } + + function settingsBlurHandler(event) { + blurHandler(event, getSettingsButton(), window.hidePopoverMenus); + } + + if (isSettingsPage) { + // We replace the existing "onclick" callback to do nothing if clicked. + getSettingsButton().onclick = function(event) { + event.preventDefault(); + }; + } else { + // We replace the existing "onclick" callback. + const settingsButton = getSettingsButton(); + const settingsMenu = document.getElementById("settings"); + settingsButton.onclick = function(event) { + if (elemIsInParent(event.target, settingsMenu)) { + return; + } + event.preventDefault(); + const shouldDisplaySettings = settingsMenu.style.display === "none"; + + window.hidePopoverMenus(); + if (shouldDisplaySettings) { + displaySettings(); + } + }; + settingsButton.onblur = settingsBlurHandler; + settingsButton.querySelector("a").onblur = settingsBlurHandler; + onEachLazy(settingsMenu.querySelectorAll("input"), el => { + el.onblur = settingsBlurHandler; + }); + settingsMenu.onblur = settingsBlurHandler; + } + + // We now wait a bit for the web browser to end re-computing the DOM... + setTimeout(() => { + setEvents(settingsMenu); + // The setting menu is already displayed if we're on the settings page. + if (!isSettingsPage) { + displaySettings(); + } + removeClass(getSettingsButton(), "rotate"); + }, 0); +})(); diff --git a/src/librustdoc/html/static/js/source-script.js b/src/librustdoc/html/static/js/source-script.js new file mode 100644 index 000000000..c45d61429 --- /dev/null +++ b/src/librustdoc/html/static/js/source-script.js @@ -0,0 +1,241 @@ +// From rust: +/* global sourcesIndex */ + +// Local js definitions: +/* global addClass, getCurrentValue, onEachLazy, removeClass, browserSupportsHistoryApi */ +/* global updateLocalStorage */ + +"use strict"; + +(function() { + +const rootPath = document.getElementById("rustdoc-vars").attributes["data-root-path"].value; +let oldScrollPosition = 0; + +const NAME_OFFSET = 0; +const DIRS_OFFSET = 1; +const FILES_OFFSET = 2; + +function closeSidebarIfMobile() { + if (window.innerWidth < window.RUSTDOC_MOBILE_BREAKPOINT) { + updateLocalStorage("source-sidebar-show", "false"); + } +} + +function createDirEntry(elem, parent, fullPath, hasFoundFile) { + const dirEntry = document.createElement("details"); + const summary = document.createElement("summary"); + + dirEntry.className = "dir-entry"; + + fullPath += elem[NAME_OFFSET] + "/"; + + summary.innerText = elem[NAME_OFFSET]; + dirEntry.appendChild(summary); + + const folders = document.createElement("div"); + folders.className = "folders"; + if (elem[DIRS_OFFSET]) { + for (const dir of elem[DIRS_OFFSET]) { + if (createDirEntry(dir, folders, fullPath, false)) { + dirEntry.open = true; + hasFoundFile = true; + } + } + } + dirEntry.appendChild(folders); + + const files = document.createElement("div"); + files.className = "files"; + if (elem[FILES_OFFSET]) { + for (const file_text of elem[FILES_OFFSET]) { + const file = document.createElement("a"); + file.innerText = file_text; + file.href = rootPath + "src/" + fullPath + file_text + ".html"; + file.addEventListener("click", closeSidebarIfMobile); + const w = window.location.href.split("#")[0]; + if (!hasFoundFile && w === file.href) { + file.className = "selected"; + dirEntry.open = true; + hasFoundFile = true; + } + files.appendChild(file); + } + } + dirEntry.appendChild(files); + parent.appendChild(dirEntry); + return hasFoundFile; +} + +function toggleSidebar() { + const child = this.parentNode.children[0]; + if (child.innerText === ">") { + if (window.innerWidth < window.RUSTDOC_MOBILE_BREAKPOINT) { + // This is to keep the scroll position on mobile. + oldScrollPosition = window.scrollY; + document.body.style.position = "fixed"; + document.body.style.top = `-${oldScrollPosition}px`; + } + addClass(document.documentElement, "source-sidebar-expanded"); + child.innerText = "<"; + updateLocalStorage("source-sidebar-show", "true"); + } else { + if (window.innerWidth < window.RUSTDOC_MOBILE_BREAKPOINT) { + // This is to keep the scroll position on mobile. + document.body.style.position = ""; + document.body.style.top = ""; + // The scroll position is lost when resetting the style, hence why we store it in + // `oldScroll`. + window.scrollTo(0, oldScrollPosition); + } + removeClass(document.documentElement, "source-sidebar-expanded"); + child.innerText = ">"; + updateLocalStorage("source-sidebar-show", "false"); + } +} + +function createSidebarToggle() { + const sidebarToggle = document.createElement("div"); + sidebarToggle.id = "sidebar-toggle"; + + const inner = document.createElement("button"); + + if (getCurrentValue("source-sidebar-show") === "true") { + inner.innerText = "<"; + } else { + inner.innerText = ">"; + } + inner.onclick = toggleSidebar; + + sidebarToggle.appendChild(inner); + return sidebarToggle; +} + +// This function is called from "source-files.js", generated in `html/render/mod.rs`. +// eslint-disable-next-line no-unused-vars +function createSourceSidebar() { + const container = document.querySelector("nav.sidebar"); + + const sidebarToggle = createSidebarToggle(); + container.insertBefore(sidebarToggle, container.firstChild); + + const sidebar = document.createElement("div"); + sidebar.id = "source-sidebar"; + + let hasFoundFile = false; + + const title = document.createElement("div"); + title.className = "title"; + title.innerText = "Files"; + sidebar.appendChild(title); + Object.keys(sourcesIndex).forEach(key => { + sourcesIndex[key][NAME_OFFSET] = key; + hasFoundFile = createDirEntry(sourcesIndex[key], sidebar, "", + hasFoundFile); + }); + + container.appendChild(sidebar); + // Focus on the current file in the source files sidebar. + const selected_elem = sidebar.getElementsByClassName("selected")[0]; + if (typeof selected_elem !== "undefined") { + selected_elem.focus(); + } +} + +const lineNumbersRegex = /^#?(\d+)(?:-(\d+))?$/; + +function highlightSourceLines(match) { + if (typeof match === "undefined") { + match = window.location.hash.match(lineNumbersRegex); + } + if (!match) { + return; + } + let from = parseInt(match[1], 10); + let to = from; + if (typeof match[2] !== "undefined") { + to = parseInt(match[2], 10); + } + if (to < from) { + const tmp = to; + to = from; + from = tmp; + } + let elem = document.getElementById(from); + if (!elem) { + return; + } + const x = document.getElementById(from); + if (x) { + x.scrollIntoView(); + } + onEachLazy(document.getElementsByClassName("line-numbers"), e => { + onEachLazy(e.getElementsByTagName("span"), i_e => { + removeClass(i_e, "line-highlighted"); + }); + }); + for (let i = from; i <= to; ++i) { + elem = document.getElementById(i); + if (!elem) { + break; + } + addClass(elem, "line-highlighted"); + } +} + +const handleSourceHighlight = (function() { + let prev_line_id = 0; + + const set_fragment = name => { + const x = window.scrollX, + y = window.scrollY; + if (browserSupportsHistoryApi()) { + history.replaceState(null, null, "#" + name); + highlightSourceLines(); + } else { + location.replace("#" + name); + } + // Prevent jumps when selecting one or many lines + window.scrollTo(x, y); + }; + + return ev => { + let cur_line_id = parseInt(ev.target.id, 10); + // It can happen when clicking not on a line number span. + if (isNaN(cur_line_id)) { + return; + } + ev.preventDefault(); + + if (ev.shiftKey && prev_line_id) { + // Swap selection if needed + if (prev_line_id > cur_line_id) { + const tmp = prev_line_id; + prev_line_id = cur_line_id; + cur_line_id = tmp; + } + + set_fragment(prev_line_id + "-" + cur_line_id); + } else { + prev_line_id = cur_line_id; + + set_fragment(cur_line_id); + } + }; +}()); + +window.addEventListener("hashchange", () => { + const match = window.location.hash.match(lineNumbersRegex); + if (match) { + return highlightSourceLines(match); + } +}); + +onEachLazy(document.getElementsByClassName("line-numbers"), el => { + el.addEventListener("click", handleSourceHighlight); +}); + +highlightSourceLines(); + +window.createSourceSidebar = createSourceSidebar; +})(); diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js new file mode 100644 index 000000000..0c5389d45 --- /dev/null +++ b/src/librustdoc/html/static/js/storage.js @@ -0,0 +1,268 @@ +// storage.js is loaded in the `<head>` of all rustdoc pages and doesn't +// use `async` or `defer`. That means it blocks further parsing and rendering +// of the page: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script. +// This makes it the correct place to act on settings that affect the display of +// the page, so we don't see major layout changes during the load of the page. +"use strict"; + +const darkThemes = ["dark", "ayu"]; +window.currentTheme = document.getElementById("themeStyle"); +window.mainTheme = document.getElementById("mainThemeStyle"); + +// WARNING: RUSTDOC_MOBILE_BREAKPOINT MEDIA QUERY +// If you update this line, then you also need to update the two media queries with the same +// warning in rustdoc.css +window.RUSTDOC_MOBILE_BREAKPOINT = 701; + +const settingsDataset = (function() { + const settingsElement = document.getElementById("default-settings"); + if (settingsElement === null) { + return null; + } + const dataset = settingsElement.dataset; + if (dataset === undefined) { + return null; + } + return dataset; +})(); + +function getSettingValue(settingName) { + const current = getCurrentValue(settingName); + if (current !== null) { + return current; + } + if (settingsDataset !== null) { + // See the comment for `default_settings.into_iter()` etc. in + // `Options::from_matches` in `librustdoc/config.rs`. + const def = settingsDataset[settingName.replace(/-/g,"_")]; + if (def !== undefined) { + return def; + } + } + return null; +} + +const localStoredTheme = getSettingValue("theme"); + +const savedHref = []; + +// eslint-disable-next-line no-unused-vars +function hasClass(elem, className) { + return elem && elem.classList && elem.classList.contains(className); +} + +// eslint-disable-next-line no-unused-vars +function addClass(elem, className) { + if (!elem || !elem.classList) { + return; + } + elem.classList.add(className); +} + +// eslint-disable-next-line no-unused-vars +function removeClass(elem, className) { + if (!elem || !elem.classList) { + return; + } + elem.classList.remove(className); +} + +/** + * Run a callback for every element of an Array. + * @param {Array<?>} arr - The array to iterate over + * @param {function(?)} func - The callback + * @param {boolean} [reversed] - Whether to iterate in reverse + */ +function onEach(arr, func, reversed) { + if (arr && arr.length > 0 && func) { + if (reversed) { + const length = arr.length; + for (let i = length - 1; i >= 0; --i) { + if (func(arr[i])) { + return true; + } + } + } else { + for (const elem of arr) { + if (func(elem)) { + return true; + } + } + } + } + return false; +} + +/** + * Turn an HTMLCollection or a NodeList into an Array, then run a callback + * for every element. This is useful because iterating over an HTMLCollection + * or a "live" NodeList while modifying it can be very slow. + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection + * https://developer.mozilla.org/en-US/docs/Web/API/NodeList + * @param {NodeList<?>|HTMLCollection<?>} lazyArray - An array to iterate over + * @param {function(?)} func - The callback + * @param {boolean} [reversed] - Whether to iterate in reverse + */ +function onEachLazy(lazyArray, func, reversed) { + return onEach( + Array.prototype.slice.call(lazyArray), + func, + reversed); +} + +function updateLocalStorage(name, value) { + try { + window.localStorage.setItem("rustdoc-" + name, value); + } catch (e) { + // localStorage is not accessible, do nothing + } +} + +function getCurrentValue(name) { + try { + return window.localStorage.getItem("rustdoc-" + name); + } catch (e) { + return null; + } +} + +function switchTheme(styleElem, mainStyleElem, newTheme, saveTheme) { + const newHref = mainStyleElem.href.replace( + /\/rustdoc([^/]*)\.css/, "/" + newTheme + "$1" + ".css"); + + // If this new value comes from a system setting or from the previously + // saved theme, no need to save it. + if (saveTheme) { + updateLocalStorage("theme", newTheme); + } + + if (styleElem.href === newHref) { + return; + } + + let found = false; + if (savedHref.length === 0) { + onEachLazy(document.getElementsByTagName("link"), el => { + savedHref.push(el.href); + }); + } + onEach(savedHref, el => { + if (el === newHref) { + found = true; + return true; + } + }); + if (found) { + styleElem.href = newHref; + } +} + +// This function is called from "main.js". +// eslint-disable-next-line no-unused-vars +function useSystemTheme(value) { + if (value === undefined) { + value = true; + } + + updateLocalStorage("use-system-theme", value); + + // update the toggle if we're on the settings page + const toggle = document.getElementById("use-system-theme"); + if (toggle && toggle instanceof HTMLInputElement) { + toggle.checked = value; + } +} + +const updateSystemTheme = (function() { + if (!window.matchMedia) { + // fallback to the CSS computed value + return () => { + const cssTheme = getComputedStyle(document.documentElement) + .getPropertyValue("content"); + + switchTheme( + window.currentTheme, + window.mainTheme, + JSON.parse(cssTheme) || "light", + true + ); + }; + } + + // only listen to (prefers-color-scheme: dark) because light is the default + const mql = window.matchMedia("(prefers-color-scheme: dark)"); + + function handlePreferenceChange(mql) { + const use = theme => { + switchTheme(window.currentTheme, window.mainTheme, theme, true); + }; + // maybe the user has disabled the setting in the meantime! + if (getSettingValue("use-system-theme") !== "false") { + const lightTheme = getSettingValue("preferred-light-theme") || "light"; + const darkTheme = getSettingValue("preferred-dark-theme") || "dark"; + + if (mql.matches) { + use(darkTheme); + } else { + // prefers a light theme, or has no preference + use(lightTheme); + } + // note: we save the theme so that it doesn't suddenly change when + // the user disables "use-system-theme" and reloads the page or + // navigates to another page + } else { + use(getSettingValue("theme")); + } + } + + mql.addListener(handlePreferenceChange); + + return () => { + handlePreferenceChange(mql); + }; +})(); + +function switchToSavedTheme() { + switchTheme( + window.currentTheme, + window.mainTheme, + getSettingValue("theme") || "light", + false + ); +} + +if (getSettingValue("use-system-theme") !== "false" && window.matchMedia) { + // update the preferred dark theme if the user is already using a dark theme + // See https://github.com/rust-lang/rust/pull/77809#issuecomment-707875732 + if (getSettingValue("use-system-theme") === null + && getSettingValue("preferred-dark-theme") === null + && darkThemes.indexOf(localStoredTheme) >= 0) { + updateLocalStorage("preferred-dark-theme", localStoredTheme); + } + + // call the function to initialize the theme at least once! + updateSystemTheme(); +} else { + switchToSavedTheme(); +} + +if (getSettingValue("source-sidebar-show") === "true") { + // At this point in page load, `document.body` is not available yet. + // Set a class on the `<html>` element instead. + addClass(document.documentElement, "source-sidebar-expanded"); +} + +// If we navigate away (for example to a settings page), and then use the back or +// forward button to get back to a page, the theme may have changed in the meantime. +// But scripts may not be re-loaded in such a case due to the bfcache +// (https://web.dev/bfcache/). The "pageshow" event triggers on such navigations. +// Use that opportunity to update the theme. +// We use a setTimeout with a 0 timeout here to put the change on the event queue. +// For some reason, if we try to change the theme while the `pageshow` event is +// running, it sometimes fails to take effect. The problem manifests on Chrome, +// specifically when talking to a remote website with no caching. +window.addEventListener("pageshow", ev => { + if (ev.persisted) { + setTimeout(switchToSavedTheme, 0); + } +}); |