Skip to content

Commit d0b61fc

Browse files
show list of contributors and their articles
1 parent cbbe707 commit d0b61fc

2 files changed

Lines changed: 117 additions & 6 deletions

File tree

source/_ext/contributors.py

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
11
from dataclasses import dataclass
2+
import enum
23
from pathlib import Path
4+
from typing import NamedTuple
35

46
import docutils.nodes as nodes
57
from docutils.parsers.rst import directives
68
from sphinx.application import Sphinx
9+
from sphinx.environment import BuildEnvironment
710
from sphinx.util.docutils import SphinxDirective
811
import yaml
912

13+
Handle = str
14+
Docname = str
15+
16+
17+
class Role(enum.Enum):
18+
author = "authors"
19+
editor = "editors"
20+
21+
22+
class ArticleCredit(NamedTuple):
23+
role: Role
24+
docname: Docname
25+
26+
27+
ContributorRoles = dict[Handle, list[ArticleCredit]]
28+
1029

1130
@dataclass
1231
class Contributor:
@@ -57,24 +76,110 @@ def contributors_field(label: str, people: list[Contributor]) -> nodes.field:
5776

5877

5978
class ContributorsDirective(SphinxDirective):
60-
option_spec = {
61-
"authors": directives.unchanged,
62-
"editors": directives.unchanged,
63-
}
79+
option_spec = {role.value: directives.unchanged for role in Role}
6480

6581
def run(self) -> list[nodes.Node]:
82+
if not hasattr(self.env, "contributors_data"):
83+
self.env.contributors_data = {}
84+
6685
field_list = nodes.field_list(classes=["contributors"])
67-
for option, label in [("authors", "Author"), ("editors", "Editor")]:
68-
raw = self.options.get(option, "")
86+
for role, label in [(Role.author, "Author"), (Role.editor, "Editor")]:
87+
raw = self.options.get(role.value, "")
6988
handles = [h.strip() for h in raw.split(",") if h.strip()]
7089
people = resolve(handles)
7190
if len(people) > 1:
7291
label += "s"
7392
if people:
7493
field_list += contributors_field(label, people)
94+
95+
# record the contributor's role on the current document
96+
for handle in handles:
97+
self.env.contributors_data.setdefault(handle, []).append(
98+
ArticleCredit(role, self.env.docname)
99+
)
100+
75101
return [field_list] if field_list.children else []
76102

77103

104+
def position_in_toc(env: BuildEnvironment) -> dict[str, int]:
105+
"""
106+
Annotate items in the table of contents with their depth-first linearisation order
107+
"""
108+
order = {}
109+
stack = [env.config.root_doc]
110+
while stack:
111+
docname = stack.pop()
112+
if docname in order:
113+
continue
114+
order[docname] = len(order)
115+
stack.extend(reversed(env.toctree_includes.get(docname, [])))
116+
return order
117+
118+
119+
class ContributorsIndex(nodes.General, nodes.Element):
120+
def render(
121+
self, app: Sphinx, fromdocname: Docname, data: ContributorRoles
122+
) -> nodes.definition_list:
123+
env = app.builder.env
124+
toc_position = position_in_toc(env)
125+
dl = nodes.definition_list()
126+
for handle in sorted(
127+
(h for h in _registry if data.get(h)),
128+
key=lambda h: -len(data[h]),
129+
):
130+
entries = data[handle]
131+
item = nodes.definition_list_item()
132+
dt = nodes.term()
133+
dt += _registry[handle].as_reference()
134+
item += dt
135+
dd = nodes.definition()
136+
field_list = nodes.field_list()
137+
for role, label in [(Role.author, "Author"), (Role.editor, "Editor")]:
138+
# sort by order of occurrence
139+
docs = sorted(
140+
(doc for r, doc in entries if r == role),
141+
key=lambda d: toc_position.get(d, float("inf")),
142+
)
143+
if not docs:
144+
continue
145+
p = nodes.paragraph()
146+
for i, docname in enumerate(docs):
147+
if i > 0:
148+
p += nodes.Text(", ")
149+
title = env.titles.get(docname)
150+
uri = app.builder.get_relative_uri(fromdocname, docname)
151+
p += nodes.reference(
152+
"",
153+
title.astext() if title else docname,
154+
internal=True,
155+
refuri=uri,
156+
)
157+
field_list += nodes.field(
158+
"", nodes.field_name("", label), nodes.field_body("", p)
159+
)
160+
dd += field_list
161+
item += dd
162+
dl += item
163+
return dl
164+
165+
166+
class ContributorsIndexDirective(SphinxDirective):
167+
def run(self) -> list[nodes.Node]:
168+
return [ContributorsIndex()]
169+
170+
171+
def process_contributors_index(
172+
app: Sphinx, doctree: nodes.document, fromdocname: str
173+
) -> None:
174+
env = app.builder.env
175+
contributors = getattr(env, "contributors_data", {})
176+
for node in doctree.findall(ContributorsIndex):
177+
node.replace_self([node.render(app, fromdocname, contributors)])
178+
179+
78180
def setup(app: Sphinx) -> dict:
79181
app.add_directive("contributors", ContributorsDirective)
182+
app.add_directive("contributors-index", ContributorsIndexDirective)
183+
app.add_node(ContributorsIndex)
184+
app.connect("doctree-resolved", process_contributors_index)
80185
return {}

source/acknowledgements/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
(acknowledgements)=
22
# Acknowledgements
33

4+
```{toctree}
5+
:hidden:
6+
7+
contributors.md
8+
```
9+
410
## Sponsoring
511

612
The following people and organisations have contributed to make this effort possible:

0 commit comments

Comments
 (0)