summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.editorconfig13
-rw-r--r--.gitignore18
-rw-r--r--.pre-commit-config.yaml28
-rw-r--r--CHANGES.rst192
-rw-r--r--LICENSE.rst28
-rw-r--r--MANIFEST.in5
-rw-r--r--README.rst29
-rw-r--r--requirements/dev.in2
-rw-r--r--requirements/dev.txt44
-rw-r--r--setup.cfg96
-rw-r--r--setup.py10
-rw-r--r--src/pallets_sphinx_themes/__init__.py189
-rw-r--r--src/pallets_sphinx_themes/theme_check.py50
-rw-r--r--src/pallets_sphinx_themes/themes/__init__.py0
-rw-r--r--src/pallets_sphinx_themes/themes/babel/static/babel.css24
-rw-r--r--src/pallets_sphinx_themes/themes/babel/theme.conf3
-rw-r--r--src/pallets_sphinx_themes/themes/click/__init__.py8
-rw-r--r--src/pallets_sphinx_themes/themes/click/domain.py275
-rw-r--r--src/pallets_sphinx_themes/themes/click/static/click.css28
-rw-r--r--src/pallets_sphinx_themes/themes/click/theme.conf4
-rw-r--r--src/pallets_sphinx_themes/themes/flask/static/flask.css15
-rw-r--r--src/pallets_sphinx_themes/themes/flask/theme.conf3
-rw-r--r--src/pallets_sphinx_themes/themes/jinja/__init__.py53
-rw-r--r--src/pallets_sphinx_themes/themes/jinja/domain.py265
-rw-r--r--src/pallets_sphinx_themes/themes/jinja/static/jinja.css20
-rw-r--r--src/pallets_sphinx_themes/themes/jinja/theme.conf4
-rw-r--r--src/pallets_sphinx_themes/themes/platter/static/platter.css26
-rw-r--r--src/pallets_sphinx_themes/themes/platter/theme.conf3
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/404.html13
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/__init__.py85
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/ethicalads.html1
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/layout.html33
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/localtoc.html4
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/project.html6
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/relations.html13
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/static/pocoo.css510
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/static/version_warning_offset.js40
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/theme.conf8
-rw-r--r--src/pallets_sphinx_themes/themes/pocoo/versions.html8
-rw-r--r--src/pallets_sphinx_themes/themes/werkzeug/static/werkzeug.css15
-rw-r--r--src/pallets_sphinx_themes/themes/werkzeug/theme.conf3
-rw-r--r--src/pallets_sphinx_themes/versions.py153
42 files changed, 2327 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e32c802
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+charset = utf-8
+max_line_length = 88
+
+[*.{yml,yaml,json,js,css,html}]
+indent_size = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f084e44
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+.DS_Store
+.idea/
+.env
+.flaskenv
+*.pyc
+*.pyo
+env/
+dist/
+build/
+*.egg
+*.egg-info/
+.tox/
+.cache/
+.pytest_cache/
+htmlcov/
+.coverage
+.coverage.*
+docs/_build/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..cc3b689
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,28 @@
+repos:
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.25.0
+ hooks:
+ - id: pyupgrade
+ args: ["--py36-plus"]
+ - repo: https://github.com/asottile/reorder_python_imports
+ rev: v2.6.0
+ hooks:
+ - id: reorder-python-imports
+ args: ["--application-directories", "src"]
+ - repo: https://github.com/psf/black
+ rev: 21.8b0
+ hooks:
+ - id: black
+ - repo: https://github.com/PyCQA/flake8
+ rev: 3.9.2
+ hooks:
+ - id: flake8
+ additional_dependencies:
+ - flake8-bugbear
+ - flake8-implicit-str-concat
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.0.1
+ hooks:
+ - id: check-byte-order-marker
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..63b7bf5
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,192 @@
+Version 2.0.3
+-------------
+
+Released 2022-12-24
+
+- Fix compatibility with ``packaging>=22``.
+
+
+Version 2.0.2
+-------------
+
+Released 2021-11-10
+
+- Detect if Sphinx dirhtml builder is generating canonical URLs with
+ ".html" and replace with the correct dir URL. :issue:`47`
+- ``canonical_url`` config is deprecated. Use Sphinx's built-in
+ ``html_baseurl`` config instead. :pr:`53`
+- Address deprecations in Jinja 2.0. :pr:`54`
+
+
+Version 2.0.1
+-------------
+
+Released 2021-05-20
+
+- Remove workaround for search URLs when using the ``dirhtml``
+ builder. The issue has been fixed in Sphinx and the workaround was
+ causing the issue again. :issue:`39`
+- Remove ``html_context["readthedocs_docsearch"]`` for controlling
+ whether Read the Docs' search is used. :issue:`40`
+- Add an ``ethicalads.html`` sidebar to have Read the Docs always show
+ ads in the sidebar instead of other possible locations. The sidebar
+ is enabled by default at the end of the list. :issue:`41`
+
+
+Version 2.0.0
+-------------
+
+Released 2021-05-11
+
+- Drop Python < 3.6.
+- Update for Jinja 2.0.
+- Update for Click 8.0.
+
+
+Version 1.2.3
+-------------
+
+Released 2020-01-02
+
+- Use built-in :mod:`importlib.metadata` on Python 3.8. :pr:`27`
+
+
+Version 1.2.2
+-------------
+
+Released 2019-07-04
+
+- Make the version warning sticky so that it appears when linking to
+ the middle of a document. :issue:`5`
+- Remove CSS for old ads.
+
+
+Version 1.2.1
+-------------
+
+Released 2019-07-29
+
+- Sort versions taken from Read the Docs so that 2.10.x is considered
+ newer than 2.9.x. :issue:`24`
+
+
+Version 1.2.0
+-------------
+
+Released 2019-07-26
+
+- Use HTTPS for font URLs in CSS. :pr:`22`
+- Don't require ``sphinx.ext.autodoc`` to be enabled.
+- Implement the Jinja directives ``jinja:filters::``,
+ ``jinja:tests::``, and ``jinja:nodes::``.
+- Generate a table of contents for Jinja filters and tests.
+- Update the ``babel`` and ``platter`` themes.
+
+
+Version 1.1.4
+-------------
+
+Released 2019-01-28
+
+- Store a page's canonical URL in
+ ``html_context["page_canonical_url"]`` rather than overwriting
+ ``canonical_url``, for compatibility with Read the Docs. :pr:`21`
+
+
+Version 1.1.3
+-------------
+
+Released 2019-01-28
+
+- Move the Read the Docs search flag to the ``footer`` block to ensure
+ it executes after Read the Docs injects its data. :pr:`20`
+
+
+Version 1.1.2
+-------------
+
+Released 2018-09-24
+
+- Strip ".x" placeholder when parsing versions for sidebar.
+ :issue:`7`, :pr:`17`
+
+
+Version 1.1.1
+-------------
+
+Released 2018-09-16
+
+- Add configurable ".x" placholder to versions, producing strings like
+ "1.2.x". :issue:`6`, :pr:`12`
+- Add dependency on "packaging" to support older Sphinx versions.
+ :issue:`9`, :pr:`11`
+- Backport ``shlex.quote`` for Python 2. :issue:`13`, :pr:`14`
+
+
+Version 1.1.0
+-------------
+
+Released 2018-08-28
+
+- Modernize ``click`` theme. The ``.. click:example::`` and
+ ``.. click:run::`` directives used by Click are available and ported
+ to Python 3.
+- Modernize ``werkzeug`` theme. :pr:`4`
+- Modernize ``jinja`` theme. Local extensions used by Jinja are not
+ available yet.
+- Remove theme entry points to make late configuration consistent. The
+ themes are available when ``"pallets_sphinx_themes"`` is added to
+ ``extensions``.
+- Only run event callbacks added by theme when the theme is actually
+ in use. This allows the package to be installed without interfering
+ with other themes.
+- Support ``html_context["versions"]`` in the format injected by
+ Read the Docs.
+- Set ``html_context["readthedocs_docsearch"]`` to opt in to replacing
+ Sphinx's built-in search with Read the Docs' new implementation.
+- Make version handling more robust for various configurations.
+- Autodoc skips docstrings that contain the line ``:internal:``.
+- Autodoc removes lines that start with ``:copyright:`` or
+ ``:license:`` from module docstrings.
+- Add ``singlehtml_sidebars`` config for Sphinx < 1.8.
+- Add ``hide-header`` CSS class to hide the page header with
+ ``.. rst-class:: hide-header``. The header is still useable by
+ assistive technology. This is useful for replacing the header with a
+ large logo image.
+- Disable the sidebar logo on the index page with
+ ``html_theme_options["index_sidebar_logo"] = False``.
+
+
+Version 1.0.1
+-------------
+
+Released 2018-04-29
+
+- Work around an issues with search when using the ``dirhtml``
+ builder. :pr:`3`
+
+
+Version 1.0.0
+-------------
+
+Released 2018-04-18
+
+- Major rewrite of CSS and HTML templates to clean up and reduce
+ complexity. Widen columns, improve responsive breakpoints. Currently
+ all themes are available, but only ``pocoo`` and ``flask`` themes
+ are modernized.
+- Parse ``html_context["versions"]``. These will be rendered in the
+ ``versions.html`` sidebar. When viewing an old version, or the
+ development version, a warning is displayed at the top of each page.
+- Add a ``ProjectLink`` named tuple. A list of these in
+ ``html_context["project_links"]`` will be rendered in the
+ ``project.html`` sidebar.
+- Add a ``get_version`` function to ensure a project is installed and
+ get its version information.
+- Use ``html_context["canonical_url"]`` as a base URL to build a
+ canonical URL link on each page.
+- Add Sphinx entry points for themes.
+- Rename from "pocoo-sphinx-themes". See commit `f675bfc`_ for the old
+ themes from the docbuilder.
+
+.. _f675bfc: https://github.com/pallets/pallets-sphinx-themes/tree/f675bfc
diff --git a/LICENSE.rst b/LICENSE.rst
new file mode 100644
index 0000000..c37cae4
--- /dev/null
+++ b/LICENSE.rst
@@ -0,0 +1,28 @@
+Copyright 2007 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..58862b5
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include CHANGES.rst
+graft src/pallets_sphinx_themes
+graft docs
+prune docs/_build
+global-exclude *.pyc
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..2791ea7
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,29 @@
+Pallets Sphinx Themes
+=====================
+
+Themes for the Pallets projects. If you're writing an extension, use the
+appropriate theme to make your documentation look consistent.
+
+Available themes:
+
+- flask
+- jinja
+- werkzeug
+- click
+
+Install this package:
+
+.. code-block:: text
+
+ pip install Pallets-Sphinx-Themes
+
+Enable the extension and choose the theme in ``docs/conf.py``:
+
+.. code-block:: python
+
+ extensions = [
+ "pallets_sphinx_themes",
+ ...
+ ]
+
+ html_theme = "flask"
diff --git a/requirements/dev.in b/requirements/dev.in
new file mode 100644
index 0000000..5d56e25
--- /dev/null
+++ b/requirements/dev.in
@@ -0,0 +1,2 @@
+pip-tools
+pre-commit
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..9a73e1f
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,44 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile requirements/dev.in
+#
+backports.entry-points-selectable==1.1.0
+ # via virtualenv
+cfgv==3.3.1
+ # via pre-commit
+click==8.0.1
+ # via pip-tools
+distlib==0.3.2
+ # via virtualenv
+filelock==3.0.12
+ # via virtualenv
+identify==2.2.13
+ # via pre-commit
+nodeenv==1.6.0
+ # via pre-commit
+pep517==0.11.0
+ # via pip-tools
+pip-tools==6.2.0
+ # via -r requirements/dev.in
+platformdirs==2.3.0
+ # via virtualenv
+pre-commit==2.15.0
+ # via -r requirements/dev.in
+pyyaml==5.4.1
+ # via pre-commit
+six==1.16.0
+ # via virtualenv
+toml==0.10.2
+ # via pre-commit
+tomli==1.2.1
+ # via pep517
+virtualenv==20.7.2
+ # via pre-commit
+wheel==0.37.0
+ # via pip-tools
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
+# setuptools
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..cd5f9af
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,96 @@
+[metadata]
+name = Pallets-Sphinx-Themes
+version = 2.0.3
+url = https://github.com/pallets/pallets-sphinx-themes/
+project_urls =
+ Donate = https://palletsprojects.com/donate
+ Source Code = https://github.com/pallets/pallets-sphinx-themes/
+ Issue Tracker = https://github.com/pallets/pallets-sphinx-themes/issues/
+ Twitter = https://twitter.com/PalletsTeam
+ Chat = https://discord.gg/pallets
+license = BSD-3-Clause
+license_files = LICENSE.rst
+author = Pallets
+author_email = contact@palletsprojects.com
+description = Sphinx themes for Pallets and related projects.
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Framework :: Sphinx
+ Framework :: Sphinx :: Theme
+ Intended Audience :: Developers
+ License :: OSI Approved :: BSD License
+ Operating System :: OS Independent
+ Programming Language :: Python
+ Topic :: Documentation
+ Topic :: Documentation :: Sphinx
+ Topic :: Software Development :: Documentation
+
+[options]
+packages = find:
+package_dir = = src
+include_package_data = True
+python_requires = >= 3.6
+# Dependencies are in setup.py for GitHub's dependency graph.
+
+[options.packages.find]
+where = src
+
+[options.entry_points]
+pygments.styles =
+ pocoo = pallets_sphinx_themes.themes.pocoo:PocooStyle
+ jinja = pallets_sphinx_themes.themes.jinja:JinjaStyle
+
+[tool:pytest]
+testpaths = tests
+filterwarnings =
+ error
+
+[coverage:run]
+branch = true
+source =
+ pallets_sphinx_themes
+ tests
+
+[coverage:paths]
+source =
+ src
+ */site-packages
+
+[flake8]
+# B = bugbear
+# E = pycodestyle errors
+# F = flake8 pyflakes
+# W = pycodestyle warnings
+# B9 = bugbear opinions,
+# ISC = implicit str concat
+select = B, E, F, W, B9, ISC
+ignore =
+ # slice notation whitespace, invalid
+ E203
+ # line length, handled by bugbear B950
+ E501
+ # bare except, handled by bugbear B001
+ E722
+ # bin op line break, invalid
+ W503
+# up to 88 allowed by bugbear B950
+max-line-length = 80
+
+[mypy]
+files = src/pallets_sphinx_themes
+python_version = 3.6
+disallow_subclassing_any = True
+disallow_untyped_calls = True
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
+no_implicit_optional = True
+local_partial_types = True
+no_implicit_reexport = True
+strict_equality = True
+warn_redundant_casts = True
+warn_unused_configs = True
+warn_unused_ignores = True
+warn_return_any = True
+warn_unreachable = True
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..f9f9c6e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,10 @@
+from setuptools import setup
+
+setup(
+ name="Pallets-Sphinx-Themes",
+ install_requires = [
+ "importlib-metadata; python_version < '3.8'",
+ "packaging",
+ "Sphinx",
+ ]
+)
diff --git a/src/pallets_sphinx_themes/__init__.py b/src/pallets_sphinx_themes/__init__.py
new file mode 100644
index 0000000..f8ed768
--- /dev/null
+++ b/src/pallets_sphinx_themes/__init__.py
@@ -0,0 +1,189 @@
+import inspect
+import os
+import re
+import sys
+import textwrap
+from collections import namedtuple
+
+from sphinx.application import Sphinx
+from sphinx.builders._epub_base import EpubBuilder
+from sphinx.builders.dirhtml import DirectoryHTMLBuilder
+from sphinx.builders.singlehtml import SingleFileHTMLBuilder
+from sphinx.errors import ExtensionError
+
+from .theme_check import only_pallets_theme
+from .theme_check import set_is_pallets_theme
+from .versions import load_versions
+
+try:
+ from importlib import metadata as importlib_metadata
+except ImportError:
+ # Python <3.8 compatibility
+ import importlib_metadata
+
+
+def setup(app):
+ base = os.path.join(os.path.dirname(__file__), "themes")
+
+ for name in os.listdir(base):
+ path = os.path.join(base, name)
+
+ if os.path.isdir(path):
+ app.add_html_theme(name, path)
+
+ app.add_config_value("is_pallets_theme", None, "html")
+
+ app.connect("builder-inited", set_is_pallets_theme)
+ app.connect("builder-inited", load_versions)
+ app.connect("html-collect-pages", add_404_page)
+ app.connect("html-page-context", canonical_url)
+
+ try:
+ app.connect("autodoc-skip-member", skip_internal)
+ app.connect("autodoc-process-docstring", cut_module_meta)
+ except ExtensionError:
+ pass
+
+ try:
+ app.add_config_value("singlehtml_sidebars", None, "html")
+ except ExtensionError:
+ pass
+ else:
+ app.connect("builder-inited", singlehtml_sidebars)
+
+ from .themes import click as click_ext
+ from .themes import jinja as jinja_ext
+
+ click_ext.setup(app)
+ jinja_ext.setup(app)
+
+ own_release, _ = get_version(__name__)
+ return {"version": own_release, "parallel_read_safe": True}
+
+
+@only_pallets_theme(default=())
+def add_404_page(app):
+ """Build an extra ``404.html`` page if no ``"404"`` key is in the
+ ``html_additional_pages`` config.
+ """
+ is_epub = isinstance(app.builder, EpubBuilder)
+ config_pages = app.config.html_additional_pages
+
+ if not is_epub and "404" not in config_pages:
+ yield ("404", {}, "404.html")
+
+
+@only_pallets_theme()
+def canonical_url(app: Sphinx, pagename, templatename, context, doctree):
+ """Sphinx 1.8 builds a canonical URL if ``html_baseurl`` config is
+ set. However, it builds a URL ending with ".html" when using the
+ dirhtml builder, which is incorrect. Detect this and generate the
+ correct URL for each page.
+
+ Also accepts the custom, deprecated ``canonical_url`` config as the
+ base URL. This will be removed in version 2.1.
+ """
+ base = app.config.html_baseurl
+
+ if not base and context.get("canonical_url"):
+ import warnings
+
+ warnings.warn(
+ "'canonical_url' config is deprecated and will be removed"
+ " in Pallets-Sphinx-Themes 2.1. Set Sphinx's 'html_baseurl'"
+ " config instead.",
+ DeprecationWarning,
+ )
+ base = context["canonical_url"]
+
+ if (
+ not base
+ or not isinstance(app.builder, DirectoryHTMLBuilder)
+ or not context["pageurl"]
+ or not context["pageurl"].endswith(".html")
+ ):
+ return
+
+ # Fix pageurl for dirhtml builder if this version of Sphinx still
+ # generates .html URLs.
+ target = app.builder.get_target_uri(pagename)
+ context["pageurl"] = base + target
+
+
+@only_pallets_theme()
+def singlehtml_sidebars(app):
+ """When using a ``singlehtml`` builder, replace the
+ ``html_sidebars`` config with ``singlehtml_sidebars``. This can be
+ used to change what sidebars are rendered for the single page called
+ ``"index"`` by the builder.
+ """
+ if app.config.singlehtml_sidebars is not None and isinstance(
+ app.builder, SingleFileHTMLBuilder
+ ):
+ app.config.html_sidebars = app.config.singlehtml_sidebars
+
+
+@only_pallets_theme()
+def skip_internal(app, what, name, obj, skip, options):
+ """Skip rendering autodoc when the docstring contains a line with
+ only the string `:internal:`.
+ """
+ docstring = inspect.getdoc(obj) or ""
+
+ if skip or re.search(r"^\s*:internal:\s*$", docstring, re.M) is not None:
+ return True
+
+
+@only_pallets_theme()
+def cut_module_meta(app, what, name, obj, options, lines):
+ """Don't render lines that start with ``:copyright:`` or
+ ``:license:`` when rendering module autodoc. These lines are useful
+ meta information in the source code, but are noisy in the docs.
+ """
+ if what != "module":
+ return
+
+ lines[:] = [
+ line for line in lines if not line.startswith((":copyright:", ":license:"))
+ ]
+
+
+def get_version(name, version_length=2, placeholder="x"):
+ """Ensures that the named package is installed and returns version
+ strings to be used by Sphinx.
+
+ Sphinx uses ``version`` to mean an abbreviated form of the full
+ version string, which is called ``release``. In ``conf.py``::
+
+ release, version = get_version("Flask")
+ # release = 1.0.x, version = 1.0.3.dev0
+
+ :param name: Name of package to get.
+ :param version_length: How many values from ``release`` to use for
+ ``version``.
+ :param placeholder: Extra suffix to add to the version. The default
+ produces versions like ``1.2.x``.
+ :return: ``(release, version)`` tuple.
+ """
+ try:
+ release = importlib_metadata.version(name)
+ except ImportError:
+ print(
+ textwrap.fill(
+ "'{name}' must be installed to build the documentation."
+ " Install from source using `pip install -e .` in a"
+ " virtualenv.".format(name=name)
+ )
+ )
+ sys.exit(1)
+
+ version = ".".join(release.split(".", version_length)[:version_length])
+
+ if placeholder:
+ version = f"{version}.{placeholder}"
+
+ return release, version
+
+
+#: ``(title, url)`` named tuple that will be rendered with
+ProjectLink = namedtuple("ProjectLink", ("title", "url"))
diff --git a/src/pallets_sphinx_themes/theme_check.py b/src/pallets_sphinx_themes/theme_check.py
new file mode 100644
index 0000000..c7c0192
--- /dev/null
+++ b/src/pallets_sphinx_themes/theme_check.py
@@ -0,0 +1,50 @@
+from functools import wraps
+
+
+def set_is_pallets_theme(app):
+ """Set the ``is_pallets_theme`` config to ``True`` if the current
+ theme is a decedent of the ``pocoo`` theme.
+ """
+ if app.config.is_pallets_theme is not None:
+ return
+
+ theme = getattr(app.builder, "theme", None)
+
+ while theme is not None:
+ if theme.name == "pocoo":
+ app.config.is_pallets_theme = True
+ break
+
+ theme = theme.base
+ else:
+ app.config.is_pallets_theme = False
+
+
+def only_pallets_theme(default=None):
+ """Create a decorator that calls a function only if the
+ ``is_pallets_theme`` config is ``True``.
+
+ Used to prevent Sphinx event callbacks from doing anything if the
+ Pallets themes are installed but not used. ::
+
+ @only_pallets_theme()
+ def inject_value(app):
+ ...
+
+ app.connect("builder-inited", inject_value)
+
+ :param default: Value to return if a Pallets theme is not in use.
+ :return: A decorator.
+ """
+
+ def decorator(f):
+ @wraps(f)
+ def wrapped(app, *args, **kwargs):
+ if not app.config.is_pallets_theme:
+ return default
+
+ return f(app, *args, **kwargs)
+
+ return wrapped
+
+ return decorator
diff --git a/src/pallets_sphinx_themes/themes/__init__.py b/src/pallets_sphinx_themes/themes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/__init__.py
diff --git a/src/pallets_sphinx_themes/themes/babel/static/babel.css b/src/pallets_sphinx_themes/themes/babel/static/babel.css
new file mode 100644
index 0000000..04870e5
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/babel/static/babel.css
@@ -0,0 +1,24 @@
+@import url(pocoo.css);
+@import url(http://fonts.googleapis.com/css?family=Bree+Serif);
+
+body {
+ font-family: "Verdana", "Garamond", "Georgia", serif;
+}
+
+h1, h2, h3, h4, h5, h6, p.admonition-title, div.sphinxsidebar input {
+ font-family: "Bree Serif", "Garamond", "Georgia", serif;
+}
+
+a {
+ color: #b00;
+ border-color: #b00;
+}
+
+a:hover {
+ color: #fc5e1e;
+ border-color: #fc5e1e;
+}
+
+p.version-warning {
+ background-color: #d40;
+}
diff --git a/src/pallets_sphinx_themes/themes/babel/theme.conf b/src/pallets_sphinx_themes/themes/babel/theme.conf
new file mode 100644
index 0000000..b62dedd
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/babel/theme.conf
@@ -0,0 +1,3 @@
+[theme]
+inherit = pocoo
+stylesheet = babel.css
diff --git a/src/pallets_sphinx_themes/themes/click/__init__.py b/src/pallets_sphinx_themes/themes/click/__init__.py
new file mode 100644
index 0000000..30f3935
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/click/__init__.py
@@ -0,0 +1,8 @@
+def setup(app):
+ """Load the Click extension if Click is installed."""
+ try:
+ from . import domain
+ except ImportError:
+ return
+
+ domain.setup(app)
diff --git a/src/pallets_sphinx_themes/themes/click/domain.py b/src/pallets_sphinx_themes/themes/click/domain.py
new file mode 100644
index 0000000..b690cbf
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/click/domain.py
@@ -0,0 +1,275 @@
+import contextlib
+import shlex
+import subprocess
+import sys
+import tempfile
+from functools import partial
+
+import click
+from click._compat import text_type
+from click.testing import CliRunner
+from click.testing import EchoingStdin
+from docutils import nodes
+from docutils.parsers.rst import Directive
+from docutils.statemachine import ViewList
+from sphinx.domains import Domain
+
+
+class EofEchoingStdin(EchoingStdin):
+ """Like :class:`click.testing.EchoingStdin` but adds a visible
+ ``^D`` in place of the EOT character (``\x04``).
+
+ :meth:`ExampleRunner.invoke` adds ``\x04`` when
+ ``terminate_input=True``.
+ """
+
+ def _echo(self, rv):
+ eof = rv[-1] == b"\x04"[0]
+
+ if eof:
+ rv = rv[:-1]
+
+ if not self._paused:
+ self._output.write(rv)
+
+ if eof:
+ self._output.write(b"^D\n")
+
+ return rv
+
+
+@contextlib.contextmanager
+def patch_modules():
+ """Patch modules to work better with :meth:`ExampleRunner.invoke`.
+
+ ``subprocess.call` output is redirected to ``click.echo`` so it
+ shows up in the example output.
+ """
+ old_call = subprocess.call
+
+ def dummy_call(*args, **kwargs):
+ with tempfile.TemporaryFile("wb+") as f:
+ kwargs["stdout"] = f
+ kwargs["stderr"] = f
+ rv = subprocess.Popen(*args, **kwargs).wait()
+ f.seek(0)
+ click.echo(f.read().decode("utf-8", "replace").rstrip())
+ return rv
+
+ subprocess.call = dummy_call
+
+ try:
+ yield
+ finally:
+ subprocess.call = old_call
+
+
+class ExampleRunner(CliRunner):
+ def __init__(self):
+ super(ExampleRunner, self).__init__(echo_stdin=True)
+ self.namespace = {"click": click, "__file__": "dummy.py", "str": text_type}
+
+ @contextlib.contextmanager
+ def isolation(self, *args, **kwargs):
+ iso = super(ExampleRunner, self).isolation(*args, **kwargs)
+
+ with iso as streams:
+ try:
+ buffer = sys.stdin.buffer
+ except AttributeError:
+ buffer = sys.stdin
+
+ # FIXME: We need to replace EchoingStdin with our custom
+ # class that outputs "^D". At this point we know sys.stdin
+ # has been patched so it's safe to reassign the class.
+ # Remove this once EchoingStdin is overridable.
+ buffer.__class__ = EofEchoingStdin
+ yield streams
+
+ def invoke(
+ self,
+ cli,
+ args=None,
+ prog_name=None,
+ input=None,
+ terminate_input=False,
+ env=None,
+ _output_lines=None,
+ **extra
+ ):
+ """Like :meth:`CliRunner.invoke` but displays what the user
+ would enter in the terminal for env vars, command args, and
+ prompts.
+
+ :param terminate_input: Whether to display "^D" after a list of
+ input.
+ :param _output_lines: A list used internally to collect lines to
+ be displayed.
+ """
+ output_lines = _output_lines if _output_lines is not None else []
+
+ if env:
+ for key, value in sorted(env.items()):
+ value = shlex.quote(value)
+ output_lines.append("$ export {}={}".format(key, value))
+
+ args = args or []
+
+ if prog_name is None:
+ prog_name = cli.name.replace("_", "-")
+
+ output_lines.append(
+ "$ {} {}".format(prog_name, " ".join(shlex.quote(x) for x in args)).rstrip()
+ )
+ # remove "python" from command
+ prog_name = prog_name.rsplit(" ", 1)[-1]
+
+ if isinstance(input, (tuple, list)):
+ input = "\n".join(input) + "\n"
+
+ if terminate_input:
+ input += "\x04"
+
+ result = super(ExampleRunner, self).invoke(
+ cli=cli, args=args, input=input, env=env, prog_name=prog_name, **extra
+ )
+ output_lines.extend(result.output.splitlines())
+ return result
+
+ def declare_example(self, source):
+ """Execute the given code, adding it to the runner's namespace."""
+ with patch_modules():
+ code = compile(source, "<docs>", "exec")
+ exec(code, self.namespace)
+
+ def run_example(self, source):
+ """Run commands by executing the given code, returning the lines
+ of input and output. The code should be a series of the
+ following functions:
+
+ * :meth:`invoke`: Invoke a command, adding env vars, input,
+ and output to the output.
+ * ``println(text="")``: Add a line of text to the output.
+ * :meth:`isolated_filesystem`: A context manager that changes
+ to a temporary directory while executing the block.
+ """
+ code = compile(source, "<docs>", "exec")
+ buffer = []
+ invoke = partial(self.invoke, _output_lines=buffer)
+
+ def println(text=""):
+ buffer.append(text)
+
+ exec(
+ code,
+ self.namespace,
+ {
+ "invoke": invoke,
+ "println": println,
+ "isolated_filesystem": self.isolated_filesystem,
+ },
+ )
+ return buffer
+
+ def close(self):
+ """Clean up the runner once the document has been read."""
+ pass
+
+
+def get_example_runner(document):
+ """Get or create the :class:`ExampleRunner` instance associated with
+ a document.
+ """
+ runner = getattr(document, "click_example_runner", None)
+ if runner is None:
+ runner = document.click_example_runner = ExampleRunner()
+ return runner
+
+
+class DeclareExampleDirective(Directive):
+ """Add the source contained in the directive's content to the
+ document's :class:`ExampleRunner`, to be run using
+ :class:`RunExampleDirective`.
+
+ See :meth:`ExampleRunner.declare_example`.
+ """
+
+ has_content = True
+ required_arguments = 0
+ optional_arguments = 0
+ final_argument_whitespace = False
+
+ def run(self):
+ doc = ViewList()
+ runner = get_example_runner(self.state.document)
+
+ try:
+ runner.declare_example("\n".join(self.content))
+ except BaseException:
+ runner.close()
+ raise
+
+ doc.append(".. sourcecode:: python", "")
+ doc.append("", "")
+
+ for line in self.content:
+ doc.append(" " + line, "")
+
+ node = nodes.section()
+ self.state.nested_parse(doc, self.content_offset, node)
+ return node.children
+
+
+class RunExampleDirective(Directive):
+ """Run commands from :class:`DeclareExampleDirective` and display
+ the input and output.
+
+ See :meth:`ExampleRunner.run_example`.
+ """
+
+ has_content = True
+ required_arguments = 0
+ optional_arguments = 0
+ final_argument_whitespace = False
+
+ def run(self):
+ doc = ViewList()
+ runner = get_example_runner(self.state.document)
+
+ try:
+ rv = runner.run_example("\n".join(self.content))
+ except BaseException:
+ runner.close()
+ raise
+
+ doc.append(".. sourcecode:: text", "")
+ doc.append("", "")
+
+ for line in rv:
+ doc.append(" " + line, "")
+
+ node = nodes.section()
+ self.state.nested_parse(doc, self.content_offset, node)
+ return node.children
+
+
+class ClickDomain(Domain):
+ name = "click"
+ label = "Click"
+ directives = {"example": DeclareExampleDirective, "run": RunExampleDirective}
+
+
+def delete_example_runner_state(app, doctree):
+ """Close and remove the :class:`ExampleRunner` instance once the
+ document has been read.
+ """
+ runner = getattr(doctree, "click_example_runner", None)
+
+ if runner is not None:
+ runner.close()
+ del doctree.click_example_runner
+
+
+def setup(app):
+ app.add_domain(ClickDomain)
+ app.connect("doctree-read", delete_example_runner_state)
diff --git a/src/pallets_sphinx_themes/themes/click/static/click.css b/src/pallets_sphinx_themes/themes/click/static/click.css
new file mode 100644
index 0000000..1c0eee4
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/click/static/click.css
@@ -0,0 +1,28 @@
+@import url("pocoo.css");
+@import url("https://fonts.googleapis.com/css?family=Ubuntu+Mono");
+@import url("https://fonts.googleapis.com/css?family=Open+Sans");
+
+body, pre, code {
+ font-family: "Ubuntu Mono", "Consolas", "Menlo", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace;
+ font-size: 15px;
+}
+
+h1, h2, h3, h4, h5, h6, p.admonition-title, div.sphinxsidebar input {
+ font-family: "Open Sans", "Helvetica", "Arial", sans-serif;
+}
+
+div.body {
+ color: #3e4349;
+}
+
+a {
+ color: #5d2cd1;
+}
+
+a:hover {
+ color: #7546e3;
+}
+
+p.version-warning {
+ background-color: #7546e3;
+}
diff --git a/src/pallets_sphinx_themes/themes/click/theme.conf b/src/pallets_sphinx_themes/themes/click/theme.conf
new file mode 100644
index 0000000..049292f
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/click/theme.conf
@@ -0,0 +1,4 @@
+[theme]
+inherit = pocoo
+stylesheet = click.css
+pygments_style = tango
diff --git a/src/pallets_sphinx_themes/themes/flask/static/flask.css b/src/pallets_sphinx_themes/themes/flask/static/flask.css
new file mode 100644
index 0000000..ae59ade
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/flask/static/flask.css
@@ -0,0 +1,15 @@
+@import url("pocoo.css");
+
+a, a.reference, a.footnote-reference {
+ color: #004b6b;
+ border-color: #004b6b;
+}
+
+a:hover {
+ color: #6d4100;
+ border-color: #6d4100;
+}
+
+p.version-warning {
+ background-color: #004b6b;
+}
diff --git a/src/pallets_sphinx_themes/themes/flask/theme.conf b/src/pallets_sphinx_themes/themes/flask/theme.conf
new file mode 100644
index 0000000..dfa6795
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/flask/theme.conf
@@ -0,0 +1,3 @@
+[theme]
+inherit = pocoo
+stylesheet = flask.css
diff --git a/src/pallets_sphinx_themes/themes/jinja/__init__.py b/src/pallets_sphinx_themes/themes/jinja/__init__.py
new file mode 100644
index 0000000..d5bd9ef
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/jinja/__init__.py
@@ -0,0 +1,53 @@
+from pygments.style import Style
+from pygments.token import Comment
+from pygments.token import Error
+from pygments.token import Generic
+from pygments.token import Keyword
+from pygments.token import Name
+from pygments.token import Number
+from pygments.token import Operator
+from pygments.token import String
+
+
+class JinjaStyle(Style):
+ background_color = "#f8f8f8"
+ default_style = ""
+ styles = {
+ Comment: "italic #aaaaaa",
+ Comment.Preproc: "noitalic #b11414",
+ Comment.Special: "italic #505050",
+ Keyword: "bold #b80000",
+ Keyword.Type: "#808080",
+ Operator.Word: "bold #b80000",
+ Name.Builtin: "#333333",
+ Name.Function: "#333333",
+ Name.Class: "bold #333333",
+ Name.Namespace: "bold #333333",
+ Name.Entity: "bold #363636",
+ Name.Attribute: "#686868",
+ Name.Tag: "bold #686868",
+ Name.Decorator: "#686868",
+ String: "#aa891c",
+ Number: "#444444",
+ Generic.Heading: "bold #000080",
+ Generic.Subheading: "bold #800080",
+ Generic.Deleted: "#aa0000",
+ Generic.Inserted: "#00aa00",
+ Generic.Error: "#aa0000",
+ Generic.Emph: "italic",
+ Generic.Strong: "bold",
+ Generic.Prompt: "#555555",
+ Generic.Output: "#888888",
+ Generic.Traceback: "#aa0000",
+ Error: "#f00 bg:#faa",
+ }
+
+
+def setup(app):
+ """Load the Jinja extension if Jinja is installed."""
+ try:
+ from . import domain
+ except ImportError:
+ return
+
+ domain.setup(app)
diff --git a/src/pallets_sphinx_themes/themes/jinja/domain.py b/src/pallets_sphinx_themes/themes/jinja/domain.py
new file mode 100644
index 0000000..c2cbe2f
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/jinja/domain.py
@@ -0,0 +1,265 @@
+import csv
+import inspect
+import re
+from io import StringIO
+
+from docutils import nodes
+from docutils.statemachine import StringList
+from sphinx.domains import Domain
+from sphinx.util import import_object
+from sphinx.util.docutils import SphinxDirective
+
+
+def build_function_directive(name, aliases, func):
+ """Build a function directive, with name, signature, docs, and
+ aliases.
+
+ .. code-block:: rst
+
+ .. function:: name(signature)
+
+ doc
+ doc
+
+ :aliases: ``name2``, ``name3``
+
+ :param name: The name mapped to the function, which may not match
+ the real name of the function.
+ :param aliases: Other names mapped to the function.
+ :param func: The function.
+ :return: A list of lines of reStructuredText to be rendered.
+
+ If the function is a Jinja environment, context, or eval context
+ filter, the first argument is omitted from the signature since it's
+ not seen by template developers.
+
+ If the filter is a Jinja async variant, it is unwrapped to its sync
+ variant to get the docs and signature.
+ """
+ if getattr(func, "jinja_async_variant", False):
+ # unwrap async filters to their normal variant
+ func = inspect.unwrap(func)
+
+ doc = inspect.getdoc(func).splitlines()
+
+ try:
+ sig = inspect.signature(func, follow_wrapped=False)
+ except ValueError:
+ # some c function that doesn't report its signature (ex. MarkupSafe.escape)
+ # try the first line of the docs, fall back to generic value
+ sig = "(value)"
+ m = re.match(r"[a-zA-Z_]\w*(\(.*?\))", doc[0])
+
+ if m is not None:
+ doc = doc[1:]
+ sig = m.group(1)
+ else:
+ if getattr(func, "jinja_pass_arg", None) is not None:
+ # remove the internal-only first argument from context filters
+ params = list(sig.parameters.values())
+
+ if params[0].kind != inspect.Parameter.VAR_POSITIONAL:
+ # only remove it if it's not "*args"
+ del params[0]
+
+ sig = sig.replace(parameters=params)
+
+ result = ["", ".. function:: {}{}".format(name, sig), ""]
+ result.extend([" {}".format(x) for x in doc])
+
+ if aliases:
+ result.append("")
+ alias_str = ", ".join(["``{}``".format(x) for x in sorted(aliases)])
+ result.append(" :aliases: {}".format(alias_str))
+
+ return result
+
+
+class MappedFunctionsDirective(SphinxDirective):
+ """Take a dict of names to functions and produce rendered docs.
+ Requires one argument, the import name of the dict to process.
+
+ Used for the ``jinja:filters::` and `jinja:tests::` directives.
+
+ Multiple names can point to the same function. In this case the
+ shortest name is used as the primary name, and other names are
+ displayed as aliases. Comparison operators are special cased to
+ prefer their two letter names, like "eq".
+
+ The docs are sorted by primary name. A table is rendered above the
+ docs as a compact table of contents linking to each function.
+ """
+
+ required_arguments = 1
+
+ def _build_functions(self):
+ """Imports the dict and builds the output for the functions.
+ This is what determines aliases and performs sorting.
+
+ Calls :func:`build_function_directive` for each function, then
+ renders the list of reStructuredText to nodes.
+
+ The list of sorted names is stored for use by
+ :meth:`_build_table`.
+
+ :return: A list of rendered nodes.
+ """
+ map_name = self.arguments[0]
+ mapping = import_object(map_name)
+ grouped = {}
+
+ # reverse the mapping to get a list of aliases for each function
+ for key, value in mapping.items():
+ grouped.setdefault(value, []).append(key)
+
+ # store the function names for use by _build_table
+ self.funcs = funcs = []
+ compare_ops = {"eq", "ge", "gt", "le", "lt", "ne"}
+
+ for func, names in grouped.items():
+ # use the longest alias as the canonical name
+ names.sort(key=len)
+ # adjust for special cases
+ names.sort(key=lambda x: x in compare_ops)
+ name = names.pop()
+ funcs.append((name, names, func))
+
+ funcs.sort()
+ result = StringList()
+
+ # generate and collect markup
+ for name, aliases, func in funcs:
+ for item in build_function_directive(name, aliases, func):
+ result.append(item, "<jinja>")
+
+ # parse the generated markup into nodes
+ node = nodes.Element()
+ self.state.nested_parse(result, self.content_offset, node)
+ return node.children
+
+ def _build_table(self):
+ """Takes the sorted list of names produced by
+ :meth:`_build_functions` and builds the nodes for the table of
+ contents.
+
+ The table is hard coded to be 5 columns wide. Names are rendered
+ in alphabetical order in columns.
+
+ :return: A list of rendered nodes.
+ """
+ # the reference markup to link to each name
+ names = [":func:`{}`".format(name) for name, _, _ in self.funcs]
+ # total number of rows, the number of names divided by the
+ # number of columns, plus one in case of overflow
+ row_size = (len(names) // 5) + bool(len(names) % 5)
+ # pivot to rows so that names remain alphabetical in columns
+ rows = [names[i::row_size] for i in range(row_size)]
+
+ # render the names to CSV for the csv-table directive
+ out = StringIO()
+ writer = csv.writer(out)
+ writer.writerows(rows)
+
+ # generate the markup for the csv-table directive
+ result = ["", ".. csv-table::", " :align: left", ""]
+ result.extend([" {}".format(line) for line in out.getvalue().splitlines()])
+
+ # parse the generated markup into nodes
+ result = StringList(result, "<jinja>")
+ node = nodes.Element()
+ self.state.nested_parse(result, self.content_offset, node)
+ return node.children
+
+ def run(self):
+ """Render the table and function docs.
+
+ Build the functions first to calculate the names and order, then
+ build the table. Return the table above the functions.
+
+ :return: A list of rendered nodes.
+ """
+ functions = self._build_functions()
+ table = self._build_table()
+ return table + functions
+
+
+class NodesDirective(SphinxDirective):
+ """Take a base Jinja ``Node`` class and render docs for it and all
+ subclasses, recursively, depth first. Requires one argument, the
+ import name of the base class.
+
+ Used for the ``jinja:nodes::` directive.
+
+ Each descendant renders a link back to its parent.
+ """
+
+ required_arguments = 1
+
+ def run(self):
+ def walk(cls):
+ """Render the given class, then recursively render its
+ descendants depth first.
+
+ Appends to the outer ``lines`` variable.
+
+ :param cls: The Jinja ``Node`` class to render.
+ """
+ lines.append(
+ ".. autoclass:: {}({})".format(cls.__name__, ", ".join(cls.fields))
+ )
+
+ # render member methods for nodes marked abstract
+ if cls.abstract:
+ members = []
+
+ for key, value in cls.__dict__.items():
+ if (
+ not key.startswith("_")
+ and not hasattr(cls.__base__, key)
+ and callable(value)
+ ):
+ members.append(key)
+
+ if members:
+ members.sort()
+ lines.append(" :members: " + ", ".join(members))
+
+ # reference the parent node, except for the base node
+ if cls.__base__ is not object:
+ lines.append("")
+ lines.append(
+ " :Node type: :class:`{}`".format(cls.__base__.__name__)
+ )
+
+ lines.append("")
+ children = cls.__subclasses__()
+ children.sort(key=lambda x: x.__name__.lower())
+
+ # render each child
+ for child in children:
+ walk(child)
+
+ # generate the markup starting at the base class
+ lines = []
+ target = import_object(self.arguments[0])
+ walk(target)
+
+ # parse the generated markup into nodes
+ doc = StringList(lines, "<jinja>")
+ node = nodes.Element()
+ self.state.nested_parse(doc, self.content_offset, node)
+ return node.children
+
+
+class JinjaDomain(Domain):
+ name = "jinja"
+ label = "Jinja"
+ directives = {
+ "filters": MappedFunctionsDirective,
+ "tests": MappedFunctionsDirective,
+ "nodes": NodesDirective,
+ }
+
+
+def setup(app):
+ app.add_domain(JinjaDomain)
diff --git a/src/pallets_sphinx_themes/themes/jinja/static/jinja.css b/src/pallets_sphinx_themes/themes/jinja/static/jinja.css
new file mode 100644
index 0000000..83de6a1
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/jinja/static/jinja.css
@@ -0,0 +1,20 @@
+@import url("pocoo.css");
+@import url("https://fonts.googleapis.com/css?family=Crimson+Text");
+
+h1, h2, h3, h4, h5, h6, p.admonition-title, div.sphinxsidebar input {
+ font-family: "Crimson Text", "Garamond", "Georgia", serif;
+}
+
+a, a.reference, a.footnote-reference {
+ color: #a00;
+ border-color: #a00;
+}
+
+a:hover {
+ color: #d00;
+ border-color: #d00;
+}
+
+p.version-warning {
+ background-color: #d40;
+}
diff --git a/src/pallets_sphinx_themes/themes/jinja/theme.conf b/src/pallets_sphinx_themes/themes/jinja/theme.conf
new file mode 100644
index 0000000..572aff5
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/jinja/theme.conf
@@ -0,0 +1,4 @@
+[theme]
+inherit = pocoo
+stylesheet = jinja.css
+pygments_style = jinja
diff --git a/src/pallets_sphinx_themes/themes/platter/static/platter.css b/src/pallets_sphinx_themes/themes/platter/static/platter.css
new file mode 100644
index 0000000..1a5a0c6
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/platter/static/platter.css
@@ -0,0 +1,26 @@
+@import url(pocoo.css);
+@import url(https://fonts.googleapis.com/css?family=Fira+Mono:400,700|Bitter:400,400italic,700);
+
+body {
+ font-family: "Bitter", "Garamond", "Georgia", serif;
+}
+
+pre, code {
+ font-family: "Fira Mono", "Consolas", "Menlo", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace;
+}
+
+div.body {
+ color: #163228;
+}
+
+a {
+ color: #bca612;
+}
+
+a:hover {
+ color: #95840e;
+}
+
+p.version-warning {
+ background-color: #e39046;
+}
diff --git a/src/pallets_sphinx_themes/themes/platter/theme.conf b/src/pallets_sphinx_themes/themes/platter/theme.conf
new file mode 100644
index 0000000..68dd806
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/platter/theme.conf
@@ -0,0 +1,3 @@
+[theme]
+inherit = pocoo
+stylesheet = platter.css
diff --git a/src/pallets_sphinx_themes/themes/pocoo/404.html b/src/pallets_sphinx_themes/themes/pocoo/404.html
new file mode 100644
index 0000000..1ccc53a
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/404.html
@@ -0,0 +1,13 @@
+{% extends 'layout.html' %}
+
+{% set title = _('Page Not Found') %}
+
+{% block body %}
+ <h1 id="notfound">Page Not Found</h1>
+ <p>
+ The page you requested does not exist. You may have followed a bad
+ link, or the page may have been moved or removed.
+ <p>
+ Go to the <a href="{{ pathto(master_doc) }}">overview</a> or
+ <a href="{{ pathto('search') }}">search</a>.
+{% endblock %}
diff --git a/src/pallets_sphinx_themes/themes/pocoo/__init__.py b/src/pallets_sphinx_themes/themes/pocoo/__init__.py
new file mode 100644
index 0000000..2540264
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/__init__.py
@@ -0,0 +1,85 @@
+from pygments.style import Style
+from pygments.token import Comment
+from pygments.token import Error
+from pygments.token import Generic
+from pygments.token import Keyword
+from pygments.token import Literal
+from pygments.token import Name
+from pygments.token import Number
+from pygments.token import Operator
+from pygments.token import Other
+from pygments.token import Punctuation
+from pygments.token import String
+from pygments.token import Whitespace
+
+
+class PocooStyle(Style):
+ background_color = "#f8f8f8"
+ default_style = ""
+ styles = {
+ # No corresponding class for the following:
+ # Text: "", # class: ''
+ Whitespace: "underline #f8f8f8", # class: 'w'
+ Error: "#a40000 border:#ef2929", # class: 'err'
+ Other: "#000000", # class 'x'
+ Comment: "italic #8f5902", # class: 'c'
+ Comment.Preproc: "noitalic", # class: 'cp'
+ Keyword: "bold #004461", # class: 'k'
+ Keyword.Constant: "bold #004461", # class: 'kc'
+ Keyword.Declaration: "bold #004461", # class: 'kd'
+ Keyword.Namespace: "bold #004461", # class: 'kn'
+ Keyword.Pseudo: "bold #004461", # class: 'kp'
+ Keyword.Reserved: "bold #004461", # class: 'kr'
+ Keyword.Type: "bold #004461", # class: 'kt'
+ Operator: "#582800", # class: 'o'
+ Operator.Word: "bold #004461", # class: 'ow' - like keywords
+ Punctuation: "bold #000000", # class: 'p'
+ # because special names such as Name.Class, Name.Function, etc.
+ # are not recognized as such later in the parsing, we choose them
+ # to look the same as ordinary variables.
+ Name: "#000000", # class: 'n'
+ Name.Attribute: "#c4a000", # class: 'na' - to be revised
+ Name.Builtin: "#004461", # class: 'nb'
+ Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
+ Name.Class: "#000000", # class: 'nc' - to be revised
+ Name.Constant: "#000000", # class: 'no' - to be revised
+ Name.Decorator: "#888", # class: 'nd' - to be revised
+ Name.Entity: "#ce5c00", # class: 'ni'
+ Name.Exception: "bold #cc0000", # class: 'ne'
+ Name.Function: "#000000", # class: 'nf'
+ Name.Property: "#000000", # class: 'py'
+ Name.Label: "#f57900", # class: 'nl'
+ Name.Namespace: "#000000", # class: 'nn' - to be revised
+ Name.Other: "#000000", # class: 'nx'
+ Name.Tag: "bold #004461", # class: 'nt' - like a keyword
+ Name.Variable: "#000000", # class: 'nv' - to be revised
+ Name.Variable.Class: "#000000", # class: 'vc' - to be revised
+ Name.Variable.Global: "#000000", # class: 'vg' - to be revised
+ Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
+ Number: "#990000", # class: 'm'
+ Literal: "#000000", # class: 'l'
+ Literal.Date: "#000000", # class: 'ld'
+ String: "#4e9a06", # class: 's'
+ String.Backtick: "#4e9a06", # class: 'sb'
+ String.Char: "#4e9a06", # class: 'sc'
+ String.Doc: "italic #8f5902", # class: 'sd' - like a comment
+ String.Double: "#4e9a06", # class: 's2'
+ String.Escape: "#4e9a06", # class: 'se'
+ String.Heredoc: "#4e9a06", # class: 'sh'
+ String.Interpol: "#4e9a06", # class: 'si'
+ String.Other: "#4e9a06", # class: 'sx'
+ String.Regex: "#4e9a06", # class: 'sr'
+ String.Single: "#4e9a06", # class: 's1'
+ String.Symbol: "#4e9a06", # class: 'ss'
+ Generic: "#000000", # class: 'g'
+ Generic.Deleted: "#a40000", # class: 'gd'
+ Generic.Emph: "italic #000000", # class: 'ge'
+ Generic.Error: "#ef2929", # class: 'gr'
+ Generic.Heading: "bold #000080", # class: 'gh'
+ Generic.Inserted: "#00A000", # class: 'gi'
+ Generic.Output: "#888", # class: 'go'
+ Generic.Prompt: "#745334", # class: 'gp'
+ Generic.Strong: "bold #000000", # class: 'gs'
+ Generic.Subheading: "bold #800080", # class: 'gu'
+ Generic.Traceback: "bold #a40000", # class: 'gt'
+ }
diff --git a/src/pallets_sphinx_themes/themes/pocoo/ethicalads.html b/src/pallets_sphinx_themes/themes/pocoo/ethicalads.html
new file mode 100644
index 0000000..e8e8f92
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/ethicalads.html
@@ -0,0 +1 @@
+<div id="ethical-ad-placement"></div>
diff --git a/src/pallets_sphinx_themes/themes/pocoo/layout.html b/src/pallets_sphinx_themes/themes/pocoo/layout.html
new file mode 100644
index 0000000..3745303
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/layout.html
@@ -0,0 +1,33 @@
+{% extends "basic/layout.html" %}
+
+{% set metatags %}
+ {{- metatags }}
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+{%- endset %}
+
+{% block sidebarlogo %}
+ {% if pagename != "index" or theme_index_sidebar_logo %}
+ {{ super() }}
+ {% endif %}
+{% endblock %}
+
+{% set version_warning = current_version.banner() if current_version %}
+
+{% block document %}
+ {%- if version_warning %}
+ <p class="version-warning"><strong>Warning:</strong> {{ version_warning }}</p>
+ {%- endif %}
+ {{- super() }}
+{%- endblock %}
+
+{% block relbar2 %}{% endblock %}
+
+{% block sidebar2 %}
+ <span id="sidebar-top"></span>
+ {{- super() }}
+{%- endblock %}
+
+{% block footer %}
+ {{ super() }}
+ {{ js_tag("_static/version_warning_offset.js") }}
+{% endblock %}
diff --git a/src/pallets_sphinx_themes/themes/pocoo/localtoc.html b/src/pallets_sphinx_themes/themes/pocoo/localtoc.html
new file mode 100644
index 0000000..6c9768e
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/localtoc.html
@@ -0,0 +1,4 @@
+{% if display_toc %}
+ <h3>Contents</h3>
+ {{ toc }}
+{%- endif %}
diff --git a/src/pallets_sphinx_themes/themes/pocoo/project.html b/src/pallets_sphinx_themes/themes/pocoo/project.html
new file mode 100644
index 0000000..5980d58
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/project.html
@@ -0,0 +1,6 @@
+{% if project_links %}
+ <h3>Project Links</h3>
+ <ul>{% for item in project_links %}
+ <li><a href="{{ item.url }}">{{ item.title }}</a>
+ {% endfor %}</ul>
+{%- endif %}
diff --git a/src/pallets_sphinx_themes/themes/pocoo/relations.html b/src/pallets_sphinx_themes/themes/pocoo/relations.html
new file mode 100644
index 0000000..ab84ec1
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/relations.html
@@ -0,0 +1,13 @@
+<h3>Navigation</h3>
+<ul>
+ <li><a href="{{ pathto(master_doc) }}">Overview</a>
+ {% if parents or prev or next %}<ul>{% for parent in parents %}
+ <li><a href="{{ parent.link|e }}">{{ parent.title }}</a>
+ {% if not loop.last or (prev or next) %}<ul>{% endif %}{% endfor %}
+ {% if prev %}<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter') }}">{{ prev.title }}</a>{% endif %}
+ {% if next %}<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter') }}">{{ next.title }}</a>{% endif %}
+ {%- for parent in parents %}</ul>
+ </li>{% endfor %}
+ </ul>{% endif %}
+ </li>
+</ul>
diff --git a/src/pallets_sphinx_themes/themes/pocoo/static/pocoo.css b/src/pallets_sphinx_themes/themes/pocoo/static/pocoo.css
new file mode 100644
index 0000000..74a6cd1
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/static/pocoo.css
@@ -0,0 +1,510 @@
+@import url("basic.css");
+
+/* -- page layout --------------------------------------------------- */
+
+body {
+ font-family: 'Garamond', 'Georgia', serif;
+ font-size: 17px;
+ background-color: #fff;
+ color: #3e4349;
+ margin: 0;
+ padding: 0;
+}
+
+div.related {
+ max-width: 1140px;
+ margin: 10px auto;
+
+ /* displayed on mobile */
+ display: none;
+}
+
+div.document {
+ max-width: 1140px;
+ margin: 10px auto;
+}
+
+div.documentwrapper {
+ float: left;
+ width: 100%;
+}
+
+div.bodywrapper {
+ margin: 0 0 0 220px;
+}
+
+div.body {
+ min-width: initial;
+ max-width: initial;
+ padding: 0 30px;
+}
+
+div.sphinxsidebarwrapper {
+ padding: 10px;
+}
+
+div.sphinxsidebar {
+ width: 220px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: #444;
+}
+
+div.sphinxsidebar a,
+div.sphinxsidebar a code {
+ color: #444;
+ border-color: #444;
+}
+
+div.sphinxsidebar p.logo {
+ margin: 0;
+ text-align: center;
+}
+
+div.sphinxsidebar h3,
+div.sphinxsidebar h4 {
+ font-size: 24px;
+ color: #444;
+}
+
+div.sphinxsidebar p.logo a,
+div.sphinxsidebar h3 a,
+div.sphinxsidebar p.logo a:hover,
+div.sphinxsidebar h3 a:hover {
+ border: none;
+}
+
+div.sphinxsidebar p,
+div.sphinxsidebar h3,
+div.sphinxsidebar h4 {
+ margin: 10px 0;
+}
+
+div.sphinxsidebar ul {
+ margin: 10px 0;
+ padding: 0;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #999;
+ font-size: 1em;
+}
+
+div.footer {
+ max-width: 1140px;
+ margin: 20px auto;
+ font-size: 14px;
+ text-align: right;
+ color: #888;
+}
+
+div.footer a {
+ color: #888;
+ border-color: #888;
+}
+
+/* -- quick search -------------------------------------------------- */
+
+div.sphinxsidebar #searchbox form {
+ display: flex;
+}
+
+div.sphinxsidebar #searchbox form > div {
+ display: flex;
+ flex: 1 1 auto;
+}
+
+div.sphinxsidebar #searchbox input[type=text] {
+ flex: 1 1 auto;
+ width: 1% !important;
+}
+
+div.sphinxsidebar #searchbox input[type=submit] {
+ border-left-width: 0;
+}
+
+/* -- versions ------------------------------------------------------ */
+
+div.sphinxsidebar ul.versions a.current {
+ font-style: italic;
+ border-bottom: 1px solid #000;
+ color: #000;
+}
+
+div.sphinxsidebar ul.versions span.note {
+ color: #999;
+}
+
+/* -- version warning ----------------------------------------------- */
+
+p.version-warning {
+ top: 10px;
+ position: sticky;
+
+ margin: 10px 0;
+ padding: 5px 10px;
+ border-radius: 4px;
+
+ letter-spacing: 1px;
+ color: #fff;
+ text-shadow: 0 0 2px #000;
+ text-align: center;
+
+ background: #d40 repeating-linear-gradient(
+ 135deg,
+ transparent,
+ transparent 56px,
+ rgba(255, 255, 255, 0.2) 56px,
+ rgba(255, 255, 255, 0.2) 112px
+ );
+}
+
+p.version-warning a {
+ color: #fff;
+ border-color: #fff;
+}
+
+/* -- body styles --------------------------------------------------- */
+
+a {
+ text-decoration: none;
+ border-bottom: 1px dotted #000;
+}
+
+a:hover {
+ border-bottom-style: solid;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: normal;
+ margin: 30px 0 10px;
+ padding: 0;
+ color: black;
+}
+
+div.body h1 {
+ font-size: 240%;
+}
+
+div.body h2 {
+ font-size: 180%;
+}
+
+div.body h3 {
+ font-size: 150%;
+}
+
+div.body h4 {
+ font-size: 130%;
+}
+
+div.body h5 {
+ font-size: 100%;
+}
+
+div.body h6 {
+ font-size: 100%;
+}
+
+div.body h1:first-of-type {
+ margin-top: 0;
+}
+
+a.headerlink {
+ color: #ddd;
+ margin: 0 0.2em;
+ padding: 0 0.2em;
+ border: none;
+}
+
+a.headerlink:hover {
+ color: #444;
+}
+
+div.body p,
+div.body dd,
+div.body li {
+ line-height: 1.4;
+}
+
+img.screenshot {
+ box-shadow: 2px 2px 4px #eee;
+}
+
+hr {
+ border: 1px solid #999;
+}
+
+blockquote {
+ margin: 0 0 0 30px;
+ padding: 0;
+}
+
+ul, ol {
+ margin: 10px 0 10px 30px;
+ padding: 0;
+}
+
+a.footnote-reference {
+ font-size: 0.7em;
+ vertical-align: top;
+}
+
+/* -- admonitions --------------------------------------------------- */
+
+div.admonition,
+div.topic {
+ background-color: #fafafa;
+ margin: 10px -10px;
+ padding: 10px;
+ border-top: 1px solid #ccc;
+ border-right: none;
+ border-bottom: 1px solid #ccc;
+ border-left: none;
+}
+
+div.admonition p.admonition-title,
+div.topic p.topic-title {
+ font-weight: normal;
+ font-size: 24px;
+ margin: 0 0 10px 0;
+ padding: 0;
+ line-height: 1;
+ display: inline;
+}
+
+p.admonition-title::after {
+ content: ":";
+}
+
+div.admonition p.last,
+div.topic p:last-child {
+ margin-bottom: 0;
+}
+
+div.danger, div.error {
+ background-color: #fff0f0;
+ border-color: #ffb0b0;
+}
+
+div.seealso {
+ background-color: #fffff0;
+ border-color: #f0f0a8;
+}
+
+/* -- changelog ----------------------------------------------------- */
+
+details.changelog summary {
+ cursor: pointer;
+ font-style: italic;
+ margin-bottom: 10px;
+}
+
+/* -- search highlight ---------------------------------------------- */
+
+dt:target,
+.footnote:target,
+span.highlighted {
+ background-color: #ffdf80;
+}
+
+rect.highlighted {
+ fill: #ffdf80;
+}
+
+/* -- code displays ------------------------------------------------- */
+
+pre, code {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.9em;
+}
+
+pre {
+ margin: 0;
+ padding: 0;
+ line-height: 1.3;
+}
+
+div.literal-block-wrapper {
+ padding: 10px 0 0;
+}
+
+div.code-block-caption {
+ padding: 0;
+}
+
+div.highlight, div.literal-block-wrapper div.highlight {
+ margin: 10px -10px;
+ padding: 10px;
+}
+
+code {
+ color: #222;
+ background: #e8eff0;
+}
+
+/* -- tables -------------------------------------------------------- */
+
+table.docutils {
+ border: 1px solid #888;
+ box-shadow: 2px 2px 4px #eee;
+}
+
+table.docutils td,
+table.docutils th {
+ border: 1px solid #888;
+ padding: 0.25em 0.7em;
+}
+
+table.field-list,
+table.footnote {
+ border: none;
+ box-shadow: none;
+}
+
+table.footnote {
+ margin: 15px 0;
+ width: 100%;
+ border: 1px solid #eee;
+ background-color: #fafafa;
+ font-size: 0.9em;
+}
+
+table.footnote + table.footnote {
+ margin-top: -15px;
+ border-top: none;
+}
+
+table.field-list th {
+ padding: 0 0.8em 0 0;
+}
+
+table.field-list td {
+ padding: 0;
+}
+
+table.footnote td.label {
+ width: 0;
+ padding: 0.3em 0 0.3em 0.5em;
+}
+
+table.footnote td {
+ padding: 0.3em 0.5em;
+}
+
+/* -- responsive screen --------------------------------------------- */
+
+@media screen and (max-width: 1139px) {
+ p.version-warning {
+ margin: 10px;
+ }
+
+ div.footer {
+ margin: 20px 10px;
+ }
+}
+
+/* -- small screen -------------------------------------------------- */
+
+@media screen and (max-width: 767px) {
+ body {
+ padding: 0 20px;
+ }
+
+ div.related {
+ display: block;
+ }
+
+ p.version-warning {
+ margin: 10px 0;
+ }
+
+ div.documentwrapper {
+ float: none;
+ }
+
+ div.bodywrapper {
+ margin: 0;
+ }
+
+ div.body {
+ min-height: 0;
+ padding: 0;
+ }
+
+ div.sphinxsidebar {
+ float: none;
+ width: 100%;
+ margin: 0 -20px -10px;
+ padding: 0 20px;
+ background-color: #333;
+ color: #ccc;
+ }
+
+ div.sphinxsidebar a,
+ div.sphinxsidebar a code,
+ div.sphinxsidebar h3,
+ div.sphinxsidebar h4,
+ div.footer a {
+ color: #ccc;
+ border-color: #ccc;
+ }
+
+ div.sphinxsidebar p.logo {
+ display: none;
+ }
+
+ div.sphinxsidebar ul.versions a.current {
+ border-bottom-color: #fff;
+ color: #fff;
+ }
+
+ div.footer {
+ text-align: left;
+ margin: 0 -20px;
+ padding: 20px;
+ background-color: #333;
+ color: #ccc;
+ }
+}
+
+/* https://github.com/twbs/bootstrap/blob
+ /0e8831505ac845f3102fa2c5996a7141c9ab01ee
+ /scss/mixins/_screen-reader.scss */
+.hide-header > h1:first-child {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* -- sphinx-tabs -------------------------------------------------- */
+
+.sphinx-tabs {
+ margin-bottom: 0;
+}
+
+.sphinx-tabs .ui.menu {
+ font-family: 'Garamond', 'Georgia', serif !important;
+}
+
+.sphinx-tabs .ui.attached.menu {
+ border-bottom: none
+}
+
+.sphinx-tabs .ui.tabular.menu .item {
+ border-bottom: 2px solid transparent;
+ border-left: none;
+ border-right: none;
+ border-top: none;
+ padding: .3em 0.6em;
+}
+
+.sphinx-tabs .ui.attached.segment, .ui.segment {
+ border: 0;
+ padding: 0;
+}
diff --git a/src/pallets_sphinx_themes/themes/pocoo/static/version_warning_offset.js b/src/pallets_sphinx_themes/themes/pocoo/static/version_warning_offset.js
new file mode 100644
index 0000000..c7f9f49
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/static/version_warning_offset.js
@@ -0,0 +1,40 @@
+/*
+When showing the sticky version warning, the warning will cover the
+scroll target when navigating to #id hash locations. Take over scrolling
+to adjust the position to account for the height of the warning.
+*/
+$(() => {
+ const versionWarning = $('.version-warning')
+
+ // Skip if there is no version warning, regular browser behavior is
+ // fine in that case.
+ if (versionWarning.length) {
+ const height = versionWarning.outerHeight(true)
+ const target = $(':target')
+
+ // Adjust position when the initial link has a hash.
+ if (target.length) {
+ // Use absolute scrollTo instead of relative scrollBy to avoid
+ // scrolling when the viewport is already at the bottom of the
+ // document and has space.
+ const y = target.offset().top - height
+ // Delayed because the initial browser scroll doesn't seem to
+ // happen until after the document ready event, so scrolling
+ // immediately will be overridden.
+ setTimeout(() => scrollTo(0, y), 100)
+ }
+
+ // Listen to clicks on hash anchors.
+ $('a[href^="#"]').on('click', e => {
+ // Stop default scroll. Also stops the automatic URL hash update.
+ e.preventDefault()
+ // Get the id to scroll to and set the URL hash manually.
+ const id = $(e.currentTarget).attr('href').substring(1)
+ location.hash = id
+ // Use getElementById since the hash may have dots in it.
+ const target = $(document.getElementById(id))
+ // Scroll to top of target with space for the version warning.
+ scrollTo(0, target.offset().top - height)
+ })
+ }
+})
diff --git a/src/pallets_sphinx_themes/themes/pocoo/theme.conf b/src/pallets_sphinx_themes/themes/pocoo/theme.conf
new file mode 100644
index 0000000..972c9c9
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/theme.conf
@@ -0,0 +1,8 @@
+[theme]
+inherit = basic
+stylesheet = pocoo.css
+pygments_style = pocoo
+sidebars = localtoc.html, relations.html, searchbox.html, ethicalads.html
+
+[options]
+index_sidebar_logo = True
diff --git a/src/pallets_sphinx_themes/themes/pocoo/versions.html b/src/pallets_sphinx_themes/themes/pocoo/versions.html
new file mode 100644
index 0000000..b3a63de
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/pocoo/versions.html
@@ -0,0 +1,8 @@
+{% if versions %}
+ <h3>Versions</h3>
+ <ul class="versions">{% for version in versions %}
+ <li><a href="{{ version.href() }}" {% if version.current %}class="current"{% endif %}>
+ {{ version.name }}
+ </a></li>
+ {% endfor %}</ul>
+{% endif %}
diff --git a/src/pallets_sphinx_themes/themes/werkzeug/static/werkzeug.css b/src/pallets_sphinx_themes/themes/werkzeug/static/werkzeug.css
new file mode 100644
index 0000000..d95da2c
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/werkzeug/static/werkzeug.css
@@ -0,0 +1,15 @@
+@import url("pocoo.css");
+
+a, a.reference, a.footnote-reference {
+ color: #185f6d;
+ border-color: #185f6d;
+}
+
+a:hover {
+ color: #2794aa;
+ border-color: #2794aa;
+}
+
+p.version-warning {
+ background-color: #ff8c08;
+}
diff --git a/src/pallets_sphinx_themes/themes/werkzeug/theme.conf b/src/pallets_sphinx_themes/themes/werkzeug/theme.conf
new file mode 100644
index 0000000..a20df14
--- /dev/null
+++ b/src/pallets_sphinx_themes/themes/werkzeug/theme.conf
@@ -0,0 +1,3 @@
+[theme]
+inherit = pocoo
+stylesheet = werkzeug.css
diff --git a/src/pallets_sphinx_themes/versions.py b/src/pallets_sphinx_themes/versions.py
new file mode 100644
index 0000000..62c77e4
--- /dev/null
+++ b/src/pallets_sphinx_themes/versions.py
@@ -0,0 +1,153 @@
+import json
+import os
+from collections import namedtuple
+
+from jinja2 import pass_context
+from packaging import version as pv
+
+from .theme_check import only_pallets_theme
+
+
+@only_pallets_theme()
+def load_versions(app):
+ if os.environ.get("READTHEDOCS"):
+ versions = readthedocs_versions(app)
+ else:
+ versions = local_versions(app)
+
+ context = app.config.html_context
+ context["versions"] = versions
+ context["current_version"] = next((v for v in versions if v.current), None)
+ context["latest_version"] = next((v for v in versions if v.latest), None)
+
+
+def local_versions(app):
+ config_versions = app.config.html_context.get("versions")
+
+ if isinstance(config_versions, str):
+ if os.path.isfile(config_versions):
+ with open(config_versions, encoding="utf8") as f:
+ config_versions = json.load(f)
+ else:
+ config_versions = json.loads(config_versions)
+
+ if not config_versions:
+ return []
+
+ versions = []
+
+ for version in config_versions:
+ if isinstance(version, dict):
+ version = DocVersion(**version)
+
+ versions.append(version)
+
+ slug = app.config.version
+ dev = "dev" in app.config.release
+ seen_latest = False
+
+ for i, version in enumerate(versions):
+ if version.slug == "dev":
+ versions[i] = version._replace(dev=True, current=dev)
+
+ if version.slug == slug:
+ versions[i] = version._replace(current=True)
+
+ if not seen_latest and version.version is not None:
+ seen_latest = True
+ versions[i] = version._replace(latest=True)
+
+ return versions
+
+
+def readthedocs_versions(app):
+ config_versions = app.config.html_context["versions"]
+ current_slug = app.config.html_context["current_version"]
+ number_versions = []
+ name_versions = []
+
+ for slug, _ in config_versions:
+ dev = slug in {"main", "master", "default", "latest"}
+ version = _parse_version(slug)
+
+ if version is not None:
+ name = slug
+ append_to = number_versions
+ else:
+ name = "Development" if dev else slug.title()
+ append_to = name_versions
+
+ append_to.append(
+ DocVersion(name=name, slug=slug, dev=dev, current=slug == current_slug)
+ )
+
+ # put the newest numbered version first
+ number_versions.sort(key=lambda x: x.version, reverse=True)
+ # put non-dev named versions first
+ name_versions.sort(key=lambda x: x.dev, reverse=True)
+ versions = number_versions + name_versions
+
+ # if there are non-dev versions, mark the newest one as the latest
+ if versions and not versions[0].dev:
+ versions[0] = versions[0]._replace(latest=True)
+
+ return versions
+
+
+def _parse_version(value: str, placeholder: str = "x"):
+ if value.endswith(f".{placeholder}"):
+ value = value[: -(len(placeholder) + 1)]
+
+ try:
+ return pv.Version(value)
+ except pv.InvalidVersion:
+ return None
+
+
+class DocVersion(
+ namedtuple("DocVersion", ("name", "slug", "version", "latest", "dev", "current"))
+):
+ __slots__ = ()
+
+ def __new__(cls, name, slug=None, latest=False, dev=False, current=False):
+ slug = slug or name
+ version = _parse_version(slug)
+
+ if version is not None:
+ name = "Version " + name
+
+ return super().__new__(cls, name, slug, version, latest, dev, current)
+
+ @pass_context
+ def href(self, context):
+ pathto = context["pathto"]
+ master_doc = context["master_doc"]
+ pagename = context["pagename"]
+
+ builder = pathto.__closure__[0].cell_contents
+ master = pathto(master_doc).rstrip("#/") or "."
+ path = builder.get_target_uri(pagename)
+ return "/".join((master, "..", self.slug, path))
+
+ @pass_context
+ def banner(self, context):
+ if self.latest:
+ return
+
+ latest = context["latest_version"]
+
+ # Don't show a banner if the latest version couldn't be determined, or if this
+ # is the "stable" version.
+ if latest is None or self.name == "stable":
+ return
+
+ if self.dev:
+ return (
+ "This is the development version. The latest stable"
+ ' version is <a href="{href}">{latest}</a>.'
+ ).format(latest=latest.name, href=latest.href(context))
+
+ return (
+ "This is an old version. The latest stable version is"
+ ' <a href="{href}">{latest}</a>.'
+ ).format(latest=latest.name, href=latest.href(context))