diff options
-rw-r--r-- | doc/conf.py | 1 | ||||
-rw-r--r-- | karma.conf.js | 2 | ||||
-rw-r--r-- | sphinx/builders/html/__init__.py | 1 | ||||
-rw-r--r-- | sphinx/themes/basic/static/doctools.js | 130 | ||||
-rw-r--r-- | sphinx/themes/basic/static/sphinx_highlight.js | 145 | ||||
-rw-r--r-- | tests/js/documentation_options.js | 1 | ||||
-rw-r--r-- | tests/js/sphinx_highlight.js (renamed from tests/js/doctools.js) | 2 | ||||
-rw-r--r-- | tests/test_build_html.py | 3 |
8 files changed, 163 insertions, 122 deletions
diff --git a/doc/conf.py b/doc/conf.py index a721508de..eafa42a7d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,6 +48,7 @@ epub_post_files = [('usage/installation.xhtml', 'Installing Sphinx'), ('develop.xhtml', 'Sphinx development')] epub_exclude_files = ['_static/opensearch.xml', '_static/doctools.js', '_static/jquery.js', '_static/searchtools.js', + '_static/sphinx_highlight.js', '_static/underscore.js', '_static/basic.css', '_static/language_data.js', 'search.html', '_static/websupport.js'] diff --git a/karma.conf.js b/karma.conf.js index 082584cf7..8a18e80ba 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,8 +15,10 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ + 'tests/js/documentation_options.js', 'sphinx/themes/basic/static/doctools.js', 'sphinx/themes/basic/static/searchtools.js', + 'sphinx/themes/basic/static/sphinx_highlight.js', 'tests/js/*.js' ], diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index e80ac3c7e..4fe40eb6f 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -354,6 +354,7 @@ class StandaloneHTMLBuilder(Builder): self.add_js_file('underscore.js', priority=200) self.add_js_file('_sphinx_javascript_frameworks_compat.js', priority=200) self.add_js_file('doctools.js', priority=200) + self.add_js_file('sphinx_highlight.js', priority=200) for filename, attrs in self.app.registry.js_files: self.add_js_file(filename, **attrs) diff --git a/sphinx/themes/basic/static/doctools.js b/sphinx/themes/basic/static/doctools.js index c3db08d1c..527b876ca 100644 --- a/sphinx/themes/basic/static/doctools.js +++ b/sphinx/themes/basic/static/doctools.js @@ -10,6 +10,13 @@ */ "use strict"; +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + const _ready = (callback) => { if (document.readyState !== "loading") { callback(); @@ -19,72 +26,10 @@ const _ready = (callback) => { }; /** - * highlight a given string on a node by wrapping it in - * span elements with the given class name. - */ -const _highlight = (node, addItems, text, className) => { - if (node.nodeType === Node.TEXT_NODE) { - const val = node.nodeValue; - const parent = node.parentNode; - const pos = val.toLowerCase().indexOf(text); - if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") - ) { - let span; - - const closestNode = parent.closest("body, svg, foreignObject"); - const isInSVG = closestNode && closestNode.matches("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.classList.add(className); - } - - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - parent.insertBefore( - span, - parent.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling - ) - ); - node.nodeValue = val.substr(0, pos); - - if (isInSVG) { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect" - ); - const bbox = parent.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute("class", className); - addItems.push({ parent: parent, target: rect }); - } - } - } else if (node.matches && !node.matches("button, select, textarea")) { - node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); - } -}; -const _highlightText = (thisNode, text, className) => { - let addItems = []; - _highlight(thisNode, addItems, text, className); - addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) - ); -}; - -/** * Small JavaScript module for the documentation. */ const Documentation = { init: () => { - Documentation.highlightSearchWords(); Documentation.initDomainIndexTable(); Documentation.initOnKeyListeners(); }, @@ -127,51 +72,6 @@ const Documentation = { }, /** - * highlight the search words provided in the url in the text - */ - highlightSearchWords: () => { - const highlight = - new URLSearchParams(window.location.search).get("highlight") || ""; - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); - if (terms.length === 0) return; // nothing to do - - // There should never be more than one element matching "div.body" - const divBody = document.querySelectorAll("div.body"); - const body = divBody.length ? divBody[0] : document.querySelector("body"); - window.setTimeout(() => { - terms.forEach((term) => _highlightText(body, term, "highlighted")); - }, 10); - - const searchBox = document.getElementById("searchbox"); - if (searchBox === null) return; - searchBox.appendChild( - document - .createRange() - .createContextualFragment( - '<p class="highlight-link">' + - '<a href="javascript:Documentation.hideSearchWords()">' + - Documentation.gettext("Hide Search Matches") + - "</a></p>" - ) - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - const url = new URL(window.location); - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); - }, - - /** * helper function to focus on search bar */ focusSearchBar: () => { @@ -210,15 +110,11 @@ const Documentation = { ) return; - const blacklistedElements = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", - ]); document.addEventListener("keydown", (event) => { - if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements - if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; if (!event.shiftKey) { switch (event.key) { @@ -240,10 +136,6 @@ const Documentation = { event.preventDefault(); } break; - case "Escape": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.hideSearchWords(); - event.preventDefault(); } } diff --git a/sphinx/themes/basic/static/sphinx_highlight.js b/sphinx/themes/basic/static/sphinx_highlight.js new file mode 100644 index 000000000..04889cc9d --- /dev/null +++ b/sphinx/themes/basic/static/sphinx_highlight.js @@ -0,0 +1,145 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + parent.insertBefore( + span, + parent.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords: () => { + const highlight = + new URLSearchParams(window.location.search).get("highlight") || ""; + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '<p class="highlight-link">' + + '<a href="javascript:SphinxHighlight.hideSearchWords()">' + + Documentation.gettext("Hide Search Matches") + + "</a></p>" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + const url = new URL(window.location); + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + const blacklistedElements = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", + ]); + document.addEventListener("keydown", (event) => { + if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements + if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys + + if (!event.shiftKey) { + switch (event.key) { + case "Escape": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + } + }); + }, +}; + +_ready(SphinxHighlight.highlightSearchWords); +_ready(SphinxHighlight.initOnKeyListeners); diff --git a/tests/js/documentation_options.js b/tests/js/documentation_options.js new file mode 100644 index 000000000..e736460a0 --- /dev/null +++ b/tests/js/documentation_options.js @@ -0,0 +1 @@ +const DOCUMENTATION_OPTIONS = {}; diff --git a/tests/js/doctools.js b/tests/js/sphinx_highlight.js index 7268a6a8c..1f52eabb9 100644 --- a/tests/js/doctools.js +++ b/tests/js/sphinx_highlight.js @@ -1,5 +1,3 @@ -const DOCUMENTATION_OPTIONS = {}; - describe('highlightText', function() { const cyrillicTerm = 'шеллы'; diff --git a/tests/test_build_html.py b/tests/test_build_html.py index c7d2daf47..0cdeb4708 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -1232,7 +1232,8 @@ def test_assets_order(app): # js_files expected = ['_static/early.js', '_static/jquery.js', '_static/underscore.js', - '_static/doctools.js', 'https://example.com/script.js', '_static/normal.js', + '_static/doctools.js', '_static/sphinx_highlight.js', + 'https://example.com/script.js', '_static/normal.js', '_static/late.js', '_static/js/custom.js', '_static/lazy.js'] pattern = '.*'.join('src="%s"' % f for f in expected) assert re.search(pattern, content, re.S) |