sunpy-sphinx-theme 2.2.2__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/.readthedocs.yaml +1 -1
  2. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/.rtd-environment.yml +1 -1
  3. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/PKG-INFO +6 -3
  4. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/_typos.toml +1 -0
  5. sunpy_sphinx_theme-2.4.0/conftest.py +142 -0
  6. sunpy_sphinx_theme-2.4.0/flake.nix +34 -0
  7. sunpy_sphinx_theme-2.4.0/package.json +8 -0
  8. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/pyproject.toml +11 -2
  9. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/ruff.toml +6 -8
  10. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/__init__.py +95 -47
  11. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/__init__.py +3 -0
  12. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/helpers.py +13 -0
  13. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/base/conf.py +9 -0
  14. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/base/index.rst +4 -0
  15. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/conf.py +16 -0
  16. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/index.rst +4 -0
  17. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/theme/theme.toml +15 -0
  18. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_a11y.py +177 -0
  19. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_config_subtheme.py +64 -0
  20. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_goatcounter.py +164 -0
  21. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_options.py +17 -0
  22. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/sunpy_style.css +12 -0
  23. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/theme.toml +4 -3
  24. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/PKG-INFO +6 -3
  25. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/SOURCES.txt +16 -0
  26. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/requires.txt +5 -1
  27. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme.egg-info/scm_file_list.json +73 -0
  28. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +8 -0
  29. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/.editorconfig +0 -0
  30. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/.stylelintrc.json +0 -0
  31. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/LICENSE.md +0 -0
  32. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/MANIFEST.in +0 -0
  33. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/README.md +0 -0
  34. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/setup.cfg +0 -0
  35. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/cards.py +0 -0
  36. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/conf.py +0 -0
  37. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/built-with.html +0 -0
  38. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/footer-links.html +0 -0
  39. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar-logo.html +0 -0
  40. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_center.html +0 -0
  41. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_start.html +0 -0
  42. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/sst-sidebar-nav.html +0 -0
  43. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/sections/header.html +0 -0
  44. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/css/rtd_enhanced_search.css +0 -0
  45. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/css/sunpy_cards.css +0 -0
  46. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/favicon-32.ico +0 -0
  47. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/sunpy_icon.svg +0 -0
  48. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/sunpy_icon_128x128.png +0 -0
  49. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/js/rtd_enhanced_search.js +0 -0
  50. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/js/submenu-concertina-toggle.js +0 -0
  51. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/dependency_links.txt +0 -0
  52. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/entry_points.txt +0 -0
  53. {sunpy_sphinx_theme-2.2.2 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/top_level.txt +0 -0
@@ -2,7 +2,7 @@ version: 2
2
2
  build:
3
3
  os: ubuntu-lts-latest
4
4
  tools:
5
- python: "mambaforge-latest"
5
+ python: "miniforge3-latest"
6
6
  jobs:
7
7
  post_checkout:
8
8
  - git fetch --unshallow || true
@@ -2,6 +2,6 @@ name: rtd_sunpy_sphinx_theme
2
2
  channels:
3
3
  - conda-forge
4
4
  dependencies:
5
- - python=3.11
5
+ - python=3.14
6
6
  - pip
7
7
  - graphviz!=2.42.*,!=2.43.*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunpy-sphinx-theme
3
- Version: 2.2.2
3
+ Version: 2.4.0
4
4
  Summary: The sphinx theme for the SunPy website and documentation.
5
5
  Author: The SunPy Developers
6
6
  License: BSD 2-Clause License
@@ -36,10 +36,10 @@ Classifier: Programming Language :: Python :: 3
36
36
  Classifier: Framework :: Sphinx
37
37
  Classifier: Framework :: Sphinx :: Theme
38
38
  Classifier: Operating System :: OS Independent
39
- Requires-Python: >=3.9
39
+ Requires-Python: >=3.11
40
40
  Description-Content-Type: text/markdown
41
41
  License-File: LICENSE.md
42
- Requires-Dist: pydata-sphinx-theme<0.19,>=0.17
42
+ Requires-Dist: pydata-sphinx-theme<0.24,>=0.22.0
43
43
  Requires-Dist: sphinxext-opengraph>=0.13
44
44
  Requires-Dist: sphinx>=7.3.0
45
45
  Provides-Extra: docs
@@ -50,6 +50,9 @@ Requires-Dist: sphinx-design; extra == "docs"
50
50
  Requires-Dist: sphinx-gallery; extra == "docs"
51
51
  Requires-Dist: sphinx-togglebutton; extra == "docs"
52
52
  Requires-Dist: sunpy; extra == "docs"
53
+ Provides-Extra: test
54
+ Requires-Dist: pytest>=7.0; extra == "test"
55
+ Requires-Dist: pytest-playwright; extra == "test"
53
56
  Dynamic: license-file
54
57
 
55
58
  # SunPy Project Sphinx Theme
@@ -21,4 +21,5 @@ default.extend-ignore-identifiers-re = [
21
21
  "ot",
22
22
  # Coords,
23
23
  "pn",
24
+ "CPY",
24
25
  ]
@@ -0,0 +1,142 @@
1
+ """Configuration of the pytest session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import time
7
+ from http.client import HTTPConnection
8
+ from os import environ
9
+ from pathlib import Path
10
+ from shutil import copytree
11
+ from subprocess import PIPE, Popen
12
+ from typing import TYPE_CHECKING
13
+
14
+ import pytest
15
+ from bs4 import BeautifulSoup
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+ from sphinx.testing.util import SphinxTestApp
21
+
22
+ pytest_plugins = "sphinx.testing.fixtures"
23
+
24
+ repo_path = Path(__file__).parent
25
+ tests_path = repo_path / "src" / "sunpy_sphinx_theme" / "tests"
26
+ docs_build_path = repo_path / "docs" / "_build" / "html"
27
+
28
+
29
+ # -- global fixture to build sphinx tmp docs ---------------------------------
30
+
31
+
32
+ class SphinxBuild:
33
+ """Helper class to build a test documentation."""
34
+
35
+ def __init__(self, app: SphinxTestApp, src: Path):
36
+ self.app = app
37
+ self.src = src
38
+
39
+ def build(self, *, no_warning: bool = True) -> SphinxBuild:
40
+ """Build the application."""
41
+ self.app.build()
42
+ if no_warning is True:
43
+ assert self.warnings == "", self.status
44
+ return self
45
+
46
+ @property
47
+ def status(self) -> str:
48
+ """Returns the status of the current build."""
49
+ return self.app.status.getvalue()
50
+
51
+ @property
52
+ def warnings(self) -> str:
53
+ """Returns the warnings raised by the current build."""
54
+ return self.app.warning.getvalue()
55
+
56
+ @property
57
+ def outdir(self) -> Path:
58
+ """Returns the output directory of the current build."""
59
+ return Path(self.app.outdir)
60
+
61
+ def html_tree(self, *path) -> BeautifulSoup:
62
+ """Returns the html tree of the current build."""
63
+ path_page = self.outdir.joinpath(*path)
64
+ if not path_page.exists():
65
+ msg = f"{path_page} does not exist"
66
+ raise ValueError(msg)
67
+ return BeautifulSoup(path_page.read_text("utf8"), "html.parser")
68
+
69
+
70
+ @pytest.fixture
71
+ def sphinx_build_factory(make_app: Callable, tmp_path: Path, request) -> Callable:
72
+ """Return a factory builder pointing to the tmp directory."""
73
+
74
+ def _func(src_folder: str, files: dict[str, str] | None = None, **kwargs) -> SphinxBuild:
75
+ """Create the SphinxBuild from the source folder."""
76
+ no_temp = environ.get("SST_TEST_HTML_DIR")
77
+ nonlocal tmp_path
78
+ if no_temp is not None:
79
+ tmp_path = Path(no_temp) / request.node.name / str(src_folder)
80
+ srcdir = tmp_path / src_folder
81
+ copytree(tests_path / "sites" / src_folder, tmp_path / src_folder)
82
+ for filename, contents in (files or {}).items():
83
+ filepath = Path(srcdir / filename).resolve()
84
+ filepath.parent.mkdir(exist_ok=True, parents=True)
85
+ filepath.write_text(contents)
86
+ app = make_app(srcdir=srcdir, **kwargs)
87
+ return SphinxBuild(app, tmp_path / src_folder)
88
+
89
+ return _func
90
+
91
+
92
+ # -- global fixture to serve the built docs ----------------------------------
93
+
94
+
95
+ @pytest.fixture(scope="module")
96
+ def url_base():
97
+ """Start local server on built docs and return the localhost URL as the base URL."""
98
+ # The accessibility tests are run against a build of our documentation, so
99
+ # fail early with a helpful message if it has not been built yet.
100
+ if not docs_build_path.is_dir():
101
+ msg = f"No docs build found at {docs_build_path}; build it first with 'tox -e build_docs'"
102
+ pytest.fail(msg, pytrace=False)
103
+
104
+ # Use a port that is not commonly used during development or else you will
105
+ # force the developer to stop running their dev server in order to run the
106
+ # tests.
107
+ port = 8213
108
+ host = "localhost"
109
+ url = f"http://{host}:{port}"
110
+
111
+ # Try starting the server
112
+ process = Popen(
113
+ [sys.executable, "-m", "http.server", str(port), "--directory", docs_build_path],
114
+ stdout=PIPE,
115
+ )
116
+
117
+ # Try connecting to the server
118
+ retries = 5
119
+ while retries > 0:
120
+ conn = HTTPConnection(host, port)
121
+ try:
122
+ conn.request("HEAD", "/")
123
+ response = conn.getresponse()
124
+ if response is not None:
125
+ yield url
126
+ break
127
+ except ConnectionRefusedError:
128
+ time.sleep(1)
129
+ retries -= 1
130
+ finally:
131
+ conn.close()
132
+
133
+ # If the code above never yields a URL, then we were never able to connect
134
+ # to the server and retries == 0.
135
+ if not retries:
136
+ msg_0 = "Failed to start http server in 5 seconds"
137
+ raise RuntimeError(msg_0)
138
+ else:
139
+ # Otherwise the server started and this fixture is done now and we clean
140
+ # up by stopping the server.
141
+ process.terminate()
142
+ process.wait()
@@ -0,0 +1,34 @@
1
+ {
2
+ description = "A very basic flake";
3
+
4
+ inputs = {
5
+ flake-parts.url = "github:hercules-ci/flake-parts";
6
+ nixpkgs.url = "github:nixos/nixpkgs";
7
+ };
8
+
9
+ outputs = { flake-parts, ... } @ inputs: flake-parts.lib.mkFlake { inherit inputs; } {
10
+ perSystem = { config, self', inputs', pkgs, system, ... }: {
11
+ devShells.default = pkgs.mkShell {
12
+ # venvShellHook needs venvDir set on the shell
13
+ venvDir = ".venv";
14
+
15
+ packages = with pkgs; [
16
+ nodejs_latest
17
+ playwright-driver.browsers
18
+ uv
19
+ ];
20
+
21
+ # runs every time you enter the shell, after activation
22
+ shellHook = ''
23
+ export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
24
+ export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true;
25
+ uv sync --all-extras
26
+ source .venv/bin/activate
27
+ npm install
28
+ '';
29
+ };
30
+ };
31
+ # Declared systems that your flake supports. These will be enumerated in perSystem
32
+ systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
33
+ };
34
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "sunpy-sphinx-theme",
3
+ "private": true,
4
+ "description": "Node dependencies for testing the sunpy-sphinx-theme",
5
+ "devDependencies": {
6
+ "axe-core": "^4.13.0"
7
+ }
8
+ }
@@ -20,9 +20,9 @@ classifiers = [
20
20
  "Framework :: Sphinx :: Theme",
21
21
  "Operating System :: OS Independent",
22
22
  ]
23
- requires-python = ">=3.9"
23
+ requires-python = ">=3.11"
24
24
  dependencies = [
25
- "pydata-sphinx-theme>=0.17,<0.19",
25
+ "pydata-sphinx-theme>=0.22.0,<0.24",
26
26
  "sphinxext-opengraph>=0.13",
27
27
  "sphinx>=7.3.0"
28
28
  ]
@@ -37,6 +37,10 @@ docs = [
37
37
  "sphinx-togglebutton",
38
38
  "sunpy",
39
39
  ]
40
+ test = [
41
+ "pytest>=7.0",
42
+ "pytest-playwright",
43
+ ]
40
44
 
41
45
  [project.entry-points]
42
46
  "sphinx.html_themes" = { sunpy = "sunpy_sphinx_theme" }
@@ -46,6 +50,11 @@ include-package-data = true
46
50
 
47
51
  [tool.setuptools_scm]
48
52
 
53
+ [tool.pytest.ini_options]
54
+ markers = "a11y: mark a test as an accessibility test"
55
+ testpaths = ["tests"]
56
+ addopts = "--durations=10"
57
+
49
58
  [ tool.gilesbot ]
50
59
  [ tool.gilesbot.circleci_artifacts.giles ]
51
60
  enabled = false
@@ -31,6 +31,7 @@ lint.extend-ignore = [
31
31
  "PLR2004", # Magic value used in comparison
32
32
  "TD002", # Missing author in TODO
33
33
  "TD003", # Missing issue link on the line following this TODO
34
+ "CPY001", # Copyright in each file
34
35
  ]
35
36
 
36
37
  [lint.per-file-ignores]
@@ -48,17 +49,14 @@ lint.extend-ignore = [
48
49
  "setup.py" = [
49
50
  "D100", # Missing docstring in public module
50
51
  ]
51
- "test_*.py" = [
52
+ "{src/sunpy_sphinx_theme/tests/*.py,conftest.py,test_*.py}" = [
52
53
  "D100", # Missing docstring in public module
53
54
  "D103", # Missing docstring in public function
54
- "N806", # in function should be lowercase
55
+ "INP001", # tests are not part of an implicit namespace package
55
56
  "S101", # Use of `assert` detected
56
- ]
57
- "sunpy_soar/version.py" = [
58
- "D100", # Missing docstring in public module
59
- ]
60
- "sunpy_soar/conftest.py" = [
61
- "D100", # Missing docstring in public module
57
+ "S404", # subprocess module is possibly insecure
58
+ "S603", # subprocess call: check for execution of untrusted input
59
+ "ARG001", # Unused function argument (kept for interface compatibility)
62
60
  ]
63
61
 
64
62
  [lint.pydocstyle]
@@ -12,6 +12,9 @@ from urllib.parse import urljoin, urlparse
12
12
 
13
13
  from pydata_sphinx_theme import utils
14
14
  from sphinx.application import Sphinx
15
+ from sphinx.util import logging
16
+
17
+ logger = logging.getLogger(__name__)
15
18
 
16
19
  __all__ = ["ON_RTD", "PNG_ICON", "SVG_ICON", "get_html_theme_path"]
17
20
 
@@ -45,7 +48,7 @@ def update_config(app) -> None:
45
48
  theme_options = utils.get_theme_options_dict(app)
46
49
 
47
50
  if theme_options_with_defaults.get("sst_logo") and not isinstance(theme_options_with_defaults["sst_logo"], dict):
48
- sst_logo = str(theme_options["sst_logo"])
51
+ sst_logo = str(theme_options_with_defaults["sst_logo"])
49
52
  theme_options["sst_logo"] = {"light": sst_logo, "dark": sst_logo}
50
53
 
51
54
  theme_options["sst_is_root"] = bool(theme_options_with_defaults.get("sst_is_root", False))
@@ -122,6 +125,28 @@ def update_html_context(app: Sphinx, pagename: str, templatename: str, context,
122
125
  context["sst_pathto"] = partial(sst_pathto, context)
123
126
 
124
127
 
128
+ def _get_sphinx_gallery_thumbnail(doctree):
129
+ """
130
+ Return the generated Sphinx-Gallery thumbnail for an example page.
131
+ """
132
+ source = doctree.get("source")
133
+ if not source:
134
+ return None
135
+
136
+ source_path = Path(source)
137
+ thumbnail_dir = source_path.parent / "images" / "thumb"
138
+ if not thumbnail_dir.is_dir():
139
+ return None
140
+
141
+ # Reuse the thumbnail Sphinx-Gallery already picked for this page.
142
+ thumbnails = sorted(thumbnail_dir.glob(f"sphx_glr_{source_path.stem}_thumb.*"))
143
+ for thumbnail in thumbnails:
144
+ if thumbnail.suffix.lower() in SPHINX_GALLERY_IMAGE_EXTENSIONS:
145
+ return thumbnail
146
+
147
+ return None
148
+
149
+
125
150
  def update_opengraph_context(app: Sphinx, pagename: str, templatename: str, context, doctree) -> None: # NOQA: ARG001
126
151
  """
127
152
  Add Sphinx-Gallery images to the Open Graph metadata context.
@@ -133,6 +158,13 @@ def update_opengraph_context(app: Sphinx, pagename: str, templatename: str, cont
133
158
  if "og:image" in metadata:
134
159
  return
135
160
 
161
+ # Prefer the user defined generated gallery thumbnail when one exists.
162
+ if thumbnail := _get_sphinx_gallery_thumbnail(doctree):
163
+ metadata["og:image"] = posixpath.join(app.builder.imagedir, thumbnail.name)
164
+ context["meta"] = metadata
165
+ return
166
+
167
+ # Fall back to the first gallery image
136
168
  for node in doctree.findall():
137
169
  if node.__class__.__name__ != "imgsgnode" or not node.get("uri"):
138
170
  continue
@@ -159,28 +191,43 @@ def generate_search_config(app):
159
191
  This function parses the config for the "Documentation" section of the theme config.
160
192
  """
161
193
  theme_config = get_theme_options(app)
162
- search_projects = theme_config.get("rtd_search_projects", None)
194
+
195
+ if not theme_config.get("rtd_search", True):
196
+ return
197
+
198
+ # Add project-wide search
199
+ app.add_css_file("css/rtd_enhanced_search.css")
200
+ app.add_js_file(
201
+ "js/rtd_enhanced_search.js",
202
+ loading_method="async",
203
+ )
204
+
205
+ def filter_doc_links(links):
206
+ out_links = []
207
+ for link in links:
208
+ if isinstance(link[1], list):
209
+ out_links += filter_doc_links(link[1])
210
+ elif isinstance(link[1], str) and link[1].startswith("http"):
211
+ out_links.append({"name": link[0], "link": link[1]})
212
+ else:
213
+ err = f"Unable to parse {link} in the nav tree. Try setting search_projects explicitly or fixing navbar_links."
214
+ raise ValueError(err)
215
+ return out_links
216
+
217
+ search_projects = theme_config.get("rtd_search_projects", []) or []
163
218
  if not search_projects:
164
219
  navbar_links = theme_config["navbar_links"]
165
- doc_links = next(section[1] for section in navbar_links if section[0] == "Documentation")
166
-
167
- def filter_doc_links(links):
168
- out_links = []
169
- for link in links:
170
- if isinstance(link[1], list):
171
- out_links += filter_doc_links(link[1])
172
- elif isinstance(link[1], str) and link[1].startswith("http"):
173
- out_links.append({"name": link[0], "link": link[1]})
174
- else:
175
- err = f"Unable to parse {link} in the nav tree. Try setting search_projects explicitly or fixing navbar_links."
176
- raise ValueError(err)
177
- return out_links
178
-
179
- search_projects = filter_doc_links(doc_links)
220
+ doc_links = next((section[1] for section in navbar_links if section[0] == "Documentation"), None)
221
+ if doc_links:
222
+ search_projects = filter_doc_links(doc_links)
180
223
 
181
224
  if extra_search_projects := theme_config.get("rtd_extra_search_projects", None):
182
225
  search_projects += filter_doc_links(extra_search_projects)
183
226
 
227
+ if not search_projects:
228
+ logger.info("No RTD search projects were found, disabling RTD search.")
229
+ return
230
+
184
231
  load_more_label = theme_config.get("rtd_search_load_more_label", "Load more results")
185
232
  no_results_label = theme_config.get("rtd_search_no_results_label", "There are no results for this search")
186
233
  script = dedent(f"""
@@ -198,30 +245,24 @@ def generate_search_config(app):
198
245
  app.add_js_file(None, body=script)
199
246
 
200
247
 
201
- def setup(app: Sphinx):
202
- app.setup_extension("sphinxext.opengraph")
203
-
204
- # Register theme
205
- theme_dir = get_html_theme_path()
206
- app.add_html_theme("sunpy", theme_dir)
207
- app.add_css_file("sunpy_style.css", priority=600)
208
- app.connect("builder-inited", update_config, priority=100)
209
- app.connect("builder-inited", generate_search_config, priority=500)
210
- app.connect("html-page-context", update_opengraph_context, priority=400)
211
- app.connect("html-page-context", update_html_context)
212
- # Conditionally include goat counter js
213
- # We can't do this in update_config as that causes the scripts to be duplicated.
214
- # Also in here none of the theme defaults have be applied by `update_config`
215
- # TODO: Improve this mess
216
- theme_options = utils.get_theme_options_dict(app)
248
+ def configure_goatcounter(app):
249
+ """
250
+ Conditionally include goat counter js and configure endpoint.
251
+ """
252
+ theme_options = get_theme_options(app)
217
253
  # We want to default to the sunpy goat counter only if the sst_site_root is sunpy.org
218
254
  root_domain = theme_options.get("sst_site_root", "https://sunpy.org")
219
255
  sunpy_goat_url = "https://sunpy.goatcounter.com/count"
220
- default_goat_url = sunpy_goat_url if root_domain == "https://sunpy.org" else None
221
- if primary_goat_url := theme_options.get("goatcounter_analytics_url", default_goat_url):
256
+ default_goat_url = (
257
+ sunpy_goat_url if root_domain == "https://sunpy.org" else False
258
+ ) # Only default on sunpy.org pages
259
+ # The theme default is 'DEFAULT', to allow disabling it
260
+ primary_goat_url = theme_options.get("goatcounter_analytics_url", "DEFAULT")
261
+ primary_goat_url = primary_goat_url if primary_goat_url != "DEFAULT" else default_goat_url
262
+ if primary_goat_url:
222
263
  root_domain = root_domain.removeprefix("https://").removeprefix("http://")
223
264
  default_endpoint = theme_options.get("goatcounter_non_domain_endpoint", False)
224
- if default_endpoint is False:
265
+ if default_endpoint in (False, "False"): # sphinx config can sometimes be str
225
266
  default_endpoint = ""
226
267
  app.add_js_file(
227
268
  None,
@@ -241,18 +282,25 @@ def setup(app: Sphinx):
241
282
  "https://gc.zgo.at/count.js",
242
283
  loading_method="async",
243
284
  )
244
- app.add_js_file(
245
- "js/submenu-concertina-toggle.js",
246
- loading_method="async",
247
- )
248
285
 
249
- if theme_options.get("rtd_search", True):
250
- # Add project-wide search
251
- app.add_css_file("css/rtd_enhanced_search.css")
252
- app.add_js_file(
253
- "js/rtd_enhanced_search.js",
254
- loading_method="async",
255
- )
286
+
287
+ def setup(app: Sphinx):
288
+ app.setup_extension("sphinxext.opengraph")
289
+
290
+ # Register theme
291
+ theme_dir = get_html_theme_path()
292
+ app.add_html_theme("sunpy", theme_dir)
293
+ app.add_css_file("sunpy_style.css", priority=600)
294
+ app.connect("builder-inited", update_config, priority=100)
295
+ app.connect("builder-inited", generate_search_config, priority=500)
296
+ app.connect("builder-inited", configure_goatcounter, priority=510)
297
+ app.connect("html-page-context", update_opengraph_context, priority=400)
298
+ app.connect("html-page-context", update_html_context)
299
+
300
+ app.add_js_file(
301
+ "js/submenu-concertina-toggle.js",
302
+ loading_method="async",
303
+ )
256
304
 
257
305
  return {
258
306
  "parallel_read_safe": True,
@@ -0,0 +1,13 @@
1
+ def inline_scripts(html_tree):
2
+ """Return the content of all inline scripts in a page."""
3
+ return [script.string for script in html_tree.select("script") if script.string]
4
+
5
+
6
+ def script_srcs(html_tree):
7
+ """Return the src URLs of all external scripts in a page."""
8
+ return [script["src"] for script in html_tree.select("script[src]")]
9
+
10
+
11
+ def stylesheet_hrefs(html_tree):
12
+ """Return the hrefs of all stylesheets in a page."""
13
+ return [link["href"] for link in html_tree.select("link[rel='stylesheet']")]
@@ -0,0 +1,9 @@
1
+ """Test conf file."""
2
+
3
+ project = "SunPy Theme Tests"
4
+ copyright = "2026, The SunPy Community" # NOQA: A001
5
+ author = "The SunPy Community"
6
+
7
+ extensions = []
8
+ html_theme = "sunpy"
9
+ html_static_path = []
@@ -0,0 +1,4 @@
1
+ Test Documentation
2
+ ==================
3
+
4
+ A page used to test the theme.
@@ -0,0 +1,16 @@
1
+ """Test conf file for a theme derived from the sunpy theme."""
2
+
3
+ from pathlib import Path
4
+
5
+ project = "SunPy Derived Theme Tests"
6
+ copyright = "2026, The SunPy Community" # NOQA: A001
7
+ author = "The SunPy Community"
8
+
9
+ extensions = ["sunpy_sphinx_theme"]
10
+ html_theme = "derived"
11
+ html_static_path = []
12
+
13
+
14
+ def setup(app):
15
+ """Register the derived theme with Sphinx."""
16
+ app.add_html_theme("derived", str(Path(__file__).parent / "theme"))
@@ -0,0 +1,4 @@
1
+ Test Documentation
2
+ ==================
3
+
4
+ A page used to test a theme derived from the sunpy theme.
@@ -0,0 +1,15 @@
1
+ [theme]
2
+ inherit = "sunpy"
3
+
4
+ [options]
5
+ sst_site_root = "https://example.org/"
6
+ navbar_links = [
7
+ ["Home", "index.html", 2],
8
+ ["Docs", "https://docs.example.org/", 3],
9
+ ]
10
+ footer_links = [
11
+ ["Footer Example", "https://footer.example.org/", 3],
12
+ ]
13
+ rtd_search_projects = [
14
+ { name = "example", link = "https://example.org/docs/" },
15
+ ]