diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:12:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:12:08 +0000 |
commit | 0d286042a60f80eff5ced1f1800395537429b9d2 (patch) | |
tree | ba4a4b89d414179cf1dfac3354f317797e3fd832 /src | |
parent | Initial commit. (diff) | |
download | python-pallets-sphinx-themes-upstream.tar.xz python-pallets-sphinx-themes-upstream.zip |
Adding upstream version 2.0.3.upstream/2.0.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
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)) |