summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-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
31 files changed, 1862 insertions, 0 deletions
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))