diff options
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)) |