// Local js definitions:
/* global addClass, getSettingValue, hasClass, updateLocalStorage */
/* global onEachLazy, removeClass, getVar */

"use strict";

// The amount of time that the cursor must remain still over a hover target before
// revealing a tooltip.
//
// https://www.nngroup.com/articles/timing-exposing-content/
window.RUSTDOC_TOOLTIP_HOVER_MS = 300;
window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS = 450;

/**
 * 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.
 *
 * @param {string} basename
 * @param {string} extension
 */
function resourcePath(basename, extension) {
    return getVar("root-path") + basename + getVar("resource-suffix") + extension;
}

function hideMain() {
    addClass(document.getElementById(MAIN_ID), "hidden");
    const toggle = document.getElementById("toggle-all-docs");
    if (toggle) {
        toggle.setAttribute("disabled", "disabled");
    }
}

function showMain() {
    const main = document.getElementById(MAIN_ID);
    if (!main) {
        return;
    }
    removeClass(main, "hidden");
    const mainHeading = main.querySelector(".main-heading");
    if (mainHeading && window.searchState.rustdocToolbar) {
        if (window.searchState.rustdocToolbar.parentElement) {
            window.searchState.rustdocToolbar.parentElement.removeChild(
                window.searchState.rustdocToolbar,
            );
        }
        mainHeading.appendChild(window.searchState.rustdocToolbar);
    }
    const toggle = document.getElementById("toggle-all-docs");
    if (toggle) {
        toggle.removeAttribute("disabled");
    }
}

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 mobileTopbar = document.querySelector(".mobile-topbar");
    const locationTitle = document.querySelector(".sidebar h2.location");
    if (mobileTopbar) {
        const mobileTitle = document.createElement("h2");
        mobileTitle.className = "location";
        if (hasClass(document.querySelector(".rustdoc"), "crate")) {
            mobileTitle.innerHTML = `Crate <a href="#">${window.currentCrate}</a>`;
        } else if (locationTitle) {
            mobileTitle.innerHTML = locationTitle.innerHTML;
        }
        mobileTopbar.appendChild(mobileTitle);
    }
}

/**
 * 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.
 *
 * @param {KeyboardEvent} ev
 */
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 & { parentNode: 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;
        // @ts-expect-error
        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|null} elemToDisplay
 */
function switchDisplayedElement(elemToDisplay) {
    const el = getAlternativeDisplayElem();

    if (el.children.length > 0) {
        // @ts-expect-error
        getNotDisplayedElem().appendChild(el.firstElementChild);
    }
    if (elemToDisplay === null) {
        addClass(el, "hidden");
        showMain();
        return;
    }
    el.appendChild(elemToDisplay);
    hideMain();
    removeClass(el, "hidden");

    const mainHeading = elemToDisplay.querySelector(".main-heading");
    if (mainHeading && window.searchState.rustdocToolbar) {
        if (window.searchState.rustdocToolbar.parentElement) {
            window.searchState.rustdocToolbar.parentElement.removeChild(
                window.searchState.rustdocToolbar,
            );
        }
        mainHeading.appendChild(window.searchState.rustdocToolbar);
    }
}

function browserSupportsHistoryApi() {
    return window.history && typeof window.history.pushState === "function";
}

/**
 * Download CSS from the web without making it the active stylesheet.
 * We use this in the settings popover so that you don't get FOUC when switching.
 *
 * @param {string} cssUrl
 */
function preLoadCss(cssUrl) {
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
    const link = document.createElement("link");
    link.href = cssUrl;
    link.rel = "preload";
    link.as = "style";
    document.getElementsByTagName("head")[0].appendChild(link);
}

(function() {
    const isHelpPage = window.location.pathname.endsWith("/help.html");

    /**
     * Run a JavaScript file asynchronously.
     * @param {string} url
     * @param {function(): any} errorCallback
     */
    function loadScript(url, errorCallback) {
        const script = document.createElement("script");
        script.src = url;
        if (errorCallback !== undefined) {
            script.onerror = errorCallback;
        }
        document.head.append(script);
    }

    const settingsButton = getSettingsButton();
    if (settingsButton) {
        settingsButton.onclick = event => {
            if (event.ctrlKey || event.altKey || event.metaKey) {
                return;
            }
            window.hideAllModals(false);
            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.
            // @ts-expect-error
            loadScript(getVar("static-root-path") + getVar("settings-js"));
            // Pre-load all theme CSS files, so that switching feels seamless.
            //
            // When loading settings.html as a standalone page, the equivalent HTML is
            // generated in context.rs.
            setTimeout(() => {
                // @ts-expect-error
                const themes = getVar("themes").split(",");
                for (const theme of themes) {
                    // if there are no themes, do nothing
                    // "".split(",") == [""]
                    if (theme !== "") {
                        preLoadCss(getVar("root-path") + theme + ".css");
                    }
                }
            }, 0);
        };
    }

    window.searchState = {
        rustdocToolbar: document.querySelector("rustdoc-toolbar"),
        loadingText: "Loading search results...",
        // This will always be an HTMLInputElement, but tsc can't see that
        // @ts-expect-error
        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 (window.searchState.timeout !== null) {
                clearTimeout(window.searchState.timeout);
                window.searchState.timeout = null;
            }
        },
        isDisplayed: () => {
            const outputElement = window.searchState.outputElement();
            return !!outputElement &&
                !!outputElement.parentElement &&
                outputElement.parentElement.id === ALTERNATIVE_DISPLAY_ID;
        },
        // Sets the focus on the search bar at the top of the page
        focus: () => {
            window.searchState.input && window.searchState.input.focus();
        },
        // Removes the focus from the search bar.
        defocus: () => {
            window.searchState.input && window.searchState.input.blur();
        },
        showResults: search => {
            if (search === null || typeof search === "undefined") {
                search = window.searchState.outputElement();
            }
            switchDisplayedElement(search);
            document.title = window.searchState.title;
        },
        removeQueryParameters: () => {
            // We change the document title.
            document.title = window.searchState.titleBeforeSearch;
            if (browserSupportsHistoryApi()) {
                history.replaceState(null, "", getNakedUrl() + window.location.hash);
            }
        },
        hideResults: () => {
            switchDisplayedElement(null);
            // We also remove the query parameter from the URL.
            window.searchState.removeQueryParameters();
        },
        getQueryStringParams: () => {
            /** @type {Object.<any, string>} */
            const params = {};
            window.location.search.substring(1).split("&").
                map(s => {
                    // https://github.com/rust-lang/rust/issues/119219
                    const pair = s.split("=").map(x => x.replace(/\+/g, " "));
                    params[decodeURIComponent(pair[0])] =
                        typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]);
                });
            return params;
        },
        setup: () => {
            const search_input = window.searchState.input;
            if (!search_input) {
                return;
            }
            let searchLoaded = false;
            // If you're browsing the nightly docs, the page might need to be refreshed for the
            // search to work because the hash of the JS scripts might have changed.
            function sendSearchForm() {
                // @ts-expect-error
                document.getElementsByClassName("search-form")[0].submit();
            }
            function loadSearch() {
                if (!searchLoaded) {
                    searchLoaded = true;
                    // @ts-expect-error
                    loadScript(getVar("static-root-path") + getVar("search-js"), sendSearchForm);
                    loadScript(resourcePath("search-index", ".js"), sendSearchForm);
                }
            }

            search_input.addEventListener("focus", () => {
                window.searchState.origPlaceholder = search_input.placeholder;
                search_input.placeholder = "Type your search here.";
                loadSearch();
            });

            if (search_input.value !== "") {
                loadSearch();
            }

            const params = window.searchState.getQueryStringParams();
            if (params.search !== undefined) {
                window.searchState.setLoadingSearch();
                loadSearch();
            }
        },
        setLoadingSearch: () => {
            const search = window.searchState.outputElement();
            if (!search) {
                return;
            }
            search.innerHTML = "<h3 class=\"search-loading\">" +
                window.searchState.loadingText +
                "</h3>";
            window.searchState.showResults(search);
        },
        descShards: new Map(),
        loadDesc: async function({descShard, descIndex}) {
            if (descShard.promise === null) {
                descShard.promise = new Promise((resolve, reject) => {
                    // The `resolve` callback is stored in the `descShard`
                    // object, which is itself stored in `this.descShards` map.
                    // It is called in `loadedDescShard` by the
                    // search.desc script.
                    descShard.resolve = resolve;
                    const ds = descShard;
                    const fname = `${ds.crate}-desc-${ds.shard}-`;
                    const url = resourcePath(
                        `search.desc/${descShard.crate}/${fname}`,
                        ".js",
                    );
                    loadScript(url, reject);
                });
            }
            const list = await descShard.promise;
            return list[descIndex];
        },
        loadedDescShard: function(crate, shard, data) {
            // If loadedDescShard gets called, then the library must have been declared.
            // @ts-expect-error
            this.descShards.get(crate)[shard].resolve(data.split("\n"));
        },
    };

    const toggleAllDocsId = "toggle-all-docs";
    let savedHash = "";

    /**
     * @param {HashChangeEvent|null} ev
     */
    function handleHashes(ev) {
        if (ev !== null && window.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.
        const pageId = window.location.hash.replace(/^#/, "");
        if (savedHash !== pageId) {
            savedHash = pageId;
            if (pageId !== "") {
                expandSection(pageId);
            }
        }
        if (savedHash.startsWith("impl-")) {
            // impl-disambiguated links, used by the search engine
            // format: impl-X[-for-Y]/method.WHATEVER
            // turn this into method.WHATEVER[-NUMBER]
            const splitAt = savedHash.indexOf("/");
            if (splitAt !== -1) {
                const implId = savedHash.slice(0, splitAt);
                const assocId = savedHash.slice(splitAt + 1);
                const implElems = document.querySelectorAll(
                    `details > summary > section[id^="${implId}"]`,
                );
                onEachLazy(implElems, implElem => {
                    const numbered = /^(.+?)-([0-9]+)$/.exec(implElem.id);
                    if (implElem.id !== implId && (!numbered || numbered[1] !== implId)) {
                        return false;
                    }
                    return onEachLazy(implElem.parentElement.parentElement.querySelectorAll(
                        `[id^="${assocId}"]`),
                        item => {
                            const numbered = /^(.+?)-([0-9]+)$/.exec(item.id);
                            if (item.id === assocId || (numbered && numbered[1] === assocId)) {
                                openParentDetails(item);
                                item.scrollIntoView();
                                // Let the section expand itself before trying to highlight
                                setTimeout(() => {
                                    window.location.replace("#" + item.id);
                                }, 0);
                                return true;
                            }
                        },
                    );
                });
            }
        }
    }

    /**
     * @param {HashChangeEvent|null} ev
     */
    function onHashChange(ev) {
        // If we're in mobile mode, we should hide the sidebar in any case.
        hideSidebar();
        handleHashes(ev);
    }

    /**
     * @param {HTMLElement|null} elem
     */
    function openParentDetails(elem) {
        while (elem) {
            if (elem.tagName === "DETAILS") {
                // @ts-expect-error
                elem.open = true;
            }
            elem = elem.parentElement;
        }
    }

    /**
     * @param {string} id
     */
    function expandSection(id) {
        openParentDetails(document.getElementById(id));
    }

    /**
     * @param {KeyboardEvent} ev
     */
    function handleEscape(ev) {
        window.searchState.clearInputTimeout();
        window.searchState.hideResults();
        ev.preventDefault();
        window.searchState.defocus();
        window.hideAllModals(true); // true = reset focus for tooltips
    }

    /**
     * @param {KeyboardEvent} ev
     */
    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 &&
            document.activeElement.tagName === "INPUT" &&
            // @ts-expect-error
            document.activeElement.type !== "checkbox" &&
            // @ts-expect-error
            document.activeElement.type !== "radio") {
            switch (getVirtualKey(ev)) {
            case "Escape":
                handleEscape(ev);
                break;
            }
        } else {
            switch (getVirtualKey(ev)) {
            case "Escape":
                handleEscape(ev);
                break;

            case "s":
            case "S":
            case "/":
                ev.preventDefault();
                window.searchState.focus();
                break;

            case "+":
                ev.preventDefault();
                expandAllDocs();
                break;
            case "-":
                ev.preventDefault();
                collapseAllDocs();
                break;

            case "?":
                showHelp();
                break;

            default:
                break;
            }
        }
    }

    document.addEventListener("keypress", handleShortcut);
    document.addEventListener("keydown", handleShortcut);

    function addSidebarItems() {
        if (!window.SIDEBAR_ITEMS) {
            return;
        }
        const sidebar = document.getElementById("rustdoc-modnav");

        /**
         * 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) {
            // @ts-expect-error
            const filtered = window.SIDEBAR_ITEMS[shortty];
            if (!filtered) {
                return;
            }

            const modpath = hasClass(document.querySelector(".rustdoc"), "mod") ? "../" : "";

            const h3 = document.createElement("h3");
            h3.innerHTML = `<a href="${modpath}index.html#${id}">${longty}</a>`;
            const ul = document.createElement("ul");
            ul.className = "block " + shortty;

            for (const name of filtered) {
                let path;
                if (shortty === "mod") {
                    path = `${modpath}${name}/index.html`;
                } else {
                    path = `${modpath}${shortty}.${name}.html`;
                }
                let current_page = document.location.href.toString();
                if (current_page.endsWith("/")) {
                    current_page += "index.html";
                }
                const link = document.createElement("a");
                link.href = path;
                link.textContent = name;
                const li = document.createElement("li");
                // Don't "optimize" this to just use `path`.
                // We want the browser to normalize this into an absolute URL.
                if (link.href === current_page) {
                    li.classList.add("current");
                }
                li.appendChild(link);
                ul.appendChild(li);
            }
            // @ts-expect-error
            sidebar.appendChild(h3);
            // @ts-expect-error
            sidebar.appendChild(ul);
        }

        if (sidebar) {
            // keep this synchronized with ItemSection::ALL in html/render/mod.rs
            // Re-exports aren't shown here, because they don't have child pages
            //block("reexport", "reexports", "Re-exports");
            block("primitive", "primitives", "Primitive Types");
            block("mod", "modules", "Modules");
            block("macro", "macros", "Macros");
            block("struct", "structs", "Structs");
            block("enum", "enums", "Enums");
            block("constant", "constants", "Constants");
            block("static", "static", "Statics");
            block("trait", "traits", "Traits");
            block("fn", "functions", "Functions");
            block("type", "types", "Type Aliases");
            block("union", "unions", "Unions");
            // No point, because these items don't appear in modules
            //block("impl", "impls", "Implementations");
            //block("tymethod", "tymethods", "Type Methods");
            //block("method", "methods", "Methods");
            //block("structfield", "fields", "Fields");
            //block("variant", "variants", "Variants");
            //block("associatedtype", "associated-types", "Associated Types");
            //block("associatedconstant", "associated-consts", "Associated Constants");
            block("foreigntype", "foreign-types", "Foreign Types");
            block("keyword", "keywords", "Keywords");
            block("attr", "attributes", "Attribute Macros");
            block("derive", "derives", "Derive Macros");
            block("traitalias", "trait-aliases", "Trait Aliases");
        }
    }

    // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code>
    window.register_implementors = imp => {
        const implementors = document.getElementById("implementors-list");
        const synthetic_implementors = document.getElementById("synthetic-implementors-list");
        const inlined_types = new Set();

        const TEXT_IDX = 0;
        const SYNTHETIC_IDX = 1;
        const TYPES_IDX = 2;

        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;
                }
                // @ts-expect-error
                aliases.split(",").forEach(alias => {
                    inlined_types.add(alias);
                });
            });
        }

        // @ts-expect-error
        let currentNbImpls = implementors.getElementsByClassName("impl").length;
        // @ts-expect-error
        const traitName = document.querySelector(".main-heading h1 > .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 script = document
            .querySelector("script[data-ignore-extern-crates]");
        const ignoreExternCrates = new Set(
            // @ts-expect-error
            (script ? script.getAttribute("data-ignore-extern-crates") : "").split(","),
        );
        for (const lib of libs) {
            if (lib === window.currentCrate || ignoreExternCrates.has(lib)) {
                continue;
            }
            const structs = imp[lib];

            struct_loop:
            for (const struct of structs) {
                const list = struct[SYNTHETIC_IDX] ? synthetic_implementors : implementors;

                // The types list is only used for synthetic impls.
                // If this changes, `main.js` and `write_shared.rs` both need changed.
                if (struct[SYNTHETIC_IDX]) {
                    for (const struct_type of struct[TYPES_IDX]) {
                        if (inlined_types.has(struct_type)) {
                            continue struct_loop;
                        }
                        inlined_types.add(struct_type);
                    }
                }

                const code = document.createElement("h3");
                code.innerHTML = struct[TEXT_IDX];
                addClass(code, "code-header");

                onEachLazy(code.getElementsByTagName("a"), elem => {
                    const href = elem.getAttribute("href");

                    if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
                        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);
                // @ts-expect-error
                list.appendChild(display);
                currentNbImpls += 1;
            }
        }
    };
    if (window.pending_implementors) {
        window.register_implementors(window.pending_implementors);
    }

    /**
     * <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+type.impl&type=code>
     *
     * [RUSTDOCIMPL] type.impl
     *
     * This code inlines implementations into the type alias docs at runtime. It's done at
     * runtime because some crates have many type aliases and many methods, and we don't want
     * to generate *O*`(types*methods)` HTML text. The data inside is mostly HTML fragments,
     * wrapped in JSON.
     *
     * - It only includes docs generated for the current crate. This function accepts an
     *   object mapping crate names to the set of impls.
     *
     * - It filters down to the set of applicable impls. The Rust type checker is used to
     *   tag each HTML blob with the set of type aliases that can actually use it, so the
     *   JS only needs to consult the attached list of type aliases.
     *
     * - It renames the ID attributes, to avoid conflicting IDs in the resulting DOM.
     *
     * - It adds the necessary items to the sidebar. If it's an inherent impl, that means
     *   adding methods, associated types, and associated constants. If it's a trait impl,
     *   that means adding it to the trait impl sidebar list.
     *
     * - It adds the HTML block itself. If it's an inherent impl, it goes after the type
     *   alias's own inherent impls. If it's a trait impl, it goes in the Trait
     *   Implementations section.
     *
     * - After processing all of the impls, it sorts the sidebar items by name.
     *
     * @param {rustdoc.TypeImpls} imp
     */
    window.register_type_impls = imp => {
        // @ts-expect-error
        if (!imp || !imp[window.currentCrate]) {
            return;
        }
        window.pending_type_impls = undefined;
        const idMap = new Map();

        let implementations = document.getElementById("implementations-list");
        let trait_implementations = document.getElementById("trait-implementations-list");
        let trait_implementations_header = document.getElementById("trait-implementations");

        // We want to include the current type alias's impls, and no others.
        const script = document.querySelector("script[data-self-path]");
        const selfPath = script ? script.getAttribute("data-self-path") : null;

        // These sidebar blocks need filled in, too.
        const mainContent = document.querySelector("#main-content");
        const sidebarSection = document.querySelector(".sidebar section");
        let methods = document.querySelector(".sidebar .block.method");
        let associatedTypes = document.querySelector(".sidebar .block.associatedtype");
        let associatedConstants = document.querySelector(".sidebar .block.associatedconstant");
        let sidebarTraitList = document.querySelector(".sidebar .block.trait-implementation");

        // @ts-expect-error
        for (const impList of imp[window.currentCrate]) {
            const types = impList.slice(2);
            const text = impList[0];
            const isTrait = impList[1] !== 0;
            const traitName = impList[1];
            if (types.indexOf(selfPath) === -1) {
                continue;
            }
            let outputList = isTrait ? trait_implementations : implementations;
            if (outputList === null) {
                const outputListName = isTrait ? "Trait Implementations" : "Implementations";
                const outputListId = isTrait ?
                    "trait-implementations-list" :
                    "implementations-list";
                const outputListHeaderId = isTrait ? "trait-implementations" : "implementations";
                const outputListHeader = document.createElement("h2");
                outputListHeader.id = outputListHeaderId;
                outputListHeader.innerText = outputListName;
                outputList = document.createElement("div");
                outputList.id = outputListId;
                if (isTrait) {
                    const link = document.createElement("a");
                    link.href = `#${outputListHeaderId}`;
                    link.innerText = "Trait Implementations";
                    const h = document.createElement("h3");
                    h.appendChild(link);
                    trait_implementations = outputList;
                    trait_implementations_header = outputListHeader;
                    // @ts-expect-error
                    sidebarSection.appendChild(h);
                    sidebarTraitList = document.createElement("ul");
                    sidebarTraitList.className = "block trait-implementation";
                    // @ts-expect-error
                    sidebarSection.appendChild(sidebarTraitList);
                    // @ts-expect-error
                    mainContent.appendChild(outputListHeader);
                    // @ts-expect-error
                    mainContent.appendChild(outputList);
                } else {
                    implementations = outputList;
                    if (trait_implementations) {
                        // @ts-expect-error
                        mainContent.insertBefore(outputListHeader, trait_implementations_header);
                        // @ts-expect-error
                        mainContent.insertBefore(outputList, trait_implementations_header);
                    } else {
                        const mainContent = document.querySelector("#main-content");
                        // @ts-expect-error
                        mainContent.appendChild(outputListHeader);
                        // @ts-expect-error
                        mainContent.appendChild(outputList);
                    }
                }
            }
            const template = document.createElement("template");
            template.innerHTML = text;

            onEachLazy(template.content.querySelectorAll("a"), elem => {
                const href = elem.getAttribute("href");

                if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
                    elem.setAttribute("href", window.rootPath + href);
                }
            });
            onEachLazy(template.content.querySelectorAll("[id]"), el => {
                let i = 0;
                if (idMap.has(el.id)) {
                    i = idMap.get(el.id);
                } else if (document.getElementById(el.id)) {
                    i = 1;
                    while (document.getElementById(`${el.id}-${2 * i}`)) {
                        i = 2 * i;
                    }
                    while (document.getElementById(`${el.id}-${i}`)) {
                        i += 1;
                    }
                }
                if (i !== 0) {
                    const oldHref = `#${el.id}`;
                    const newHref = `#${el.id}-${i}`;
                    el.id = `${el.id}-${i}`;
                    onEachLazy(template.content.querySelectorAll("a[href]"), link => {
                        if (link.getAttribute("href") === oldHref) {
                            link.href = newHref;
                        }
                    });
                }
                idMap.set(el.id, i + 1);
            });
            const templateAssocItems = template.content.querySelectorAll("section.tymethod, " +
                "section.method, section.associatedtype, section.associatedconstant");
            if (isTrait) {
                const li = document.createElement("li");
                const a = document.createElement("a");
                // @ts-expect-error
                a.href = `#${template.content.querySelector(".impl").id}`;
                a.textContent = traitName;
                li.appendChild(a);
                // @ts-expect-error
                sidebarTraitList.append(li);
            } else {
                onEachLazy(templateAssocItems, item => {
                    let block = hasClass(item, "associatedtype") ? associatedTypes : (
                        hasClass(item, "associatedconstant") ? associatedConstants : (
                        methods));
                    if (!block) {
                        const blockTitle = hasClass(item, "associatedtype") ? "Associated Types" : (
                            hasClass(item, "associatedconstant") ? "Associated Constants" : (
                            "Methods"));
                        const blockClass = hasClass(item, "associatedtype") ? "associatedtype" : (
                            hasClass(item, "associatedconstant") ? "associatedconstant" : (
                            "method"));
                        const blockHeader = document.createElement("h3");
                        const blockLink = document.createElement("a");
                        blockLink.href = "#implementations";
                        blockLink.innerText = blockTitle;
                        blockHeader.appendChild(blockLink);
                        block = document.createElement("ul");
                        block.className = `block ${blockClass}`;
                        const insertionReference = methods || sidebarTraitList;
                        if (insertionReference) {
                            const insertionReferenceH = insertionReference.previousElementSibling;
                            // @ts-expect-error
                            sidebarSection.insertBefore(blockHeader, insertionReferenceH);
                            // @ts-expect-error
                            sidebarSection.insertBefore(block, insertionReferenceH);
                        } else {
                            // @ts-expect-error
                            sidebarSection.appendChild(blockHeader);
                            // @ts-expect-error
                            sidebarSection.appendChild(block);
                        }
                        if (hasClass(item, "associatedtype")) {
                            associatedTypes = block;
                        } else if (hasClass(item, "associatedconstant")) {
                            associatedConstants = block;
                        } else {
                            methods = block;
                        }
                    }
                    const li = document.createElement("li");
                    const a = document.createElement("a");
                    a.innerText = item.id.split("-")[0].split(".")[1];
                    a.href = `#${item.id}`;
                    li.appendChild(a);
                    block.appendChild(li);
                });
            }
            outputList.appendChild(template.content);
        }

        for (const list of [methods, associatedTypes, associatedConstants, sidebarTraitList]) {
            if (!list) {
                continue;
            }
            const newChildren = Array.prototype.slice.call(list.children);
            newChildren.sort((a, b) => {
                const aI = a.innerText;
                const bI = b.innerText;
                return aI < bI ? -1 :
                    aI > bI ? 1 :
                    0;
            });
            list.replaceChildren(...newChildren);
        }
    };
    if (window.pending_type_impls) {
        window.register_type_impls(window.pending_type_impls);
    }

    function addSidebarCrates() {
        // @ts-expect-error
        if (!window.ALL_CRATES) {
            return;
        }
        const sidebarElems = document.getElementById("rustdoc-modnav");
        if (!sidebarElems) {
            return;
        }
        // Draw a convenient sidebar of known crates if we have a listing
        const h3 = document.createElement("h3");
        h3.innerHTML = "Crates";
        const ul = document.createElement("ul");
        ul.className = "block crate";

        // @ts-expect-error
        for (const crate of window.ALL_CRATES) {
            const link = document.createElement("a");
            link.href = window.rootPath + crate + "/index.html";
            link.textContent = crate;

            const li = document.createElement("li");
            if (window.rootPath !== "./" && crate === window.currentCrate) {
                li.className = "current";
            }
            li.appendChild(link);
            ul.appendChild(li);
        }
        sidebarElems.appendChild(h3);
        sidebarElems.appendChild(ul);
    }

    function expandAllDocs() {
        const innerToggle = document.getElementById(toggleAllDocsId);
        removeClass(innerToggle, "will-expand");
        onEachLazy(document.getElementsByClassName("toggle"), e => {
            if (!hasClass(e, "type-contents-toggle") && !hasClass(e, "more-examples-toggle")) {
                e.open = true;
            }
        });
        // @ts-expect-error
        innerToggle.children[0].innerText = "Summary";
    }

    function collapseAllDocs() {
        const innerToggle = document.getElementById(toggleAllDocsId);
        addClass(innerToggle, "will-expand");
        onEachLazy(document.getElementsByClassName("toggle"), e => {
            if (e.parentNode.id !== "implementations-list" ||
                (!hasClass(e, "implementors-toggle") &&
                 !hasClass(e, "type-contents-toggle"))
            ) {
                e.open = false;
            }
        });
        // @ts-expect-error
        innerToggle.children[0].innerText = "Show all";
    }

    function toggleAllDocs() {
        const innerToggle = document.getElementById(toggleAllDocsId);
        if (!innerToggle) {
            return;
        }
        if (hasClass(innerToggle, "will-expand")) {
            expandAllDocs();
        } else {
            collapseAllDocs();
        }
    }

    (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";

        // @ts-expect-error
        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("toggle"), e => {
            if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) {
                e.open = true;
            }
            if (hideMethodDocs && hasClass(e, "method-toggle")) {
                e.open = false;
            }

        });
    }());

    window.rustdoc_add_line_numbers_to_examples = () => {
        // @ts-expect-error
        function generateLine(nb) {
            return `<span data-nosnippet>${nb}</span>`;
        }

        onEachLazy(document.querySelectorAll(
            ".rustdoc:not(.src) :not(.scraped-example) > .example-wrap > pre > code",
        ), code => {
            if (hasClass(code.parentElement.parentElement, "hide-lines")) {
                removeClass(code.parentElement.parentElement, "hide-lines");
                return;
            }
            const lines = code.innerHTML.split("\n");
            const digits = (lines.length + "").length;
            // @ts-expect-error
            code.innerHTML = lines.map((line, index) => generateLine(index + 1) + line).join("\n");
            addClass(code.parentElement.parentElement, `digits-${digits}`);
        });
    };

    window.rustdoc_remove_line_numbers_from_examples = () => {
        onEachLazy(
            document.querySelectorAll(".rustdoc:not(.src) :not(.scraped-example) > .example-wrap"),
            x => addClass(x, "hide-lines"),
        );
    };

    if (getSettingValue("line-numbers") === "true") {
        window.rustdoc_add_line_numbers_to_examples();
    }

    function showSidebar() {
        window.hideAllModals(false);
        const sidebar = document.getElementsByClassName("sidebar")[0];
        addClass(sidebar, "shown");
    }

    function hideSidebar() {
        const sidebar = document.getElementsByClassName("sidebar")[0];
        removeClass(sidebar, "shown");
    }

    window.addEventListener("resize", () => {
        // @ts-expect-error
        if (window.CURRENT_TOOLTIP_ELEMENT) {
            // As a workaround to the behavior of `contains: layout` used in doc togglers,
            // tooltip popovers are positioned using javascript.
            //
            // This means when the window is resized, we need to redo the layout.
            // @ts-expect-error
            const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE;
            const force_visible = base.TOOLTIP_FORCE_VISIBLE;
            hideTooltip(false);
            if (force_visible) {
                showTooltip(base);
                base.TOOLTIP_FORCE_VISIBLE = true;
            }
        }
    });

    const mainElem = document.getElementById(MAIN_ID);
    if (mainElem) {
        mainElem.addEventListener("click", hideSidebar);
    }

    onEachLazy(document.querySelectorAll("a[href^='#']"), 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.
        el.addEventListener("click", () => {
            expandSection(el.hash.slice(1));
            hideSidebar();
        });
    });

    onEachLazy(document.querySelectorAll(".toggle > summary:not(.hideme)"), el => {
        // @ts-expect-error
        // Clicking on the summary's contents should not collapse it,
        // but links within should still fire.
        el.addEventListener("click", e => {
            if (!e.target.matches("summary, a, a *")) {
                e.preventDefault();
            }
        });
    });

    /**
     * Show a tooltip immediately.
     *
     * @param {HTMLElement} e - The tooltip's anchor point. The DOM is consulted to figure
     *                          out what the tooltip should contain, and where it should be
     *                          positioned.
     */
    function showTooltip(e) {
        const notable_ty = e.getAttribute("data-notable-ty");
        // @ts-expect-error
        if (!window.NOTABLE_TRAITS && notable_ty) {
            const data = document.getElementById("notable-traits-data");
            if (data) {
                // @ts-expect-error
                window.NOTABLE_TRAITS = JSON.parse(data.innerText);
            } else {
                throw new Error("showTooltip() called with notable without any notable traits!");
            }
        }
        // Make this function idempotent. If the tooltip is already shown, avoid doing extra work
        // and leave it alone.
        // @ts-expect-error
        if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
            // @ts-expect-error
            clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
            return;
        }
        window.hideAllModals(false);
        const wrapper = document.createElement("div");
        if (notable_ty) {
            wrapper.innerHTML = "<div class=\"content\">" +
                // @ts-expect-error
                window.NOTABLE_TRAITS[notable_ty] + "</div>";
        } else {
            // Replace any `title` attribute with `data-title` to avoid double tooltips.
            const ttl = e.getAttribute("title");
            if (ttl !== null) {
                e.setAttribute("data-title", ttl);
                e.removeAttribute("title");
            }
            const dttl = e.getAttribute("data-title");
            if (dttl !== null) {
                const titleContent = document.createElement("div");
                titleContent.className = "content";
                titleContent.appendChild(document.createTextNode(dttl));
                wrapper.appendChild(titleContent);
            }
        }
        wrapper.className = "tooltip popover";
        const focusCatcher = document.createElement("div");
        focusCatcher.setAttribute("tabindex", "0");
        // @ts-expect-error
        focusCatcher.onfocus = hideTooltip;
        wrapper.appendChild(focusCatcher);
        const pos = e.getBoundingClientRect();
        // 5px overlap so that the mouse can easily travel from place to place
        wrapper.style.top = (pos.top + window.scrollY + pos.height) + "px";
        // @ts-expect-error
        wrapper.style.left = 0;
        wrapper.style.right = "auto";
        wrapper.style.visibility = "hidden";
        document.body.appendChild(wrapper);
        const wrapperPos = wrapper.getBoundingClientRect();
        // offset so that the arrow points at the center of the "(i)"
        const finalPos = pos.left + window.scrollX - wrapperPos.width + 24;
        if (finalPos > 0) {
            wrapper.style.left = finalPos + "px";
        } else {
            wrapper.style.setProperty(
                "--popover-arrow-offset",
                (wrapperPos.right - pos.right + 4) + "px",
            );
        }
        wrapper.style.visibility = "";
        // @ts-expect-error
        window.CURRENT_TOOLTIP_ELEMENT = wrapper;
        // @ts-expect-error
        window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
        // @ts-expect-error
        clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
        wrapper.onpointerenter = ev => {
            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
            if (ev.pointerType !== "mouse") {
                return;
            }
            clearTooltipHoverTimeout(e);
        };
        wrapper.onpointerleave = ev => {
            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
            if (ev.pointerType !== "mouse" || !(ev.relatedTarget instanceof HTMLElement)) {
                return;
            }
            if (!e.TOOLTIP_FORCE_VISIBLE && !e.contains(ev.relatedTarget)) {
                // See "Tooltip pointer leave gesture" below.
                setTooltipHoverTimeout(e, false);
                addClass(wrapper, "fade-out");
            }
        };
    }

    /**
     * Show or hide the tooltip after a timeout. If a timeout was already set before this function
     * was called, that timeout gets cleared. If the tooltip is already in the requested state,
     * this function will still clear any pending timeout, but otherwise do nothing.
     *
     * @param {HTMLElement} element - The tooltip's anchor point. The DOM is consulted to figure
     *                                out what the tooltip should contain, and where it should be
     *                                positioned.
     * @param {boolean}    show    - If true, the tooltip will be made visible. If false, it will
     *                               be hidden.
     */
    function setTooltipHoverTimeout(element, show) {
        clearTooltipHoverTimeout(element);
        // @ts-expect-error
        if (!show && !window.CURRENT_TOOLTIP_ELEMENT) {
            // To "hide" an already hidden element, just cancel its timeout.
            return;
        }
        // @ts-expect-error
        if (show && window.CURRENT_TOOLTIP_ELEMENT) {
            // To "show" an already visible element, just cancel its timeout.
            return;
        }
        // @ts-expect-error
        if (window.CURRENT_TOOLTIP_ELEMENT &&
            // @ts-expect-error
            window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) {
            // Don't do anything if another tooltip is already visible.
            return;
        }
        element.TOOLTIP_HOVER_TIMEOUT = setTimeout(() => {
            if (show) {
                showTooltip(element);
            } else if (!element.TOOLTIP_FORCE_VISIBLE) {
                hideTooltip(false);
            }
        }, show ? window.RUSTDOC_TOOLTIP_HOVER_MS : window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS);
    }

    /**
     * If a show/hide timeout was set by `setTooltipHoverTimeout`, cancel it. If none exists,
     * do nothing.
     *
     * @param {HTMLElement} element - The tooltip's anchor point,
     *                                as passed to `setTooltipHoverTimeout`.
     */
    function clearTooltipHoverTimeout(element) {
        if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) {
            // @ts-expect-error
            removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
            clearTimeout(element.TOOLTIP_HOVER_TIMEOUT);
            delete element.TOOLTIP_HOVER_TIMEOUT;
        }
    }

    // @ts-expect-error
    function tooltipBlurHandler(event) {
        // @ts-expect-error
        if (window.CURRENT_TOOLTIP_ELEMENT &&
            // @ts-expect-error
            !window.CURRENT_TOOLTIP_ELEMENT.contains(document.activeElement) &&
            // @ts-expect-error
            !window.CURRENT_TOOLTIP_ELEMENT.contains(event.relatedTarget) &&
            // @ts-expect-error
            !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(document.activeElement) &&
            // @ts-expect-error
            !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(event.relatedTarget)
        ) {
            // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari.
            // When I click the button on an already-opened tooltip popover, Safari
            // hides the popover and then immediately shows it again, while everyone else hides it
            // and it stays hidden.
            //
            // To work around this, make sure the click finishes being dispatched before
            // hiding the popover. Since `hideTooltip()` is idempotent, this makes Safari behave
            // consistently with the other two.
            setTimeout(() => hideTooltip(false), 0);
        }
    }

    /**
     * Hide the current tooltip immediately.
     *
     * @param {boolean} focus - If set to `true`, move keyboard focus to the tooltip anchor point.
     *                          If set to `false`, leave keyboard focus alone.
     */
    function hideTooltip(focus) {
        // @ts-expect-error
        if (window.CURRENT_TOOLTIP_ELEMENT) {
            // @ts-expect-error
            if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
                if (focus) {
                    // @ts-expect-error
                    window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus();
                }
                // @ts-expect-error
                window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false;
            }
            // @ts-expect-error
            document.body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
            // @ts-expect-error
            clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
            // @ts-expect-error
            window.CURRENT_TOOLTIP_ELEMENT = null;
        }
    }

    onEachLazy(document.getElementsByClassName("tooltip"), e => {
        e.onclick = () => {
            e.TOOLTIP_FORCE_VISIBLE = e.TOOLTIP_FORCE_VISIBLE ? false : true;
            // @ts-expect-error
            if (window.CURRENT_TOOLTIP_ELEMENT && !e.TOOLTIP_FORCE_VISIBLE) {
                hideTooltip(true);
            } else {
                showTooltip(e);
                // @ts-expect-error
                window.CURRENT_TOOLTIP_ELEMENT.setAttribute("tabindex", "0");
                // @ts-expect-error
                window.CURRENT_TOOLTIP_ELEMENT.focus();
                // @ts-expect-error
                window.CURRENT_TOOLTIP_ELEMENT.onblur = tooltipBlurHandler;
            }
            return false;
        };
        // @ts-expect-error
        e.onpointerenter = ev => {
            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
            if (ev.pointerType !== "mouse") {
                return;
            }
            setTooltipHoverTimeout(e, true);
        };
        // @ts-expect-error
        e.onpointermove = ev => {
            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
            if (ev.pointerType !== "mouse") {
                return;
            }
            setTooltipHoverTimeout(e, true);
        };
        // @ts-expect-error
        e.onpointerleave = ev => {
            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
            if (ev.pointerType !== "mouse") {
                return;
            }
            // @ts-expect-error
            if (!e.TOOLTIP_FORCE_VISIBLE && window.CURRENT_TOOLTIP_ELEMENT &&
                // @ts-expect-error
                !window.CURRENT_TOOLTIP_ELEMENT.contains(ev.relatedTarget)) {
                // Tooltip pointer leave gesture:
                //
                // Designing a good hover microinteraction is a matter of guessing user
                // intent from what are, literally, vague gestures. In this case, guessing if
                // hovering in or out of the tooltip base is intentional or not.
                //
                // To figure this out, a few different techniques are used:
                //
                // * When the mouse pointer enters a tooltip anchor point, its hitbox is grown
                //   on the bottom, where the popover is/will appear. Search "hover tunnel" in
                //   rustdoc.css for the implementation.
                // * There's a delay when the mouse pointer enters the popover base anchor, in
                //   case the mouse pointer was just passing through and the user didn't want
                //   to open it.
                // * Similarly, a delay is added when exiting the anchor, or the popover
                //   itself, before hiding it.
                // * A fade-out animation is layered onto the pointer exit delay to immediately
                //   inform the user that they successfully dismissed the popover, while still
                //   providing a way for them to cancel it if it was a mistake and they still
                //   wanted to interact with it.
                // * No animation is used for revealing it, because we don't want people to try
                //   to interact with an element while it's in the middle of fading in: either
                //   they're allowed to interact with it while it's fading in, meaning it can't
                //   serve as mistake-proofing for the popover, or they can't, but
                //   they might try and be frustrated.
                //
                // See also:
                // * https://www.nngroup.com/articles/timing-exposing-content/
                // * https://www.nngroup.com/articles/tooltip-guidelines/
                // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown
                setTooltipHoverTimeout(e, false);
                // @ts-expect-error
                addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
            }
        };
    });

    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];
            // @ts-expect-error
            if (!hasClass(sidebar, "shown")) {
                showSidebar();
            } else {
                hideSidebar();
            }
        });
    }

    // @ts-expect-error
    function helpBlurHandler(event) {
        // @ts-expect-error
        if (!getHelpButton().contains(document.activeElement) &&
            // @ts-expect-error
            !getHelpButton().contains(event.relatedTarget) &&
            // @ts-expect-error
            !getSettingsButton().contains(document.activeElement) &&
            // @ts-expect-error
            !getSettingsButton().contains(event.relatedTarget)
        ) {
            window.hidePopoverMenus();
        }
    }

    function buildHelpMenu() {
        const book_info = document.createElement("span");
        const drloChannel = `https://doc.rust-lang.org/${getVar("channel")}`;
        book_info.className = "top";
        book_info.innerHTML = `You can find more information in \
<a href="${drloChannel}/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)"],
            ["&#9166;", "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 = [
            `For a full list of all search features, take a look \
             <a href="${drloChannel}/rustdoc/read-documentation/search.html">here</a>.`,
            "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 -&gt; usize</code> or \
             <code>-&gt; vec</code> or <code>String, enum:Cow -&gt; bool</code>)",
            "You can look for items with an exact name by putting double quotes around \
             your request: <code>\"string\"</code>",
             `Look for functions that accept or return \
              <a href="${drloChannel}/std/primitive.slice.html">slices</a> and \
              <a href="${drloChannel}/std/primitive.array.html">arrays</a> by writing square \
              brackets (e.g., <code>-&gt; [u8]</code> or <code>[] -&gt; Option</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");
        if (!isHelpPage) {
            container.className = "popover";
        }
        container.id = "help";
        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);

        if (isHelpPage) {
            const help_section = document.createElement("section");
            help_section.appendChild(container);
            // @ts-expect-error
            document.getElementById("main-content").appendChild(help_section);
            container.style.display = "block";
        } else {
            const help_button = getHelpButton();
            // @ts-expect-error
            help_button.appendChild(container);

            container.onblur = helpBlurHandler;
            // @ts-expect-error
            help_button.onblur = helpBlurHandler;
            // @ts-expect-error
            help_button.children[0].onblur = helpBlurHandler;
        }

        return container;
    }

    /**
     * Hide popover menus, clickable tooltips, and the sidebar (if applicable).
     *
     * Pass `true` to reset focus for tooltip popovers.
     */
    window.hideAllModals = switchFocus => {
        hideSidebar();
        window.hidePopoverMenus();
        hideTooltip(switchFocus);
    };

    /**
     * Hide all the popover menus.
     */
    window.hidePopoverMenus = () => {
        onEachLazy(document.querySelectorAll("rustdoc-toolbar .popover"), elem => {
            elem.style.display = "none";
        });
        const button = getHelpButton();
        if (button) {
            removeClass(button, "help-open");
        }
    };

    /**
     * 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) {
        // @ts-expect-error
        let menu = getHelpButton().querySelector(".popover");
        if (!menu && buildNeeded) {
            menu = buildHelpMenu();
        }
        // @ts-expect-error
        return menu;
    }

    /**
     * Show the help popup menu.
     */
    function showHelp() {
        // Prevent `blur` events from being dispatched as a result of closing
        // other modals.
        const button = getHelpButton();
        addClass(button, "help-open");
        // @ts-expect-error
        button.querySelector("a").focus();
        const menu = getHelpMenu(true);
        if (menu.style.display === "none") {
            // @ts-expect-error
            window.hideAllModals();
            menu.style.display = "";
        }
    }

    const helpLink = document.querySelector(`#${HELP_BUTTON_ID} > a`);
    if (isHelpPage) {
        buildHelpMenu();
    } else if (helpLink) {
        helpLink.addEventListener("click", event => {
            // By default, have help button open docs in a popover.
            // If user clicks with a moderator, though, use default browser behavior,
            // probably opening in a new window or tab.
            if (!helpLink.contains(helpLink) ||
                // @ts-expect-error
                event.ctrlKey ||
                // @ts-expect-error
                event.altKey ||
                // @ts-expect-error
                event.metaKey) {
                return;
            }
            event.preventDefault();
            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);
    window.searchState.setup();
}());

// Hide, show, and resize the sidebar
//
// The body class and CSS variable are initially set up in storage.js,
// but in this file, we implement:
//
//   * the show sidebar button, which appears if the sidebar is hidden
//     and, by clicking on it, will bring it back
//   * the sidebar resize handle, which appears only on large viewports
//     with a [fine precision pointer] to allow the user to change
//     the size of the sidebar
//
// [fine precision pointer]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer
(function() {
    // 100 is the size of the logo
    // don't let the sidebar get smaller than that, or it'll get squished
    const SIDEBAR_MIN = 100;
    // Don't let the sidebar get bigger than this
    const SIDEBAR_MAX = 500;
    // Don't let the body (including the gutter) get smaller than this
    //
    // WARNING: RUSTDOC_MOBILE_BREAKPOINT MEDIA QUERY
    // Acceptable values for BODY_MIN are constrained by the mobile breakpoint
    // (which is the minimum size of the whole page where the sidebar exists)
    // and the default sidebar width:
    //
    //     BODY_MIN <= RUSTDOC_MOBILE_BREAKPOINT - DEFAULT_SIDEBAR_WIDTH
    //
    // At the time of this writing, the DEFAULT_SIDEBAR_WIDTH on src pages is
    // 300px, and the RUSTDOC_MOBILE_BREAKPOINT is 700px, so BODY_MIN must be
    // at most 400px. Otherwise, it would start out at the default size, then
    // grabbing the resize handle would suddenly cause it to jank to
    // its contraint-generated maximum.
    const RUSTDOC_MOBILE_BREAKPOINT = 700;
    const BODY_MIN = 400;
    // At half-way past the minimum size, vanish the sidebar entirely
    const SIDEBAR_VANISH_THRESHOLD = SIDEBAR_MIN / 2;

    // Toolbar button to show the sidebar.
    //
    // On small, "mobile-sized" viewports, it's not persistent and it
    // can only be activated by going into Settings and hiding the nav bar.
    // On larger, "desktop-sized" viewports (though that includes many
    // tablets), it's fixed-position, appears in the left side margin,
    // and it can be activated by resizing the sidebar into nothing.
    const sidebarButton = document.getElementById("sidebar-button");
    if (sidebarButton) {
        sidebarButton.addEventListener("click", e => {
            removeClass(document.documentElement, "hide-sidebar");
            updateLocalStorage("hide-sidebar", "false");
            if (document.querySelector(".rustdoc.src")) {
                // @ts-expect-error
                window.rustdocToggleSrcSidebar();
            }
            e.preventDefault();
        });
    }

    /**
     * Pointer capture.
     *
     * Resizing is a single-pointer gesture. Any secondary pointer is ignored
     *
     * @type {null|number}
     */
    let currentPointerId = null;

    /**
     * "Desired" sidebar size.
     *
     * This is stashed here for window resizing. If the sidebar gets
     * shrunk to maintain BODY_MIN, and then the user grows the window again,
     * it gets the sidebar to restore its size.
     *
     * @type {null|number}
     */
    let desiredSidebarSize = null;

    /**
     * Sidebar resize debouncer.
     *
     * The sidebar itself is resized instantly, but the body HTML can be too
     * big for that, causing reflow jank. To reduce this, we queue up a separate
     * animation frame and throttle it.
     *
     * @type {false|ReturnType<typeof setTimeout>}
     */
    let pendingSidebarResizingFrame = false;

    /** @type {HTMLElement|null} */
    const resizer = document.querySelector(".sidebar-resizer");
    /** @type {HTMLElement|null} */
    const sidebar = document.querySelector(".sidebar");
    // If this page has no sidebar at all, bail out.
    if (!resizer || !sidebar) {
        return;
    }

    // src page and docs page use different variables, because the contents of
    // the sidebar are so different that it's reasonable to thing the user
    // would want them to have different sizes
    const isSrcPage = hasClass(document.body, "src");

    // Call this function to hide the sidebar when using the resize handle
    //
    // This function also nulls out the sidebar width CSS variable and setting,
    // causing it to return to its default. This does not happen if you do it
    // from settings.js, which uses a separate function. It's done here because
    // the minimum sidebar size is rather uncomfortable, and it must pass
    // through that size when using the shrink-to-nothing gesture.
    const hideSidebar = function() {
        if (isSrcPage) {
            window.rustdocCloseSourceSidebar();
            updateLocalStorage("src-sidebar-width", null);
            // [RUSTDOCIMPL] CSS variable fast path
            //
            // The sidebar width variable is attached to the <html> element by
            // storage.js, because the sidebar and resizer don't exist yet.
            // But the resize code, in `resize()`, sets the property on the
            // sidebar and resizer elements (which are the only elements that
            // use the variable) to avoid recalculating CSS on the entire
            // document on every frame.
            //
            // So, to clear it, we need to clear all three.
            document.documentElement.style.removeProperty("--src-sidebar-width");
            sidebar.style.removeProperty("--src-sidebar-width");
            resizer.style.removeProperty("--src-sidebar-width");
        } else {
            addClass(document.documentElement, "hide-sidebar");
            updateLocalStorage("hide-sidebar", "true");
            updateLocalStorage("desktop-sidebar-width", null);
            document.documentElement.style.removeProperty("--desktop-sidebar-width");
            sidebar.style.removeProperty("--desktop-sidebar-width");
            resizer.style.removeProperty("--desktop-sidebar-width");
        }
    };

    // Call this function to show the sidebar from the resize handle.
    // On docs pages, this can only happen if the user has grabbed the resize
    // handle, shrunk the sidebar down to nothing, and then pulls back into
    // the visible range without releasing it. You can, however, grab the
    // resize handle on a source page with the sidebar closed, because it
    // remains visible all the time on there.
    const showSidebar = function() {
        if (isSrcPage) {
            window.rustdocShowSourceSidebar();
        } else {
            removeClass(document.documentElement, "hide-sidebar");
            updateLocalStorage("hide-sidebar", "false");
        }
    };

    /**
     * Call this to set the correct CSS variable and setting.
     * This function doesn't enforce size constraints. Do that before calling it!
     *
     * @param {number} size - CSS px width of the sidebar.
     */
    const changeSidebarSize = function(size) {
        if (isSrcPage) {
            updateLocalStorage("src-sidebar-width", size.toString());
            // [RUSTDOCIMPL] CSS variable fast path
            //
            // While this property is set on the HTML element at load time,
            // because the sidebar isn't actually loaded yet,
            // we scope this update to the sidebar to avoid hitting a slow
            // path in WebKit.
            sidebar.style.setProperty("--src-sidebar-width", size + "px");
            resizer.style.setProperty("--src-sidebar-width", size + "px");
        } else {
            updateLocalStorage("desktop-sidebar-width", size.toString());
            sidebar.style.setProperty("--desktop-sidebar-width", size + "px");
            resizer.style.setProperty("--desktop-sidebar-width", size + "px");
        }
    };

    // Check if the sidebar is hidden. Since src pages and doc pages have
    // different settings, this function has to check that.
    const isSidebarHidden = function() {
        return isSrcPage ?
            !hasClass(document.documentElement, "src-sidebar-expanded") :
            hasClass(document.documentElement, "hide-sidebar");
    };

    /**
     * Respond to the resize handle event.
     * This function enforces size constraints, and implements the
     * shrink-to-nothing gesture based on thresholds defined above.
     *
     * @param {PointerEvent} e
     */
    const resize = function(e) {
        if (currentPointerId === null || currentPointerId !== e.pointerId) {
            return;
        }
        e.preventDefault();
        const pos = e.clientX - 3;
        if (pos < SIDEBAR_VANISH_THRESHOLD) {
            hideSidebar();
        } else if (pos >= SIDEBAR_MIN) {
            if (isSidebarHidden()) {
                showSidebar();
            }
            // don't let the sidebar get wider than SIDEBAR_MAX, or the body narrower
            // than BODY_MIN
            const constrainedPos = Math.min(pos, window.innerWidth - BODY_MIN, SIDEBAR_MAX);
            changeSidebarSize(constrainedPos);
            desiredSidebarSize = constrainedPos;
            if (pendingSidebarResizingFrame !== false) {
                clearTimeout(pendingSidebarResizingFrame);
            }
            pendingSidebarResizingFrame = setTimeout(() => {
                if (currentPointerId === null || pendingSidebarResizingFrame === false) {
                    return;
                }
                pendingSidebarResizingFrame = false;
                document.documentElement.style.setProperty(
                    "--resizing-sidebar-width",
                    desiredSidebarSize + "px",
                );
            }, 100);
        }
    };
    // Respond to the window resize event.
    window.addEventListener("resize", () => {
        if (window.innerWidth < RUSTDOC_MOBILE_BREAKPOINT) {
            return;
        }
        stopResize();
        if (desiredSidebarSize !== null && desiredSidebarSize >= (window.innerWidth - BODY_MIN)) {
            changeSidebarSize(window.innerWidth - BODY_MIN);
        } else if (desiredSidebarSize !== null && desiredSidebarSize > SIDEBAR_MIN) {
            changeSidebarSize(desiredSidebarSize);
        }
    });

    /**
     * @param {PointerEvent=} e
     */
    const stopResize = function(e) {
        if (currentPointerId === null) {
            return;
        }
        if (e) {
            e.preventDefault();
        }
        desiredSidebarSize = sidebar.getBoundingClientRect().width;
        removeClass(resizer, "active");
        window.removeEventListener("pointermove", resize, false);
        window.removeEventListener("pointerup", stopResize, false);
        removeClass(document.documentElement, "sidebar-resizing");
        document.documentElement.style.removeProperty( "--resizing-sidebar-width");
        if (resizer.releasePointerCapture) {
            resizer.releasePointerCapture(currentPointerId);
            currentPointerId = null;
        }
    };

    /**
     * @param {PointerEvent} e
     */
    const initResize = function(e) {
        if (currentPointerId !== null || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
            return;
        }
        if (resizer.setPointerCapture) {
            resizer.setPointerCapture(e.pointerId);
            if (!resizer.hasPointerCapture(e.pointerId)) {
                // unable to capture pointer; something else has it
                // on iOS, this usually means you long-clicked a link instead
                resizer.releasePointerCapture(e.pointerId);
                return;
            }
            currentPointerId = e.pointerId;
        }
        window.hideAllModals(false);
        e.preventDefault();
        window.addEventListener("pointermove", resize, false);
        window.addEventListener("pointercancel", stopResize, false);
        window.addEventListener("pointerup", stopResize, false);
        addClass(resizer, "active");
        addClass(document.documentElement, "sidebar-resizing");
        const pos = e.clientX - sidebar.offsetLeft - 3;
        document.documentElement.style.setProperty( "--resizing-sidebar-width", pos + "px");
        desiredSidebarSize = null;
    };
    resizer.addEventListener("pointerdown", initResize, false);
}());

// This section handles the copy button that appears next to the path breadcrumbs
// and the copy buttons on the code examples.
(function() {
    // Common functions to copy buttons.
    /**
     * @param {string|null} content
     */
    function copyContentToClipboard(content) {
        if (content === null) {
            return;
        }
        const el = document.createElement("textarea");
        el.value = content;
        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);
    }

    /**
     * @param {HTMLElement & {reset_button_timeout?: ReturnType<typeof setTimeout>}} button
     */
    function copyButtonAnimation(button) {
        button.classList.add("clicked");

        if (button.reset_button_timeout !== undefined) {
            clearTimeout(button.reset_button_timeout);
        }

        button.reset_button_timeout = setTimeout(() => {
            button.reset_button_timeout = undefined;
            button.classList.remove("clicked");
        }, 1000);
    }

    // Copy button that appears next to the path breadcrumbs.
    const but = document.getElementById("copy-path");
    if (!but) {
        return;
    }
    but.onclick = () => {
        // Most page titles are '<Item> in <path::to::module> - Rust', except
        // modules (which don't have the first part) and keywords/primitives
        // (which don't have a module path)
        const titleElement = document.querySelector("title");
        const title = titleElement && titleElement.textContent ?
                      titleElement.textContent.replace(" - Rust", "") : "";
        const [item, module] = title.split(" in ");
        const path = [item];
        if (module !== undefined) {
            path.unshift(module);
        }

        copyContentToClipboard(path.join("::"));
        copyButtonAnimation(but);
    };

    /**
     * Copy buttons on code examples.
     * @param {HTMLElement|null} codeElem
     */
    function copyCode(codeElem) {
        if (!codeElem) {
            // Should never happen, but the world is a dark and dangerous place.
            return;
        }
        copyContentToClipboard(codeElem.textContent);
    }

    /**
     * @param {UIEvent} event
     * @returns {HTMLElement|null}
     */
    function getExampleWrap(event) {
        const target = event.target;
        if (target instanceof HTMLElement) {
            /** @type {HTMLElement|null} */
            let elem = target;
            while (elem !== null && !hasClass(elem, "example-wrap")) {
                if (elem === document.body ||
                    elem.tagName === "A" ||
                    elem.tagName === "BUTTON" ||
                    hasClass(elem, "docblock")
                ) {
                    return null;
                }
                elem = elem.parentElement;
            }
            return elem;
        } else {
            return null;
        }
    }

    /**
     * @param {UIEvent} event
     */
    function addCopyButton(event) {
        const elem = getExampleWrap(event);
        if (elem === null) {
            return;
        }
        // Since the button will be added, no need to keep this listener around.
        elem.removeEventListener("mouseover", addCopyButton);

        const parent = document.createElement("div");
        parent.className = "button-holder";

        const runButton = elem.querySelector(".test-arrow");
        if (runButton !== null) {
            // If there is a run button, we move it into the same div.
            parent.appendChild(runButton);
        }
        elem.appendChild(parent);
        const copyButton = document.createElement("button");
        copyButton.className = "copy-button";
        copyButton.title = "Copy code to clipboard";
        copyButton.addEventListener("click", () => {
            copyCode(elem.querySelector("pre > code"));
            copyButtonAnimation(copyButton);
        });
        parent.appendChild(copyButton);

        if (!elem.parentElement || !elem.parentElement.classList.contains("scraped-example") ||
            !window.updateScrapedExample) {
            return;
        }
        const scrapedWrapped = elem.parentElement;
        window.updateScrapedExample(scrapedWrapped, parent);
    }

    /**
     * @param {UIEvent} event
     */
    function showHideCodeExampleButtons(event) {
        const elem = getExampleWrap(event);
        if (elem === null) {
            return;
        }
        let buttons = elem.querySelector(".button-holder");
        if (buttons === null) {
            // On mobile, you can't hover an element so buttons need to be created on click
            // if they're not already there.
            addCopyButton(event);
            buttons = elem.querySelector(".button-holder");
            if (buttons === null) {
                return;
            }
        }
        buttons.classList.toggle("keep-visible");
    }

    onEachLazy(document.querySelectorAll(".docblock .example-wrap"), elem => {
        elem.addEventListener("mouseover", addCopyButton);
        elem.addEventListener("click", showHideCodeExampleButtons);
    });
}());
