1/* 2 * doctools.js 3 * ~~~~~~~~~~~ 4 * 5 * Base JavaScript utilities for all Sphinx HTML documentation. 6 * 7 * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. 8 * :license: BSD, see LICENSE for details. 9 * 10 */ 11"use strict"; 12 13const _ready = (callback) => { 14 if (document.readyState !== "loading") { 15 callback(); 16 } else { 17 document.addEventListener("DOMContentLoaded", callback); 18 } 19}; 20 21/** 22 * highlight a given string on a node by wrapping it in 23 * span elements with the given class name. 24 */ 25const _highlight = (node, addItems, text, className) => { 26 if (node.nodeType === Node.TEXT_NODE) { 27 const val = node.nodeValue; 28 const parent = node.parentNode; 29 const pos = val.toLowerCase().indexOf(text); 30 if ( 31 pos >= 0 && 32 !parent.classList.contains(className) && 33 !parent.classList.contains("nohighlight") 34 ) { 35 let span; 36 37 const closestNode = parent.closest("body, svg, foreignObject"); 38 const isInSVG = closestNode && closestNode.matches("svg"); 39 if (isInSVG) { 40 span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 41 } else { 42 span = document.createElement("span"); 43 span.classList.add(className); 44 } 45 46 span.appendChild(document.createTextNode(val.substr(pos, text.length))); 47 parent.insertBefore( 48 span, 49 parent.insertBefore( 50 document.createTextNode(val.substr(pos + text.length)), 51 node.nextSibling 52 ) 53 ); 54 node.nodeValue = val.substr(0, pos); 55 56 if (isInSVG) { 57 const rect = document.createElementNS( 58 "http://www.w3.org/2000/svg", 59 "rect" 60 ); 61 const bbox = parent.getBBox(); 62 rect.x.baseVal.value = bbox.x; 63 rect.y.baseVal.value = bbox.y; 64 rect.width.baseVal.value = bbox.width; 65 rect.height.baseVal.value = bbox.height; 66 rect.setAttribute("class", className); 67 addItems.push({ parent: parent, target: rect }); 68 } 69 } 70 } else if (node.matches && !node.matches("button, select, textarea")) { 71 node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); 72 } 73}; 74const _highlightText = (thisNode, text, className) => { 75 let addItems = []; 76 _highlight(thisNode, addItems, text, className); 77 addItems.forEach((obj) => 78 obj.parent.insertAdjacentElement("beforebegin", obj.target) 79 ); 80}; 81 82/** 83 * Small JavaScript module for the documentation. 84 */ 85const Documentation = { 86 init: () => { 87 Documentation.highlightSearchWords(); 88 Documentation.initDomainIndexTable(); 89 Documentation.initOnKeyListeners(); 90 }, 91 92 /** 93 * i18n support 94 */ 95 TRANSLATIONS: {}, 96 PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), 97 LOCALE: "unknown", 98 99 // gettext and ngettext don't access this so that the functions 100 // can safely bound to a different name (_ = Documentation.gettext) 101 gettext: (string) => { 102 const translated = Documentation.TRANSLATIONS[string]; 103 switch (typeof translated) { 104 case "undefined": 105 return string; // no translation 106 case "string": 107 return translated; // translation exists 108 default: 109 return translated[0]; // (singular, plural) translation tuple exists 110 } 111 }, 112 113 ngettext: (singular, plural, n) => { 114 const translated = Documentation.TRANSLATIONS[singular]; 115 if (typeof translated !== "undefined") 116 return translated[Documentation.PLURAL_EXPR(n)]; 117 return n === 1 ? singular : plural; 118 }, 119 120 addTranslations: (catalog) => { 121 Object.assign(Documentation.TRANSLATIONS, catalog.messages); 122 Documentation.PLURAL_EXPR = new Function( 123 "n", 124 `return (${catalog.plural_expr})` 125 ); 126 Documentation.LOCALE = catalog.locale; 127 }, 128 129 /** 130 * highlight the search words provided in the url in the text 131 */ 132 highlightSearchWords: () => { 133 const highlight = 134 new URLSearchParams(window.location.search).get("highlight") || ""; 135 const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); 136 if (terms.length === 0) return; // nothing to do 137 138 // There should never be more than one element matching "div.body" 139 const divBody = document.querySelectorAll("div.body"); 140 const body = divBody.length ? divBody[0] : document.querySelector("body"); 141 window.setTimeout(() => {
142 terms.forEach((term) => _highlightText(body, term, "highlighted")); 143 }, 10); 144 145 const searchBox = document.getElementById("searchbox"); 146 if (searchBox === null) return; 147 searchBox.appendChild( 148 document 149 .createRange() 150 .createContextualFragment( 151 '<p class="highlight-link">' + 152 '<a href="javascript:Documentation.hideSearchWords()">' + 153 Documentation.gettext("Hide Search Matches") + 154 "</a></p>" 155 ) 156 ); 157 }, 158 159 /** 160 * helper function to hide the search marks again 161 */ 162 hideSearchWords: () => { 163 document 164 .querySelectorAll("#searchbox .highlight-link") 165 .forEach((el) => el.remove()); 166 document 167 .querySelectorAll("span.highlighted") 168 .forEach((el) => el.classList.remove("highlighted")); 169 const url = new URL(window.location); 170 url.searchParams.delete("highlight"); 171 window.history.replaceState({}, "", url); 172 }, 173 174 /** 175 * helper function to focus on search bar 176 */ 177 focusSearchBar: () => { 178 document.querySelectorAll("input[name=q]")[0]?.focus(); 179 }, 180 181 /** 182 * Initialise the domain index toggle buttons 183 */ 184 initDomainIndexTable: () => { 185 const toggler = (el) => { 186 const idNumber = el.id.substr(7); 187 const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); 188 if (el.src.substr(-9) === "minus.png") { 189 el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; 190 toggledRows.forEach((el) => (el.style.display = "none")); 191 } else { 192 el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; 193 toggledRows.forEach((el) => (el.style.display = "")); 194 } 195 }; 196 197 const togglerElements = document.querySelectorAll("img.toggler"); 198 togglerElements.forEach((el) => 199 el.addEventListener("click", (event) => toggler(event.currentTarget)) 200 ); 201 togglerElements.forEach((el) => (el.style.display = "")); 202 if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); 203 }, 204 205 initOnKeyListeners: () => { 206 // only install a listener if it is really needed 207 if ( 208 !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && 209 !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS 210 ) 211 return; 212 213 const blacklistedElements = new Set([ 214 "TEXTAREA", 215 "INPUT", 216 "SELECT", 217 "BUTTON", 218 ]); 219 document.addEventListener("keydown", (event) => { 220 if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements 221 if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys 222 223 if (!event.shiftKey) { 224 switch (event.key) { 225 case "ArrowLeft": 226 if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 227 228 const prevLink = document.querySelector('link[rel="prev"]'); 229 if (prevLink && prevLink.href) { 230 window.location.href = prevLink.href; 231 event.preventDefault(); 232 } 233 break; 234 case "ArrowRight": 235 if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; 236 237 const nextLink = document.querySelector('link[rel="next"]'); 238 if (nextLink && nextLink.href) { 239 window.location.href = nextLink.href; 240 event.preventDefault(); 241 } 242 break; 243 case "Escape": 244 if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; 245 Documentation.hideSearchWords(); 246 event.preventDefault(); 247 } 248 } 249 250 // some keyboard layouts may need Shift to get / 251 switch (event.key) { 252 case "/": 253 if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; 254 Documentation.focusSearchBar(); 255 event.preventDefault(); 256 } 257 }); 258 }, 259}; 260 261// quick alias for translations 262const _ = Documentation.gettext; 263 264_ready(Documentation.init);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.