From 79f6959ed234342b57524a3e7a674757ae158518 Mon Sep 17 00:00:00 2001 From: James Addison Date: Fri, 26 Jul 2024 17:09:56 +0100 Subject: [PATCH] HTML search: allow configuration of the search index filename --- doc/usage/configuration.rst | 7 +++++++ sphinx/builders/html/__init__.py | 23 ++++++++++++++++++++-- sphinx/themes/basic/search.html | 2 +- tests/test_search.py | 33 ++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 5ffe2ea4a38..b9dafcdbe3c 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -2129,6 +2129,13 @@ and also make use of these options. .. versionadded:: 1.2 +.. confval:: html_search_filename + :type: :code-py:`str` + :default: :code-py:`searchindex.js` + + Controls the filename of the JavaScript search index that will be + written in HTML output. + .. confval:: html_scaled_image_link :type: :code-py:`bool` :default: :code-py:`True` diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 06917232f6b..433726c7a8c 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -189,7 +189,6 @@ class StandaloneHTMLBuilder(Builder): 'image/gif', 'image/jpeg'] supported_remote_images = True supported_data_uri_images = True - searchindex_filename = 'searchindex.js' add_permalinks = True allow_sharp_as_current_path = True embedded = False # for things like HTML help or Qt help: suppresses sidebar @@ -249,6 +248,8 @@ def init(self) -> None: self.use_index = self.get_builder_config('use_index', 'html') + self.searchindex_filename = self.get_builder_config('search_filename', 'html') + def create_build_info(self) -> BuildInfo: return BuildInfo(self.config, self.tags, frozenset({'html'})) @@ -716,7 +717,8 @@ def gen_additional_pages(self) -> None: # the search page if self.search: logger.info('search ', nonl=True) - self.handle_page('search', {}, 'search.html') + searchcontext = {'search_index': self.searchindex_filename} + self.handle_page('search', searchcontext, 'search.html') # the opensearch xml file if self.config.html_use_opensearch and self.search: @@ -1320,6 +1322,21 @@ def validate_html_favicon(app: Sphinx, config: Config) -> None: config.html_favicon = None +def validate_html_search_filename(app: Sphinx, config: Config) -> None: + """Check html_search_filename setting.""" + if config.html_search_filename: + if isurl(config.html_search_filename): + logger.warning(__('html_search_filename must not be a URL')) + config.html_search_filename = 'searchindex.js' + if not config.html_search_filename.endswith('.js'): + logger.warning(__('html_search_filename must have a .js suffix')) + config.html_search_filename = 'searchindex.js' + search_path = path.normpath(path.join(app.outdir, config.html_search_filename)) + if path.commonpath((app.outdir, search_path)) != path.normpath(app.outdir): + logger.warning(__('html_search_filename must be within the output directory')) + config.html_search_filename = 'searchindex.js' + + def error_on_html_sidebars_string_values(app: Sphinx, config: Config) -> None: """Support removed in Sphinx 2.""" errors = {} @@ -1387,6 +1404,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value('html_search_language', None, 'html', str) app.add_config_value('html_search_options', {}, 'html') app.add_config_value('html_search_scorer', '', '') + app.add_config_value('html_search_filename', 'searchindex.js', 'html', str) app.add_config_value('html_scaled_image_link', True, 'html') app.add_config_value('html_baseurl', '', 'html') # removal is indefinitely on hold (ref: https://github.com/sphinx-doc/sphinx/issues/10265) @@ -1406,6 +1424,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: 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', validate_html_search_filename, priority=800) app.connect('config-inited', error_on_html_sidebars_string_values, priority=800) app.connect('config-inited', error_on_html_4, priority=800) app.connect('builder-inited', validate_math_renderer) diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html index 8bad82a51be..53e3a398153 100644 --- a/sphinx/themes/basic/search.html +++ b/sphinx/themes/basic/search.html @@ -15,7 +15,7 @@ {%- endblock %} {% block extrahead %} - + {{ super() }} {% endblock %} diff --git a/tests/test_search.py b/tests/test_search.py index 3687911e488..658ab4101e4 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -349,6 +349,39 @@ def test_search_index_is_deterministic(app): assert_is_sorted(index, '') +@pytest.mark.sphinx(testroot='search', + confoverrides={'html_search_filename': '_static/searchindex.js'}) +def test_search_index_alternate_filename(app): + app.build(force_all=True) + assert (app.outdir / '_static' / 'searchindex.js').exists() + + +@pytest.mark.sphinx(testroot='search', + confoverrides={'html_search_filename': 'http://example.invalid/searchindex.js'}) +def test_search_index_url_filename_rejected(app): + app.build(force_all=True) + assert (app.outdir / 'searchindex.js').exists() # default searchindex.js location + assert "must not be a URL" in app.warning.getvalue() + + +@pytest.mark.sphinx(testroot='search', + confoverrides={'html_search_filename': 'searchindex.html'}) +def test_search_index_non_js_filename_rejected(app): + app.build(force_all=True) + assert not (app.outdir / 'searchindex.html').exists() + assert (app.outdir / 'searchindex.js').exists() # default searchindex.js location + assert "must have a .js suffix" in app.warning.getvalue() + + +@pytest.mark.sphinx(testroot='search', + confoverrides={'html_search_filename': '../disallowed.js'}) +def test_search_index_filename_outside_outdir_rejected(app): + app.build(force_all=True) + assert not (app.outdir / '..' / 'disallowed.js').exists() + assert (app.outdir / 'searchindex.js').exists() # default searchindex.js location + assert "must be within the output directory" in app.warning.getvalue() + + def is_title_tuple_type(item: list[int | str]): """ In the search index, titles inside .alltitles are stored as a tuple of