doctools.js 8.0 KB

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