1// storage.js is loaded in the `<head>` of all rustdoc pages and doesn't 2// use `async` or `defer`. That means it blocks further parsing and rendering 3// of the page: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script. 4// This makes it the correct place to act on settings that affect the display of 5// the page, so we don't see major layout changes during the load of the page. 6"use strict"; 7 8const darkThemes = ["dark", "ayu"]; 9window.currentTheme = document.getElementById("themeStyle"); 10 11const settingsDataset = (function() { 12 const settingsElement = document.getElementById("default-settings"); 13 return settingsElement && settingsElement.dataset ? settingsElement.dataset : null; 14})(); 15 16function getSettingValue(settingName) { 17 const current = getCurrentValue(settingName); 18 if (current === null && settingsDataset !== null) { 19 // See the comment for `default_settings.into_iter()` etc. in 20 // `Options::from_matches` in `librustdoc/config.rs`. 21 const def = settingsDataset[settingName.replace(/-/g,"_")]; 22 if (def !== undefined) { 23 return def; 24 } 25 } 26 return current; 27} 28 29const localStoredTheme = getSettingValue("theme"); 30 31// eslint-disable-next-line no-unused-vars 32function hasClass(elem, className) { 33 return elem && elem.classList && elem.classList.contains(className); 34} 35 36function addClass(elem, className) { 37 if (elem && elem.classList) { 38 elem.classList.add(className); 39 } 40} 41 42// eslint-disable-next-line no-unused-vars 43function removeClass(elem, className) { 44 if (elem && elem.classList) { 45 elem.classList.remove(className); 46 } 47} 48 49/** 50 * Run a callback for every element of an Array. 51 * @param {Array<?>} arr - The array to iterate over 52 * @param {function(?)} func - The callback 53 * @param {boolean} [reversed] - Whether to iterate in reverse 54 */ 55function onEach(arr, func, reversed) { 56 if (arr && arr.length > 0) { 57 if (reversed) { 58 for (let i = arr.length - 1; i >= 0; --i) { 59 if (func(arr[i])) { 60 return true; 61 } 62 } 63 } else { 64 for (const elem of arr) { 65 if (func(elem)) { 66 return true; 67 } 68 } 69 } 70 } 71 return false; 72} 73 74/** 75 * Turn an HTMLCollection or a NodeList into an Array, then run a callback 76 * for every element. This is useful because iterating over an HTMLCollection 77 * or a "live" NodeList while modifying it can be very slow. 78 * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection 79 * https://developer.mozilla.org/en-US/docs/Web/API/NodeList 80 * @param {NodeList<?>|HTMLCollection<?>} lazyArray - An array to iterate over 81 * @param {function(?)} func - The callback 82 * @param {boolean} [reversed] - Whether to iterate in reverse 83 */ 84// eslint-disable-next-line no-unused-vars 85function onEachLazy(lazyArray, func, reversed) { 86 return onEach( 87 Array.prototype.slice.call(lazyArray), 88 func, 89 reversed); 90} 91 92function updateLocalStorage(name, value) { 93 try { 94 window.localStorage.setItem("rustdoc-" + name, value); 95 } catch (e) { 96 // localStorage is not accessible, do nothing 97 } 98} 99 100function getCurrentValue(name) { 101 try { 102 return window.localStorage.getItem("rustdoc-" + name); 103 } catch (e) { 104 return null; 105 } 106} 107 108// Get a value from the rustdoc-vars div, which is used to convey data from 109// Rust to the JS. If there is no such element, return null. 110const getVar = (function getVar(name) { 111 const el = document.querySelector("head > meta[name='rustdoc-vars']"); 112 return el ? el.attributes["data-" + name].value : null; 113}); 114 115function switchTheme(newThemeName, saveTheme) { 116 // If this new value comes from a system setting or from the previously 117 // saved theme, no need to save it. 118 if (saveTheme) { 119 updateLocalStorage("theme", newThemeName); 120 } 121 122 let newHref; 123 124 if (newThemeName === "light" || newThemeName === "dark" || newThemeName === "ayu") { 125 newHref = getVar("static-root-path") + getVar("theme-" + newThemeName + "-css"); 126 } else { 127 newHref = getVar("root-path") + newThemeName + getVar("resource-suffix") + ".css"; 128 } 129 130 if (!window.currentTheme) { 131 document.write(`<link rel="stylesheet" id="themeStyle" href="${newHref}">`); 132 window.currentTheme = document.getElementById("themeStyle"); 133 } else if (newHref !== window.currentTheme.href) { 134 window.currentTheme.href = newHref; 135 } 136} 137 138const updateTheme = (function() { 139 // only listen to (prefers-color-scheme: dark) because light is the default 140 const mql = window.matchMedia("(prefers-color-scheme: dark)"); 141 142 /** 143 * Update the current theme to match whatever the current combination of 144 * * the preference for using the system theme 145 * (if this is the case, the value of preferred-light-theme, if the 146 * system theme is light, otherwise if dark, the value of 147 * preferred-dark-theme.) 148 * * the preferred theme 149 * … dictates that it should be. 150 */ 151 function updateTheme() { 152 // maybe the user has disabled the setting in the meantime! 153 if (getSettingValue("use-system-theme") !== "false") { 154 const lightTheme = getSettingValue("preferred-light-theme") || "light"; 155 const darkTheme = getSettingValue("preferred-dark-theme") || "dark"; 156 updateLocalStorage("use-system-theme", "true"); 157 158 // use light theme if user prefers it, or has no preference 159 switchTheme(mql.matches ? darkTheme : lightTheme, true); 160 // note: we save the theme so that it doesn't suddenly change when 161 // the user disables "use-system-theme" and reloads the page or 162 // navigates to another page 163 } else { 164 switchTheme(getSettingValue("theme"), false); 165 } 166 } 167 168 mql.addEventListener("change", updateTheme); 169 170 return updateTheme; 171})(); 172 173if (getSettingValue("use-system-theme") !== "false" && window.matchMedia) { 174 // update the preferred dark theme if the user is already using a dark theme 175 // See https://github.com/rust-lang/rust/pull/77809#issuecomment-707875732 176 if (getSettingValue("use-system-theme") === null 177 && getSettingValue("preferred-dark-theme") === null 178 && darkThemes.indexOf(localStoredTheme) >= 0) { 179 updateLocalStorage("preferred-dark-theme", localStoredTheme); 180 } 181} 182 183updateTheme(); 184 185if (getSettingValue("source-sidebar-show") === "true") { 186 // At this point in page load, `document.body` is not available yet. 187 // Set a class on the `<html>` element instead. 188 addClass(document.documentElement, "source-sidebar-expanded"); 189} 190 191// If we navigate away (for example to a settings page), and then use the back or 192// forward button to get back to a page, the theme may have changed in the meantime. 193// But scripts may not be re-loaded in such a case due to the bfcache 194// (https://web.dev/bfcache/). The "pageshow" event triggers on such navigations. 195// Use that opportunity to update the theme. 196// We use a setTimeout with a 0 timeout here to put the change on the event queue. 197// For some reason, if we try to change the theme while the `pageshow` event is 198// running, it sometimes fails to take effect. The problem manifests on Chrome, 199// specifically when talking to a remote website with no caching. 200window.addEventListener("pageshow", ev => { 201 if (ev.persisted) { 202 setTimeout(updateTheme, 0); 203 } 204}); 205