From 58a44497079f46b9854c66a2b2ad8cfadf7a25b9 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 23 Sep 2022 18:12:54 +0100 Subject: [PATCH 01/21] Deprecate HTML 4 support (#10843) --- CHANGES | 2 ++ doc/extdev/deprecated.rst | 5 +++++ sphinx/builders/html/__init__.py | 13 ++++++++++++- sphinx/writers/html.py | 1 + tests/test_build_html.py | 10 ++++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index eb75defe4..637b6b618 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,8 @@ Incompatible changes Deprecated ---------- +* #10843: Support for HTML 4 output. Patch by Adam Turner. + Features added -------------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 18b0e6d04..8c850ceb6 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -22,6 +22,11 @@ The following is a list of deprecated interfaces. - (will be) Removed - Alternatives + * - HTML 4 support + - 5.2 + - 7.0 + - N/A + * - ``sphinx.util.path_stabilize`` - 5.1 - 7.0 diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index c75338d8f..e80ac3c7e 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -374,7 +374,7 @@ class StandaloneHTMLBuilder(Builder): @property def default_translator_class(self) -> Type[nodes.NodeVisitor]: # type: ignore if self.config.html4_writer: - return HTMLTranslator + return HTMLTranslator # RemovedInSphinx70Warning else: return HTML5Translator @@ -1338,6 +1338,16 @@ def migrate_html_add_permalinks(app: Sphinx, config: Config) -> None: html_add_permalinks ) + +def deprecate_html_4(_app: Sphinx, config: Config) -> None: + """Warn on HTML 4.""" + # RemovedInSphinx70Warning + if config.html4_writer: + logger.warning(_('Support for emitting HTML 4 output is deprecated and ' + 'will be removed in Sphinx 7. ("html4_writer=True ' + 'detected in configuration options)')) + + # for compatibility import sphinxcontrib.serializinghtml # NOQA @@ -1414,6 +1424,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.connect('config-inited', validate_html_static_path, priority=800) app.connect('config-inited', validate_html_logo, priority=800) app.connect('config-inited', validate_html_favicon, priority=800) + app.connect('config-inited', deprecate_html_4, priority=800) app.connect('builder-inited', validate_math_renderer) app.connect('html-page-context', setup_css_tag_helper) app.connect('html-page-context', setup_js_tag_helper) diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 1846dd675..48183204d 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -67,6 +67,7 @@ class HTMLWriter(Writer): self.clean_meta = ''.join(self.visitor.meta[2:]) +# RemovedInSphinx70Warning class HTMLTranslator(SphinxTranslator, BaseTranslator): """ Our custom HTML translator. diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 8fd83b438..c7d2daf47 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -131,6 +131,16 @@ def test_html4_output(app, status, warning): app.build() +def test_html4_deprecation(make_app, tempdir): + (tempdir / 'conf.py').write_text('', encoding='utf-8') + app = make_app( + buildername='html', + srcdir=tempdir, + confoverrides={'html4_writer': True}, + ) + assert 'HTML 4 output is deprecated and will be removed' in app._warning.getvalue() + + @pytest.mark.parametrize("fname,expect", flat_dict({ 'images.html': [ (".//img[@src='_images/img.png']", ''), From 5ce15606dd7543e7c71a74feec119d09e7a2919b Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 23 Sep 2022 18:47:28 +0100 Subject: [PATCH 02/21] Warn on using the `:property:` flag with `.. py:method::` (#10852) --- sphinx/domains/python.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 43a845889..6076eb7fb 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -828,6 +828,8 @@ class PyMethod(PyObject): prefix.append(nodes.Text('classmethod')) prefix.append(addnodes.desc_sig_space()) if 'property' in self.options: + logger.warning(_('Using the :property: flag with the py:method directive' + 'is deprecated, use ".. py:property::" instead.')) prefix.append(nodes.Text('property')) prefix.append(addnodes.desc_sig_space()) if 'staticmethod' in self.options: From 25cbe68c11dd43d6797ecb06597b658fa14f468e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Li=C5=A1ka?= Date: Fri, 23 Sep 2022 19:49:25 +0200 Subject: [PATCH 03/21] Fix `IndexLoader.load()` for _all_titles (#10851) When there is a document with no entry in _all_titles, we must add empty array when loading from frozenset. --- sphinx/search/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 0cff8fd25..5330d7e7c 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -290,9 +290,11 @@ class IndexBuilder: self._titles = dict(zip(index2fn, frozen['titles'])) self._all_titles = {} + for docname in self._titles.keys(): + self._all_titles[docname] = [] for title, doc_tuples in frozen['alltitles'].items(): for doc, titleid in doc_tuples: - self._all_titles.setdefault(index2fn[doc], []).append((title, titleid)) + self._all_titles[index2fn[doc]].append((title, titleid)) def load_terms(mapping: Dict[str, Any]) -> Dict[str, Set[str]]: rv = {} @@ -380,12 +382,12 @@ class IndexBuilder: alltitles: Dict[str, List[Tuple[int, str]]] = {} for docname, titlelist in self._all_titles.items(): for title, titleid in titlelist: - alltitles.setdefault(title, []).append((fn2index[docname], titleid)) + alltitles.setdefault(title, []).append((fn2index[docname], titleid)) index_entries: Dict[str, List[Tuple[int, str]]] = {} for docname, entries in self._index_entries.items(): for entry, entry_id, main_entry in entries: - index_entries.setdefault(entry.lower(), []).append((fn2index[docname], entry_id)) + index_entries.setdefault(entry.lower(), []).append((fn2index[docname], entry_id)) return dict(docnames=docnames, filenames=filenames, titles=titles, terms=terms, objects=objects, objtypes=objtypes, objnames=objnames, From 73a1ee4560d95b72762a6efded586ee174b5e4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Li=C5=A1ka?= Date: Fri, 23 Sep 2022 19:57:14 +0200 Subject: [PATCH 04/21] search: use both title and subtitle only if different (#10848) --- sphinx/themes/basic/static/searchtools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 0073f7e94..f16afbcbb 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -286,7 +286,7 @@ const Search = { let score = Math.round(100 * queryLower.length / title.length) results.push([ docNames[file], - `${titles[file]} > ${title}`, + titles[file] !== title ? `${titles[file]} > ${title}` : title, id !== null ? "#" + id : "", null, score, From 8db24515ea4298207a71556518e9a91bfc5ad6a3 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sat, 24 Sep 2022 00:31:26 +0100 Subject: [PATCH 05/21] Clear `record_dependencies` for each document (#10855) --- sphinx/builders/__init__.py | 4 ++++ .../roots/test-environment-record-dependencies/api.rst | 4 ++++ .../roots/test-environment-record-dependencies/conf.py | 5 +++++ .../example_module.py | 2 ++ .../test-environment-record-dependencies/index.rst | 3 +++ tests/test_environment_record_dependencies.py | 10 ++++++++++ 6 files changed, 28 insertions(+) create mode 100644 tests/roots/test-environment-record-dependencies/api.rst create mode 100644 tests/roots/test-environment-record-dependencies/conf.py create mode 100644 tests/roots/test-environment-record-dependencies/example_module.py create mode 100644 tests/roots/test-environment-record-dependencies/index.rst create mode 100644 tests/test_environment_record_dependencies.py diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 2aede5c24..dd0c4328e 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -10,6 +10,7 @@ from typing import (TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Sequence from docutils import nodes from docutils.nodes import Node +from docutils.utils import DependencyList from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx70Warning @@ -490,6 +491,9 @@ class Builder: filename = self.env.doc2path(docname) filetype = get_filetype(self.app.config.source_suffix, filename) publisher = self.app.registry.get_publisher(self.app, filetype) + # record_dependencies is mutable even though it is in settings, + # explicitly re-initialise for each document + publisher.settings.record_dependencies = DependencyList() with sphinx_domains(self.env), rst.default_role(docname, self.config.default_role): # set up error_handler for the target document codecs.register_error('sphinx', UnicodeDecodeErrorHandler(docname)) # type: ignore diff --git a/tests/roots/test-environment-record-dependencies/api.rst b/tests/roots/test-environment-record-dependencies/api.rst new file mode 100644 index 000000000..acfb89696 --- /dev/null +++ b/tests/roots/test-environment-record-dependencies/api.rst @@ -0,0 +1,4 @@ +API +=== + +.. automodule:: example_module diff --git a/tests/roots/test-environment-record-dependencies/conf.py b/tests/roots/test-environment-record-dependencies/conf.py new file mode 100644 index 000000000..107480e40 --- /dev/null +++ b/tests/roots/test-environment-record-dependencies/conf.py @@ -0,0 +1,5 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) +extensions = ['sphinx.ext.autodoc'] diff --git a/tests/roots/test-environment-record-dependencies/example_module.py b/tests/roots/test-environment-record-dependencies/example_module.py new file mode 100644 index 000000000..d12dc742d --- /dev/null +++ b/tests/roots/test-environment-record-dependencies/example_module.py @@ -0,0 +1,2 @@ +def example_function(): + return 42 diff --git a/tests/roots/test-environment-record-dependencies/index.rst b/tests/roots/test-environment-record-dependencies/index.rst new file mode 100644 index 000000000..21d88a053 --- /dev/null +++ b/tests/roots/test-environment-record-dependencies/index.rst @@ -0,0 +1,3 @@ +.. toctree:: + + api diff --git a/tests/test_environment_record_dependencies.py b/tests/test_environment_record_dependencies.py new file mode 100644 index 000000000..0a17253c0 --- /dev/null +++ b/tests/test_environment_record_dependencies.py @@ -0,0 +1,10 @@ +"""Tests for ``record_dependencies``.""" + +import pytest + + +@pytest.mark.sphinx('html', testroot='environment-record-dependencies') +def test_record_dependencies_cleared(app): + app.builder.read() + assert app.env.dependencies['index'] == set() + assert app.env.dependencies['api'] == {'example_module.py'} From c7c0e4048d72d4125a63ba9b83effe59d253774c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 23 Sep 2022 20:06:48 +0100 Subject: [PATCH 06/21] Split out `sphinx_highlight.js` --- doc/conf.py | 1 + karma.conf.js | 2 + sphinx/builders/html/__init__.py | 1 + sphinx/themes/basic/static/doctools.js | 130 ++-------------- .../themes/basic/static/sphinx_highlight.js | 145 ++++++++++++++++++ tests/js/documentation_options.js | 1 + tests/js/{doctools.js => sphinx_highlight.js} | 2 - tests/test_build_html.py | 3 +- 8 files changed, 163 insertions(+), 122 deletions(-) create mode 100644 sphinx/themes/basic/static/sphinx_highlight.js create mode 100644 tests/js/documentation_options.js rename tests/js/{doctools.js => sphinx_highlight.js} (98%) 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(); @@ -18,73 +25,11 @@ 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(); }, @@ -126,51 +71,6 @@ const Documentation = { Documentation.LOCALE = catalog.locale; }, - /** - * 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( - '" - ) - ); - }, - - /** - * 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 */ @@ -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( + '" + ) + ); + }, + + /** + * 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 similarity index 98% rename from tests/js/doctools.js rename to 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) From e2b3b3f060909f40b24ab0add54a83cea0b898d5 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sat, 24 Sep 2022 14:01:51 +0100 Subject: [PATCH 07/21] Move to localStorage for search highlighting --- CHANGES | 2 + sphinx/themes/basic/static/searchtools.js | 29 ++++------ .../themes/basic/static/sphinx_highlight.js | 55 +++++++++---------- 3 files changed, 41 insertions(+), 45 deletions(-) diff --git a/CHANGES b/CHANGES index 637b6b618..ed91dbfd6 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,8 @@ Features added * #6692: HTML Search: Include explicit :rst:dir:`index` directive index entries in the search index and search results. Patch by Adam Turner * #10816: imgmath: Allow embedding images in HTML as base64 +* #10854: HTML Search: Use browser localstorage for highlight control, stop + storing highlight parameters in URL query strings. Patch by Adam Turner. Bugs fixed ---------- diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index f16afbcbb..e89e34d4e 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -57,7 +57,7 @@ const _removeChildren = (element) => { const _escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string -const _displayItem = (item, highlightTerms, searchTerms) => { +const _displayItem = (item, searchTerms) => { const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT; const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; @@ -82,10 +82,8 @@ const _displayItem = (item, highlightTerms, searchTerms) => { requestUrl = docUrlRoot + docName + docFileSuffix; linkUrl = docName + docLinkSuffix; } - const params = new URLSearchParams(); - params.set("highlight", [...highlightTerms].join(" ")); let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + "?" + params.toString() + anchor; + linkEl.href = linkUrl + anchor; linkEl.dataset.score = score; linkEl.innerHTML = title; if (descr) @@ -97,7 +95,7 @@ const _displayItem = (item, highlightTerms, searchTerms) => { .then((data) => { if (data) listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, highlightTerms) + Search.makeSearchSummary(data, searchTerms) ); }); Search.output.appendChild(listItem); @@ -117,15 +115,14 @@ const _finishSearch = (resultCount) => { const _displayNextItem = ( results, resultCount, - highlightTerms, searchTerms ) => { // results left, load the summary and display it // this is intended to be dynamic (don't sub resultsCount) if (results.length) { - _displayItem(results.pop(), highlightTerms, searchTerms); + _displayItem(results.pop(), searchTerms); setTimeout( - () => _displayNextItem(results, resultCount, highlightTerms, searchTerms), + () => _displayNextItem(results, resultCount, searchTerms), 5 ); } @@ -271,6 +268,10 @@ const Search = { } }); + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + // console.debug("SEARCH: searching for:"); // console.info("required: ", [...searchTerms]); // console.info("excluded: ", [...excludedTerms]); @@ -359,7 +360,7 @@ const Search = { // console.info("search results:", Search.lastresults); // print the results - _displayNextItem(results, results.length, highlightTerms, searchTerms); + _displayNextItem(results, results.length, searchTerms); }, /** @@ -538,11 +539,9 @@ const Search = { /** * helper function to return a node containing the * search summary for a given text. keywords is a list - * of stemmed words, highlightWords is the list of normal, unstemmed - * words. the first one is used to find the occurrence, the - * latter for highlighting it. + * of stemmed words. */ - makeSearchSummary: (htmlText, keywords, highlightWords) => { + makeSearchSummary: (htmlText, keywords) => { const text = Search.htmlToText(htmlText); if (text === "") return null; @@ -560,10 +559,6 @@ const Search = { summary.classList.add("context"); summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; - highlightWords.forEach((highlightWord) => - _highlightText(summary, highlightWord, "highlighted") - ); - return summary; }, }; diff --git a/sphinx/themes/basic/static/sphinx_highlight.js b/sphinx/themes/basic/static/sphinx_highlight.js index 04889cc9d..aae669d7e 100644 --- a/sphinx/themes/basic/static/sphinx_highlight.js +++ b/sphinx/themes/basic/static/sphinx_highlight.js @@ -1,6 +1,8 @@ /* Highlighting utilities for Sphinx HTML documentation. */ "use strict"; +const SPHINX_HIGHLIGHT_ENABLED = true + /** * highlight a given string on a node by wrapping it in * span elements with the given class name. @@ -68,11 +70,22 @@ const _highlightText = (thisNode, text, className) => { const SphinxHighlight = { /** - * highlight the search words provided in the url in the text + * highlight the search words provided in localstorage in the text */ highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); const highlight = - new URLSearchParams(window.location.search).get("highlight") || ""; + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); if (terms.length === 0) return; // nothing to do @@ -91,7 +104,7 @@ const SphinxHighlight = { .createContextualFragment( '" ) ); @@ -107,39 +120,25 @@ const SphinxHighlight = { document .querySelectorAll("span.highlighted") .forEach((el) => el.classList.remove("highlighted")); - const url = new URL(window.location); - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); + localStorage.removeItem("sphinx_highlight_terms") }, - initOnKeyListeners: () => { + initEscapeListener: () => { // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; + 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(); - } + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); } }); }, }; _ready(SphinxHighlight.highlightSearchWords); -_ready(SphinxHighlight.initOnKeyListeners); +_ready(SphinxHighlight.initEscapeListener); From b842cabd4ff3613bd3d207cafd6fdea4357cba2a Mon Sep 17 00:00:00 2001 From: danieleades <33452915+danieleades@users.noreply.github.com> Date: Sat, 24 Sep 2022 15:12:01 +0100 Subject: [PATCH 08/21] Add a new mypy whitelist for '`Any` generics' (#10845) --- pyproject.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b5ebce1b6..14ca5d092 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,7 @@ show_error_context = true strict_optional = true warn_redundant_casts = true warn_unused_ignores = true +disallow_any_generics = true [[tool.mypy.overrides]] module = [ @@ -182,6 +183,30 @@ module = [ ] strict_optional = false +[[tool.mypy.overrides]] +module = [ + "sphinx.application", + "sphinx.builders.*", + "sphinx.cmd.*", + "sphinx.config", + "sphinx.deprecation", + "sphinx.domains.*", + "sphinx.environment.*", + "sphinx.events", + "sphinx.ext.*", + "sphinx.highlighting", + "sphinx.jinja2glue", + "sphinx.locale", + "sphinx.pycode.*", + "sphinx.registry", + "sphinx.roles", + "sphinx.search.*", + "sphinx.testing.*", + "sphinx.util.*", + "sphinx.writers.*", +] +disallow_any_generics = false + [tool.pytest.ini_options] filterwarnings = [ "all", From 65081218ce41b1bac03de567ae8b3cde08094b72 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sat, 24 Sep 2022 16:30:57 +0200 Subject: [PATCH 09/21] Docstring clarifications (#9877) Co-authored-by: Jakob Lykke Andersen Co-authored-by: Takeshi KOMIYA Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- sphinx/addnodes.py | 43 +++++++------- sphinx/application.py | 90 +++++++++++++++++------------ sphinx/builders/latex/transforms.py | 2 +- sphinx/domains/std.py | 6 +- sphinx/extension.py | 9 ++- 5 files changed, 85 insertions(+), 65 deletions(-) diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index e23f570d7..e22ce5fa2 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -1,4 +1,4 @@ -"""Additional docutils nodes.""" +"""Document tree nodes that Sphinx defines on top of those in Docutils.""" from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence @@ -119,7 +119,7 @@ class toctree(nodes.General, nodes.Element, translatable): ############################################################# class _desc_classes_injector(nodes.Element, not_smartquotable): - """Helper base class for injecting a fixes list of classes. + """Helper base class for injecting a fixed list of classes. Use as the first base class. """ @@ -390,7 +390,7 @@ class index(nodes.Invisible, nodes.Inline, nodes.TextElement): class centered(nodes.Part, nodes.TextElement): - """Deprecated.""" + """This node is deprecated.""" class acks(nodes.Element): @@ -455,13 +455,18 @@ class pending_xref(nodes.Inline, nodes.Element): class pending_xref_condition(nodes.Inline, nodes.TextElement): - """Node for cross-references that are used to choose appropriate - content of the reference by conditions on the resolving phase. + """Node representing a potential way to create a cross-reference and the + condition in which this way should be used. - When the :py:class:`pending_xref` node contains one or more - **pending_xref_condition** nodes, the cross-reference resolver - should choose the content of the reference using defined conditions - in ``condition`` attribute of each pending_xref_condition nodes:: + This node is only allowed to be placed under a :py:class:`pending_xref` + node. A **pending_xref** node must contain either no **pending_xref_condition** + nodes or it must only contains **pending_xref_condition** nodes. + + The cross-reference resolver will replace a :py:class:`pending_xref` which + contains **pending_xref_condition** nodes by the content of exactly one of + those **pending_xref_condition** nodes' content. It uses the **condition** + attribute to decide which **pending_xref_condition** node's content to + use. For example, let us consider how the cross-reference resolver acts on:: @@ -471,32 +476,26 @@ class pending_xref_condition(nodes.Inline, nodes.TextElement): io.StringIO - After the processing of cross-reference resolver, one of the content node - under pending_xref_condition node is chosen by its condition and to be - removed all of pending_xref_condition nodes:: + If the cross-reference resolver successfully resolves the cross-reference, + then it rewrites the **pending_xref** as:: - # When resolved the cross-reference successfully StringIO - # When resolution is failed + Otherwise, if the cross-reference resolution failed, it rewrites the + **pending_xref** as:: + io.StringIO - .. note:: This node is only allowed to be placed under pending_xref node. - It is not allows to place it under other nodes. In addition, - pending_xref node must contain only pending_xref_condition - nodes if it contains one or more pending_xref_condition nodes. - - The pending_xref_condition node should have **condition** attribute. + The **pending_xref_condition** node should have **condition** attribute. Domains can be store their individual conditions into the attribute to filter contents on resolving phase. As a reserved condition name, ``condition="*"`` is used for the fallback of resolution failure. Additionally, as a recommended condition name, ``condition="resolved"`` - is used for the representation of resolstion success in the intersphinx - module. + represents a resolution success in the intersphinx module. .. versionadded:: 4.0 """ diff --git a/sphinx/application.py b/sphinx/application.py index 565125b1e..10c03a268 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -639,8 +639,9 @@ class Sphinx: :param name: The name of the directive :param cls: A directive class - :param override: If true, install the directive forcedly even if another directive + :param override: If false, do not install it if another directive is already installed as the same name + If true, unconditionally install the directive. For example, a custom directive named ``my-directive`` would be added like this: @@ -687,8 +688,9 @@ class Sphinx: :param name: The name of role :param role: A role function - :param override: If true, install the role forcedly even if another role is already - installed as the same name + :param override: If false, do not install it if another role + is already installed as the same name + If true, unconditionally install the role. For more details about role functions, see `the Docutils docs `__ . @@ -708,8 +710,9 @@ class Sphinx: Register a Docutils role that does nothing but wrap its contents in the node given by *nodeclass*. - If *override* is True, the given *nodeclass* is forcedly installed even if - a role named as *name* is already installed. + :param override: If false, do not install it if another role + is already installed as the same name + If true, unconditionally install the role. .. versionadded:: 0.6 .. versionchanged:: 1.8 @@ -728,8 +731,9 @@ class Sphinx: """Register a domain. :param domain: A domain class - :param override: If true, install the domain forcedly even if another domain + :param override: If false, do not install it if another domain is already installed as the same name + If true, unconditionally install the domain. .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -747,8 +751,9 @@ class Sphinx: :param domain: The name of target domain :param name: A name of directive :param cls: A directive class - :param override: If true, install the directive forcedly even if another directive + :param override: If false, do not install it if another directive is already installed as the same name + If true, unconditionally install the directive. .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -766,8 +771,9 @@ class Sphinx: :param domain: The name of the target domain :param name: The name of the role :param role: The role function - :param override: If true, install the role forcedly even if another role is already - installed as the same name + :param override: If false, do not install it if another role + is already installed as the same name + If true, unconditionally install the role. .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -783,8 +789,9 @@ class Sphinx: :param domain: The name of the target domain :param index: The index class - :param override: If true, install the index forcedly even if another index is - already installed as the same name + :param override: If false, do not install it if another index + is already installed as the same name + If true, unconditionally install the index. .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -889,8 +896,10 @@ class Sphinx: (Of course, the element following the ``topic`` directive needn't be a section.) - If *override* is True, the given crossref_type is forcedly installed even if - a crossref_type having the same name is already installed. + + :param override: If false, do not install it if another cross-reference type + is already installed as the same name + If true, unconditionally install the cross-reference type. .. versionchanged:: 1.8 Add *override* keyword. @@ -949,20 +958,22 @@ class Sphinx: loading_method: Optional[str] = None, **kwargs: Any) -> None: """Register a JavaScript file to include in the HTML output. - :param filename: The filename of the JavaScript file. It must be relative to the HTML - static path, a full URI with scheme, or ``None`` value. The ``None`` - value is used to create inline ``