/ docs / manual / _static / sphinx_highlight.js
sphinx_highlight.js
  1  /* Highlighting utilities for Sphinx HTML documentation. */
  2  "use strict";
  3  
  4  const SPHINX_HIGHLIGHT_ENABLED = true
  5  
  6  /**
  7   * highlight a given string on a node by wrapping it in
  8   * span elements with the given class name.
  9   */
 10  const _highlight = (node, addItems, text, className) => {
 11    if (node.nodeType === Node.TEXT_NODE) {
 12      const val = node.nodeValue;
 13      const parent = node.parentNode;
 14      const pos = val.toLowerCase().indexOf(text);
 15      if (
 16        pos >= 0 &&
 17        !parent.classList.contains(className) &&
 18        !parent.classList.contains("nohighlight")
 19      ) {
 20        let span;
 21  
 22        const closestNode = parent.closest("body, svg, foreignObject");
 23        const isInSVG = closestNode && closestNode.matches("svg");
 24        if (isInSVG) {
 25          span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
 26        } else {
 27          span = document.createElement("span");
 28          span.classList.add(className);
 29        }
 30  
 31        span.appendChild(document.createTextNode(val.substr(pos, text.length)));
 32        const rest = document.createTextNode(val.substr(pos + text.length));
 33        parent.insertBefore(
 34          span,
 35          parent.insertBefore(
 36            rest,
 37            node.nextSibling
 38          )
 39        );
 40        node.nodeValue = val.substr(0, pos);
 41        /* There may be more occurrences of search term in this node. So call this
 42         * function recursively on the remaining fragment.
 43         */
 44        _highlight(rest, addItems, text, className);
 45  
 46        if (isInSVG) {
 47          const rect = document.createElementNS(
 48            "http://www.w3.org/2000/svg",
 49            "rect"
 50          );
 51          const bbox = parent.getBBox();
 52          rect.x.baseVal.value = bbox.x;
 53          rect.y.baseVal.value = bbox.y;
 54          rect.width.baseVal.value = bbox.width;
 55          rect.height.baseVal.value = bbox.height;
 56          rect.setAttribute("class", className);
 57          addItems.push({ parent: parent, target: rect });
 58        }
 59      }
 60    } else if (node.matches && !node.matches("button, select, textarea")) {
 61      node.childNodes.forEach((el) => _highlight(el, addItems, text, className));
 62    }
 63  };
 64  const _highlightText = (thisNode, text, className) => {
 65    let addItems = [];
 66    _highlight(thisNode, addItems, text, className);
 67    addItems.forEach((obj) =>
 68      obj.parent.insertAdjacentElement("beforebegin", obj.target)
 69    );
 70  };
 71  
 72  /**
 73   * Small JavaScript module for the documentation.
 74   */
 75  const SphinxHighlight = {
 76  
 77    /**
 78     * highlight the search words provided in localstorage in the text
 79     */
 80    highlightSearchWords: () => {
 81      if (!SPHINX_HIGHLIGHT_ENABLED) return;  // bail if no highlight
 82  
 83      // get and clear terms from localstorage
 84      const url = new URL(window.location);
 85      const highlight =
 86          localStorage.getItem("sphinx_highlight_terms")
 87          || url.searchParams.get("highlight")
 88          || "";
 89      localStorage.removeItem("sphinx_highlight_terms")
 90      url.searchParams.delete("highlight");
 91      window.history.replaceState({}, "", url);
 92  
 93      // get individual terms from highlight string
 94      const terms = highlight.toLowerCase().split(/\s+/).filter(x => x);
 95      if (terms.length === 0) return; // nothing to do
 96  
 97      // There should never be more than one element matching "div.body"
 98      const divBody = document.querySelectorAll("div.body");
 99      const body = divBody.length ? divBody[0] : document.querySelector("body");
100      window.setTimeout(() => {
101        terms.forEach((term) => _highlightText(body, term, "highlighted"));
102      }, 10);
103  
104      const searchBox = document.getElementById("searchbox");
105      if (searchBox === null) return;
106      searchBox.appendChild(
107        document
108          .createRange()
109          .createContextualFragment(
110            '<p class="highlight-link">' +
111              '<a href="javascript:SphinxHighlight.hideSearchWords()">' +
112              _("Hide Search Matches") +
113              "</a></p>"
114          )
115      );
116    },
117  
118    /**
119     * helper function to hide the search marks again
120     */
121    hideSearchWords: () => {
122      document
123        .querySelectorAll("#searchbox .highlight-link")
124        .forEach((el) => el.remove());
125      document
126        .querySelectorAll("span.highlighted")
127        .forEach((el) => el.classList.remove("highlighted"));
128      localStorage.removeItem("sphinx_highlight_terms")
129    },
130  
131    initEscapeListener: () => {
132      // only install a listener if it is really needed
133      if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return;
134  
135      document.addEventListener("keydown", (event) => {
136        // bail for input elements
137        if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
138        // bail with special keys
139        if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
140        if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) {
141          SphinxHighlight.hideSearchWords();
142          event.preventDefault();
143        }
144      });
145    },
146  };
147  
148  _ready(() => {
149    /* Do not call highlightSearchWords() when we are on the search page.
150     * It will highlight words from the *previous* search query.
151     */
152    if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords();
153    SphinxHighlight.initEscapeListener();
154  });