From e588a2db2dc46d8ef744674740f6bba0e5c72648 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Mon, 23 Feb 2026 22:34:14 +0000 Subject: [PATCH 1/5] vendor docc --- pyproject.toml | 3 +- uv.lock | 21 +- vendor/docc/docc/__init__.py | 21 + vendor/docc/docc/__main__.py | 22 + vendor/docc/docc/build.py | 74 + vendor/docc/docc/cli.py | 179 +++ vendor/docc/docc/context.py | 116 ++ vendor/docc/docc/discover.py | 63 + vendor/docc/docc/document.py | 289 ++++ vendor/docc/docc/performance.py | 39 + vendor/docc/docc/plugins/__init__.py | 18 + vendor/docc/docc/plugins/debug.py | 80 ++ vendor/docc/docc/plugins/files.py | 155 +++ vendor/docc/docc/plugins/html/__init__.py | 965 +++++++++++++ vendor/docc/docc/plugins/html/static/docc.css | 294 ++++ .../docc/docc/plugins/html/static/search.js | 134 ++ .../docc/plugins/html/templates/base.html | 68 + .../docc/plugins/html/templates/search.html | 12 + vendor/docc/docc/plugins/listing/__init__.py | 219 +++ .../plugins/listing/templates/listing.html | 23 + vendor/docc/docc/plugins/loader.py | 65 + vendor/docc/docc/plugins/mistletoe.py | 605 +++++++++ vendor/docc/docc/plugins/python/__init__.py | 38 + vendor/docc/docc/plugins/python/cst.py | 1199 +++++++++++++++++ vendor/docc/docc/plugins/python/html.py | 212 +++ vendor/docc/docc/plugins/python/nodes.py | 320 +++++ .../plugins/python/templates/html/access.html | 17 + .../python/templates/html/attribute.html | 30 + .../templates/html/binary_operation.html | 17 + .../plugins/python/templates/html/bit_or.html | 17 + .../plugins/python/templates/html/class.html | 68 + .../python/templates/html/function.html | 35 + .../plugins/python/templates/html/list.html | 22 + .../plugins/python/templates/html/module.html | 103 ++ .../plugins/python/templates/html/name.html | 16 + .../python/templates/html/parameter.html | 24 + .../python/templates/html/subscript.html | 20 + .../plugins/python/templates/html/tuple.html | 22 + .../plugins/python/templates/html/type.html | 17 + vendor/docc/docc/plugins/references.py | 200 +++ vendor/docc/docc/plugins/resources.py | 150 +++ vendor/docc/docc/plugins/search.py | 333 +++++ vendor/docc/docc/plugins/verbatim/__init__.py | 614 +++++++++ vendor/docc/docc/plugins/verbatim/html.py | 114 ++ vendor/docc/docc/py.typed | 0 vendor/docc/docc/settings.py | 270 ++++ vendor/docc/docc/source.py | 83 ++ vendor/docc/docc/transform.py | 61 + vendor/docc/pyproject.toml | 88 ++ 49 files changed, 7548 insertions(+), 7 deletions(-) create mode 100644 vendor/docc/docc/__init__.py create mode 100644 vendor/docc/docc/__main__.py create mode 100644 vendor/docc/docc/build.py create mode 100644 vendor/docc/docc/cli.py create mode 100644 vendor/docc/docc/context.py create mode 100644 vendor/docc/docc/discover.py create mode 100644 vendor/docc/docc/document.py create mode 100644 vendor/docc/docc/performance.py create mode 100644 vendor/docc/docc/plugins/__init__.py create mode 100644 vendor/docc/docc/plugins/debug.py create mode 100644 vendor/docc/docc/plugins/files.py create mode 100644 vendor/docc/docc/plugins/html/__init__.py create mode 100644 vendor/docc/docc/plugins/html/static/docc.css create mode 100644 vendor/docc/docc/plugins/html/static/search.js create mode 100644 vendor/docc/docc/plugins/html/templates/base.html create mode 100644 vendor/docc/docc/plugins/html/templates/search.html create mode 100644 vendor/docc/docc/plugins/listing/__init__.py create mode 100644 vendor/docc/docc/plugins/listing/templates/listing.html create mode 100644 vendor/docc/docc/plugins/loader.py create mode 100644 vendor/docc/docc/plugins/mistletoe.py create mode 100644 vendor/docc/docc/plugins/python/__init__.py create mode 100644 vendor/docc/docc/plugins/python/cst.py create mode 100644 vendor/docc/docc/plugins/python/html.py create mode 100644 vendor/docc/docc/plugins/python/nodes.py create mode 100644 vendor/docc/docc/plugins/python/templates/html/access.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/attribute.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/binary_operation.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/bit_or.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/class.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/function.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/list.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/module.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/name.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/parameter.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/subscript.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/tuple.html create mode 100644 vendor/docc/docc/plugins/python/templates/html/type.html create mode 100644 vendor/docc/docc/plugins/references.py create mode 100644 vendor/docc/docc/plugins/resources.py create mode 100644 vendor/docc/docc/plugins/search.py create mode 100644 vendor/docc/docc/plugins/verbatim/__init__.py create mode 100644 vendor/docc/docc/plugins/verbatim/html.py create mode 100644 vendor/docc/docc/py.typed create mode 100644 vendor/docc/docc/settings.py create mode 100644 vendor/docc/docc/source.py create mode 100644 vendor/docc/docc/transform.py create mode 100644 vendor/docc/pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index f91a8695bd..024e17d6b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -496,7 +496,8 @@ required-version = ">=0.7.0" extra-build-dependencies = { ethash = ["setuptools", "cmake>=4.2.1,<5"] } [tool.uv.workspace] -members = ["packages/*"] +members = ["packages/*", "vendor/*"] [tool.uv.sources] ethereum-execution-testing = { workspace = true } +docc = { workspace = true } diff --git a/uv.lock b/uv.lock index 47ff3bfd8d..2da02b5f33 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,7 @@ resolution-markers = [ [manifest] members = [ + "docc", "ethereum-execution", "ethereum-execution-testing", ] @@ -726,7 +727,7 @@ wheels = [ [[package]] name = "docc" version = "0.3.1" -source = { registry = "https://pypi.org/simple" } +source = { editable = "vendor/docc" } dependencies = [ { name = "importlib-resources" }, { name = "inflection" }, @@ -737,9 +738,17 @@ dependencies = [ { name = "tomli" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/94/5a6164c4df2137bd7b249afb76690ed5f893caa319387b435ffcd9801f6f/docc-0.3.1.tar.gz", hash = "sha256:6424e286feba301a97f6d5f3c63b08f4f80dd3c2dec5306814d47f1f4a6de06e", size = 69245, upload-time = "2025-06-17T20:48:17.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/35/f32a8791c6159cfef87f77dff9d8c219a558241631b80f164386c7592767/docc-0.3.1-py3-none-any.whl", hash = "sha256:172f4d538f9d31f3cff6d029ecc1ad7858ed7a9af5e7504f5afee1cec5db0663", size = 96477, upload-time = "2025-06-17T20:48:16.415Z" }, + +[package.metadata] +requires-dist = [ + { name = "importlib-resources", specifier = ">=6.0.1,<7" }, + { name = "inflection", specifier = ">=0.5.1,<0.6" }, + { name = "jinja2", specifier = ">=3.1.2,<4" }, + { name = "libcst", specifier = ">=1.0.1,<2" }, + { name = "mistletoe", specifier = ">=1.2.1,<2" }, + { name = "rich", specifier = ">=13.5.2,<14" }, + { name = "tomli", specifier = ">=2.0.1,<3" }, + { name = "typing-extensions", specifier = ">=4.7.1,<5" }, ] [[package]] @@ -953,7 +962,7 @@ dev = [ { name = "cairosvg", specifier = ">=2.7.0,<3" }, { name = "codespell", specifier = "==2.4.1" }, { name = "codespell", specifier = ">=2.4.1,<3" }, - { name = "docc", specifier = ">=0.3.0,<0.4.0" }, + { name = "docc", editable = "vendor/docc" }, { name = "ethereum-execution", extras = ["optimized"] }, { name = "ethereum-execution-testing", editable = "packages/testing" }, { name = "filelock", specifier = ">=3.15.1,<4" }, @@ -991,7 +1000,7 @@ dev = [ { name = "vulture", specifier = "==2.14.0" }, ] doc = [ - { name = "docc", specifier = ">=0.3.0,<0.4.0" }, + { name = "docc", editable = "vendor/docc" }, { name = "fladrif", specifier = ">=0.2.0,<0.3.0" }, { name = "mistletoe", specifier = ">=1.5.0,<2" }, ] diff --git a/vendor/docc/docc/__init__.py b/vendor/docc/docc/__init__.py new file mode 100644 index 0000000000..b37d72fe9a --- /dev/null +++ b/vendor/docc/docc/__init__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2022-2024 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +The documentation compiler. +""" + +__version__ = "0.3.1" +"Current version of docc" diff --git a/vendor/docc/docc/__main__.py b/vendor/docc/docc/__main__.py new file mode 100644 index 0000000000..228e3f6e80 --- /dev/null +++ b/vendor/docc/docc/__main__.py @@ -0,0 +1,22 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Forwards to `docc.cli` when the module is executed directly. +""" + +from docc.cli import main + +main() diff --git a/vendor/docc/docc/build.py b/vendor/docc/docc/build.py new file mode 100644 index 0000000000..2a783fcd86 --- /dev/null +++ b/vendor/docc/docc/build.py @@ -0,0 +1,74 @@ +# Copyright (C) 2022-2024 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implementations of `Builder` convert each `Source` into a `Document`. +""" + +from abc import ABC, abstractmethod +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Dict, Iterator, Optional, Set, Tuple, Type + +from .document import Document +from .plugins.loader import Loader +from .settings import PluginSettings, Settings +from .source import Source + + +class Builder(AbstractContextManager["Builder", None], ABC): + """ + Consumes unprocessed `Source` instances and creates `Document`s. + """ + + @abstractmethod + def __init__(self, config: PluginSettings) -> None: + """ + Create a Builder with the given configuration. + """ + raise NotImplementedError() + + @abstractmethod + def build( + self, + unprocessed: Set[Source], + processed: Dict[Source, Document], + ) -> None: + """ + Consume unprocessed Sources and insert their Documents into processed. + """ + raise NotImplementedError() + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + """ + Context handler clean-up function. + """ + + +def load(settings: Settings) -> Iterator[Tuple[str, Builder]]: + """ + Load the builder plugins as requested in settings. + """ + loader = Loader() + + for name in settings.build: + class_ = loader.load(Builder, name) + plugin_settings = settings.for_plugin(name) + yield (name, class_(plugin_settings)) diff --git a/vendor/docc/docc/cli.py b/vendor/docc/docc/cli.py new file mode 100644 index 0000000000..52c6e72cab --- /dev/null +++ b/vendor/docc/docc/cli.py @@ -0,0 +1,179 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Command line interface to docc. +""" + +import argparse +import logging +import sys +from contextlib import ExitStack +from io import TextIOBase +from pathlib import Path +from shutil import rmtree +from typing import Dict, Set, Type + +from . import build, context, discover, transform +from .context import Context +from .document import Document, Node, OutputNode, Visit, Visitor +from .performance import measure +from .settings import Settings +from .source import Source + + +class _OutputVisitor(Visitor): + destination: TextIOBase + context: Context + + def __init__(self, context_: Context, destination: TextIOBase) -> None: + self.context = context_ + self.destination = destination + + def enter(self, node: Node) -> Visit: + if isinstance(node, OutputNode): + node.output(self.context, self.destination) + return Visit.SkipChildren + else: + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + pass + + +def main() -> None: + """ + Entry-point for the command line tool. + """ + logging.basicConfig() + logging.getLogger().setLevel(logging.INFO) + + parser = argparse.ArgumentParser( + description="Python documentation generator." + ) + + parser.add_argument( + "--output", help="The directory to write documentation to." + ) + + args = parser.parse_args() + settings = Settings(Path.cwd()) + + if args.output is None: + output_root = settings.output.path + + if output_root is None: + logging.critical( + "Output path is required. " + "Either define `tool.docc.output.path` in `pyproject.toml` " + "or use `--output foo/bar` on the command line." + ) + sys.exit(1) + else: + output_root = Path(args.output) + + with measure("Loaded plugins (%.4f s)", level=logging.INFO): + discover_plugins = list(discover.load(settings)) + transform_plugins = list(transform.load(settings)) + context_plugins = list(context.load(settings)) + + with measure("Created contexts (%.4f s)", level=logging.INFO): + context_types = {} + for name, context_plugin in context_plugins: + class_ = context_plugin.provides() + + try: + exists = context_types[class_] + raise Exception( + f"context provider `{name}`" + f" conflicts with `{exists}`" + f" (on `{class_.__name__}`)" + ) + except KeyError: + pass + + context_types[class_] = name + + known: Set[Source] = set() + + with measure("Discovered sources (%.4f s)", level=logging.INFO): + for name, instance in discover_plugins: + found = instance.discover(frozenset(known)) + for item in found: + if item.relative_path is None: + logging.debug("[%s] found source without a path", name) + else: + logging.debug( + "[%s] found source: %s", name, item.relative_path + ) + known.add(item) + + with ExitStack() as exit_stack: + build_plugins = [ + (n, exit_stack.enter_context(c)) for (n, c) in build.load(settings) + ] + + with measure("Built documents (%.4f s)", level=logging.INFO): + documents: Dict[Source, Document] = {} + for name, build_plugin in build_plugins: + before = len(documents) + build_plugin.build(known, documents) + after = len(documents) + logging.debug("[%s] built %s documents", name, after - before) + + with measure("Provided contexts (%.4f s)", level=logging.INFO): + contexts = {} + for source, document in documents.items(): + context_dict: Dict[Type[object], object] = { + Document: document, + Source: source, + } + + for _, context_plugin in context_plugins: + provided = context_plugin.provide() + class_ = context_plugin.provides() + assert class_ not in context_dict + context_dict[class_] = provided + + contexts[source] = Context(context_dict) + + with measure("Transformed documents (%.4f s)", level=logging.INFO): + for _name, transform_plugin in transform_plugins: + for context_ in contexts.values(): + transform_plugin.transform(context_) + + rmtree(output_root, ignore_errors=True) + + with measure("Wrote outputs (%.4f s)", level=logging.INFO): + for source, context_ in contexts.items(): + document = context_[Document] + extension = document.extension() + + if extension is None: + logging.error( + "document from `%s` does not specify a file extension", + source.relative_path, + ) + continue + + output_path = output_root / source.output_path + output_path = Path( + output_path.with_suffix(output_path.suffix + extension) + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + + with open(output_path, "w", encoding="utf-8") as destination: + document.root.visit(_OutputVisitor(context_, destination)) diff --git a/vendor/docc/docc/context.py b/vendor/docc/docc/context.py new file mode 100644 index 0000000000..284603caab --- /dev/null +++ b/vendor/docc/docc/context.py @@ -0,0 +1,116 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Additional context provided alongside a Document. +""" + +from abc import ABC, abstractmethod +from typing import Generic, Iterator, Mapping, Optional, Tuple, Type, TypeVar + +from .plugins.loader import Loader +from .settings import PluginSettings, Settings + +Q = TypeVar("Q") + + +class Context: + """ + A single "unit" of transformation, typically containing a Document and + Source, among other things. + """ + + __slots__ = ("_items",) + + _items: Mapping[Type[object], object] + + def __init__( + self, items: Optional[Mapping[Type[object], object]] = None + ) -> None: + if items is None: + items = {} + + for key, value in items.items(): + if not isinstance(value, key): + raise ValueError(f"`{value}` is not an instance of `{key}`") + + self._items = items + + def __contains__(self, class_: Type[Q]) -> bool: + """ + `True` if the Context contains `class_`, `False` otherwise. + """ + return class_ in self._items + + def __getitem__(self, class_: Type[Q]) -> Q: + """ + Given a type, return an instance of that type if one has been stored in + this Context. + + For example: + + ```python + document = context[Document] + ``` + """ + item = self._items[class_] + assert isinstance(item, class_) + return item + + def __repr__(self) -> str: + """ + Returns a string representation of this object. + """ + return f"{self.__class__.__name__}({self._items!r})" + + +R_co = TypeVar("R_co", covariant=True) + + +class Provider(ABC, Generic[R_co]): + """ + Creates objects to be inserted into Context instances. + """ + + @classmethod + @abstractmethod + def provides(class_) -> Type[R_co]: + """ + Return the type to be used as the key in the Context. + """ + + @abstractmethod + def __init__(self, config: PluginSettings) -> None: + """ + Create a Provider with the given configuration. + """ + + @abstractmethod + def provide(self) -> R_co: + """ + Create the object to be inserted into the Context. + """ + + +def load(settings: Settings) -> Iterator[Tuple[str, Provider[object]]]: + """ + Load the context plugins as requested in settings. + """ + loader = Loader() + + for name in settings.context: + class_ = loader.load(Provider, name) + plugin_settings = settings.for_plugin(name) + yield (name, class_(plugin_settings)) diff --git a/vendor/docc/docc/discover.py b/vendor/docc/docc/discover.py new file mode 100644 index 0000000000..ef0fe45d73 --- /dev/null +++ b/vendor/docc/docc/discover.py @@ -0,0 +1,63 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Discovery is the process of finding sources. +""" + +from abc import ABC, abstractmethod +from typing import FrozenSet, Iterator, Sequence, Tuple, TypeVar + +from .plugins.loader import Loader +from .settings import PluginSettings, Settings +from .source import Source + +T = TypeVar("T", bound=Source) + + +class Discover(ABC): + """ + Finds sources for which to generate documentation. + """ + + @abstractmethod + def __init__(self, config: PluginSettings) -> None: + """ + Construct a new instance with the given configuration. + """ + raise NotImplementedError() + + @abstractmethod + def discover(self, known: FrozenSet[T]) -> Iterator[Source]: + """ + Find sources. + """ + raise NotImplementedError() + + +def load(settings: Settings) -> Sequence[Tuple[str, Discover]]: + """ + Load the discovery plugins as requested in settings. + """ + loader = Loader() + + sources = [] + + for name in settings.discovery: + class_ = loader.load(Discover, name) + plugin_settings = settings.for_plugin(name) + sources.append((name, class_(plugin_settings))) + + return sources diff --git a/vendor/docc/docc/document.py b/vendor/docc/docc/document.py new file mode 100644 index 0000000000..7726a831b6 --- /dev/null +++ b/vendor/docc/docc/document.py @@ -0,0 +1,289 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Documents are the in-flight representation of a Source. +""" + +import logging +from abc import ABC, abstractmethod +from enum import Enum, auto +from io import StringIO, TextIOBase +from typing import IO, Iterable, List, Optional, Tuple, Union + +import rich.markup +import rich.tree +from rich.console import Console + +from .context import Context + + +class Node(ABC): + """ + Representation of a node in a Document. + """ + + __slots__: Tuple[str, ...] = () + + @property + @abstractmethod + def children(self) -> Iterable["Node"]: + """ + Child nodes belonging to this node. + """ + + @abstractmethod + def replace_child(self, old: "Node", new: "Node") -> None: + """ + Replace the old node with the given new node. + """ + + def visit(self, visitor: "Visitor") -> None: + """ + Visit, in depth-first order, this node and its children. + """ + if visitor.enter(self) == Visit.SkipChildren: + visitor.exit(self) + return + + stack = [(self, iter(self.children))] + + while stack: + node, children = stack.pop() + + try: + child = next(children) + except StopIteration: + visitor.exit(node) + continue + + stack.append((node, children)) + + if visitor.enter(child) == Visit.SkipChildren: + visitor.exit(child) + else: + stack.append((child, iter(child.children))) + + def dump(self, file: Optional[IO[str]] = None) -> None: + """ + Render the tree to the console or given file. + """ + visitor = _StrVisitor() + self.visit(visitor) + + console = Console(file=file) + console.print(visitor.root) + + def dumps(self) -> str: + """ + Render the tree to a str. + """ + io = StringIO() + self.dump(file=io) + return io.getvalue() + + +class ListNode(Node): + """ + A node containing a list of children. + """ + + __slots__ = ("children",) + + children: List[Node] + + def __init__(self, children: Optional[List[Node]] = None) -> None: + if children is None: + self.children = [] + else: + self.children = children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + self.children = [new if x == old else x for x in self.children] + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return "" + + def __bool__(self) -> bool: + """ + Cast this instance to a bool. + """ + return len(self.children) > 0 + + def __iter__(self) -> Iterable[Node]: + """ + Return an iterator for the children of this node. + """ + return iter(self.children) + + def __len__(self) -> int: + """ + The number of children of this node. + """ + return len(self.children) + + +class BlankNode(Node): + """ + A placeholder node with no conent and no children. + """ + + __slots__: Tuple[str, ...] = tuple() + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return tuple() + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError() + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return "" + + def __bool__(self) -> bool: + """ + Cast this instance to a bool. + """ + return False + + +class OutputNode(Node): + """ + A Node that understands how to write to a file. + """ + + @property + @abstractmethod + def extension(self) -> str: + """ + The preferred file extension for this node. + """ + + @abstractmethod + def output(self, context: Context, destination: TextIOBase) -> None: + """ + Write this Node to destination. + """ + + +class Visit(Enum): + """ + How to proceed after visiting a Node. + """ + + TraverseChildren = auto() + SkipChildren = auto() + + +class Visitor(ABC): + """ + Base class for visitors. + """ + + @abstractmethod + def enter(self, node: Node) -> Visit: + """ + Called when visiting the given node, before any children (if any) are + visited. + """ + + @abstractmethod + def exit(self, node: Node) -> None: + """ + Called after visiting the last child of the given node (or immediately + if the node has no children.) + """ + + +class _StrVisitor(Visitor): + root: Union[None, rich.tree.Tree] + stack: List[rich.tree.Tree] + + def __init__(self) -> None: + self.stack = [] + self.root = None + + def enter(self, node: Node) -> Visit: + tree = rich.tree.Tree(rich.markup.escape(repr(node))) + if self.root is None: + assert 0 == len(self.stack) + self.root = tree + else: + self.stack[-1].add(tree) + self.stack.append(tree) + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + self.stack.pop() + + +class _ExtensionVisitor(Visitor): + extension: Optional[str] + + def __init__(self) -> None: + self.extension = None + + def enter(self, node: Node) -> Visit: + if isinstance(node, OutputNode): + extension = node.extension + if self.extension is not None and self.extension != extension: + logging.warning( + "document has extension `%s` but node wants `%s`", + self.extension, + extension, + ) + else: + self.extension = extension + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + pass + + +class Document: + """ + In-flight representation of a Source. + """ + + root: Node + + def __init__( + self, + root: Node, + ) -> None: + self.root = root + + def extension(self) -> Optional[str]: + """ + Find the file extension for this document. + """ + visitor = _ExtensionVisitor() + self.root.visit(visitor) + return visitor.extension diff --git a/vendor/docc/docc/performance.py b/vendor/docc/docc/performance.py new file mode 100644 index 0000000000..67e796f65e --- /dev/null +++ b/vendor/docc/docc/performance.py @@ -0,0 +1,39 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Timing measurement utilities. +""" + +import logging +import time +from contextlib import contextmanager +from typing import Iterator + + +@contextmanager +def measure( + message: str, + level: int = logging.DEBUG, +) -> Iterator[None]: + """ + Log how long a block took to execute. + """ + start = time.perf_counter() + try: + yield + finally: + end = time.perf_counter() + logging.log(level, message, end - start) diff --git a/vendor/docc/docc/plugins/__init__.py b/vendor/docc/docc/plugins/__init__.py new file mode 100644 index 0000000000..528030ce25 --- /dev/null +++ b/vendor/docc/docc/plugins/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Plugins for docc. +""" diff --git a/vendor/docc/docc/plugins/debug.py b/vendor/docc/docc/plugins/debug.py new file mode 100644 index 0000000000..44a1096b6d --- /dev/null +++ b/vendor/docc/docc/plugins/debug.py @@ -0,0 +1,80 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Plugin for dumping the node tree to text files. +""" + +from io import TextIOBase +from typing import Iterable + +from docc.context import Context +from docc.document import Document, Node, OutputNode +from docc.settings import PluginSettings +from docc.transform import Transform + + +class DebugNode(OutputNode): + """ + An `OuputNode` that renders its contents as a tree. + """ + + child: Node + + def __init__(self, child: Node) -> None: + self.child = child + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return (self.child,) + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + if old == self.child: + self.child = new + + def output(self, context: Context, destination: TextIOBase) -> None: + """ + Attempt to write this node to destination. + """ + self.dump(destination) # pyre-ignore[6] + + @property + def extension(self) -> str: + """ + The preferred file extension for this node. + """ + return ".txt" + + +class DebugTransform(Transform): + """ + A plugin that renders to a human-readable format. + """ + + def __init__(self, settings: PluginSettings) -> None: + pass + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + document = context[Document] + document.root = DebugNode(document.root) diff --git a/vendor/docc/docc/plugins/files.py b/vendor/docc/docc/plugins/files.py new file mode 100644 index 0000000000..c52b9b37ea --- /dev/null +++ b/vendor/docc/docc/plugins/files.py @@ -0,0 +1,155 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Plugin for working with files. +""" + +import shutil +from io import TextIOBase +from pathlib import Path, PurePath +from typing import Dict, Final, FrozenSet, Iterator, Sequence, Set, Tuple + +from docc.build import Builder +from docc.context import Context +from docc.discover import Discover, T +from docc.document import Document, Node, OutputNode +from docc.settings import PluginSettings +from docc.source import Source + + +class FileSource(Source): + """ + A Source representing a file. + """ + + _relative_path: Final[PurePath] + absolute_path: Final[PurePath] + + def __init__( + self, relative_path: PurePath, absolute_path: PurePath + ) -> None: + self._relative_path = relative_path + self.absolute_path = absolute_path + + @property + def relative_path(self) -> PurePath: + """ + Location of this source, relative to the project root. + """ + return self._relative_path + + @property + def output_path(self) -> PurePath: + """ + Where the output of this source should end up. + """ + return self.relative_path.with_suffix("") + + +class FileNode(OutputNode): + """ + A node representing a file to be copied to the output directory. + """ + + path: Path + + def __init__(self, path: Path) -> None: + self.path = path + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError + + @property + def children(self) -> Tuple[()]: + """ + Child nodes belonging to this node. + """ + return tuple() + + @property + def extension(self) -> str: + """ + The preferred file extension for this node. + """ + return self.path.suffix + + def output(self, context: Context, destination: TextIOBase) -> None: + """ + Write this Node to destination. + """ + with self.path.open("r") as f: + shutil.copyfileobj(f, destination) + + +class FilesBuilder(Builder): + """ + Collect file sources and prepare them for reading. + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Create a builder with the given configuration. + """ + + def build( + self, + unprocessed: Set[Source], + processed: Dict[Source, Document], + ) -> None: + """ + Consume unprocessed Sources and insert their Documents into processed. + """ + source_set = set(s for s in unprocessed if isinstance(s, FileSource)) + unprocessed -= source_set + + for source in source_set: + processed[source] = Document( + FileNode(Path(source.absolute_path)), + ) + + +class FilesDiscover(Discover): + """ + Create sources for static files. + """ + + sources: Sequence[FileSource] + + def __init__(self, config: PluginSettings) -> None: + """ + Construct a new instance with the given configuration. + """ + files = config.get("files") + if files is None: + self.sources = [] + else: + sources = [] + + for item in files: + absolute_path = config.resolve_path(PurePath(item)) + relative_path = config.unresolve_path(absolute_path) + sources.append(FileSource(relative_path, absolute_path)) + + self.sources = sources + + def discover(self, known: FrozenSet[T]) -> Iterator[Source]: + """ + Find sources. + """ + return iter(self.sources) diff --git a/vendor/docc/docc/plugins/html/__init__.py b/vendor/docc/docc/plugins/html/__init__.py new file mode 100644 index 0000000000..f9b8c41e9a --- /dev/null +++ b/vendor/docc/docc/plugins/html/__init__.py @@ -0,0 +1,965 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Plugin that renders to HTML. +""" + + +import html.parser +import sys +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from io import StringIO, TextIOBase +from os.path import commonpath +from pathlib import PurePath +from typing import ( + Callable, + Dict, + Final, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Type, + Union, +) +from urllib.parse import urlunsplit +from urllib.request import pathname2url + +import markupsafe +from jinja2 import Environment, PackageLoader +from jinja2 import nodes as j2 +from jinja2 import pass_context, select_autoescape +from jinja2.ext import Extension +from jinja2.parser import Parser +from jinja2.runtime import Context as JinjaContext + +from docc.context import Context, Provider +from docc.discover import Discover, T +from docc.document import ( + BlankNode, + Document, + ListNode, + Node, + OutputNode, + Visit, + Visitor, +) +from docc.plugins import references +from docc.plugins.loader import PluginError +from docc.plugins.references import Index, ReferenceError +from docc.plugins.resources import ResourceSource +from docc.plugins.search import Search +from docc.settings import PluginSettings, SettingsError +from docc.source import Source +from docc.transform import Transform + +if sys.version_info < (3, 10): + from importlib_metadata import EntryPoint, entry_points +else: + from importlib.metadata import EntryPoint, entry_points + + +RenderResult = Optional[Union["HTMLTag", "HTMLRoot"]] +""" +Possible output from rendering to HTML. +""" + + +@dataclass(frozen=True) +class HTML: + """ + Configuration for HTML output. + """ + + extra_css: Sequence[str] + """ + List of paths to CSS files to include in the final rendered documentation. + """ + + breadcrumbs: bool + """ + Whether to render breadcrumbs (links to parent pages). + """ + + +class HTMLContext(Provider[HTML]): + """ + Store HTML configuration options in the global context. + """ + + html: Final[HTML] + + @classmethod + def provides(class_) -> Type[HTML]: + """ + Return the type to be used as the key in the Context. + """ + return HTML + + def __init__(self, config: PluginSettings) -> None: + """ + Create a Provider with the given configuration. + """ + extra_css = config.get("extra_css", []) + if any(not isinstance(x, str) for x in extra_css): + raise SettingsError("`extra_css` items must be strings") + + breadcrumbs = config.get("breadcrumbs", True) + if not isinstance(breadcrumbs, bool): + raise SettingsError("breadcrumbs must be boolean") + + self.html = HTML(extra_css=extra_css, breadcrumbs=breadcrumbs) + + def provide(self) -> HTML: + """ + Create the object to be inserted into the Context. + """ + return self.html + + +class HTMLDiscover(Discover): + """ + Create sources for static files necessary for HTML output. + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Construct a new instance with the given configuration. + """ + + def discover(self, known: FrozenSet[T]) -> Iterator[Source]: + """ + Find sources. + """ + yield ResourceSource.with_path( + "docc.plugins.html", + PurePath("static") / "chota" / "dist" / "chota.min.css", + PurePath("static") / "chota", + ) + yield ResourceSource.with_path( + "docc.plugins.html", + PurePath("static") / "docc.css", + PurePath("static") / "docc", + ) + yield ResourceSource.with_path( + "docc.plugins.html", + PurePath("static") / "fuse" / "dist" / "fuse.min.js", + PurePath("static") / "fuse", + ) + yield ResourceSource.with_path( + "docc.plugins.html", + PurePath("static") / "search.js", + PurePath("static") / "search", + ) + + +class TextNode(Node): + """ + Node containing text. + """ + + _value: str + + def __init__(self, value: str) -> None: + self._value = value + + @property + def children(self) -> Sequence[Node]: + """ + Child nodes belonging to this node. + """ + return tuple() + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError("text nodes have no children") + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return repr(self._value) + + +class HTMLTag(Node): + """ + A node holding HTML. + """ + + tag_name: str + attributes: Dict[str, Optional[str]] + _children: List[Node] + + def __init__( + self, + tag_name: str, + attributes: Optional[Dict[str, Optional[str]]] = None, + ) -> None: + self.tag_name = tag_name + self.attributes = {} if attributes is None else attributes + self._children = [] + + @property + def children(self) -> Sequence[Node]: + """ + Child nodes belonging to this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + self._children = [new if x == old else x for x in self._children] + + def append(self, node: Union["HTMLTag", TextNode]) -> None: + """ + Add the given node to the end of this instance's children. + """ + self._children.append(node) + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + output = f"<{self.tag_name}" + for name, value in self.attributes.items(): + output += f" {name}" + if value is not None: + output += '="' + output += html.escape(value, True) + output += '"' + output += ">" + return output + + def _to_element(self) -> ET.Element: + visitor = _ElementTreeVisitor() + self.visit(visitor) + return visitor.builder.close() + + +class HTMLRoot(OutputNode): + """ + Node representing the top-level of an HTML document or fragment. + """ + + _children: List[Union[HTMLTag, TextNode]] + extra_css: Sequence[str] + breadcrumbs: bool + context: Context + + def __init__(self, context: Context) -> None: + self._children = [] + self.context = context + + try: + html = context[HTML] + except KeyError: + self.extra_css = [] + self.breadcrumbs = True + else: + self.extra_css = html.extra_css + self.breadcrumbs = html.breadcrumbs + + @property + def children(self) -> Iterable[Union[HTMLTag, TextNode]]: + """ + Child nodes belonging to this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + assert isinstance(new, (HTMLTag, TextNode)) + self._children = [new if x == old else x for x in self._children] + + def append(self, node: Union["HTMLTag", TextNode]) -> None: + """ + Add a new HTML or text node to the end of this node's children. + """ + self._children.append(node) + + def output(self, context: Context, destination: TextIOBase) -> None: + """ + Attempt to write this node to destination as HTML. + """ + rendered = StringIO() + for child in self.children: + if isinstance(child, TextNode): + rendered.write(child._value) + continue + + assert isinstance(child, HTMLTag) + element = child._to_element() + markup = ET.tostring(element, encoding="unicode", method="html") + rendered.write(markup) + + env = Environment( + extensions=[_ReferenceExtension], + loader=PackageLoader("docc.plugins.html"), + autoescape=select_autoescape(), + ) + template = env.get_template("base.html") + body = rendered.getvalue() + static_path = _static_path_from(context) + + search_path = None + search_base = None + if Search in context: + search_path = _search_path_from(context) + search_base = _project_path_from(context) + + extra_css = [ + f"{_project_path_from(context)}/{x}" for x in self.extra_css + ] + + breadcrumbs = [] + path = self.context[Source].output_path + + if self.breadcrumbs: + for parent in reversed(path.parents): + index_path = parent / "index.html" + relative_path = _make_relative(path, index_path) + if relative_path is None: + relative_path_str = "" + else: + relative_path_str = str(relative_path) + url = pathname2url(relative_path_str) + breadcrumbs.append((parent, url)) + + destination.write( + template.render( + body=markupsafe.Markup(body), + static_path=static_path, + search_path=search_path, + search_base=search_base, + extra_css=extra_css, + output_path=path, + breadcrumbs=breadcrumbs, + ) + ) + + @property + def extension(self) -> str: + """ + The preferred file extension for this node. + """ + return ".html" + + +class _ElementTreeVisitor(Visitor): + builder: ET.TreeBuilder + + def __init__(self) -> None: + self.builder = ET.TreeBuilder() + + def enter_tag(self, node: HTMLTag) -> Visit: + attributes: Dict[Union[str, bytes], Union[str, bytes]] = { + name: value if value else "" + for name, value in node.attributes.items() + } + self.builder.start(node.tag_name, attributes) + return Visit.TraverseChildren + + def enter_text(self, node: TextNode) -> Visit: + self.builder.data(node._value) + return Visit.TraverseChildren + + def enter(self, node: Node) -> Visit: + if isinstance(node, HTMLTag): + return self.enter_tag(node) + elif isinstance(node, TextNode): + return self.enter_text(node) + else: + raise TypeError(f"unsupported node {node.__class__.__name__}") + + def exit_tag(self, node: HTMLTag) -> None: + self.builder.end(node.tag_name) + + def exit_text(self, node: TextNode) -> None: + pass # Do nothing + + def exit(self, node: Node) -> None: + if isinstance(node, HTMLTag): + return self.exit_tag(node) + elif isinstance(node, TextNode): + return self.exit_text(node) + else: + raise TypeError(f"unsupported node {node.__class__.__name__}") + + +class HTMLVisitor(Visitor): + """ + Visits a Document's tree and converts Nodes to HTML. + """ + + entry_points: Dict[str, EntryPoint] + renderers: Dict[ + Type[Node], + Callable[..., object], + ] + root: HTMLRoot + stack: List[Union[HTMLRoot, HTMLTag, TextNode, BlankNode]] + context: Context + + def __init__(self, context: Context) -> None: + # Discover render functions. + found = entry_points(group="docc.plugins.html") + self.entry_points = {entry.name: entry for entry in found} + self.root = HTMLRoot(context) + self.stack = [self.root] + self.renderers = {} + self.context = context + + def _renderer(self, node: Node) -> Callable[..., object]: + type_ = node.__class__ + try: + return self.renderers[type_] + except KeyError: + pass + + key = f"{type_.__module__}:{type_.__qualname__}" + try: + renderer = self.entry_points[key].load() + except KeyError as e: + raise PluginError( + f"no renderer found for `{key}` (for node `{node}`)" + ) from e + + if not callable(renderer): + raise PluginError(f"renderer for `{key}` is not callable") + + self.renderers[type_] = renderer + return renderer + + def enter(self, node: Node) -> Visit: + """ + Called when visiting the given node, before any children (if any) are + visited. + """ + top = self.stack[-1] + assert isinstance(top, (HTMLRoot, HTMLTag)) + + renderer = self._renderer(node) + result = renderer(self.context, top, node) + + if result is None: + # Always append something so the exit implementation is simpler. + self.stack.append(BlankNode()) + return Visit.SkipChildren + elif isinstance(result, (HTMLTag, HTMLRoot)): + self.stack.append(result) + return Visit.TraverseChildren + else: + raise PluginError( + f"`{renderer.__module__}:{renderer.__qualname__}` " + "did not return `None` or `HTMLTag` instance" + ) + + def exit(self, node: Node) -> None: + """ + Called after visiting the last child of the given node (or immediately + if the node has no children.) + """ + self.stack.pop() + + +class HTMLTransform(Transform): + """ + A plugin that renders to HTML. + """ + + def __init__(self, settings: PluginSettings) -> None: + pass + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + document = context[Document] + if isinstance(document.root, OutputNode): + return None + + visitor = HTMLVisitor(context) + document.root.visit(visitor) + assert visitor.root is not None + document.root = visitor.root + + +class HTMLParser(html.parser.HTMLParser): + """ + Subclass of Python's HTMLParser that converts into docc's syntax tree. + """ + + root: HTMLRoot + stack: List[Union[HTMLRoot, HTMLTag]] + + def __init__(self, context: Context) -> None: + super().__init__() + self.root = HTMLRoot(context) + self.stack = [self.root] + + def handle_starttag( + self, tag: str, attrs: Sequence[Tuple[str, Optional[str]]] + ) -> None: + """ + Handle opening tags. + """ + element = HTMLTag(tag, dict(attrs)) + self.stack[-1].append(element) + self.stack.append(element) + + def handle_endtag(self, tag: str) -> None: + """ + Handle closing tags. + """ + ended = self.stack.pop() + assert isinstance(ended, HTMLTag) + assert ( + ended.tag_name == tag + ), f"mismatched tag `{ended.tag_name}` and `{tag}`" + + def handle_data(self, data: str) -> None: + """ + Handle data. + """ + self.stack[-1].append(TextNode(data)) + + def handle_entityref(self, name: str) -> None: + """ + Handle an entity reference. + """ + raise TypeError() # Not called when convert_charrefs is True. + + def handle_charref(self, name: str) -> None: + """ + Handle a character reference. + """ + raise TypeError() # Not called when convert_charrefs is True. + + def handle_comment(self, data: str) -> None: + """ + Handle an HTML comment. + """ + raise NotImplementedError("HTML comments not yet supported") + + def handle_decl(self, decl: str) -> None: + """ + Handle a doctype. + """ + raise NotImplementedError("HTML doctypes not yet supported") + + def handle_pi(self, data: str) -> None: + """ + Handle a processing instruction. + """ + raise NotImplementedError( + "HTML processing instructions not yet supported" + ) + + def unknown_decl(self, data: str) -> None: + """ + Handle an unknown HTML declaration. + """ + raise NotImplementedError("unknown HTML declaration") + + +class _FindVisitor(Visitor): + class_: str + found: List[Tuple[references.Definition, Node]] + max_depth: int + _definitions: List[references.Definition] + + def __init__(self, class_: str, max_depth: int = 1) -> None: + self.class_ = class_ + self.found = [] + self.max_depth = max_depth + self._definitions = [] + + def enter(self, node: Node) -> Visit: + if isinstance(node, references.Definition): + self._definitions.append(node) + + if len(self._definitions) > self.max_depth: + return Visit.SkipChildren + + try: + definition = self._definitions[-1] + except IndexError: + return Visit.TraverseChildren + + type_ = node.__class__ + full_name = f"{type_.__module__}:{type_.__qualname__}" + + if full_name == self.class_: + self.found.append((definition, node)) + return Visit.SkipChildren + + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + if isinstance(node, references.Definition): + popped = self._definitions.pop() + assert node == popped + + +class _ReferenceExtension(Extension): + tags = {"reference"} + + def parse(self, parser: Parser) -> j2.Node: + lineno = next(parser.stream).lineno + + # two arguments: the identifier of the reference, and the context + args = [parser.parse_expression(), j2.ContextReference()] + + body = parser.parse_statements( + ("name:endreference",), drop_needle=True + ) + + return j2.CallBlock( + self.call_method("_reference_support", args), [], [], body + ).set_lineno(lineno) + + def _reference_support( + self, identifier: str, context: JinjaContext, caller: Callable[[], str] + ) -> markupsafe.Markup: + parser = HTMLParser(Context()) + parser.feed(caller()) + + children = parser.root._children + + output = "" + for child in children: + if isinstance(child, markupsafe.Markup): + output += child + elif isinstance(child, str): + output += markupsafe.escape(child) + else: + reference = references.Reference( + identifier=identifier, child=child + ) + output += _html_filter(context, reference) + + return markupsafe.Markup(output) + + +def _find_filter( + value: object, + class_: object, +) -> Sequence[Tuple[references.Definition, Node]]: + assert isinstance(value, Node) + assert isinstance(class_, str) + + visitor = _FindVisitor(class_) + value.visit(visitor) + return visitor.found + + +@pass_context +def _html_filter( + context: JinjaContext, value: object +) -> Union[markupsafe.Markup, str]: + ctx = context["context"] + assert isinstance(ctx, Context) + assert isinstance(value, Node), f"expected Node, got {type(value)}" + visitor = HTMLVisitor(ctx) + value.visit(visitor) + + children = [] + for child in visitor.root.children: + if isinstance(child, TextNode): + children.append(html.escape(child._value)) + continue + + assert isinstance(child, HTMLTag) + element = child._to_element() + markup = ET.tostring(element, encoding="unicode", method="html") + children.append(markup) + + rendered = "".join(children) + eval_context = context.eval_ctx + return markupsafe.Markup(rendered) if eval_context.autoescape else rendered + + +def _project_path_from(context: Context) -> str: + return pathname2url( + str( + _make_relative(context[Source].output_path, PurePath(".")) + or PurePath() + ) + ) + + +def _search_path_from(context: Context) -> str: + return pathname2url( + str( + _make_relative(context[Source].output_path, PurePath("search.js")) + or PurePath() + ) + ) + + +def _static_path_from(context: Context) -> str: + return pathname2url( + str( + _make_relative(context[Source].output_path, PurePath("static")) + or PurePath() + ) + ) + + +def render_template( + package: str, + context: Context, + parent: Union[HTMLTag, HTMLRoot], + template_name: str, + node: Node, +) -> RenderResult: + """ + Render a template as a child of the given parent. + """ + static_path = _static_path_from(context) + env = Environment( + extensions=[_ReferenceExtension], + loader=PackageLoader(package), + autoescape=select_autoescape(), + ) + env.filters["html"] = _html_filter + env.filters["find"] = _find_filter + template = env.get_template(template_name) + parser = HTMLParser(context) + parser.feed( + template.render(context=context, node=node, static_path=static_path) + ) + for child in parser.root._children: + parent.append(child) + return None + + +def _render_template( + context: object, parent: object, template_name: str, node: Node +) -> RenderResult: + assert isinstance(context, Context) + assert isinstance(parent, (HTMLTag, HTMLRoot)) + return render_template( + "docc.plugins.html", context, parent, template_name, node + ) + + +def blank_node( + context: object, + parent: object, + blank: object, +) -> RenderResult: + """ + Render a blank node. + """ + assert isinstance(blank, BlankNode) + return None + + +def references_definition( + context: object, + parent: object, + definition: object, +) -> RenderResult: + """ + Render a Definition as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(definition, references.Definition) + + new_id = f"{definition.identifier}:{definition.specifier}" + + visitor = HTMLVisitor(context) + definition.child.visit(visitor) + + children = list(visitor.root.children) + + if not children: + children.append(HTMLTag("span")) + + first_child = children[0] + + if isinstance(first_child, TextNode): + span = HTMLTag("span") + span.append(first_child) + children[0] = span + first_child = span + + if "id" in first_child.attributes: + raise NotImplementedError( + f"multiple ids (adding {new_id} to {first_child.attributes['id']})" + ) + + first_child.attributes["id"] = new_id + + for child in children: + parent.append(child) + + return None + + +def references_reference( + context: object, + parent: object, + reference: object, +) -> RenderResult: + """ + Render a Reference as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(reference, references.Reference) + + anchor = render_reference(context, reference) + parent.append(anchor) + + if not reference.child: + anchor.append(TextNode(reference.identifier)) + return None + + # TODO: handle tr, td, and other elements that can't be wrapped in an . + + return anchor + + +def list_node( + context: object, + parent: object, + node: object, +) -> RenderResult: + """ + Render a ListNode as HTML. + """ + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(node, ListNode) + return parent + + +def html_tag( + context: object, + parent: object, + html_tag: object, +) -> RenderResult: + """ + Render an HTMLTag as HTML. + """ + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(html_tag, HTMLTag) + parent.append(html_tag) + return None + + +def text_node( + context: object, + parent: object, + text_node: object, +) -> RenderResult: + """ + Render TextNode as HTML. + """ + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(text_node, TextNode) + parent.append(text_node) + return None + + +def _make_relative(from_: PurePath, to: PurePath) -> Optional[PurePath]: + # XXX: This path stuff is most certainly broken. + + if from_ == to: + # Can't represent an empty path with PurePath (becomes "." instead) + return None + + common_path = commonpath((from_, to)) + + parents = len(from_.relative_to(common_path).parents) - 1 + + return PurePath(*[".."] * parents) / to.relative_to(common_path) + + +def render_reference( + context: Context, reference: references.Reference +) -> HTMLTag: + """ + Render a Reference node into an HTMLTag. + """ + try: + definitions = list(context[Index].lookup(reference.identifier)) + except ReferenceError as error: + raise ReferenceError(reference.identifier, context=context) from error + + if not definitions: + raise NotImplementedError( + f"no definitions for `{reference.identifier}`" + ) + + multi = len(definitions) > 1 + anchors = HTMLTag( + "div", attributes={"class": "tooltip-content", "role": "tooltip"} + ) + + for definition in definitions: + anchor = HTMLTag("a") + anchors.append(anchor) + + # XXX: This path stuff is most certainly broken. + + output_path = context[Source].output_path + definition_path = definition.source.output_path + + relative_path = _make_relative(output_path, definition_path) + if relative_path is None: + relative_path_str = "" + else: + # TODO: Don't hardcode extension. + relative_path_str = str(relative_path) + ".html" + + fragment = f"{definition.identifier}:{definition.specifier}" + anchor.attributes["href"] = urlunsplit( + ( + "", # scheme + "", # host + pathname2url(relative_path_str), # path + "", # query + fragment, # fragment + ) + ) + + if multi: + anchor.append(TextNode(fragment)) + + if not multi: + anchor = anchors.children[0] + assert isinstance(anchor, HTMLTag) + return anchor + + container = HTMLTag( + "div", attributes={"class": "tooltip", "tabindex": "0"} + ) + container.append(anchors) + + return container diff --git a/vendor/docc/docc/plugins/html/static/docc.css b/vendor/docc/docc/plugins/html/static/docc.css new file mode 100644 index 0000000000..205d7d480e --- /dev/null +++ b/vendor/docc/docc/plugins/html/static/docc.css @@ -0,0 +1,294 @@ +/*! + * docc | GPL-3.0 License | https://github.com/SamWilsn/docc + */ +:root { + --default-color: black; + --default-font-weight: normal; + + --keyword-color: blue; + --keyword-font-weight: bold; + + --literal-color: teal; + + --name-color: maroon; + + --code-max-width: 100%; + + --grid-maxWidth: 100%; +} + +.row.reverse { + /* Possible bug in chota? Reversed rows overflow to the left on narrow + * screens. */ + justify-content: flex-end; +} + +pre { + margin: 0; + padding: 0; +} + +a > code { + text-decoration: underline dotted; +} + +.nobr, .code-like { + white-space: nowrap; +} + +.code-like { + font-family: var(--font-family-mono); + background-color: var(--bg-secondary-color); + max-width: var(--code-max-width); +} + +table.verbatim { + max-width: var(--code-max-width); +} + +table.verbatim > tbody > tr > td { + width: 100%; + background-color: var(--bg-secondary-color); +} + +table.verbatim > tbody > tr > th { + white-space: nowrap; + user-select: none; +} + +table.verbatim > tbody > tr > td, +table.verbatim > tbody > tr > th { + padding: 0 0.5ex; +} + +section section { + padding-left: 1em; + margin-top: 2em; + border-left: 3px solid var(--bg-secondary-color); +} + +details > summary > code { + color: inherit; + margin-left: 2em; + text-indent: -2em; +} + +details > summary { + list-style: none; + display: flex; + flex-grow: 1; +} + +details > summary::marker { + content: ''; + display: none; +} + +details > summary::after { + content: 'show source'; + margin-left: auto; + margin-right: 1ex; + cursor: pointer; + color: var(--color-primary); + white-space: nowrap; +} + +details[open] > summary::after { + content: 'hide source'; +} + +.scroll-x { + max-width: 100%; + overflow-x: auto; +} + +/* Breadcrumbs */ +.breadcrumbs-row > [class*="col"] { + padding: 0; + margin-bottom: 0; + margin-top: 0; +} + +.breadcrumbs { + padding: 0 .5rem; +} + +.breadcrumbs ul { + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0; + padding: 0; + width: fit-content; +} + +.breadcrumbs li:not(:last-child)::after { + display: inline-block; + margin: 0 .5rem; + content: "/"; + color: var(--color-grey); +} + +/* Module Table of Contents */ +.width-limiter { + max-width: 960px; +} + +.members, .module { + max-width: 100%; +} + +.module > .members > .width-limiter > section { + width: 100%; +} + +.module > .toc { + padding-left: 1em; + margin-top: 3.85em; + word-wrap: anywhere; +} + +.module > .toc ul, .module > .toc ol { + list-style: none; + padding-left: 0; + font-size: 1.3rem; +} + +/* Class Table of Contents */ +.class > .toc ul, .class > .toc ol { + list-style: none; + margin: 0; +} + +/* Reference Tooltips */ +.tooltip { + display: inline; + position: relative; +} + +.tooltip > :not(:first-child) { + color: #177cb9; +} + +.tooltip-content { + display: none; + position: absolute; + padding: 1ex; + box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.1); + background-color: var(--bg-secondary-color); + min-width: 100%; + min-height: 100%; + top: 0; + left: 0; + z-index: 1; + width: max-content; +} + +.tooltip:hover > .tooltip-content, +.tooltip:focus-within > .tooltip-content { + display: block; +} + +.tooltip-content > a { + display: block; +} + +/* Markdown */ +.markdown pre { + margin-bottom: 16px; +} + +/* Search */ +#search { + max-width: 100%; + margin: 0; +} + +#search > * { + margin: 0; + padding: 0; +} + +#search-results-container { + display: none; +} + +.search-bar { + margin: 1em; +} + +.search-path { + color: var(--color-grey); + font-family: var(--font-family-mono); +} + +#search-results > li:not(:last-child) { + margin-bottom: 1em; +} + +/* Resets */ +.hi * { + color: var(--default-color); + font-weight: var(--default-font-weight); +} + +/* Theme */ +.hi a { + text-decoration: none; + border-bottom: 1px dotted rgba(0, 0, 0, 0.3); +} + +.hi-function-def, +.hi-and, +.hi-or, +.hi-for, +.hi-while, +.hi-comp-for, +.hi-comp-if, +.hi-if, +.hi-else, +.hi-elif, +.hi-if-exp, +.hi-is, +.hi-is-not, +.hi-continue, +.hi-break, +.hi-assert, +.hi-return, +.hi-yield, +.hi-pass, +.hi-not, +.hi-raise, +.hi-from, +.hi-try, +.hi-except-handler, +.hi-with, +.hi-class, +.hi-in, +.hi-lambda, +.hi-finally, +.hi-as-name { + color: var(--keyword-color); + font-weight: var(--keyword-font-weight); +} + +.hi-simple-string, +.hi-formatted-string, +.hi-formatted-string-text, +.hi-formatted-string-expression, +.hi-integer { + color: var(--literal-color); +} + +.hi-formatted-string-expression { + font-weight: bold; +} + +.hi-name { + color: var(--name-color); +} + +.hi-call > .hi-name, +.hi-call > .hi-attribute > .hi-name:last-child { + font-weight: bold; +} diff --git a/vendor/docc/docc/plugins/html/static/search.js b/vendor/docc/docc/plugins/html/static/search.js new file mode 100644 index 0000000000..b1146f5b72 --- /dev/null +++ b/vendor/docc/docc/plugins/html/static/search.js @@ -0,0 +1,134 @@ +/*! + * docc | GPL-3.0 License | https://github.com/SamWilsn/docc + */ +(function() { + "use strict"; + + const onLoad = () => { + const searchBase = document.querySelector("meta[name='docc:search']"); + const searchPath = document.getElementById("search-path"); + const searchContainer = document.getElementById("search-results-container"); + const searchElement = document.getElementById("search-results"); + const mainElement = document.getElementById("main-content"); + + const tag = document.createElement("script"); + tag.async = true; + tag.src = searchPath.href; + + const searcherPromise = new Promise((resolve, reject) => { + const onLoad = () => { + tag.removeEventListener("load", onLoad); + tag.removeEventListener("error", onError); + + const options = { + keys: [ + "content.name", + { + name: "content.text", + weight: 0.75 + }, + { + name: "source.path", + weight: 0.5 + } + ] + }; + + resolve(new Fuse(window.SEARCH_INDEX, options)); + }; + + const onError = (e) => { + tag.removeEventListener("load", onLoad); + tag.removeEventListener("error", onError); + reject(e); + }; + + tag.addEventListener("load", onLoad); + tag.addEventListener("error", onError); + + document.body.appendChild(tag); + }); + + const bars = document.querySelectorAll(".search-bar input[type='search']"); + + const clearSearch = () => { + for (const bar of bars) { + bar.value = ""; + const event = new Event('input', { + bubbles: true, + cancelable: true, + }); + bar.dispatchEvent(event); + } + }; + + const onTimeout = async (e) => { + delete e.target.dataset.timeoutId; + if (!e.target.value) { + searchElement.replaceChildren(); + mainElement.style.display = "initial"; + searchContainer.style.display = "none"; + return; + } + + const searcher = await searcherPromise; + const results = searcher.search(e.target.value); + + mainElement.style.display = "none"; + searchContainer.style.display = "initial"; + searchElement.replaceChildren(...results.map((r) => { + const href = new URL( + r.item.source.path + ".html", + new URL(searchBase.getAttribute("value"), window.location) + ); + + if (r.item.source.identifier) { + const specifier = r.item.source.specifier || 0; + href.hash = `#${r.item.source.identifier}:${specifier}`; + } + + const anchor = document.createElement("a"); + anchor.innerText = r.item.content.name; + anchor.href = href; + anchor.addEventListener("click", clearSearch); + + const path = document.createElement("span"); + path.classList.add("search-path"); + path.innerText = " " + r.item.source.path + + const elem = document.createElement("li"); + elem.appendChild(anchor); + elem.appendChild(path); + + if (r.item.content.text) { + const text = document.createElement("div"); + text.classList.add("search-text"); + text.innerText = " " + r.item.content.text; + elem.appendChild(text); + } + + return elem; + })); + }; + + const onInput = async (e) => { + const timeoutIdText = e.target.dataset.timeoutId; + if (timeoutIdText !== undefined) { + const timeoutId = Number.parseInt(timeoutIdText); + clearTimeout(timeoutId); + } + + e.target.dataset.timeoutId = setTimeout(() => onTimeout(e), 300); + }; + + for (const bar of bars) { + bar.addEventListener("input", onInput); + } + }; + + if ("complete" === document.readyState) { + onLoad(); + } else { + window.addEventListener("DOMContentLoaded", onLoad); + } +})(); diff --git a/vendor/docc/docc/plugins/html/templates/base.html b/vendor/docc/docc/plugins/html/templates/base.html new file mode 100644 index 0000000000..5a79e4b737 --- /dev/null +++ b/vendor/docc/docc/plugins/html/templates/base.html @@ -0,0 +1,68 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + #} + + + + + {{ output_path.name }} (in {{ output_path.parent }}) + + + {% if search_path %} + + + + + {% endif %} + {% for sheet in extra_css %} + + {% endfor %} + + + +
+ {{ body }} +
+ + diff --git a/vendor/docc/docc/plugins/html/templates/search.html b/vendor/docc/docc/plugins/html/templates/search.html new file mode 100644 index 0000000000..4a89665bb6 --- /dev/null +++ b/vendor/docc/docc/plugins/html/templates/search.html @@ -0,0 +1,12 @@ + diff --git a/vendor/docc/docc/plugins/listing/__init__.py b/vendor/docc/docc/plugins/listing/__init__.py new file mode 100644 index 0000000000..1aeb80885e --- /dev/null +++ b/vendor/docc/docc/plugins/listing/__init__.py @@ -0,0 +1,219 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Plugin that renders directory listings. +""" + +from abc import ABC, abstractmethod +from os.path import commonpath +from pathlib import PurePath +from typing import Dict, Final, FrozenSet, Iterator, Set, Tuple + +from jinja2 import Environment, PackageLoader, select_autoescape + +from docc.build import Builder +from docc.context import Context +from docc.discover import Discover, T +from docc.document import Document, Node +from docc.plugins import html +from docc.settings import PluginSettings +from docc.source import Source + + +class Listable(ABC): + """ + Mixin to change visibility of a Source in a directory listing. + """ + + @property + @abstractmethod + def show_in_listing(self) -> bool: + """ + `True` if this `Source` should be shown in directory listings. + """ + raise NotImplementedError() + + +class ListingDiscover(Discover): + """ + Creates listing sources for each directory. + """ + + def __init__(self, config: PluginSettings) -> None: + pass + + def discover(self, known: FrozenSet[T]) -> Iterator["ListingSource"]: + """ + Find sources. + """ + listings = {} + + for source in known: + path = source.relative_path + if isinstance(source, Listable): + if not source.show_in_listing: + continue + elif not path: + continue + + if not path: + path = source.output_path + + for parent in path.parents: + try: + listing = listings[parent] + except KeyError: + listing = ListingSource(parent, parent / "index", set()) + listings[parent] = listing + yield listing + + listing.sources.add(source) + source = listing + + +class ListingSource(Source): + """ + A synthetic source that describes the contents of a directory. + """ + + _relative_path: Final[PurePath] + _output_path: Final[PurePath] + sources: Final[Set[Source]] + + def __init__( + self, + relative_path: PurePath, + output_path: PurePath, + sources: Set[Source], + ) -> None: + self._relative_path = relative_path + self.sources = sources + self._output_path = output_path + + @property + def output_path(self) -> PurePath: + """ + Where to write the output from this Source relative to the output path. + """ + return self._output_path + + @property + def relative_path(self) -> PurePath: + """ + Path to the Source (if one exists) relative to the project root. + """ + return self._relative_path + + +class ListingBuilder(Builder): + """ + Converts ListingSource instances into Documents. + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Create a Builder with the given configuration. + """ + + def build( + self, + unprocessed: Set[Source], + processed: Dict[Source, Document], + ) -> None: + """ + Consume unprocessed Sources and insert their Documents into processed. + """ + to_process = set( + x for x in unprocessed if isinstance(x, ListingSource) + ) + unprocessed -= to_process + + for source in to_process: + processed[source] = Document(ListingNode(source.sources)) + + +class ListingNode(Node): + """ + A node representing a directory listing. + """ + + sources: Final[Set[Source]] + + def __init__(self, sources: Set[Source]) -> None: + self.sources = sources + + @property + def children(self) -> Tuple[()]: + """ + Child nodes belonging to this node. + """ + return () + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError() + + +def render_html( + context: object, + parent: object, + node: object, +) -> html.RenderResult: + """ + Render a ListingNode as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (html.HTMLRoot, html.HTMLTag)) + assert isinstance(node, ListingNode) + + output_path = context[Source].output_path + entries = [] + + for source in node.sources: + entry_path = source.output_path + + if output_path == entry_path: + relative_path = "" + else: + common_path = commonpath((output_path, entry_path)) + + parents = len(output_path.relative_to(common_path).parents) - 1 + + relative_path = ( + str( + PurePath(*[".."] * parents) + / entry_path.relative_to(common_path) + ) + + ".html" + ) # TODO: Don't hardcode extension. + + path = source.relative_path or source.output_path + entries.append((path, relative_path)) + + entries.sort() + + env = Environment( + loader=PackageLoader("docc.plugins.listing"), + autoescape=select_autoescape(), + ) + template = env.get_template("listing.html") + parser = html.HTMLParser(context) + parser.feed(template.render(context=context, entries=entries)) + for child in parser.root._children: + parent.append(child) + return None diff --git a/vendor/docc/docc/plugins/listing/templates/listing.html b/vendor/docc/docc/plugins/listing/templates/listing.html new file mode 100644 index 0000000000..47e8587f6f --- /dev/null +++ b/vendor/docc/docc/plugins/listing/templates/listing.html @@ -0,0 +1,23 @@ +{# + # Copyright (C) 2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} + diff --git a/vendor/docc/docc/plugins/loader.py b/vendor/docc/docc/plugins/loader.py new file mode 100644 index 0000000000..98e8c7ffc5 --- /dev/null +++ b/vendor/docc/docc/plugins/loader.py @@ -0,0 +1,65 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Load plugins. +""" + +import sys +from inspect import isabstract +from typing import Callable, Dict, Type, TypeVar + +if sys.version_info < (3, 10): + from importlib_metadata import EntryPoint, entry_points +else: + from importlib.metadata import EntryPoint, entry_points + + +class PluginError(Exception): + """ + An error encountered while loading a plugin. + """ + + +L = TypeVar("L") + + +class Loader: + """ + Facilitates loading plugins. + """ + + entry_points: Dict[str, EntryPoint] + + def __init__(self) -> None: + """ + Create an instance and populate it with the discovered plugins. + """ + found = set(entry_points(group="docc.plugins")) + self.entry_points = {entry.name: entry for entry in found} + + def load(self, base: Type[L], name: str) -> Callable[..., L]: + """ + Load a plugin by name. + """ + class_ = self.entry_points[name].load() + + if isabstract(class_): + raise PluginError(f"type {class_} is abstract") + + if not issubclass(class_, base): + raise PluginError(f"type {class_} is not a subclass of {base}") + + return class_ diff --git a/vendor/docc/docc/plugins/mistletoe.py b/vendor/docc/docc/plugins/mistletoe.py new file mode 100644 index 0000000000..1edd661468 --- /dev/null +++ b/vendor/docc/docc/plugins/mistletoe.py @@ -0,0 +1,605 @@ +# Copyright (C) 2022-2024 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Markdown support for docc. +""" + +from typing import ( + Callable, + Final, + Iterable, + List, + Mapping, + Optional, + Protocol, + Sequence, + Union, + runtime_checkable, +) + +import mistletoe as md +from mistletoe import block_token as blocks +from mistletoe import span_token as spans +from mistletoe.token import Token as MarkdownToken +from typing_extensions import TypeAlias + +from docc.context import Context +from docc.document import Document, ListNode, Node, Visit, Visitor +from docc.plugins import html, python, references, search +from docc.settings import PluginSettings +from docc.transform import Transform + + +class MarkdownNode(Node, search.Searchable): + """ + Representation of a markdown node. + """ + + __slots__ = ("token", "_children") + + token: Final[MarkdownToken] + _children: Optional[List[Node]] + + def __init__(self, token: MarkdownToken) -> None: + self.token = token + self._children = None + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + current = self._children + if current is not None: + return current + + children = getattr(self.token, "children", tuple()) + if children is None: + children = tuple() + replacement: List[Node] = [MarkdownNode(c) for c in children] + self._children = replacement + return replacement + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + self._children = [new if x == old else x for x in self.children] + + def to_search(self) -> search.Content: + """ + Extract the text from this node to put in the search index. + """ + return " ".join(_SearchVisitor.collect(self)) + + def search_children(self) -> bool: + """ + `True` if the children of this node should be searched, `False` + otherwise. + """ + return False + + +class DocstringTransform(Transform): + """ + Replaces python docstring nodes with markdown nodes. + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Create a Transform with the given configuration. + """ + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + visitor = _DocstringVisitor() + context[Document].root.visit(visitor) + assert visitor.root is not None + context[Document].root = visitor.root + + +class _DocstringVisitor(Visitor): + root: Optional[Node] + stack: Final[List[Node]] + + def __init__(self) -> None: + self.stack = [] + self.root = None + + def enter(self, node: Node) -> Visit: + self.stack.append(node) + if self.root is None: + self.root = node + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + popped = self.stack.pop() + assert node == popped + + if not isinstance(node, python.Docstring): + return + + document = md.Document(node.text) + new_node = MarkdownNode(document) + + if self.stack: + self.stack[-1].replace_child(node, new_node) + else: + self.root = new_node + + +class ReferenceTransform(Transform): + """ + Replaces markdown link and autolink nodes with [`Reference`] nodes instead. + + [`Reference`]: ref:docc.plugins.references.Reference + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Create a Transform with the given configuration. + """ + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + visitor = _ReferenceVisitor() + context[Document].root.visit(visitor) + assert visitor.root is not None + context[Document].root = visitor.root + + +class _ReferenceVisitor(Visitor): + root: Optional[Node] + stack: Final[List[Node]] + + def __init__(self) -> None: + self.stack = [] + self.root = None + + def enter(self, node: Node) -> Visit: + self.stack.append(node) + if self.root is None: + self.root = node + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + popped = self.stack.pop() + assert node == popped + + if not isinstance(node, MarkdownNode): + return + + token = node.token + if not isinstance(token, (spans.Link, spans.AutoLink)): + return + + ref = token.target.removeprefix("ref:") + if ref == token.target: + return + + new_node = references.Reference(ref) + + children = list(node.children) + if len(children) == 1: + new_node.child = children[0] + elif len(children) > 1: + new_node.child = ListNode(children) + + if self.stack: + self.stack[-1].replace_child(node, new_node) + else: + self.root = new_node + + +def _render_strong( + context: Context, + parent: html.HTMLRoot | html.HTMLTag, + node: MarkdownNode, +) -> html.RenderResult: + tag = html.HTMLTag("strong") + parent.append(tag) + return tag + + +def _render_emphasis( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + tag = html.HTMLTag("em") + parent.append(tag) + return tag + + +def _render_inline_code( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + tag = html.HTMLTag("code") + parent.append(tag) + return tag + + +def _render_raw_text( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, spans.RawText) + parent.append(html.TextNode(token.content)) + return None + + +def _render_strikethrough( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + tag = html.HTMLTag("del") + parent.append(tag) + return tag + + +def _render_image( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, spans.Image) + attributes = { + "src": token.src, + "alt": token.content, + } + if token.title: + attributes["title"] = token.title + tag = html.HTMLTag("img", attributes) + parent.append(tag) + return None + + +def _render_link( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, spans.Link) + attributes = {"href": token.target} + if token.title: + attributes["title"] = token.title + tag = html.HTMLTag("a", attributes) + parent.append(tag) + return tag + + +def _render_auto_link( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, spans.AutoLink) + if token.mailto: + href = f"mailto:{token.target}" + else: + href = token.target + attributes = {"href": href} + tag = html.HTMLTag("a", attributes) + parent.append(tag) + return tag + + +def _render_escape_sequence( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + raise NotImplementedError() + + +def _render_heading( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, (blocks.Heading, blocks.SetextHeading)) + tag = html.HTMLTag(f"h{token.level}") + parent.append(tag) + return tag + + +def _render_quote( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: Understand what mistletoe's ptag stack is for. + token = node.token + assert isinstance(token, blocks.Quote) + tag = html.HTMLTag("blockquote") + parent.append(tag) + return tag + + +def _render_paragraph( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: Understand what mistletoe's ptag stack is for. + token = node.token + assert isinstance(token, blocks.Paragraph) + tag = html.HTMLTag("p") + parent.append(tag) + return tag + + +def _render_block_code( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + pre = html.HTMLTag("pre") + code = html.HTMLTag("code") + pre.append(code) + parent.append(pre) + return code + + +def _render_list( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: Understand what mistletoe's ptag stack is for. + token = node.token + assert isinstance(token, blocks.List) + if token.start is None: + tag = html.HTMLTag("ul") + else: + attributes = {} + if token.start != 1: + attributes["start"] = token.start + tag = html.HTMLTag("ol", attributes) + parent.append(tag) + return tag + + +def _render_list_item( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: Understand what mistletoe's ptag stack is for. + token = node.token + assert isinstance(token, blocks.ListItem) + tag = html.HTMLTag("li") + parent.append(tag) + return tag + + +@runtime_checkable +class _TableWithHeader(Protocol): + header: MarkdownToken + + +def _render_table( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: mistletoe had a much more complicated implementation. + table = html.HTMLTag("table") + token = node.token + + if isinstance(token, _TableWithHeader): + # TODO: The table header should appear in node's `.children`. + header_node = MarkdownNode(token.header) + + visitor = html.HTMLVisitor(context) + header_node.visit(visitor) + + thead = html.HTMLTag("thead") + for node in visitor.root.children: + thead.append(node) + table.append(thead) + + parent.append(table) + return table + + +def _render_table_row( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: mistletoe had a much more complicated implementation. + row = html.HTMLTag("tr") + parent.append(row) + return row + + +def _render_table_cell( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: mistletoe had a much more complicated implementation. + token = node.token + assert isinstance(token, blocks.TableCell) + if token.align is None: + align = "left" + elif token.align == 0: + align = "center" + elif token.align == 2: + align = "right" + else: + raise NotImplementedError(f"table alignment {token.align}") + cell = html.HTMLTag("td", {"align": align}) + parent.append(cell) + return cell + + +def _render_thematic_break( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + parent.append(html.HTMLTag("hr")) + return None + + +def _render_line_break( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, spans.LineBreak) + tag = html.TextNode("\n") if token.soft else html.HTMLTag("br") + parent.append(tag) + return None + + +def _render_html_span( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, spans.HTMLSpan) + parser = html.HTMLParser(context) + parser.feed(token.content) + for child in parser.root.children: + parent.append(child) + return None + + +def _render_html_block( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + token = node.token + assert isinstance(token, blocks.HTMLBlock) + parser = html.HTMLParser(context) + parser.feed(token.content) + for child in parser.root.children: + parent.append(child) + return None + + +def _render_document( + context: Context, + parent: Union[html.HTMLRoot, html.HTMLTag], + node: MarkdownNode, +) -> html.RenderResult: + # TODO: footnotes? + token = node.token + assert isinstance(token, blocks.Document) + tag = html.HTMLTag("div", {"class": "markdown"}) + parent.append(tag) + return tag + + +_RENDER_FUNC: TypeAlias = Callable[ + [Context, Union[html.HTMLRoot, html.HTMLTag], MarkdownNode], + html.RenderResult, +] + +_RENDERERS: Mapping[str, _RENDER_FUNC] = { + "Strong": _render_strong, + "Emphasis": _render_emphasis, + "InlineCode": _render_inline_code, + "RawText": _render_raw_text, + "Strikethrough": _render_strikethrough, + "Image": _render_image, + "Link": _render_link, + "AutoLink": _render_auto_link, + "EscapeSequence": _render_escape_sequence, + "Heading": _render_heading, + "SetextHeading": _render_heading, + "Quote": _render_quote, + "Paragraph": _render_paragraph, + "CodeFence": _render_block_code, + "BlockCode": _render_block_code, + "List": _render_list, + "ListItem": _render_list_item, + "Table": _render_table, + "TableRow": _render_table_row, + "TableCell": _render_table_cell, + "ThematicBreak": _render_thematic_break, + "LineBreak": _render_line_break, + "Document": _render_document, + "HTMLBlock": _render_html_block, + "HTMLSpan": _render_html_span, +} + + +def render_html( + context: object, + parent: object, + node: object, +) -> html.RenderResult: + """ + Render a markdown node as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (html.HTMLRoot, html.HTMLTag)) + assert isinstance(node, MarkdownNode) + + return _RENDERERS[node.token.__class__.__name__](context, parent, node) + + +class _SearchVisitor(Visitor): + texts: List[str] + + @staticmethod + def collect(nodes: Union[Node, Sequence[Node]]) -> List[str]: + if isinstance(nodes, Node): + nodes = [nodes] + + visitor = _SearchVisitor() + + for node in nodes: + node.visit(visitor) + + return visitor.texts + + def __init__(self) -> None: + self.texts = [] + + def enter(self, node: Node) -> Visit: + # TODO: Doesn't consider non-markdown nodes or HTML correctly. + if not isinstance(node, MarkdownNode): + return Visit.TraverseChildren + + token = node.token + if isinstance(token, spans.RawText): + self.texts.append(token.content) + return Visit.SkipChildren + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + pass diff --git a/vendor/docc/docc/plugins/python/__init__.py b/vendor/docc/docc/plugins/python/__init__.py new file mode 100644 index 0000000000..ea38d2c266 --- /dev/null +++ b/vendor/docc/docc/plugins/python/__init__.py @@ -0,0 +1,38 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# flake8: noqa: F401 + +""" +Plugin that parses Python. +""" + +from .cst import PythonBuilder as PythonBuilder +from .cst import PythonDiscover as PythonDiscover +from .cst import PythonTransform as PythonTransform + +# Use redundant "as" statement from PEP 484 when re-exporting. +from .nodes import Access as Access +from .nodes import Attribute as Attribute +from .nodes import Class as Class +from .nodes import Docstring as Docstring +from .nodes import Function as Function +from .nodes import List as List +from .nodes import Module as Module +from .nodes import Name as Name +from .nodes import Parameter as Parameter +from .nodes import PythonNode as PythonNode +from .nodes import Tuple as Tuple +from .nodes import Type as Type diff --git a/vendor/docc/docc/plugins/python/cst.py b/vendor/docc/docc/plugins/python/cst.py new file mode 100644 index 0000000000..51bc4aaa88 --- /dev/null +++ b/vendor/docc/docc/plugins/python/cst.py @@ -0,0 +1,1199 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Documentation plugin for Python. +""" + +import glob +import logging +import os.path +from collections import defaultdict +from copy import deepcopy +from dataclasses import dataclass +from pathlib import PurePath +from textwrap import dedent +from typing import ( + Dict, + Final, + FrozenSet, + Iterator, + List, + Optional, + Sequence, + Set, + TextIO, + Tuple, + Type, +) + +import libcst as cst +from inflection import dasherize, underscore +from libcst.metadata import ExpressionContext +from typing_extensions import assert_never + +from docc.build import Builder +from docc.context import Context +from docc.discover import Discover, T +from docc.document import BlankNode, Document, ListNode, Node, Visit, Visitor +from docc.plugins.references import Definition, Reference +from docc.plugins.verbatim import Fragment, Pos, Verbatim +from docc.settings import PluginSettings +from docc.source import Source, TextSource +from docc.transform import Transform + +from . import nodes + +WHITESPACE: Tuple[Type[cst.CSTNode], ...] = ( + cst.TrailingWhitespace, + cst.EmptyLine, + cst.SimpleWhitespace, + cst.ParenthesizedWhitespace, + cst.Newline, +) +""" +libcst nodes that count as whitespace and should be ignored. +""" + + +class PythonDiscover(Discover): + """ + Find Python source files. + """ + + paths: Sequence[str] + """ + File system paths to search for Python files. + """ + + excluded_paths: Final[Sequence[PurePath]] + """ + File system paths to exclude from the search. Excluding a parent directory + excludes all children. + """ + + settings: PluginSettings + + def __init__(self, config: PluginSettings) -> None: + self.settings = config + + paths = config.get("paths", []) + if not isinstance(paths, Sequence): + raise TypeError("python paths must be a list") + + if any(not isinstance(path, str) for path in paths): + raise TypeError("every python path must be a string") + + if not paths: + raise ValueError("python needs at least one path") + + self.paths = [str(config.resolve_path(path)) for path in paths] + + excluded_paths = config.get("excluded_paths", []) + if not isinstance(excluded_paths, Sequence): + raise TypeError("python excluded paths must be a list") + + if any(not isinstance(path, str) for path in excluded_paths): + raise TypeError("every python path must be a string") + + self.excluded_paths = [PurePath(p) for p in excluded_paths] + + def discover(self, known: FrozenSet[T]) -> Iterator[Source]: + """ + Find sources. + """ + escaped = ((path, glob.escape(path)) for path in self.paths) + joined = ((r, os.path.join(pat, "**", "*.py")) for r, pat in escaped) + globbed = ((r, glob.glob(pat, recursive=True)) for r, pat in joined) + + for root_text, absolute_texts in globbed: + root_path = PurePath(root_text) + for absolute_text in absolute_texts: + absolute_path = PurePath(absolute_text) + relative_path = self.settings.unresolve_path(absolute_path) + + parents = relative_path.parents + if not any(p in parents for p in self.excluded_paths): + yield PythonSource(root_path, relative_path, absolute_path) + + +class PythonSource(TextSource): + """ + A Source representing a Python file. + """ + + root_path: Final[PurePath] + absolute_path: Final[PurePath] + _relative_path: Final[PurePath] + + def __init__( + self, + root_path: PurePath, + relative_path: PurePath, + absolute_path: PurePath, + ) -> None: + self.root_path = root_path + self._relative_path = relative_path + self.absolute_path = absolute_path + + @property + def relative_path(self) -> Optional[PurePath]: + """ + The relative path to the Source. + """ + return self._relative_path + + @property + def output_path(self) -> PurePath: + """ + Where to put the output derived from this source. + """ + return self._relative_path + + def open(self) -> TextIO: + """ + Open the source for reading. + """ + return open(self.absolute_path, "r") + + +class PythonBuilder(Builder): + """ + Convert python source files into syntax trees. + """ + + settings: PluginSettings + + def __init__(self, config: PluginSettings) -> None: + """ + Create a PythonBuilder with the given configuration. + """ + self.settings = config + + def build( + self, + unprocessed: Set[Source], + processed: Dict[Source, Document], + ) -> None: + """ + Consume unprocessed Sources and insert their Documents into processed. + """ + source_set = set(s for s in unprocessed if isinstance(s, PythonSource)) + unprocessed -= source_set + + all_modules = set() + + sources_by_root = defaultdict(set) + for source in source_set: + root = self.settings.resolve_path(source.root_path) + sources_by_root[root].add(source) + + for root, sources in sources_by_root.items(): + paths = set() + + for source in sources: + if source.relative_path is None: + continue + abs_path = self.settings.resolve_path(source.relative_path) + paths.add(str(abs_path)) + + repo_manager = cst.metadata.FullRepoManager( + repo_root_dir=str(root), + paths=list(paths), + providers=_CstVisitor.METADATA_DEPENDENCIES, + ) + + for source in sources: + assert source.relative_path + abs_path = self.settings.resolve_path(source.relative_path) + + visitor = _CstVisitor(all_modules, source) + repo_manager.get_metadata_wrapper_for_path( + str(abs_path) + ).visit(visitor) + assert visitor.root is not None + + document = Document( + visitor.root, + ) + + processed[source] = document + + +class CstNode(Node): + """ + A python concrete syntax tree node. + """ + + cst_node: cst.CSTNode + source: Source + _children: List[Node] + start: Pos + end: Pos + type: Optional[str] + global_scope: Optional[bool] + class_scope: Optional[bool] + expression_context: Optional[ExpressionContext] + names: Set[str] + all_modules: Set[str] # TODO: Make this a FrozenSet + + def __init__( + self, + all_modules: Set[str], + cst_node: cst.CSTNode, + source: Source, + start: Pos, + end: Pos, + type_: Optional[str], + global_scope: Optional[bool], + class_scope: Optional[bool], + expression_context: Optional[ExpressionContext], + names: Set[str], + children: List[Node], + ) -> None: + self.all_modules = all_modules + self.cst_node = cst_node + self.source = source + self._children = children + self.start = start + self.end = end + self.type = type_ + self.global_scope = global_scope + self.class_scope = class_scope + self.expression_context = expression_context + self.names = names + + @property + def children(self) -> Sequence[Node]: + """ + Child nodes belonging to this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + self._children = [new if c == old else c for c in self.children] + + def find_child(self, cst_node: cst.CSTNode) -> Node: + """ + Given a libcst node, find the CstNode in the same position. + """ + index = self.cst_node.children.index(cst_node) + return self.children[index] + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + cst_node = self.cst_node + text = f"{self.__class__.__name__}({cst_node.__class__.__name__}(...)" + text += f", start={self.start}" + text += f", end={self.end}" + if self.type is not None: + text += f", type={self.type!r}" + if self.names: + text += f", names={self.names!r}" + return text + ")" + + +class _CstVisitor(cst.CSTVisitor): + METADATA_DEPENDENCIES = ( + cst.metadata.PositionProvider, + cst.metadata.FullyQualifiedNameProvider, + # cst.metadata.TypeInferenceProvider, + cst.metadata.ScopeProvider, + cst.metadata.ExpressionContextProvider, + ) + + stack: Final[List[CstNode]] + source: Final[Source] + all_modules: Final[Set[str]] + root: Optional[CstNode] + + def __init__(self, all_modules: Set[str], source: Source) -> None: + super().__init__() + self.stack = [] + self.root = None + self.source = source + self.all_modules = all_modules + + def on_visit(self, node: cst.CSTNode) -> bool: + try: + position = self.get_metadata(cst.metadata.PositionProvider, node) + except KeyError: + return True + + type_ = None # self.get_metadata( + # cst.metadata.TypeInferenceProvider, node, None + # ) + + scope = self.get_metadata(cst.metadata.ScopeProvider, node, None) + global_scope = isinstance(scope, cst.metadata.GlobalScope) + class_scope = isinstance(scope, cst.metadata.ClassScope) + + expression_context = self.get_metadata( + cst.metadata.ExpressionContextProvider, node, None + ) + + qualified_names = self.get_metadata( + cst.metadata.FullyQualifiedNameProvider, node, None + ) + names: Set[str] = set() + if qualified_names: + names = set(n.name for n in qualified_names) + + start = Pos( + line=position.start.line, + column=position.start.column, + ) + end = Pos( + line=position.end.line, + column=position.end.column, + ) + new = CstNode( + self.all_modules, + node, + self.source, + start, + end, + type_, + global_scope, + class_scope, + expression_context, + names, + [], + ) + + if self.stack: + self.stack[-1]._children.append(new) + else: + assert self.root is None + + if self.root is None: + self.root = new + + if isinstance(node, cst.Module): + self.all_modules.update(names) + + self.stack.append(new) + return True + + def on_leave(self, original_node: cst.CSTNode) -> None: + try: + self.get_metadata(cst.metadata.PositionProvider, original_node) + except KeyError: + return + self.stack.pop() + + +class PythonTransform(Transform): + """ + Transforms CstNode instances into Python language nodes. + """ + + excluded_references: Final[FrozenSet[str]] + + def __init__(self, config: PluginSettings) -> None: + """ + Create a Transform with the given configuration. + """ + self.excluded_references = frozenset( + config.get("excluded_references", []) + ) + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + document = context[Document] + + document.root.visit( + _AnnotationReferenceTransformVisitor(self.excluded_references) + ) + + visitor = _ReplaceVisitor(context) + document.root.visit(visitor) + + document.root.visit(_AnnotationTransformVisitor()) + document.root.visit(_NameTransformVisitor()) + + +class _ReplaceVisitor(Visitor): + context: Final[Context] + parents: Final[List[Node]] + + def __init__(self, context: Context) -> None: + super().__init__() + self.parents = [] + self.context = context + + def _replace_child(self, old: Node, new: Node) -> None: + if self.parents: + self.parents[-1].replace_child(old, new) + else: + document = self.context[Document] + assert document.root == old + document.root = new + + def enter(self, node: Node) -> Visit: + if not isinstance(node, CstNode): + self.parents.append(node) + return Visit.TraverseChildren + + transformer = _TransformVisitor(self.context) + + node.visit(transformer) + assert transformer.root is not None + + self._replace_child(node, transformer.root) + return Visit.SkipChildren + + def exit(self, node: Node) -> None: + if not isinstance(node, CstNode): + popped = self.parents.pop() + assert popped == node + + +@dataclass +class _TransformContext: + node: "CstNode" + child_offset: int = 0 + + +class _TransformVisitor(Visitor): + root: Optional[Node] + old_stack: Final[List[_TransformContext]] + new_stack: Final[List[Node]] + context: Final[Context] + document: Final[Document] + + def __init__(self, context: Context) -> None: + self.root = None + self.old_stack = [] + self.new_stack = [] + self.document = context[Document] + self.context = context + + def push_new(self, node: Node) -> None: + if self.root is None: + assert 0 == len(self.new_stack) + self.root = node + self.new_stack.append(node) + + def enter_module(self, node: CstNode, cst_node: cst.Module) -> Visit: + assert 0 == len(self.new_stack) + module = nodes.Module() + + names = sorted(node.names) + + if names: + module.name = nodes.Name(names[0], names[0]) + + maybe_definition: Node = module + for name in names: + maybe_definition = Definition( + identifier=name, child=maybe_definition + ) + self.push_new(maybe_definition) + + self.push_new(module) + + docstring = cst_node.get_docstring(True) + if docstring is not None: + module.docstring = nodes.Docstring(docstring) + + return Visit.TraverseChildren + + def exit_module(self, node: CstNode) -> None: + self.new_stack.pop() + for _name in node.names: + self.new_stack.pop() + + def enter_class_def(self, node: CstNode, cst_node: cst.ClassDef) -> Visit: + assert 0 < len(self.new_stack) + + class_def = nodes.Class() + self.push_new(class_def) + + docstring = cst_node.get_docstring(True) + if docstring is not None: + class_def.docstring = nodes.Docstring(docstring) + + class_def.name = node.find_child(cst_node.name) + + source = node.source + if isinstance(source, TextSource): + assert isinstance(class_def.decorators, ListNode) + decorators = class_def.decorators.children + for cst_decorator in cst_node.decorators: + decorator = node.find_child(cst_decorator) + + # Trim whitespace from each decorator. + if isinstance(decorator, CstNode): + decorator = decorator.find_child(cst_decorator.decorator) + + decorators.append(_VerbatimTransform.apply(source, decorator)) + + maybe_definition: Node = class_def + for name in node.names: + maybe_definition = Definition( + identifier=name, child=maybe_definition + ) + + if 1 < len(self.new_stack): + parent = self.new_stack[-2] + members = getattr(parent, "members", None) + if isinstance(members, ListNode): + members.children.append(maybe_definition) + + body = node.find_child(cst_node.body) + assert isinstance(body, CstNode) + + class_context = _TransformContext(node=node) + body_context = _TransformContext(node=body) + + self.old_stack.append(class_context) + self.old_stack.append(body_context) + + for index, cst_statement in enumerate(cst_node.body.body): + self.old_stack[-1].child_offset = index + if isinstance(cst_statement, cst.SimpleStatementLine): + if len(cst_statement.body) == 1: + cst_first = cst_statement.body[0] + if isinstance(cst_first, cst.Expr): + if isinstance(cst_first.value, cst.SimpleString): + continue + statement = body.find_child(cst_statement) + statement.visit(self) + + popped = self.old_stack.pop() + assert popped == body_context + + popped = self.old_stack.pop() + assert popped == class_context + + # TODO: base classes + # TODO: metaclass + + return Visit.SkipChildren + + def exit_class_def(self) -> None: + self.new_stack.pop() + + def enter_function_def( + self, node: CstNode, cst_node: cst.FunctionDef + ) -> Visit: + assert 0 < len(self.new_stack) + + parameters = [] + function_def = nodes.Function( + asynchronous=cst_node.asynchronous is not None, + parameters=ListNode(parameters), + ) + self.push_new(function_def) + + docstring = cst_node.get_docstring(True) + if docstring is not None: + function_def.docstring = nodes.Docstring(docstring) + + function_def.name = node.find_child(cst_node.name) + + if cst_node.returns is not None: + function_def.return_type = node.find_child(cst_node.returns) + + source = node.source + if isinstance(source, TextSource): + body = node.find_child(cst_node.body) + function_def.body = _VerbatimTransform.apply(source, body) + + assert isinstance(function_def.decorators, ListNode) + decorators = function_def.decorators.children + for cst_decorator in cst_node.decorators: + decorator = node.find_child(cst_decorator) + + # Trim whitespace from each decorator. + if isinstance(decorator, CstNode): + decorator = decorator.find_child(cst_decorator.decorator) + + decorators.append(_VerbatimTransform.apply(source, decorator)) + + maybe_definition: Node = function_def + for name in node.names: + maybe_definition = Definition( + identifier=name, child=maybe_definition + ) + + if 1 < len(self.new_stack): + parent = self.new_stack[-2] + members = getattr(parent, "members", None) + if isinstance(members, ListNode): + members.children.append(maybe_definition) + + for param in node.find_child(cst_node.params).children: + if not isinstance(param, CstNode): + parameters.append(param) + continue + + parameter = nodes.Parameter() + parameters.append(parameter) + + cst_param = param.cst_node + if isinstance(cst_param, cst.Param): + parameter.name = param.find_child(cst_param.name) + if cst_param.star == "*": + parameter.star = "*" + elif cst_param.star == "**": + parameter.star = "**" + + if cst_param.annotation is not None: + parameter.type_annotation = param.find_child( + cst_param.annotation + ) + + if cst_param.default is not None: + # TODO: parameter default + logging.warning("parameter default values not implemented") + elif isinstance(cst_param, cst.ParamSlash): + parameter.name = nodes.Name("/") + elif isinstance(cst_param, cst.ParamStar): + parameter.name = nodes.Name("*") + else: + raise NotImplementedError(f"parameter type `{param}`") + + return Visit.SkipChildren + + def exit_function_def(self) -> None: + self.new_stack.pop() + + def _assign_docstring(self) -> Optional[nodes.Docstring]: + parent_context = self.old_stack[-1] + parent = parent_context.node + if not isinstance(parent, CstNode): + return None + + cst_parent = parent.cst_node + if not isinstance(cst_parent, cst.SimpleStatementLine): + return None + + try: + line_parent_context = self.old_stack[-2] + if isinstance(line_parent_context.node.cst_node, cst.Module): + sibling_index = line_parent_context.child_offset + 1 + else: + sibling_index = line_parent_context.child_offset + 2 + sibling = line_parent_context.node.children[sibling_index] + except IndexError: + return None + + if not isinstance(sibling, CstNode): + return None + + cst_sibling = sibling.cst_node + if not isinstance(cst_sibling, cst.SimpleStatementLine): + return None + + if len(cst_sibling.body) != 1: + return None + + cst_body = cst_sibling.body[0] + + if not isinstance(cst_body, cst.Expr): + return None + + cst_value = cst_body.value + + if not isinstance(cst_value, cst.SimpleString): + return None + + value = cst_value.evaluated_value + if isinstance(value, str): + text = value + elif isinstance(value, bytes): + text = value.decode(encoding="utf-8", errors="strict") + else: + assert_never(value) + raise AssertionError() + + text = dedent(text) + return nodes.Docstring(text=text) + + def _enter_assignment( + self, + node: CstNode, + targets: Sequence[Node], + value: Optional[Node], + ) -> Visit: + if not node.global_scope and not node.class_scope: + return Visit.SkipChildren + + if not self.new_stack: + return Visit.SkipChildren + + parent = self.new_stack[-1] + members = getattr(parent, "members", None) + if not isinstance(members, ListNode): + return Visit.SkipChildren + + names: List[Node] = [] + attribute = nodes.Attribute(names=ListNode(names)) + + docstring = self._assign_docstring() + if docstring: + attribute.docstring = docstring + + for target in targets: + names.append(deepcopy(target)) + + source = node.source + if isinstance(source, TextSource): + attribute.body = _VerbatimTransform.apply(source, node) + + maybe_definition = attribute + for name_node in names: + if not isinstance(name_node, CstNode): + continue + + for name in name_node.names: + maybe_definition = Definition( + identifier=name, child=maybe_definition + ) + + members.children.append(maybe_definition) + return Visit.SkipChildren + + def enter_ann_assign( + self, node: CstNode, cst_node: cst.AnnAssign + ) -> Visit: + value = None + if cst_node.value is not None: + value = node.find_child(cst_node.value) + return self._enter_assignment( + node, + [node.find_child(cst_node.target)], + value, + ) + + def exit_ann_assign(self) -> None: + pass + + def enter_assign(self, node: CstNode, cst_node: cst.Assign) -> Visit: + targets = [] + for cst_target in cst_node.targets: + target = node.find_child(cst_target) + assert isinstance(target, CstNode) # TODO: Assumes only CstNodes. + targets.append(target.find_child(cst_target.target)) + + return self._enter_assignment( + node, + targets, + node.find_child(cst_node.value), + ) + + def exit_assign(self) -> None: + pass + + def enter(self, node: Node) -> Visit: + if not isinstance(node, CstNode): + raise ValueError( + "expected `" + + CstNode.__name__ + + "` but got `" + + node.__class__.__name__ + + "`" + ) + + cst_node = node.cst_node + try: + parent = self.old_stack[-1].node.cst_node + except IndexError: + parent = None + + module_member = isinstance(parent, cst.Module) + + visit: Visit + + if isinstance(cst_node, cst.Module): + visit = self.enter_module(node, cst_node) + elif isinstance(cst_node, cst.ClassDef): + visit = self.enter_class_def(node, cst_node) + elif isinstance(cst_node, cst.FunctionDef): + visit = self.enter_function_def(node, cst_node) + elif isinstance(cst_node, cst.AnnAssign): + visit = self.enter_ann_assign(node, cst_node) + elif isinstance(cst_node, cst.Assign): + visit = self.enter_assign(node, cst_node) + elif isinstance(cst_node, cst.SimpleStatementLine): + visit = Visit.TraverseChildren + elif isinstance(cst_node, cst.Expr): + visit = Visit.SkipChildren + elif isinstance(cst_node, (cst.Import, cst.ImportFrom)): + visit = Visit.SkipChildren + elif isinstance(cst_node, cst.Pass): + visit = Visit.SkipChildren + elif isinstance(cst_node, WHITESPACE): + visit = Visit.TraverseChildren + elif isinstance(cst_node, cst.Comment): + visit = Visit.SkipChildren + elif module_member and isinstance(cst_node, cst.CSTNode): + logging.debug("skipping module member node %s", node) + visit = Visit.SkipChildren + else: + raise Exception(f"unknown node type {node}") + + self.old_stack.append(_TransformContext(node=node)) + + return visit + + def exit(self, node: Node) -> None: + module_member = False + + self.old_stack.pop() + if self.old_stack: + self.old_stack[-1].child_offset += 1 + parent = self.old_stack[-1].node.cst_node + module_member = isinstance(parent, cst.Module) + + assert isinstance(node, CstNode) + cst_node = node.cst_node + + if isinstance(cst_node, cst.Module): + self.exit_module(node) + elif isinstance(cst_node, cst.ClassDef): + self.exit_class_def() + elif isinstance(cst_node, cst.FunctionDef): + self.exit_function_def() + elif isinstance(cst_node, cst.AnnAssign): + self.exit_ann_assign() + elif isinstance(cst_node, cst.Assign): + self.exit_assign() + elif isinstance(cst_node, cst.SimpleStatementLine): + pass + elif isinstance(cst_node, cst.Expr): + pass + elif isinstance(cst_node, (cst.ImportFrom, cst.Import)): + pass + elif isinstance(cst_node, cst.Pass): + pass + elif isinstance(cst_node, WHITESPACE): + pass + elif isinstance(cst_node, cst.Comment): + pass + elif module_member and isinstance(cst_node, cst.CSTNode): + pass + else: + raise Exception(f"unknown node type {cst_node}") + + +class _AnnotationReferenceTransformVisitor(Visitor): + root: Optional[Node] + stack: Final[List[Node]] + excluded_references: Final[FrozenSet[str]] + + def __init__(self, excluded_references: FrozenSet[str]) -> None: + self.stack = [] + self.root = None + self.excluded_references = excluded_references + + def enter(self, node: Node) -> Visit: + if self.root is None: + assert not self.stack + self.root = node + + self.stack.append(node) + + if not isinstance(node, CstNode): + return Visit.TraverseChildren + + cst_node = node.cst_node + + if isinstance(cst_node, cst.Attribute): + # TODO: Need to figure out how to detect if a method is defined in + # a superclass (ex. `Foo.__name__`.) + pass + elif isinstance(cst_node, (cst.Name, cst.SimpleString)): + if node.expression_context != ExpressionContext.STORE: + self._make_reference(node) + + return Visit.TraverseChildren + + def _in_module(self, name: str, module: str) -> bool: + module_parts = module.split(".") + name_parts = name.split(".") + + try: + return name_parts[: len(module_parts)] == module_parts + except IndexError: + return False + + def _make_reference(self, node: CstNode) -> None: + for name in node.names: + if name in self.excluded_references: + continue + + if any(self._in_module(name, m) for m in node.all_modules): + reference = Reference( + identifier=name, + child=node, + ) + if len(self.stack) > 1: + self.stack[-2].replace_child(node, reference) + else: + self.root = reference + + def exit(self, node: Node) -> None: + self.stack.pop() + + +class _TypeVisitor(Visitor): + root: Final[Node] + type: Final[nodes.Type] + + def __init__(self, root: Node, type_: nodes.Type) -> None: + self.root = root + self.type = type_ + + def enter(self, node: Node) -> Visit: + if not isinstance(node, CstNode): + return Visit.TraverseChildren + + cst_node = node.cst_node + if isinstance(cst_node, cst.Name): + self.type.child = self.root + elif isinstance(cst_node, cst.SimpleString): + self.type.child = self.root + elif isinstance(cst_node, cst.Ellipsis): + # TODO: check this. + self.type.child = nodes.Name(name="...") + elif isinstance(cst_node, cst.Attribute): + if not node.names: + raise NotImplementedError("attributes without full names") + names = sorted(node.names) + # TODO: While accurate, this doesn't match the exact text from the + # source file. + self.type.child = nodes.Name(names[0], names[0]) + elif isinstance(cst_node, cst.Subscript): + arguments = [] + generics = nodes.List(elements=ListNode(arguments)) + type_ = nodes.Type() + + self.type.child = nodes.Subscript(name=type_, generics=generics) + + value = node.find_child(cst_node.value) + value.visit(_TypeVisitor(value, type_)) + + for cst_element in cst_node.slice: + # TODO: This traversal assumes no new nodes were added between + # the Subscript and the Index's contents. + assert isinstance(cst_element, cst.SubscriptElement) + element = node.find_child(cst_element) + + cst_index = cst_element.slice + assert isinstance(cst_index, cst.Index) + + assert isinstance(element, CstNode) + index = element.find_child(cst_index) + generic = nodes.Type() + + cst_index_value = cst_index.value + assert cst_index.star is None + assert isinstance(index, CstNode) + index_value = index.find_child(cst_index_value) + + index_value.visit(_TypeVisitor(index_value, generic)) + + arguments.append(generic) + elif isinstance(cst_node, (cst.List, cst.Tuple)): + # For example: Callable[[], None], Tuple[()] + subscript = nodes.Subscript() + self.type.child = subscript + elements = [] + + # TODO: This is a bit of a hack, since the argument list of a + # Callable isn't a type of its own. + + if isinstance(cst_node, cst.List): + subscript.generics = nodes.List(elements=ListNode(elements)) + elif isinstance(cst_node, cst.Tuple): + subscript.generics = nodes.Tuple(elements=ListNode(elements)) + else: + raise NotImplementedError() + + for cst_element in cst_node.elements: + # TODO: This traversal assumes no new nodes were added between + # the Subscript and the Index's contents. + assert isinstance(cst_element, cst.Element) + element = node.find_child(cst_element) + + cst_value = cst_element.value + assert isinstance(element, CstNode) + value = element.find_child(cst_value) + + type_ = nodes.Type() + value.visit(_TypeVisitor(value, type_)) + elements.append(type_) + elif isinstance(cst_node, cst.BinaryOperation): + assert isinstance(node, CstNode) + self._binary_operation(node, cst_node) + else: + raise Exception(str(node) + str(cst_node)) + + return Visit.SkipChildren + + def exit(self, node: Node) -> None: + pass + + def _binary_operation( + self, node: CstNode, cst_node: cst.BinaryOperation + ) -> None: + if not isinstance(cst_node.operator, cst.BitOr): + raise NotImplementedError(f"binary operation {cst_node.operator}") + + left_node = nodes.Type() + right_node = nodes.Type() + + self.type.child = nodes.BinaryOperation( + left=left_node, right=right_node, operator=nodes.BitOr() + ) + + left_child = node.find_child(cst_node.left) + left_child.visit(_TypeVisitor(left_child, left_node)) + + right_child = node.find_child(cst_node.right) + right_child.visit(_TypeVisitor(right_child, right_node)) + + +class _AnnotationTransformVisitor(Visitor): + stack: Final[List[Node]] + + def __init__(self) -> None: + self.stack = [] + + def enter(self, node: Node) -> Visit: + self.stack.append(node) + + if not isinstance(node, CstNode): + return Visit.TraverseChildren + + cst_node = node.cst_node + + if not isinstance(cst_node, cst.Annotation): + return Visit.TraverseChildren + + type_ = nodes.Type() + + self.stack[-2].replace_child(node, type_) + self.stack[-1] = type_ + + annotation = node.find_child(cst_node.annotation) + + annotation.visit(_TypeVisitor(annotation, type_)) + + return Visit.SkipChildren + + def exit(self, node: Node) -> None: + self.stack.pop() + + +class _NameTransformVisitor(Visitor): + stack: Final[List[Node]] + + def __init__(self) -> None: + self.stack = [] + + def enter(self, node: Node) -> Visit: + new_node = node + if isinstance(node, CstNode): + cst_node = node.cst_node + if isinstance(cst_node, (cst.Name, cst.SimpleString)): + names = sorted(node.names) + try: + full_name = names[0] + except IndexError: + full_name = None + + new_node = nodes.Name(cst_node.value, full_name) + elif isinstance(cst_node, cst.Attribute): + new_node = nodes.Access( + value=node.find_child(cst_node.value), + attribute=node.find_child(cst_node.attr), + ) + + if new_node != node: + self.stack[-1].replace_child(node, new_node) + + self.stack.append(new_node) + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + self.stack.pop() + + +class _VerbatimTransform(Visitor): + root: Optional[Node] + stack: Final[List[Node]] + + @staticmethod + def apply(source: TextSource, node: Node) -> Node: + transform = _VerbatimTransform() + + node.visit(transform) + + verbatim = Verbatim(source) + assert transform.root is not None + verbatim.append(transform.root) + return verbatim + + def __init__(self) -> None: + self.stack = [] + self.root = None + + def enter(self, node: Node) -> Visit: + if self.root is None: + assert 0 == len(self.stack) + self.root = node + else: + assert 0 < len(self.stack) + + self.stack.append(node) + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + popped = self.stack.pop() + assert popped == node + + if not isinstance(node, CstNode): + return + + if isinstance(node.cst_node, WHITESPACE): + new = BlankNode() + else: + name = dasherize(underscore(node.cst_node.__class__.__name__)) + + new = Fragment( + start=node.start, + end=node.end, + highlights=[name], + ) + + for child in node.children: + new.append(child) + + if self.stack: + self.stack[-1].replace_child(node, new) + else: + assert self.root == node + self.root = new diff --git a/vendor/docc/docc/plugins/python/html.py b/vendor/docc/docc/plugins/python/html.py new file mode 100644 index 0000000000..bdc1a949d1 --- /dev/null +++ b/vendor/docc/docc/plugins/python/html.py @@ -0,0 +1,212 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Rendering functions to transform Python nodes into HTML. +""" + +from docc.context import Context +from docc.document import Node +from docc.plugins.html import ( + HTMLRoot, + HTMLTag, + RenderResult, + TextNode, + render_template, +) + +from . import nodes + + +def _render_template( + context: object, parent: object, template_name: str, node: Node +) -> RenderResult: + assert isinstance(context, Context) + assert isinstance(parent, (HTMLTag, HTMLRoot)) + return render_template( + "docc.plugins.python", context, parent, template_name, node + ) + + +def render_module( + context: object, + parent: object, + module: object, +) -> RenderResult: + """ + Render a python Module as HTML. + """ + assert isinstance(module, nodes.Module) + return _render_template(context, parent, "html/module.html", module) + + +def render_class( + context: object, + parent: object, + class_: object, +) -> RenderResult: + """ + Render a python Class as HTML. + """ + assert isinstance(class_, nodes.Class) + return _render_template(context, parent, "html/class.html", class_) + + +def render_attribute( + context: object, + parent: object, + attribute: object, +) -> RenderResult: + """ + Render a python assignment as HTML. + """ + assert isinstance(attribute, nodes.Attribute) + return _render_template(context, parent, "html/attribute.html", attribute) + + +def render_function( + context: object, + parent: object, + function: object, +) -> RenderResult: + """ + Render a python Function as HTML. + """ + assert isinstance(function, nodes.Function) + return _render_template(context, parent, "html/function.html", function) + + +def render_access( + context: object, + parent: object, + access: object, +) -> RenderResult: + """ + Render a python Access as HTML. + """ + assert isinstance(access, nodes.Access) + return _render_template(context, parent, "html/access.html", access) + + +def render_name( + context: object, + parent: object, + name: object, +) -> RenderResult: + """ + Render a python Name as HTML. + """ + assert isinstance(name, nodes.Name) + return _render_template(context, parent, "html/name.html", name) + + +def render_type( + context: object, + parent: object, + type_: object, +) -> RenderResult: + """ + Render a python Type as HTML. + """ + assert isinstance(type_, nodes.Type) + return _render_template(context, parent, "html/type.html", type_) + + +def render_list( + context: object, + parent: object, + list_: object, +) -> RenderResult: + """ + Render a python List as HTML. + """ + assert isinstance(list_, nodes.List) + return _render_template(context, parent, "html/list.html", list_) + + +def render_tuple( + context: object, + parent: object, + tuple_: object, +) -> RenderResult: + """ + Render a python List as HTML. + """ + assert isinstance(tuple_, nodes.Tuple) + return _render_template(context, parent, "html/tuple.html", tuple_) + + +def render_docstring( + context: object, + parent: object, + docstring: object, +) -> RenderResult: + """ + Render a python Docstring as HTML. + """ + assert isinstance(docstring, nodes.Docstring) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + parent.append(TextNode(docstring.text)) + return None + + +def render_parameter( + context: object, + parent: object, + parameter: object, +) -> RenderResult: + """ + Render a python Parameter as HTML. + """ + assert isinstance(parameter, nodes.Parameter) + return _render_template(context, parent, "html/parameter.html", parameter) + + +def render_subscript( + context: object, + parent: object, + subscript: object, +) -> RenderResult: + """ + Render a python Subscript as HTML. + """ + assert isinstance(subscript, nodes.Subscript) + return _render_template(context, parent, "html/subscript.html", subscript) + + +def render_binary_operation( + context: object, + parent: object, + binary: object, +) -> RenderResult: + """ + Render a python BinaryOperation as HTML. + """ + assert isinstance(binary, nodes.BinaryOperation) + return _render_template( + context, parent, "html/binary_operation.html", binary + ) + + +def render_bit_or( + context: object, + parent: object, + bit_or: object, +) -> RenderResult: + """ + Render a python BitOr as HTML. + """ + assert isinstance(bit_or, nodes.BitOr) + return _render_template(context, parent, "html/bit_or.html", bit_or) diff --git a/vendor/docc/docc/plugins/python/nodes.py b/vendor/docc/docc/plugins/python/nodes.py new file mode 100644 index 0000000000..e92c923c7d --- /dev/null +++ b/vendor/docc/docc/plugins/python/nodes.py @@ -0,0 +1,320 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Python language support for docc. +""" + +import dataclasses +import typing +from dataclasses import dataclass, fields +from typing import Iterable, Literal, Optional, Sequence, Union + +from docc.document import BlankNode, ListNode, Node, Visit, Visitor +from docc.plugins.search import Content, Searchable + + +class PythonNode(Node): + """ + Base implementation of Node operations for Python nodes. + """ + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + for field in fields(self): + value = getattr(self, field.name) + + if field.type == Node: + if not isinstance(value, field.type): + raise TypeError("child not Node") + yield value + else: + # Not a child, so just ignore it. + pass + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + for field in fields(self): + value = getattr(self, field.name) + if value == old: + assert isinstance(new, field.type) + setattr(self, field.name, new) + continue + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return self.__class__.__name__ + "(...)" + + +@dataclass(repr=False) +class Module(PythonNode, Searchable): + """ + A Python module. + """ + + name: Node = dataclasses.field(default_factory=BlankNode) + docstring: Node = dataclasses.field(default_factory=BlankNode) + members: Node = dataclasses.field(default_factory=ListNode) + + def to_search(self) -> Content: + """ + Extract the searchable fields from this node. + """ + return { + "type": "module", + "name": _NameVisitor.collect(self.name), + } + + +@dataclass(repr=False) +class Class(PythonNode, Searchable): + """ + A class declaration. + """ + + decorators: Node = dataclasses.field(default_factory=ListNode) + name: Node = dataclasses.field(default_factory=BlankNode) + bases: Node = dataclasses.field(default_factory=ListNode) + metaclass: Node = dataclasses.field(default_factory=BlankNode) + docstring: Node = dataclasses.field(default_factory=BlankNode) + members: Node = dataclasses.field(default_factory=ListNode) + + def to_search(self) -> Content: + """ + Extract the searchable fields from this node. + """ + return { + "type": "class", + "name": _NameVisitor.collect(self.name), + } + + +@dataclass(repr=False) +class Function(PythonNode, Searchable): + """ + A function definition. + """ + + asynchronous: bool + decorators: Node = dataclasses.field(default_factory=ListNode) + name: Node = dataclasses.field(default_factory=BlankNode) + parameters: Node = dataclasses.field(default_factory=ListNode) + return_type: Node = dataclasses.field(default_factory=BlankNode) + docstring: Node = dataclasses.field(default_factory=BlankNode) + body: Node = dataclasses.field(default_factory=BlankNode) + + def to_search(self) -> Content: + """ + Extract the searchable fields from this node. + """ + return { + "type": "function", + "name": _NameVisitor.collect(self.name), + } + + +@dataclass(repr=False) +class Type(PythonNode): + """ + A type, usually used in a PEP 484 annotation. + """ + + child: Node = dataclasses.field(default_factory=BlankNode) + + +@dataclass(repr=False) +class Subscript(PythonNode): + """ + A subscript expression, of the form `name[...]`. + """ + + name: Node = dataclasses.field(default_factory=BlankNode) + generics: Node = dataclasses.field(default_factory=BlankNode) + + +@dataclass(repr=False) +class BinaryOperation(PythonNode): + """ + An operation with two inputs. + """ + + left: Node = dataclasses.field(default_factory=BlankNode) + operator: Node = dataclasses.field(default_factory=BlankNode) + right: Node = dataclasses.field(default_factory=BlankNode) + + +@dataclass(repr=False) +class BitOr(PythonNode): + """ + A bitwise or operation. + """ + + +@dataclass(repr=False) +class List(PythonNode): + """ + Square brackets wrapping a list of elements, usually separated by commas. + """ + + elements: Node = dataclasses.field(default_factory=ListNode) + + +@dataclass(repr=False) +class Tuple(PythonNode): + """ + Parentheses wrapping a list of elements, usually separated by commas. + """ + + elements: Node = dataclasses.field(default_factory=ListNode) + + +@dataclass(repr=False) +class Parameter(PythonNode): + """ + A parameter descriptor in a function definition. + """ + + star: Optional[Union[Literal["*"], Literal["**"]]] = None + name: Node = dataclasses.field(default_factory=BlankNode) + type_annotation: Node = dataclasses.field(default_factory=BlankNode) + default_value: Node = dataclasses.field(default_factory=BlankNode) + + +@dataclass(repr=False) +class Attribute(PythonNode, Searchable): + """ + An assignment. + """ + + names: Node = dataclasses.field(default_factory=ListNode) + body: Node = dataclasses.field(default_factory=BlankNode) + docstring: Node = dataclasses.field(default_factory=BlankNode) + + def to_search(self) -> Content: + """ + Extract the searchable fields from this node. + """ + return { + "type": "attribute", + "name": _NameVisitor.collect(self.names), + } + + +@dataclass +class Name(Node): + """ + The name of a class, function, variable, or other Python member. + """ + + name: str + full_name: Optional[str] = dataclasses.field(default=None) + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return tuple() + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError() + + +@dataclass(repr=False) +class Access(PythonNode): + """ + One level of attribute access. + + The example `foo.bar.baz` should be represented as: + + ```python + Access( + value=Access( + value=Name(name="foo"), + attribute=Name(name="bar"), + ), + attribute=Name(name="baz"), + ) + ``` + """ + + value: Node = dataclasses.field(default_factory=BlankNode) + attribute: Node = dataclasses.field(default_factory=BlankNode) + + +@dataclass +class Docstring(Node, Searchable): + """ + Node representing a documentation string. + """ + + text: str + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return tuple() + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError() + + def to_search(self) -> Content: + """ + Extract the searchable fields from this node. + """ + return self.text + + +class _NameVisitor(Visitor): + names: typing.List[str] + + @staticmethod + def collect(nodes: Union[Node, Sequence[Node]]) -> typing.List[str]: + if isinstance(nodes, Node): + nodes = [nodes] + + visitor = _NameVisitor() + + for node in nodes: + node.visit(visitor) + + return visitor.names + + def __init__(self) -> None: + self.names = [] + + def enter(self, node: Node) -> Visit: + if isinstance(node, Name): + self.names.append(node.name) + return Visit.SkipChildren + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + pass diff --git a/vendor/docc/docc/plugins/python/templates/html/access.html b/vendor/docc/docc/plugins/python/templates/html/access.html new file mode 100644 index 0000000000..bf90b7c543 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/access.html @@ -0,0 +1,17 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +{% if node.value %}{{ node.value|html }}.{% endif %}{{ node.attribute|html }} diff --git a/vendor/docc/docc/plugins/python/templates/html/attribute.html b/vendor/docc/docc/plugins/python/templates/html/attribute.html new file mode 100644 index 0000000000..e0650413d0 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/attribute.html @@ -0,0 +1,30 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +
+

+ {% for name in node.names %} + {{ name|html -}} + {%- if not loop.last %}, {% endif %} + {% endfor %} +

+ {% if node.docstring %} +
+ {{ node.docstring|html }} +
+ {% endif %} + {{- node.body|html -}} +
diff --git a/vendor/docc/docc/plugins/python/templates/html/binary_operation.html b/vendor/docc/docc/plugins/python/templates/html/binary_operation.html new file mode 100644 index 0000000000..59d385941c --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/binary_operation.html @@ -0,0 +1,17 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +{{- node.left|html }} {{ node.operator|html }} {{ node.right|html -}} diff --git a/vendor/docc/docc/plugins/python/templates/html/bit_or.html b/vendor/docc/docc/plugins/python/templates/html/bit_or.html new file mode 100644 index 0000000000..098eefcc62 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/bit_or.html @@ -0,0 +1,17 @@ +{# + # Copyright (C) 2024 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +| diff --git a/vendor/docc/docc/plugins/python/templates/html/class.html b/vendor/docc/docc/plugins/python/templates/html/class.html new file mode 100644 index 0000000000..e1f9aac7ef --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/class.html @@ -0,0 +1,68 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +
+

{{ node.name|html }}

+ + {% if node.docstring %} +
+ {{ node.docstring|html }} +
+ {% endif %} + + {% for decorator in node.decorators %}{{ decorator|html }}{% endfor %} +
class {{ node.name|html }}{%- if node.bases -%}
+    (
+    {%- for base in node.bases -%}
+    {{- base|html -}}
+    {%- if not loop.last -%}, {% endif -%}
+    {%- endfor -%}
+    )
+    {%- endif %}:
+ + {% with %} + {% set attributes = node|find("docc.plugins.python.nodes:Attribute") %} + {% set functions = node|find("docc.plugins.python.nodes:Function") %} + {% if attributes or functions %} + + {% endif %} + {% endwith %} + + {% for member in node.members %} + {{ member|html }} + {% endfor %} +
diff --git a/vendor/docc/docc/plugins/python/templates/html/function.html b/vendor/docc/docc/plugins/python/templates/html/function.html new file mode 100644 index 0000000000..ccd9a3e81e --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/function.html @@ -0,0 +1,35 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +
+

{{ node.name|html }}

+ {% if node.docstring %} +
+ {{ node.docstring|html }} +
+ {% endif %} +
+ {% for decorator in node.decorators %}{{ decorator|html }}{% endfor %} +
+ + + {%- if node.asynchronous %}async {% endif -%} + def {{ node.name|html }}({% for arg in node.parameters %}​{{ arg|html }}{% if not loop.last %}, {% endif %}​{% endfor %}){% if node.return_type %} -> {{ node.return_type|html }}{% endif %}: + + {{- node.body|html -}} +
+
+
diff --git a/vendor/docc/docc/plugins/python/templates/html/list.html b/vendor/docc/docc/plugins/python/templates/html/list.html new file mode 100644 index 0000000000..8701584241 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/list.html @@ -0,0 +1,22 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +[ +{%- for element in node.elements -%} +{{- element|html -}} +{%- if not loop.last %}, {% endif -%} +{%- endfor -%} +] diff --git a/vendor/docc/docc/plugins/python/templates/html/module.html b/vendor/docc/docc/plugins/python/templates/html/module.html new file mode 100644 index 0000000000..f430ec066e --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/module.html @@ -0,0 +1,103 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +
+
+
+ {% if node.name %} +

{{ node.name|html }}

+ {% endif %} + + {% if node.docstring %} +
+ {{ node.docstring|html }} +
+ {% endif %} + + + {% for member in node.members %} + {{ member|html }} + {% endfor %} +
+
+ + +
diff --git a/vendor/docc/docc/plugins/python/templates/html/name.html b/vendor/docc/docc/plugins/python/templates/html/name.html new file mode 100644 index 0000000000..fc3f6d34c9 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/name.html @@ -0,0 +1,16 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + #}{{ node.name }} diff --git a/vendor/docc/docc/plugins/python/templates/html/parameter.html b/vendor/docc/docc/plugins/python/templates/html/parameter.html new file mode 100644 index 0000000000..24ce72294d --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/parameter.html @@ -0,0 +1,24 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +{% if node.star == "*" -%} +* +{%- elif node.star == "**" -%} +** +{%- endif -%} +{{- node.name|html -}} +{%- if node.type_annotation -%}: {{ node.type_annotation|html -}}{%- endif -%} +{#- TODO: default value -#} diff --git a/vendor/docc/docc/plugins/python/templates/html/subscript.html b/vendor/docc/docc/plugins/python/templates/html/subscript.html new file mode 100644 index 0000000000..6e08db2c44 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/subscript.html @@ -0,0 +1,20 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +{{- node.name|html -}} + {%- if node.generics -%}{{- node.generics|html -}}{%- endif -%} + diff --git a/vendor/docc/docc/plugins/python/templates/html/tuple.html b/vendor/docc/docc/plugins/python/templates/html/tuple.html new file mode 100644 index 0000000000..cd7cc9d4f2 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/tuple.html @@ -0,0 +1,22 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +( +{%- for element in node.elements -%} +{{- element|html -}} +{%- if not loop.last %}, {% endif -%} +{%- endfor -%} +) diff --git a/vendor/docc/docc/plugins/python/templates/html/type.html b/vendor/docc/docc/plugins/python/templates/html/type.html new file mode 100644 index 0000000000..18a33939d5 --- /dev/null +++ b/vendor/docc/docc/plugins/python/templates/html/type.html @@ -0,0 +1,17 @@ +{# + # Copyright (C) 2022-2023 Ethereum Foundation + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +-#} +{{- node.child|html -}} diff --git a/vendor/docc/docc/plugins/references.py b/vendor/docc/docc/plugins/references.py new file mode 100644 index 0000000000..2dfb0cad9a --- /dev/null +++ b/vendor/docc/docc/plugins/references.py @@ -0,0 +1,200 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Definitions and references for interlinking documents. +""" + +import dataclasses +from collections import defaultdict +from dataclasses import dataclass +from typing import Dict, Iterable, Optional, Set, Tuple, Type + +from docc.context import Context, Provider +from docc.document import BlankNode, Document, Node, Visit, Visitor +from docc.settings import PluginSettings +from docc.source import Source +from docc.transform import Transform + + +@dataclass(eq=True, frozen=True) +class Location: + """ + Location of a node. + """ + + source: Source + identifier: str + specifier: int + + +class ReferenceError(Exception): + """ + Exception raised when a reference doesn't match a definition. + """ + + identifier: str + context: Optional[Context] + + def __init__( + self, identifier: str, context: Optional[Context] = None + ) -> None: + message = f"undefined identifier: `{identifier}`" + source = None + if context: + try: + source = context[Source] + except KeyError: + pass + + if source: + if source.relative_path: + message = f"in `{source.relative_path}`, {message}" + else: + message = f"writing to `{source.output_path}`, {message}" + + super().__init__(message) + self.identifier = identifier + self.context = context + + +class Index: + """ + Tracks the location of definitions. + """ + + _index: Dict[str, Set[Location]] + + def __init__(self) -> None: + self._index = defaultdict(set) + + def define(self, source: Source, identifier: str) -> Location: + """ + Register a new definition in the index. + """ + existing = self._index[identifier] + definition = Location( + source=source, identifier=identifier, specifier=len(existing) + ) + existing.add(definition) + return definition + + def lookup(self, identifier: str) -> Iterable[Location]: + """ + Find a definition that was previously registered. + """ + got = self._index[identifier] + if not got: + raise ReferenceError(identifier) + return got + + +@dataclass(repr=False) +class Base(Node): + """ + Node implementation for Definition and Reference. + """ + + identifier: str + child: Node = dataclasses.field(default_factory=BlankNode) + + @property + def children(self) -> Tuple[Node]: + """ + Return the children of this node. + """ + return (self.child,) + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace a child with a different node. + """ + if old == self.child: + self.child = new + + +@dataclass +class Definition(Base): + """ + A target for a Reference. + """ + + specifier: Optional[int] = dataclasses.field(default=None) + + +@dataclass +class Reference(Base): + """ + A link to a Definition. + """ + + +class IndexContext(Provider[Index]): + """ + Injects an Index instance into the Context. + """ + + index: Index + + def __init__(self, config: PluginSettings) -> None: + super().__init__(config) + self.index = Index() + + @classmethod + def provides(class_) -> Type[Index]: + """ + Return the type used as the key in the Context. + """ + return Index + + def provide(self) -> Index: + """ + Return the object to add to the Context. + """ + return self.index + + +class IndexTransform(Transform): + """ + Collect Definition nodes and insert them into the index. + """ + + def __init__(self, config: PluginSettings) -> None: + pass + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + context[Document].root.visit(_TransformVisitor(context)) + + +class _TransformVisitor(Visitor): + context: Context + + def __init__(self, context: Context) -> None: + self.context = context + + def enter(self, node: Node) -> Visit: + if isinstance(node, Definition): + definition = self.context[Index].define( + self.context[Source], node.identifier + ) + assert node.specifier is None + node.specifier = definition.specifier + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + pass diff --git a/vendor/docc/docc/plugins/resources.py b/vendor/docc/docc/plugins/resources.py new file mode 100644 index 0000000000..e8ec55811b --- /dev/null +++ b/vendor/docc/docc/plugins/resources.py @@ -0,0 +1,150 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Plugin for working with importlib resources. +""" + +import shutil +from io import TextIOBase +from pathlib import PurePath +from typing import Dict, Set, Tuple, Type, TypeVar + +from importlib_resources import files +from importlib_resources.abc import Traversable + +from docc.build import Builder +from docc.context import Context +from docc.document import Document, Node, OutputNode +from docc.settings import PluginSettings +from docc.source import Source + +R = TypeVar("R", bound="ResourceSource") + + +class ResourceSource(Source): + """ + A Source representing an importlib file. + """ + + resource: Traversable + _output_path: PurePath + extension: str + + @classmethod + def with_path( + cls: Type[R], mod: str, input_path: PurePath, output_path: PurePath + ) -> R: + """ + Create a source for a resource `input_path`, relative to the Python + module `mod`, to be output at `output_path`. Note that `output_path` + should not have a file extension (or "suffix".) + """ + return cls( + files(mod) / input_path, + output_path, + "".join(input_path.suffixes), + ) + + def __init__( + self, resource: Traversable, output_path: PurePath, extension: str + ) -> None: + self._output_path = output_path + self.resource = resource + self.extension = extension + + @property + def relative_path(self) -> None: + """ + Path relative to the project root. + """ + return None + + @property + def output_path(self) -> PurePath: + """ + Where to write the output from this Source relative to the output path. + """ + return self._output_path + + +class ResourceNode(OutputNode): + """ + A node representing an `importlib` resource, to be copied to the output + directory. + """ + + _extension: str + resource: Traversable + + def __init__(self, resource: Traversable, extension: str) -> None: + self.resource = resource + self._extension = extension + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + raise TypeError + + @property + def children(self) -> Tuple[()]: + """ + Child nodes belonging to this node. + """ + return tuple() + + @property + def extension(self) -> str: + """ + The preferred file extension for this node. + """ + return self._extension + + def output(self, context: Context, destination: TextIOBase) -> None: + """ + Write this Node to destination. + """ + with self.resource.open("r") as f: + shutil.copyfileobj(f, destination) + + +class ResourceBuilder(Builder): + """ + Collect resource sources and open them for reading. + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Create a builder with the given configuration. + """ + + def build( + self, + unprocessed: Set[Source], + processed: Dict[Source, Document], + ) -> None: + """ + Consume unprocessed Sources and insert their Documents into processed. + """ + source_set = set( + s for s in unprocessed if isinstance(s, ResourceSource) + ) + unprocessed -= source_set + + for source in source_set: + processed[source] = Document( + ResourceNode(source.resource, source.extension), + ) diff --git a/vendor/docc/docc/plugins/search.py b/vendor/docc/docc/plugins/search.py new file mode 100644 index 0000000000..26ff1fabc8 --- /dev/null +++ b/vendor/docc/docc/plugins/search.py @@ -0,0 +1,333 @@ +# Copyright (C) 2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Utilities for search. +""" + +import json +from abc import ABC, abstractmethod +from collections import defaultdict +from dataclasses import dataclass +from io import TextIOBase +from pathlib import PurePath +from typing import ( + DefaultDict, + Dict, + Iterator, + List, + Mapping, + Optional, + Sequence, + Set, + Type, + Union, +) + +from typing_extensions import TypeAlias, assert_never + +from docc.build import Builder +from docc.context import Context, Provider +from docc.discover import Discover +from docc.document import BlankNode, Document, Node, OutputNode, Visit, Visitor +from docc.plugins.references import Definition, Index, ReferenceError +from docc.settings import PluginSettings +from docc.source import Source +from docc.transform import Transform + + +@dataclass(eq=True, frozen=True) +class BySource: + """ + Location of a search item using a source. + """ + + source: Source + + +@dataclass(eq=True, frozen=True) +class ByReference: + """ + Location of a search item using a reference. + """ + + identifier: str + specifier: Optional[int] + + +Location: TypeAlias = BySource | ByReference +Content: TypeAlias = Union[str, Mapping[str, Union[str, Sequence[str]]]] + + +@dataclass(eq=True, frozen=True) +class Item: + """ + A searchable item. + """ + + location: Location + content: Content + + +class Search: + """ + Tracks searchable items. + """ + + _items: DefaultDict[Location, DefaultDict[str, List[str]]] + + def __init__(self) -> None: + self._items = defaultdict(lambda: defaultdict(list)) + + def add(self, item: Item) -> None: + """ + Add the given `item` to the search index. + """ + existing = self._items[item.location] + raw_content = item.content + + if isinstance(raw_content, str): + raw_content = {"text": [raw_content]} + + for key, value in raw_content.items(): + if isinstance(value, str): + existing[key].append(value) + else: + existing[key].extend(value) + + +class SearchSource(Source): + """ + A virtual source for a search index. + """ + + @property + def relative_path(self) -> None: + """ + Path to the Source (if one exists) relative to the project root. + """ + return None + + @property + def output_path(self) -> PurePath: + """ + Where to write the output from this Source relative to the output path. + """ + return PurePath("search") + + +class SearchBuilder(Builder): + """ + Consumes unprocessed Sources and creates Documents. + """ + + def __init__(self, config: PluginSettings) -> None: + """ + Create a Builder with the given configuration. + """ + + def build( + self, + unprocessed: Set[Source], + processed: Dict[Source, Document], + ) -> None: + """ + Consume unprocessed Sources and insert their Documents into processed. + """ + to_process = set(x for x in unprocessed if isinstance(x, SearchSource)) + unprocessed -= to_process + + for source in to_process: + processed[source] = Document(SearchNode()) + + +class SearchNode(BlankNode, OutputNode): + """ + Placeholder for a search index. + """ + + @property + def extension(self) -> str: + """ + The preferred file extension for this node. + """ + return ".js" + + def output(self, context: Context, destination: TextIOBase) -> None: + """ + Write this Node to destination. + """ + items = context[Search]._items + + output = [] + for location, content in items.items(): + output_source = {} + + # Find the source associated with the item. + if isinstance(location, BySource): + source = location.source + elif isinstance(location, ByReference): + definitions = context[Index].lookup(location.identifier) + output_source["identifier"] = location.identifier + + if location.specifier is None: + source = list(definitions)[0].source + else: + output_source["specifier"] = location.specifier + source_opt = None + for definition in definitions: + if definition.specifier == location.specifier: + source_opt = definition.source + break + + if source_opt: + source = source_opt + else: + raise ReferenceError(location.identifier) + else: + assert_never(location.__class__) + + output_source["path"] = str(source.output_path) + + output.append( + { + "source": output_source, + "content": content, + } + ) + + destination.write("this.SEARCH_INDEX = ") + json.dump(output, destination) # type: ignore + destination.write("; Object.freeze(this.SEARCH_INDEX);") + + +class SearchDiscover(Discover): + """ + Finds sources for which to generate documentation. + """ + + def __init__(self, settings: PluginSettings) -> None: + pass + + def discover(self, known: object) -> Iterator[Source]: + """ + Find sources. + """ + yield SearchSource() + + +class SearchContext(Provider[Search]): + """ + Provides a Search item for the Context. + """ + + search: Search + + def __init__(self, settings: PluginSettings) -> None: + super().__init__(settings) + self.search = Search() + + @classmethod + def provides(class_) -> Type[Search]: + """ + Return the type to be used as the key in the Context. + """ + return Search + + def provide(self) -> Search: + """ + Create the object to be inserted into the Context. + """ + return self.search + + +class SearchTransform(Transform): + """ + Walks the document tree to discover searchable nodes. + """ + + def __init__(self, settings: PluginSettings) -> None: + pass + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + document = context[Document] + document.root.visit(_SearchVisitor(context)) + + +class Searchable(ABC): + """ + Base class for objects that can be searched. + """ + + @abstractmethod + def to_search(self) -> Content: + """ + Extract searchable fields from the node. + """ + + def search_children(self) -> bool: + """ + `True` if the children of this node should be searched, or `False` + otherwise. + """ + return True + + +class _SearchVisitor(Visitor): + context: Context + _definitions: List[Definition] + + def __init__(self, context: Context) -> None: + self.context = context + self._definitions = [] + + def _enter_searchable(self, node: Searchable) -> Visit: + if self._definitions: + definition = self._definitions[-1] + location = ByReference( + identifier=definition.identifier, + specifier=definition.specifier, + ) + else: + location = BySource(source=self.context[Source]) + + self.context[Search].add( + Item( + location=location, + content=node.to_search(), + ) + ) + + if node.search_children(): + return Visit.TraverseChildren + else: + return Visit.SkipChildren + + def enter(self, node: Node) -> Visit: + if isinstance(node, Definition): + self._definitions.append(node) + + if isinstance(node, Searchable): + return self._enter_searchable(node) + + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + if isinstance(node, Definition): + popped = self._definitions.pop() + assert node == popped diff --git a/vendor/docc/docc/plugins/verbatim/__init__.py b/vendor/docc/docc/plugins/verbatim/__init__.py new file mode 100644 index 0000000000..f54d850862 --- /dev/null +++ b/vendor/docc/docc/plugins/verbatim/__init__.py @@ -0,0 +1,614 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Support for copying verbatim text from a Source into the output, with syntax +highlighting support. +""" + +import logging +from abc import abstractmethod +from dataclasses import dataclass, field +from typing import Final, Iterable, List, Optional, Sequence, Tuple, Union + +from docc.context import Context +from docc.document import Document, Node, Visit, Visitor +from docc.plugins import references +from docc.settings import PluginSettings +from docc.source import TextSource +from docc.transform import Transform + + +@dataclass +class Transcribed(Node): + """ + A block of verbatim text. + + Unlike `Verbatim` nodes, `Transcribed` blocks actually contain the text + from the `Source`, instead of a line numbers and ranges. This makes them + more useful for further processing. + """ + + _children: List[Node] = field(default_factory=list) + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the new node. + """ + self._children = [new if x == old else x for x in self._children] + + def __repr__(self) -> str: + """ + String representation of this node. + """ + return "Transcribed(...)" + + +@dataclass +class Line(Node): + """ + A grouping of nodes occupying the same line. + """ + + number: int + _children: List[Node] = field(default_factory=list) + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old child with the new one. + """ + self._children = [new if x == old else x for x in self._children] + + def __repr__(self) -> str: + """ + String representation of this node. + """ + return f"Line(number={self.number}, ...)" + + +@dataclass +class Highlight(Node): + """ + Node with highlighting information. + """ + + highlights: List[str] = field(default_factory=list) + _children: List[Node] = field(default_factory=list) + + @property + def children(self) -> Sequence[Node]: + """ + Return the children of this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old child with the provided new one. + """ + self._children = [new if x == old else x for x in self._children] + + def __repr__(self) -> str: + """ + String representation of this node. + """ + return f"Highlight(highlights={self.highlights!r}, ...)" + + +@dataclass +class Text(Node): + """ + Node containing simple text. + """ + + text: str + + @property + def children(self) -> Tuple[()]: + """ + Return the children of this node. + """ + return () + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old child with a new one. + """ + raise TypeError() + + +class VerbatimNode(Node): + """ + Base class for verbatim nodes. + """ + + _children: List[Node] + + def __init__(self) -> None: + self._children = [] + + @property + def children(self) -> Iterable[Node]: + """ + Child nodes belonging to this node. + """ + return self._children + + def replace_child(self, old: Node, new: Node) -> None: + """ + Replace the old node with the given new node. + """ + self._check(new) + for index, child in enumerate(self._children): + if child == old: + self._children[index] = new + + def append(self, new: Node) -> None: + """ + Append a new child node. + """ + self._check(new) + self._children.append(new) + + def _check(self, new: Node) -> None: + if isinstance(new, Verbatim): + # TODO: is it worth checking children of children? + class_ = new.__class__.__name__ + raise ValueError(f"cannot nest a {class_} in a verbatim node") + + +@dataclass(order=True) +class _Pos: + line: int + column: int + + +@dataclass(order=True, frozen=True, repr=False) +class Pos: + """ + Position in a Source. + """ + + line: int + column: int + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return f"{self.line}:{self.column}" + + +class Verbatim(VerbatimNode): + """ + A block of lines of text from one Source. + """ + + source: TextSource + + def __init__(self, source: TextSource) -> None: + super().__init__() + self.source = source + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return f"Verbatim({self.source!r})" + + +class Fragment(VerbatimNode): + """ + A snippet of text from a Source. + """ + + start: Pos + end: Pos + + highlights: List[str] + + def __init__( + self, start: Pos, end: Pos, highlights: Optional[List[str]] = None + ) -> None: + super().__init__() + self.start = start + self.end = end + self.highlights = [] if highlights is None else highlights + + def __repr__(self) -> str: + """ + Textual representation of this instance. + """ + return f"Fragment({self.start}, {self.end}, {self.highlights!r})" + + +@dataclass +class _VerbatimContext: + node: Verbatim + written: Optional[_Pos] = None + + @property + def source(self) -> TextSource: + return self.node.source + + +class _BoundsVisitor(Visitor): + start: Optional[Pos] + end: Optional[Pos] + + def __init__(self) -> None: + self.start = None + self.end = None + + def enter(self, node: Node) -> Visit: + if isinstance(node, Fragment): + if self.start is None or node.start < self.start: + self.start = node.start + + if self.end is None or node.end > self.end: + self.end = node.end + + return Visit.TraverseChildren + + def exit(self, node: Node) -> None: + pass + + +class VerbatimVisitor(Visitor): + """ + Visitor for verbatim node trees that emits a series of events. + """ + + # TODO: I wonder if there's a way to visit all nodes, put them in a heap, + # and read the text that way. + + _depth: Optional[int] + _verbatim: Optional[_VerbatimContext] + + def __init__(self) -> None: + super().__init__() + self._depth = None + self._verbatim = None + + # + # Override in Subclasses: + # + + @abstractmethod + def line(self, source: TextSource, line: int) -> None: + """ + Marks the start of a new line. + """ + + @abstractmethod + def text(self, text: str) -> None: + """ + String copied from the Source. + """ + + @abstractmethod + def begin_highlight(self, highlights: Sequence[str]) -> None: + """ + Marks the start of a highlighted section. + """ + + @abstractmethod + def end_highlight(self) -> None: + """ + Marks the end of a highlighted section. + """ + + def enter_node(self, node: Node) -> Visit: + """ + Visit a non-verbatim Node. + """ + if node: + logging.warning( + "`%s` entered non-verbatim node `%s`", + self.__class__.__name__, + node.__class__.__name__, + ) + return Visit.TraverseChildren + + def exit_node(self, node: Node) -> None: + """ + Leave a non-verbatim Node. + """ + if node: + logging.warning( + "`%s` exited non-verbatim node `%s`", + self.__class__.__name__, + node.__class__.__name__, + ) + + # + # Implementation Details: + # + + def _copy(self, start_line: int, until: Pos) -> None: + verbatim = self._verbatim + + assert verbatim is not None + assert until.line > 0 + assert until.column >= 0 + + if verbatim.written is None: + verbatim.written = _Pos( + line=start_line, + column=0, + ) + self.line(verbatim.source, verbatim.written.line) + + written = verbatim.written + assert written is not None + + while written.line < until.line: + text = verbatim.source.line(written.line) + self.text(text[written.column :]) + + written.line += 1 + written.column = 0 + + self.line(verbatim.source, written.line) + + if written.line > until.line: + return + + if written.column >= until.column: + return + + text = verbatim.source.line(written.line) + self.text(text[written.column : until.column]) + + written.column = until.column + + def _enter_fragment(self, node: Fragment) -> Visit: + depth = self._depth + + if depth is None or depth < 1: + raise Exception("Fragment nodes must appear inside Verbatim") + + self._copy(node.start.line, node.start) + + self._depth = depth + 1 + self.begin_highlight(node.highlights) + + return Visit.TraverseChildren + + def _exit_fragment(self, node: Fragment) -> None: + depth = self._depth + assert depth is not None + assert depth > 1 + + self._copy(node.start.line, node.end) + self.end_highlight() + + self._depth = depth - 1 + + def _enter_verbatim(self, node: Verbatim) -> Visit: + if self._depth is not None: + raise Exception("Verbatim nodes cannot be nested") + self._depth = 1 + self._verbatim = _VerbatimContext(node) + return Visit.TraverseChildren + + def _exit_verbatim(self, node: Verbatim) -> None: + assert self._depth == 1 + assert self._verbatim is not None + assert self._verbatim.node == node + self._verbatim = None + self._depth = None + + def enter(self, node: Node) -> Visit: + """ + Visit a node. + """ + if isinstance(node, Fragment): + return self._enter_fragment(node) + elif isinstance(node, Verbatim): + return self._enter_verbatim(node) + else: + # TODO: Save the results somewhere so we don't visit twice. + visitor = _BoundsVisitor() + node.visit(visitor) + if visitor.start is not None: + self._copy(visitor.start.line, visitor.start) + return self.enter_node(node) + + def exit(self, node: Node) -> None: + """ + Depart a node. + """ + if isinstance(node, Fragment): + return self._exit_fragment(node) + elif isinstance(node, Verbatim): + return self._exit_verbatim(node) + else: + # TODO: Save the results somewhere so we don't visit twice. + visitor = _BoundsVisitor() + node.visit(visitor) + if visitor.end is not None: + start = visitor.start or visitor.end + self._copy(start.line, visitor.end) + return self.exit_node(node) + + +class _TranscribeVisitor(VerbatimVisitor): + context: Final[Context] + document: Final[Document] + root: Transcribed + output_stack: List[Node] + input_stack: List[Sequence[str] | references.Reference] + + def __init__(self, context: Context) -> None: + super().__init__() + self.context = context + self.document = context[Document] + + self.root = Transcribed() + + self.output_stack = [] + self.input_stack = [] + + def line(self, source: TextSource, line: int) -> None: + line_node = Line(number=line) + self.root._children.append(line_node) + self.output_stack = [line_node] + self._highlight(self.input_stack) + + def _highlight( + self, + highlight_groups: Sequence[Union[Sequence[str], references.Reference]], + ) -> None: + for item in highlight_groups: + top = self.output_stack[-1] + + if isinstance(item, references.Reference): + new_node = references.Reference(identifier=item.identifier) + else: + new_node = Highlight(highlights=list(item)) + + if isinstance(top, references.Reference): + assert not top.child + top.child = new_node + elif isinstance(top, (Highlight, Line)): + top._children.append(new_node) + else: + raise TypeError( + f"expected Highlight or Line, got `{type(top)}`" + ) + + self.output_stack.append(new_node) + + def text(self, text: str) -> None: + top = self.output_stack[-1] + new_node = Text(text) + if isinstance(top, references.Reference): + assert not top.child + top.child = new_node + elif isinstance(top, (Highlight, Line)): + top._children.append(new_node) + else: + raise TypeError(f"expected Highlight or Line, got `{type(top)}`") + + def begin_highlight(self, highlights: Sequence[str]) -> None: + self.input_stack.append(highlights) + self._highlight([highlights]) + + def end_highlight(self) -> None: + self.input_stack.pop() + popped_node = self.output_stack.pop() + assert isinstance(popped_node, Highlight) + + def enter_node(self, node: Node) -> Visit: + """ + Visit a non-verbatim Node. + """ + if isinstance(node, references.Reference): + if "<" in node.identifier: + # TODO: Create definitions for local variables. + return Visit.TraverseChildren + self.input_stack.append(node) + if self.output_stack: + self._highlight([node]) + return Visit.TraverseChildren + else: + return super().enter_node(node) + + def exit_node(self, node: Node) -> None: + """ + Leave a non-verbatim Node. + """ + if isinstance(node, references.Reference): + if "<" in node.identifier: + # TODO: Create definitions for local variables. + return + + popped = self.input_stack.pop() + assert popped == node + + popped_output = self.output_stack.pop() + assert isinstance( + popped_output, (Line, Highlight, references.Reference) + ) + else: + return super().exit_node(node) + + +class _FindVisitor(Visitor): + context: Context + stack: List[Node] + root: Optional[Node] + + def __init__(self, context: Context) -> None: + self.context = context + self.stack = [] + self.root = None + + def enter(self, node: Node) -> Visit: + self.stack.append(node) + if self.root is None: + self.root = node + + if not isinstance(node, Verbatim): + return Visit.TraverseChildren + + visitor = _TranscribeVisitor(self.context) + node.visit(visitor) + + if len(self.stack) == 1: + self.root = visitor.root + else: + self.stack[-2].replace_child(node, visitor.root) + + return Visit.SkipChildren + + def exit(self, node: Node) -> None: + self.stack.pop() + + +class Transcribe(Transform): + """ + A plugin that converts position-based Verbatim nodes into transcribed + nodes. + """ + + def __init__(self, settings: PluginSettings) -> None: + pass + + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + document = context[Document] + + visitor = _FindVisitor(context) + document.root.visit(visitor) + assert visitor.root is not None + document.root = visitor.root diff --git a/vendor/docc/docc/plugins/verbatim/html.py b/vendor/docc/docc/plugins/verbatim/html.py new file mode 100644 index 0000000000..191e11d9d4 --- /dev/null +++ b/vendor/docc/docc/plugins/verbatim/html.py @@ -0,0 +1,114 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Rendering functions to transform verbatim nodes into HTML. +""" + +from docc.context import Context +from docc.plugins.html import HTMLRoot, HTMLTag, RenderResult, TextNode + +from . import Highlight, Line, Text, Transcribed + + +def render_transcribed( + context: Context, + parent: object, + node: object, +) -> RenderResult: + """ + Render a transcribed block as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(node, Transcribed) + + table = HTMLTag("table", attributes={"class": "verbatim"}) + parent.append(table) + return table + + +def render_line( + context: Context, + parent: object, + node: object, +) -> RenderResult: + """ + Render a transcribed line as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(node, Line) + + line_text = TextNode(str(node.number)) + + line_cell = HTMLTag("th") + line_cell.append(line_text) + + code_pre = HTMLTag("pre") + + code_cell = HTMLTag("td") + code_cell.append(code_pre) + + row = HTMLTag("tr") + row.append(line_cell) + row.append(code_cell) + + if isinstance(parent, HTMLTag) and parent.tag_name.casefold() == "table": + tbody = HTMLTag("tbody") + tbody.append(row) + parent.append(tbody) + else: + parent.append(row) + return code_pre + + +def render_text( + context: Context, + parent: object, + node: object, +) -> RenderResult: + """ + Render transcribed text as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(node, Text) + + parent.append(TextNode(node.text)) + return None + + +def render_highlight( + context: Context, + parent: object, + node: object, +) -> RenderResult: + """ + Render transcribed text as HTML. + """ + assert isinstance(context, Context) + assert isinstance(parent, (HTMLRoot, HTMLTag)) + assert isinstance(node, Highlight) + + classes = [f"hi-{h}" for h in node.highlights] + ["hi"] + new_node = HTMLTag( + "span", + attributes={ + "class": " ".join(classes), + }, + ) + parent.append(new_node) + return new_node diff --git a/vendor/docc/docc/py.typed b/vendor/docc/docc/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/docc/docc/settings.py b/vendor/docc/docc/settings.py new file mode 100644 index 0000000000..2ef306d9b1 --- /dev/null +++ b/vendor/docc/docc/settings.py @@ -0,0 +1,270 @@ +# Copyright (C) 2022-2024 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Find and load configuration. +""" + +from collections.abc import Mapping +from dataclasses import dataclass +from io import BytesIO +from itertools import chain, islice +from pathlib import Path, PurePath +from typing import Any, Dict, Iterator, Optional, Sequence + +import tomli + +MAX_DEPTH = 10 +FILE_NAME = "pyproject.toml" + + +class SettingsError(Exception): + """ + An error encountered while attempting to load the settings. + """ + + +class PluginSettings(Mapping): + """ + Stores settings for individual plugins. + """ + + _settings: "Settings" + _store: Dict[str, Any] + + def __init__(self, settings: "Settings", store: Dict[str, Any]) -> None: + """ + Create an instance with the given backing store. + """ + self._settings = settings + + # Copy the dict since we're a Mapping and not a MappingView. + self._store = dict(store) + + def __len__(self) -> int: + """ + Return len(self). + """ + return len(self._store) + + def __iter__(self) -> Iterator[str]: + """ + Implement iter(self). + """ + return iter(self._store) + + def __getitem__(self, k: str) -> object: + """ + Return the item with the given key. + """ + return self._store[k] + + def unresolve_path(self, path: PurePath) -> PurePath: + """ + Convert an absolute path to a path relative to the settings file. + """ + return self._settings.unresolve_path(path) + + def resolve_path(self, path: PurePath) -> Path: + """ + Convert the given path to an absolute path relative to the settings + file. + """ + return self._settings.resolve_path(path) + + +@dataclass +class Output: + """ + Settings for the output of the documentation process. + """ + + path: Optional[Path] + + +class Settings: + """ + Handles loading settings for generating documentation. + """ + + _settings: Dict[str, Any] + _root: Path + output: Output + + def __init__(self, path: Path) -> None: + """ + Load the settings in the nearest configuration file to path. + """ + settings_bytes = None + + search_directories = islice(chain([path], path.parents), MAX_DEPTH) + + for current_directory in search_directories: + settings_file = current_directory / FILE_NAME + try: + settings_bytes = settings_file.read_bytes() + self._root = settings_file.parent + break + except FileNotFoundError: + pass + + if settings_bytes is None: + raise SettingsError( + f"could not find {FILE_NAME} (max depth: {MAX_DEPTH})" + ) + + settings_toml = tomli.load(BytesIO(settings_bytes)) + + try: + self._settings = settings_toml["tool"]["docc"] + except KeyError: + # TODO: Come up with some defaults. + self._settings = {} + + try: + output_path = Path(self._settings["output"]["path"]) + except KeyError: + output_path = None + + self.output = Output( + path=output_path, + ) + + def for_plugin(self, name: str) -> PluginSettings: + """ + Retrieve the settings for the given plugin. + """ + try: + settings = self._settings["plugins"][name] + except KeyError: + settings = {} + + assert isinstance(settings, dict) + + for k in settings.keys(): + assert isinstance(k, str) + + return PluginSettings(self, settings) + + @property + def context(self) -> Sequence[str]: + """ + Retrieve a list of enabled context plugins. + """ + try: + context = self._settings["context"] + except KeyError: + context = [ + "docc.references.context", + "docc.search.context", + "docc.html.context", + ] + + assert isinstance(context, list) + + for item in context: + assert isinstance(item, str) + + return context + + @property + def discovery(self) -> Sequence[str]: + """ + Retrieve a list of enabled discovery plugins. + """ + try: + discovery = self._settings["discovery"] + except KeyError: + discovery = [ + "docc.search.discover", + "docc.html.discover", + "docc.python.discover", + "docc.listing.discover", + "docc.files.discover", + ] + + assert isinstance(discovery, list) + + for item in discovery: + assert isinstance(item, str) + + return discovery + + @property + def build(self) -> Sequence[str]: + """ + Retrieve a list of enabled build plugins. + """ + try: + build = self._settings["build"] + except KeyError: + build = [ + "docc.search.build", + "docc.python.build", + "docc.files.build", + "docc.listing.build", + "docc.resources.build", + ] + + assert isinstance(build, list) + + for item in build: + assert isinstance(item, str) + + return build + + @property + def transform(self) -> Sequence[str]: + """ + Retrieve a list of enabled transform plugins. + """ + try: + transform = self._settings["transform"] + except KeyError: + transform = [ + "docc.python.transform", + "docc.mistletoe.transform", + "docc.mistletoe.reference", + "docc.verbatim.transform", + "docc.references.index", + "docc.search.transform", + "docc.html.transform", + ] + + assert isinstance(transform, list) + + for item in transform: + assert isinstance(item, str) + + return transform + + def resolve_path(self, path: PurePath) -> Path: + """ + Convert the given path to an absolute path relative to the settings + file. + """ + joined = self._root / path + resolved = joined.resolve() + + # relative_to raises an error if the argument isn't a super-path. + resolved.relative_to(self._root.resolve()) + + return resolved + + def unresolve_path(self, path: PurePath) -> PurePath: + """ + Convert an absolute path to a path relative to the settings file. + """ + return path.relative_to(self._root) diff --git a/vendor/docc/docc/source.py b/vendor/docc/docc/source.py new file mode 100644 index 0000000000..6e0dd3816b --- /dev/null +++ b/vendor/docc/docc/source.py @@ -0,0 +1,83 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Sources are the inputs for documentation generation. +""" + +from abc import ABC, abstractmethod +from pathlib import PurePath +from typing import Optional, TextIO + + +class Source(ABC): + """ + An input to generate documentation for. + """ + + @property + @abstractmethod + def relative_path(self) -> Optional[PurePath]: + """ + Path to the Source (if one exists) relative to the project root. + """ + raise NotImplementedError() + + @property + @abstractmethod + def output_path(self) -> PurePath: + """ + Where to write the output from this Source relative to the output path. + """ + raise NotImplementedError() + + def __repr__(self) -> str: + """ + String representation of the source. + """ + if self.relative_path is None: + return super().__repr__() + else: + return ( + f"<{self.__module__}." + f"{self.__class__.__qualname__}: " + f'"{self.relative_path}">' + ) + + +class TextSource(Source): + """ + A Source that supports reading text snippets. + """ + + @abstractmethod + def open(self) -> TextIO: + """ + Open the source for reading. + """ + + def line(self, number: int) -> str: + """ + Extract a line of text from the source. + """ + # TODO: Don't reopen and reread the file every time... + with self.open() as f: + lines = f.read().split("\n") + try: + return lines[number - 1] + except IndexError as e: + raise IndexError( + f"line {number} out of range for `{self.relative_path}`" + ) from e diff --git a/vendor/docc/docc/transform.py b/vendor/docc/docc/transform.py new file mode 100644 index 0000000000..9db8fe1181 --- /dev/null +++ b/vendor/docc/docc/transform.py @@ -0,0 +1,61 @@ +# Copyright (C) 2022-2023 Ethereum Foundation +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Transforms modify Documents. +""" + +from abc import ABC, abstractmethod +from typing import Sequence, Tuple + +from .context import Context +from .plugins.loader import Loader +from .settings import PluginSettings, Settings + + +class Transform(ABC): + """ + Applies a transformation to a Document. + """ + + @abstractmethod + def __init__(self, config: PluginSettings) -> None: + """ + Create a Transform with the given configuration. + """ + raise NotImplementedError() + + @abstractmethod + def transform(self, context: Context) -> None: + """ + Apply the transformation to the given document. + """ + raise NotImplementedError() + + +def load(settings: Settings) -> Sequence[Tuple[str, Transform]]: + """ + Load the transform plugins as requested in settings. + """ + loader = Loader() + + sources = [] + + for name in settings.transform: + class_ = loader.load(Transform, name) + plugin_settings = settings.for_plugin(name) + sources.append((name, class_(plugin_settings))) + + return sources diff --git a/vendor/docc/pyproject.toml b/vendor/docc/pyproject.toml new file mode 100644 index 0000000000..c25182e3ba --- /dev/null +++ b/vendor/docc/pyproject.toml @@ -0,0 +1,88 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "docc" +description = "Python code documentation compiler" +version = "0.3.1" +requires-python = ">=3.10" +dependencies = [ + "libcst>=1.0.1,<2", + "mistletoe>=1.2.1,<2", + "importlib-resources>=6.0.1,<7", + "tomli>=2.0.1,<3", + "rich>=13.5.2,<14", + "Jinja2>=3.1.2,<4", + "inflection>=0.5.1,<0.6", + "typing-extensions>=4.7.1,<5", +] + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.package-data] +docc = ["py.typed"] +"docc.plugins.html" = [ + "templates/**", + "static/docc.css", + "static/search.js", + "static/chota/dist/chota.min.css", + "static/fuse/dist/fuse.min.js", +] +"docc.plugins.listing" = ["templates/**"] +"docc.plugins.python" = ["templates/**"] + +[project.scripts] +docc = "docc.cli:main" + +[project.entry-points."docc.plugins"] +"docc.python.discover" = "docc.plugins.python:PythonDiscover" +"docc.python.build" = "docc.plugins.python:PythonBuilder" +"docc.python.transform" = "docc.plugins.python:PythonTransform" +"docc.html.transform" = "docc.plugins.html:HTMLTransform" +"docc.html.discover" = "docc.plugins.html:HTMLDiscover" +"docc.html.context" = "docc.plugins.html:HTMLContext" +"docc.references.context" = "docc.plugins.references:IndexContext" +"docc.references.index" = "docc.plugins.references:IndexTransform" +"docc.mistletoe.transform" = "docc.plugins.mistletoe:DocstringTransform" +"docc.mistletoe.reference" = "docc.plugins.mistletoe:ReferenceTransform" +"docc.listing.discover" = "docc.plugins.listing:ListingDiscover" +"docc.listing.build" = "docc.plugins.listing:ListingBuilder" +"docc.resources.build" = "docc.plugins.resources:ResourceBuilder" +"docc.files.build" = "docc.plugins.files:FilesBuilder" +"docc.files.discover" = "docc.plugins.files:FilesDiscover" +"docc.search.context" = "docc.plugins.search:SearchContext" +"docc.search.transform" = "docc.plugins.search:SearchTransform" +"docc.search.discover" = "docc.plugins.search:SearchDiscover" +"docc.search.build" = "docc.plugins.search:SearchBuilder" +"docc.debug.transform" = "docc.plugins.debug:DebugTransform" +"docc.verbatim.transform" = "docc.plugins.verbatim:Transcribe" + +[project.entry-points."docc.plugins.html"] +"docc.document:BlankNode" = "docc.plugins.html:blank_node" +"docc.document:ListNode" = "docc.plugins.html:list_node" +"docc.plugins.python.nodes:Module" = "docc.plugins.python.html:render_module" +"docc.plugins.python.nodes:Class" = "docc.plugins.python.html:render_class" +"docc.plugins.python.nodes:Attribute" = "docc.plugins.python.html:render_attribute" +"docc.plugins.python.nodes:Name" = "docc.plugins.python.html:render_name" +"docc.plugins.python.nodes:Access" = "docc.plugins.python.html:render_access" +"docc.plugins.python.nodes:Function" = "docc.plugins.python.html:render_function" +"docc.plugins.python.nodes:Parameter" = "docc.plugins.python.html:render_parameter" +"docc.plugins.python.nodes:Type" = "docc.plugins.python.html:render_type" +"docc.plugins.python.nodes:List" = "docc.plugins.python.html:render_list" +"docc.plugins.python.nodes:Tuple" = "docc.plugins.python.html:render_tuple" +"docc.plugins.python.nodes:Docstring" = "docc.plugins.python.html:render_docstring" +"docc.plugins.python.nodes:Subscript" = "docc.plugins.python.html:render_subscript" +"docc.plugins.python.nodes:BinaryOperation" = "docc.plugins.python.html:render_binary_operation" +"docc.plugins.python.nodes:BitOr" = "docc.plugins.python.html:render_bit_or" +"docc.plugins.verbatim:Transcribed" = "docc.plugins.verbatim.html:render_transcribed" +"docc.plugins.verbatim:Line" = "docc.plugins.verbatim.html:render_line" +"docc.plugins.verbatim:Highlight" = "docc.plugins.verbatim.html:render_highlight" +"docc.plugins.verbatim:Text" = "docc.plugins.verbatim.html:render_text" +"docc.plugins.references:Definition" = "docc.plugins.html:references_definition" +"docc.plugins.references:Reference" = "docc.plugins.html:references_reference" +"docc.plugins.mistletoe:MarkdownNode" = "docc.plugins.mistletoe:render_html" +"docc.plugins.listing:ListingNode" = "docc.plugins.listing:render_html" +"docc.plugins.html:HTMLTag" = "docc.plugins.html:html_tag" +"docc.plugins.html:TextNode" = "docc.plugins.html:text_node" From 1893ba296561220f8d74ff53250a3044190dcbe8 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Mon, 23 Feb 2026 22:39:21 +0000 Subject: [PATCH 2/5] optimize html rendering --- vendor/docc/docc/plugins/html/__init__.py | 53 ++++++++++++++++------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/vendor/docc/docc/plugins/html/__init__.py b/vendor/docc/docc/plugins/html/__init__.py index f9b8c41e9a..3b92ae19a2 100644 --- a/vendor/docc/docc/plugins/html/__init__.py +++ b/vendor/docc/docc/plugins/html/__init__.py @@ -315,11 +315,7 @@ def output(self, context: Context, destination: TextIOBase) -> None: markup = ET.tostring(element, encoding="unicode", method="html") rendered.write(markup) - env = Environment( - extensions=[_ReferenceExtension], - loader=PackageLoader("docc.plugins.html"), - autoescape=select_autoescape(), - ) + env = _get_env("docc.plugins.html") template = env.get_template("base.html") body = rendered.getvalue() static_path = _static_path_from(context) @@ -409,6 +405,39 @@ def exit(self, node: Node) -> None: raise TypeError(f"unsupported node {node.__class__.__name__}") +# Cache entry_points and loaded renderers globally - they never change +_cached_entry_points: Optional[Dict[str, EntryPoint]] = None +_cached_renderers: Dict[Type[Node], Callable[..., object]] = {} + + +def _get_entry_points() -> Dict[str, EntryPoint]: + global _cached_entry_points + if _cached_entry_points is None: + found = entry_points(group="docc.plugins.html") + _cached_entry_points = {entry.name: entry for entry in found} + return _cached_entry_points + + +# Cache Jinja Environments per package +_cached_envs: Dict[str, Environment] = {} + + +def _get_env(package: str) -> Environment: + try: + return _cached_envs[package] + except KeyError: + pass + env = Environment( + extensions=[_ReferenceExtension], + loader=PackageLoader(package), + autoescape=select_autoescape(), + ) + env.filters["html"] = _html_filter + env.filters["find"] = _find_filter + _cached_envs[package] = env + return env + + class HTMLVisitor(Visitor): """ Visits a Document's tree and converts Nodes to HTML. @@ -424,12 +453,10 @@ class HTMLVisitor(Visitor): context: Context def __init__(self, context: Context) -> None: - # Discover render functions. - found = entry_points(group="docc.plugins.html") - self.entry_points = {entry.name: entry for entry in found} + self.entry_points = _get_entry_points() self.root = HTMLRoot(context) self.stack = [self.root] - self.renderers = {} + self.renderers = _cached_renderers self.context = context def _renderer(self, node: Node) -> Callable[..., object]: @@ -740,13 +767,7 @@ def render_template( Render a template as a child of the given parent. """ static_path = _static_path_from(context) - env = Environment( - extensions=[_ReferenceExtension], - loader=PackageLoader(package), - autoescape=select_autoescape(), - ) - env.filters["html"] = _html_filter - env.filters["find"] = _find_filter + env = _get_env(package) template = env.get_template(template_name) parser = HTMLParser(context) parser.feed( From a3d71e1588d27db479992420315a4ebd26eb6d54 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Mon, 23 Feb 2026 23:03:39 +0000 Subject: [PATCH 3/5] DOCC_SKIP_DIFFS --- .github/workflows/gh-pages.yaml | 2 ++ src/ethereum_spec_tools/docc.py | 5 +++++ tox.ini | 1 + 3 files changed, 8 insertions(+) diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index c2a90f8f63..8eda5e439d 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -43,6 +43,8 @@ jobs: run: | tox -e spec-docs touch .tox/docs/.nojekyll + env: + DOCC_SKIP_DIFFS: ${{ (github.event_name != 'push' || github.ref_name != github.event.repository.default_branch) && '1' || '' }} - name: Upload Pages Artifact id: artifact diff --git a/src/ethereum_spec_tools/docc.py b/src/ethereum_spec_tools/docc.py index cc1623174a..3cf4e47b95 100644 --- a/src/ethereum_spec_tools/docc.py +++ b/src/ethereum_spec_tools/docc.py @@ -19,6 +19,7 @@ import dataclasses import logging +import os from collections import defaultdict from itertools import tee, zip_longest from pathlib import PurePath @@ -89,6 +90,10 @@ def discover(self, known: FrozenSet[T]) -> Iterator[Source]: """ Find sources. """ + if os.environ.get("DOCC_SKIP_DIFFS"): + logging.info("Skipping diff discovery (DOCC_SKIP_DIFFS)") + return + forks = {f.path: f for f in self.forks if f.path is not None} by_fork: Dict[Hardfork, Dict[PurePath, Source]] = defaultdict(dict) diff --git a/tox.ini b/tox.ini index f23707dc35..ed61282c7b 100644 --- a/tox.ini +++ b/tox.ini @@ -194,6 +194,7 @@ commands = [testenv:spec-docs] description = Generate documentation for the specification code using docc basepython = python3 +passenv = DOCC_SKIP_DIFFS commands = docc --output "{toxworkdir}/docs" python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs" / "index.html"))' From 610344e489abafcf6329bc339ec5a75728e281f5 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Mon, 23 Feb 2026 23:06:26 +0000 Subject: [PATCH 4/5] exclude --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 024e17d6b5..7fe2d8d235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -364,6 +364,7 @@ ignore_names = ["pytest_*"] [tool.ruff] line-length = 79 +exclude = ["vendor"] [tool.ruff.lint] exclude = ["tests/fixtures"] @@ -458,6 +459,7 @@ skip = [ "tmp", "*.coverage*", "uv.lock", + "vendor", ] ignore-words = "whitelist.txt" # Custom whitelist file count = true # Display counts of errors From fec82349341d5e7ba436c8655c8a51b0c5866d58 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Mon, 23 Feb 2026 23:15:23 +0000 Subject: [PATCH 5/5] add missing dist files --- .gitignore | 3 +++ .../docc/plugins/html/static/chota/dist/chota.min.css | 3 +++ .../docc/docc/plugins/html/static/fuse/dist/fuse.min.js | 9 +++++++++ 3 files changed, 15 insertions(+) create mode 100644 vendor/docc/docc/plugins/html/static/chota/dist/chota.min.css create mode 100644 vendor/docc/docc/plugins/html/static/fuse/dist/fuse.min.js diff --git a/.gitignore b/.gitignore index 827d04d6a6..67bf4e9a32 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ logs/ env.yaml site/ + +# Vendored static assets (overrides dist/ rule) +!vendor/docc/docc/plugins/html/static/**/dist/ diff --git a/vendor/docc/docc/plugins/html/static/chota/dist/chota.min.css b/vendor/docc/docc/plugins/html/static/chota/dist/chota.min.css new file mode 100644 index 0000000000..e73dc6c20d --- /dev/null +++ b/vendor/docc/docc/plugins/html/static/chota/dist/chota.min.css @@ -0,0 +1,3 @@ +/*! + * chota.css v0.9.2 | MIT License | https://github.com/jenil/chota + */:root{--bg-color:#fff;--bg-secondary-color:#f3f3f6;--color-primary:#14854f;--color-lightGrey:#d2d6dd;--color-grey:#747681;--color-darkGrey:#3f4144;--color-error:#d43939;--color-success:#28bd14;--grid-maxWidth:120rem;--grid-gutter:2rem;--font-size:1.6rem;--font-color:#333;--font-family-sans:-apple-system,"BlinkMacSystemFont","Avenir","Avenir Next","Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;--font-family-mono:monaco,"Consolas","Lucida Console",monospace}html{-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:62.5%;line-height:1.15}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}body{background-color:var(--bg-color);color:var(--font-color);font-family:Segoe UI,Helvetica Neue,sans-serif;font-family:var(--font-family-sans);font-size:var(--font-size);line-height:1.6;margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-weight:500;margin:.35em 0 .7em}h1{font-size:2em}h2{font-size:1.75em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1em}h6{font-size:.85em}a{color:var(--color-primary);text-decoration:none}a:hover:not(.button){opacity:.75}button{font-family:inherit}p{margin-top:0}blockquote{background-color:var(--bg-secondary-color);border-left:3px solid var(--color-lightGrey);padding:1.5rem 2rem}dl dt{font-weight:700}hr{background-color:var(--color-lightGrey);height:1px;margin:1rem 0}hr,table{border:none}table{border-collapse:collapse;border-spacing:0;text-align:left;width:100%}table.striped tr:nth-of-type(2n){background-color:var(--bg-secondary-color)}td,th{padding:1.2rem .4rem;vertical-align:middle}thead{border-bottom:2px solid var(--color-lightGrey)}tfoot{border-top:2px solid var(--color-lightGrey)}code,kbd,pre,samp,tt{font-family:var(--font-family-mono)}code,kbd{border-radius:4px;color:var(--color-error);font-size:90%;padding:.2em .4em;white-space:pre-wrap}code,kbd,pre{background-color:var(--bg-secondary-color)}pre{font-size:1em;overflow-x:auto;padding:1rem}pre code{background:none;padding:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}img{max-width:100%}fieldset{border:1px solid var(--color-lightGrey)}iframe{border:0}.container{margin:0 auto;max-width:var(--grid-maxWidth);padding:0 calc(var(--grid-gutter)/2);width:96%}.row{-webkit-box-direction:normal;-webkit-box-pack:start;-ms-flex-pack:start;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;justify-content:flex-start;margin-left:calc(var(--grid-gutter)/-2);margin-right:calc(var(--grid-gutter)/-2)}.row,.row.reverse{-webkit-box-orient:horizontal}.row.reverse{-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.col{-webkit-box-flex:1;-ms-flex:1;flex:1}.col,[class*=" col-"],[class^=col-]{margin:0 calc(var(--grid-gutter)/2) calc(var(--grid-gutter)/2)}.col-1{-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-1,.col-2{-webkit-box-flex:0}.col-2{-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3{-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-3,.col-4{-webkit-box-flex:0}.col-4{-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5{-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-5,.col-6{-webkit-box-flex:0}.col-6{-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7{-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-7,.col-8{-webkit-box-flex:0}.col-8{-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9{-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10,.col-9{-webkit-box-flex:0}.col-10{-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11{-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-11,.col-12{-webkit-box-flex:0}.col-12{-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}@media screen and (max-width:599px){.container{width:100%}.col,[class*=col-],[class^=col-]{-webkit-box-flex:0;-ms-flex:0 1 100%;flex:0 1 100%;max-width:100%}}@media screen and (min-width:900px){.col-1-md{-webkit-box-flex:0;-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-2-md{-webkit-box-flex:0;-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3-md{-webkit-box-flex:0;-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-4-md{-webkit-box-flex:0;-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5-md{-webkit-box-flex:0;-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-6-md{-webkit-box-flex:0;-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7-md{-webkit-box-flex:0;-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-8-md{-webkit-box-flex:0;-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9-md{-webkit-box-flex:0;-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10-md{-webkit-box-flex:0;-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11-md{-webkit-box-flex:0;-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-12-md{-webkit-box-flex:0;-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}}@media screen and (min-width:1200px){.col-1-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-2-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-4-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-6-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-8-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-12-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}}fieldset{padding:.5rem 2rem}legend{font-size:.8em;letter-spacing:.1rem;text-transform:uppercase}input:not([type=checkbox],[type=radio],[type=submit],[type=color],[type=button],[type=reset]),select,textarea,textarea[type=text]{border:1px solid var(--color-lightGrey);border-radius:4px;display:block;font-family:inherit;font-size:1em;padding:.8rem 1rem;-webkit-transition:all .2s ease;transition:all .2s ease;width:100%}select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#f3f3f6 no-repeat 100%;background-image:url("data:image/svg+xml;utf8,");background-origin:content-box;background-size:1ex}.button,[type=button],[type=reset],[type=submit],button{background:var(--color-lightGrey);border:1px solid transparent;border-radius:4px;color:var(--color-darkGrey);cursor:pointer;display:inline-block;font-size:var(--font-size);line-height:1;padding:1rem 2.5rem;text-align:center;text-decoration:none;-webkit-transform:scale(1);transform:scale(1);-webkit-transition:opacity .2s ease;transition:opacity .2s ease}.button.dark,.button.error,.button.primary,.button.secondary,.button.success,[type=submit]{background-color:#000;background-color:var(--color-primary);color:#fff;z-index:1}.button:hover,[type=button]:hover,[type=reset]:hover,[type=submit]:hover,button:hover{opacity:.8}button:disabled,button:disabled:hover,input:disabled,input:disabled:hover{cursor:not-allowed;opacity:.4}.grouped{display:-webkit-box;display:-ms-flexbox;display:flex}.grouped>:not(:last-child){margin-right:16px}.grouped.gapless>*{border-radius:0!important;margin:0 0 0 -1px!important}.grouped.gapless>:first-child{border-radius:4px 0 0 4px!important;margin:0!important}.grouped.gapless>:last-child{border-radius:0 4px 4px 0!important}input:not([type=checkbox],[type=radio],[type=submit],[type=color],[type=button],[type=reset],:disabled):hover,select:hover,textarea:hover,textarea[type=text]:hover{border-color:var(--color-grey)}input:not([type=checkbox],[type=radio],[type=submit],[type=color],[type=button],[type=reset]):focus,select:focus,textarea:focus,textarea[type=text]:focus{border-color:var(--color-primary);-webkit-box-shadow:0 0 1px var(--color-primary);box-shadow:0 0 1px var(--color-primary);outline:none}input.error:not([type=checkbox],[type=radio],[type=submit],[type=color],[type=button],[type=reset]),textarea.error{border-color:var(--color-error)}input.success:not([type=checkbox],[type=radio],[type=submit],[type=color],[type=button],[type=reset]),textarea.success{border-color:var(--color-success)}[type=checkbox],[type=radio]{height:1.6rem;width:2rem}.button+.button{margin-left:1rem}.button.secondary{background-color:var(--color-grey)}.button.dark{background-color:var(--color-darkGrey)}.button.error{background-color:var(--color-error)}.button.success{background-color:var(--color-success)}.button.outline{background-color:transparent;border-color:var(--color-lightGrey)}.button.outline.primary{border-color:var(--color-primary);color:var(--color-primary)}.button.outline.secondary{border-color:var(--color-grey);color:var(--color-grey)}.button.outline.dark{border-color:var(--color-darkGrey);color:var(--color-darkGrey)}.button.clear{background-color:transparent;border-color:transparent;color:var(--color-primary)}.button.icon{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.button.icon>img{margin-left:2px}.button.icon-only{padding:1rem}.button:active:not(:disabled),[type=button]:active:not(:disabled),[type=reset]:active:not(:disabled),[type=submit]:active:not(:disabled),button:active:not(:disabled){-webkit-transform:scale(.98);transform:scale(.98)}::-webkit-input-placeholder{color:#bdbfc4}::-moz-placeholder{color:#bdbfc4}:-ms-input-placeholder{color:#bdbfc4}::-ms-input-placeholder{color:#bdbfc4}::placeholder{color:#bdbfc4}.nav{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;display:-webkit-box;display:-ms-flexbox;display:flex;min-height:5rem}.nav img{max-height:3rem}.nav-center,.nav-left,.nav-right,.nav>.container{display:-webkit-box;display:-ms-flexbox;display:flex}.nav-center,.nav-left,.nav-right{-webkit-box-flex:1;-ms-flex:1;flex:1}.nav-left{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.nav-right{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.nav-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}@media screen and (max-width:480px){.nav,.nav>.container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.nav-center,.nav-left,.nav-right{-webkit-box-pack:center;-ms-flex-pack:center;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:center}}.nav .brand,.nav a{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:var(--color-darkGrey);display:-webkit-box;display:-ms-flexbox;display:flex;padding:1rem 2rem;text-decoration:none}.nav .active:not(.button),.nav [aria-current=page]:not(.button){color:#000;color:var(--color-primary)}.nav .brand{font-size:1.75em;padding-bottom:0;padding-top:0}.nav .brand img{padding-right:1rem}.nav .button{margin:auto 1rem}.card{background:var(--bg-color);border-radius:4px;-webkit-box-shadow:0 1px 3px var(--color-grey);box-shadow:0 1px 3px var(--color-grey);padding:1rem 2rem}.card p:last-child{margin:0}.card header>*{margin-bottom:1rem;margin-top:0}.tabs{display:-webkit-box;display:-ms-flexbox;display:flex}.tabs a{text-decoration:none}.tabs>.dropdown>summary,.tabs>a{-webkit-box-flex:0;border-bottom:2px solid var(--color-lightGrey);color:var(--color-darkGrey);-ms-flex:0 1 auto;flex:0 1 auto;padding:1rem 2rem;text-align:center}.tabs>a.active,.tabs>a:hover,.tabs>a[aria-current=page]{border-bottom:2px solid var(--color-darkGrey);opacity:1}.tabs>a.active,.tabs>a[aria-current=page]{border-color:var(--color-primary)}.tabs.is-full a{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.tag{border:1px solid var(--color-lightGrey);color:var(--color-grey);display:inline-block;letter-spacing:.5px;line-height:1;padding:.5rem;text-transform:uppercase}.tag.is-small{font-size:.75em;padding:.4rem}.tag.is-large{font-size:1.125em;padding:.7rem}.tag+.tag{margin-left:1rem}details.dropdown{display:inline-block;position:relative}details.dropdown>:last-child{left:0;position:absolute;white-space:nowrap}.bg-primary{background-color:var(--color-primary)!important}.bg-light{background-color:var(--color-lightGrey)!important}.bg-dark{background-color:var(--color-darkGrey)!important}.bg-grey{background-color:var(--color-grey)!important}.bg-error{background-color:var(--color-error)!important}.bg-success{background-color:var(--color-success)!important}.bd-primary{border:1px solid var(--color-primary)!important}.bd-light{border:1px solid var(--color-lightGrey)!important}.bd-dark{border:1px solid var(--color-darkGrey)!important}.bd-grey{border:1px solid var(--color-grey)!important}.bd-error{border:1px solid var(--color-error)!important}.bd-success{border:1px solid var(--color-success)!important}.text-primary{color:var(--color-primary)!important}.text-light{color:var(--color-lightGrey)!important}.text-dark{color:var(--color-darkGrey)!important}.text-grey{color:var(--color-grey)!important}.text-error{color:var(--color-error)!important}.text-success{color:var(--color-success)!important}.text-white{color:#fff!important}.pull-right{float:right!important}.pull-left{float:left!important}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-justify{text-align:justify}.text-uppercase{text-transform:uppercase}.text-lowercase{text-transform:lowercase}.text-capitalize{text-transform:capitalize}.is-full-screen{min-height:100vh;width:100%}.is-full-width{width:100%!important}.is-vertical-align{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.is-center,.is-horizontal-align{-webkit-box-pack:center;-ms-flex-pack:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center}.is-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.is-right{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.is-left,.is-right{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.is-left{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.is-fixed{position:fixed;width:100%}.is-paddingless{padding:0!important}.is-marginless{margin:0!important}.is-pointer{cursor:pointer!important}.is-rounded{border-radius:100%}.clearfix{clear:both;content:"";display:table}.is-hidden{display:none!important}@media screen and (max-width:599px){.hide-xs{display:none!important}}@media screen and (min-width:600px) and (max-width:899px){.hide-sm{display:none!important}}@media screen and (min-width:900px) and (max-width:1199px){.hide-md{display:none!important}}@media screen and (min-width:1200px){.hide-lg{display:none!important}}@media print{.hide-pr{display:none!important}} \ No newline at end of file diff --git a/vendor/docc/docc/plugins/html/static/fuse/dist/fuse.min.js b/vendor/docc/docc/plugins/html/static/fuse/dist/fuse.min.js new file mode 100644 index 0000000000..adc28356e2 --- /dev/null +++ b/vendor/docc/docc/plugins/html/static/fuse/dist/fuse.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2022 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(C).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}var $=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?I.getFn:n,o=t.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o;r(this,e),this.norm=E(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?I.getFn:r,o=n.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o,a=new $({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(_)),a.setSources(t),a.create(),a}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?I.distance:s,h=t.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}function N(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:I.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}var P=32;function W(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?I.location:o,a=i.threshold,s=void 0===a?I.threshold:a,u=i.distance,h=void 0===u?I.distance:u,l=i.includeMatches,f=void 0===l?I.includeMatches:l,d=i.findAllMatches,v=void 0===d?I.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?I.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?I.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?I.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:W(e),startIndex:t})},x=this.pattern.length;if(x>P){for(var w=0,L=x%P,S=x-L;w3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?I.location:i,c=r.distance,a=void 0===c?I.distance:c,s=r.threshold,u=void 0===s?I.threshold:s,h=r.findAllMatches,l=void 0===h?I.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?I.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?I.includeMatches:v,y=r.ignoreLocation,p=void 0===y?I.ignoreLocation:y;if(t.length>P)throw new Error(w(P));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,L=b,S=d>1||g,_=S?Array(M):[];(m=e.indexOf(t,L))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),L=m+k,S)for(var j=0;j=z;q-=1){var B=q-1,J=n[e.charAt(B)];if(S&&(_[B]=+!!J),K[q]=(K[q+1]<<1|1)&J,F&&(K[q]|=(A[q+1]|A[q])<<1|1|A[q+1]),K[q]&$&&(C=R(t,{errors:F,currentLocation:B,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(L=B)<=b)break;z=Math.max(1,2*b-L)}}if(R(t,{errors:F+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;A=K}var U={isMatch:L>=0,score:Math.max(.001,C)};if(S){var V=N(_,d);V.length?g&&(U.indices=V):U.isMatch=!1}return U}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),z=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return D(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return D(e,this.singleRegex)}}]),e}();function D(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(z),q=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(z),B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(z),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(z),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(z),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(z),G=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?I.location:c,s=o.threshold,u=void 0===s?I.threshold:s,h=o.distance,l=void 0===h?I.distance:h,f=o.includeMatches,d=void 0===f?I.includeMatches:f,v=o.findAllMatches,g=void 0===v?I.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?I.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?I.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?I.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new T(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(z),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(z),Q=[K,H,B,J,V,U,q,G],X=Q.length,Y=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;function Z(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Y).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?I.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?I.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?I.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?I.findAllMatches:f,v=n.location,g=void 0===v?I.location:v,y=n.threshold,p=void 0===y?I.threshold:y,m=n.distance,k=void 0===m?I.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=Z(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ve(e,t){t.score=e.score}function ge(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?I.includeMatches:r,o=n.includeScore,c=void 0===o?I.includeScore:o,a=[];return i&&a.push(de),c&&a.push(ve),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ye=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},I),i),this.options.useExtendedSearch,this._keyStore=new S(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof $))throw new Error("Incorrect 'index' type");this._myIndex=t||F(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return fe(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),ge(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=re(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n,i=function e(n){var i=Object.keys(n),o=ue(n);if(!o&&i.length>1&&!se(n))return e(le(n));if(he(n)){var c=o?n[ce]:i[0],a=o?n[ae]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return r&&(s.searcher=re(a,t)),s}var u={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return se(e)||(e=le(e)),i(e)}(e,this.options),r=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?I.getFn:n,i=t.fieldNormWeight,o=void 0===i?I.fieldNormWeight:i,c=e.keys,a=e.records,s=new $({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ye.config=I,function(){ne.push.apply(ne,arguments)}(te),ye},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t(); \ No newline at end of file