Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ docc.plugins =
docc.mistletoe.reference = docc.plugins.mistletoe:ReferenceTransform
docc.listing.discover = docc.plugins.listing:ListingDiscover
docc.listing.build = docc.plugins.listing:ListingBuilder
docc.listing.context = docc.plugins.listing:ListingContext
docc.listing.transform = docc.plugins.listing:ListingTransform
docc.resources.build = docc.plugins.resources:ResourceBuilder
docc.files.build = docc.plugins.files:FilesBuilder
docc.files.discover = docc.plugins.files:FilesDiscover
Expand Down
2 changes: 1 addition & 1 deletion src/docc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
The documentation compiler.
"""

__version__ = "0.4.1"
__version__ = "0.5.0"
"Current version of docc"
190 changes: 174 additions & 16 deletions src/docc/plugins/listing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2023 Ethereum Foundation
# Copyright (C) 2023,2026 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
Expand All @@ -18,26 +18,64 @@
"""

from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Iterable
from os.path import commonpath
from pathlib import PurePath
from typing import Dict, Final, FrozenSet, Iterator, Set, Tuple
from typing import (
TYPE_CHECKING,
Any,
Dict,
Final,
FrozenSet,
Iterator,
Set,
Tuple,
Type,
Union,
)

if TYPE_CHECKING:
from _typeshed import SupportsDunderGT, SupportsDunderLT

from jinja2 import Environment, PackageLoader, select_autoescape

from docc.build import Builder
from docc.context import Context
from docc.context import Context, Provider
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
from docc.transform import Transform


class Listable(ABC):
"""
Mixin to change visibility of a Source in a directory listing.
"""

@staticmethod
def _sorting_key(
thing: object,
) -> Union["SupportsDunderGT[Any]", "SupportsDunderLT[Any]"]:
if isinstance(thing, Listable):
return thing.listing_order_key()
elif isinstance(thing, Source):
path = thing.relative_path or thing.output_path
return (True, path, None)
return (True, None, thing)

@staticmethod
def _show_source(source: Source) -> bool:
if isinstance(source, Listable):
if not source.show_in_listing:
return False
elif not source.relative_path:
return False

return True

@property
@abstractmethod
def show_in_listing(self) -> bool:
Expand All @@ -46,6 +84,24 @@ def show_in_listing(self) -> bool:
"""
raise NotImplementedError()

@property
def is_leaf(self) -> bool:
"""
`True` if this `Source` cannot contain child sources (eg. a file).
"""
return True

def listing_order_key(
self,
) -> Union["SupportsDunderGT[Any]", "SupportsDunderLT[Any]"]:
"""
Key to use when sorting instances while rendering.
"""
if isinstance(self, Source):
path = self.relative_path or self.output_path
return (self.is_leaf, path, None)
return (self.is_leaf, None, self)


class ListingDiscover(Discover):
"""
Expand All @@ -62,13 +118,10 @@ def discover(self, known: FrozenSet[T]) -> Iterator["ListingSource"]:
listings = {}

for source in known:
path = source.relative_path
if isinstance(source, Listable):
if not source.show_in_listing:
continue
elif not path:
if not Listable._show_source(source):
continue

path = source.relative_path
if not path:
path = source.output_path

Expand All @@ -84,7 +137,73 @@ def discover(self, known: FrozenSet[T]) -> Iterator["ListingSource"]:
source = listing


class ListingSource(Source):
class Listing:
"""
Tracks listable [`Source`]s.

[`Source`]: ref:docc.source.Source
"""

sources: Final[Dict[PurePath, Set[Source]]]

def __init__(self) -> None:
self.sources = defaultdict(set)

def add_source(self, source: Source) -> None:
"""
Register a source.
"""
path = source.relative_path or source.output_path
self.sources[path.parent].add(source)

def descendants(self, source: Source) -> Iterable[Source]:
"""
All children of the given source.
"""
source_path = source.relative_path or source.output_path
return self.sources[source_path]

def siblings(self, source: Source) -> Iterable[Source]:
"""
All sources with the same parent as the given source.
"""
source_path = source.relative_path or source.output_path
return self.sources[source_path.parent]


class ListingContext(Provider[Listing]):
"""
Injects a [`Listing`] instance into the [`Context`].

[`Listing`]: ref:docc.plugins.listing.Listing
[`Context`]: ref:docc.context.Context
"""

listing: Listing

def __init__(self, config: PluginSettings) -> None:
super().__init__(config)
self.listing = Listing()

@classmethod
def provides(class_) -> Type[Listing]:
"""
Return the type used as a key in the [`Context`].

[`Context`]: ref:docc.context.Context
"""
return Listing

def provide(self) -> Listing:
"""
Return the object to add to the [`Context`].

[`Context`]: ref:docc.context.Context
"""
return self.listing


class ListingSource(Source, Listable):
"""
A synthetic source that describes the contents of a directory.
"""
Expand All @@ -93,6 +212,9 @@ class ListingSource(Source):
_output_path: Final[PurePath]
sources: Final[Set[Source]]

show_in_listing: bool = True
is_leaf: bool = False

def __init__(
self,
relative_path: PurePath,
Expand Down Expand Up @@ -142,18 +264,18 @@ def build(
unprocessed -= to_process

for source in to_process:
processed[source] = Document(ListingNode(source.sources))
processed[source] = Document(ListingNode(leaf=False))


class ListingNode(Node):
"""
A node representing a directory listing.
"""

sources: Final[Set[Source]]
leaf: bool

def __init__(self, sources: Set[Source]) -> None:
self.sources = sources
def __init__(self, leaf: bool) -> None:
self.leaf = leaf

@property
def children(self) -> Tuple[()]:
Expand All @@ -169,6 +291,29 @@ def replace_child(self, old: Node, new: Node) -> None:
raise TypeError()


class ListingTransform(Transform):
"""
Collect [`Source`]s and insert them into the [`Listing`] context.

[`Source`]: ref:docc.source.Source
[`Listing`]: ref:docc.plugins.listing.Listing
"""

def __init__(self, config: PluginSettings) -> None:
pass

def transform(self, context: Context) -> None:
"""
Apply the transformation to the given document.
"""
source = context[Source]
if not Listable._show_source(source):
return

listing = context[Listing]
listing.add_source(source)


def render_html(
context: object,
parent: object,
Expand All @@ -181,10 +326,17 @@ def render_html(
assert isinstance(parent, (html.HTMLRoot, html.HTMLTag))
assert isinstance(node, ListingNode)

if node.leaf:
sources = context[Listing].siblings(context[Source])
else:
sources = context[Listing].descendants(context[Source])

sources = sorted(sources, key=Listable._sorting_key)

output_path = context[Source].output_path
entries = []

for source in node.sources:
for source in sources:
entry_path = source.output_path

if output_path == entry_path:
Expand All @@ -202,10 +354,16 @@ def render_html(
+ ".html"
) # TODO: Don't hardcode extension.

active = source is context[Source]
path = source.relative_path or source.output_path
entries.append((path, relative_path))

entries.sort()
if node.leaf:
path = path.name

if isinstance(source, Listable) and not source.is_leaf:
path = str(path) + "/"

entries.append((path, relative_path, active))

env = Environment(
loader=PackageLoader("docc.plugins.listing"),
Expand Down
12 changes: 9 additions & 3 deletions src/docc/plugins/listing/templates/listing.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{#
# Copyright (C) 2023 Ethereum Foundation
# Copyright (C) 2023,2026 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
Expand All @@ -16,8 +16,14 @@
-#}
<nav>
<ul>
{% for (path, href) in entries %}
<li><a href="{{ href }}">{{ path }}</a></li>
{% for (path, href, active) in entries %}
<li>
{% if active %}
<span>{{ path }}</span>
{% else %}
<a href="{{ href }}">{{ path }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
4 changes: 3 additions & 1 deletion src/docc/plugins/python/cst.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2022-2023 Ethereum Foundation
# Copyright (C) 2022-2023,2026 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
Expand Down Expand Up @@ -48,6 +48,7 @@
from docc.context import Context
from docc.discover import Discover, T
from docc.document import BlankNode, Document, ListNode, Node, Visit, Visitor
from docc.plugins.listing import ListingNode
from docc.plugins.references import Definition, Reference
from docc.plugins.verbatim import Fragment, Pos, Verbatim
from docc.settings import PluginSettings
Expand Down Expand Up @@ -497,6 +498,7 @@ def push_new(self, node: Node) -> None:
def enter_module(self, node: CstNode, cst_node: cst.Module) -> Visit:
assert 0 == len(self.new_stack)
module = nodes.Module()
module.listing = ListingNode(leaf=True)

names = sorted(node.names)

Expand Down
3 changes: 2 additions & 1 deletion src/docc/plugins/python/nodes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2022-2023 Ethereum Foundation
# Copyright (C) 2022-2023,2026 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
Expand Down Expand Up @@ -71,6 +71,7 @@ class Module(PythonNode, Searchable):
A Python module.
"""

listing: Node = dataclasses.field(default_factory=BlankNode)
name: Node = dataclasses.field(default_factory=BlankNode)
docstring: Node = dataclasses.field(default_factory=BlankNode)
members: Node = dataclasses.field(default_factory=ListNode)
Expand Down
Loading
Loading