Skip to content

Commit 574458f

Browse files
authored
✨ NEW: Add tableofcontents directive (#11)
This is a re-working of jupyter-book/jupyter-book#757 (by @choldgraf and @AakashGfude), but improved since the replacement is made in the `doctree-read` phase (rather than a post transform) and so does not have to deal with any builder specific logic. Also better warnings and testing 😉 Note, the current implementation does not wrap the toctrees in `compound(classes=["tableofcontents-wrapper"])`, like in jupyter-book, but I feel this is unnecessary.
1 parent 10246dc commit 574458f

19 files changed

+197
-22
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
exclude: >
55
(?x)^(
66
\.vscode/settings\.json|
7+
tests/.*xml|
78
)$
89
910
repos:

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ main:
114114

115115
You can also **limit the TOC numbering depth** by setting the `numbered` flag to an integer instead of `true`, e.g., `numbered: 3`.
116116

117+
:::{note}
118+
By default, section numbering restarts for each `part`.
119+
If you want want this numbering to be continuous, check-out the [sphinx-multitoc-numbering extension](https://github.com/executablebooks/sphinx-multitoc-numbering).
120+
:::
121+
117122
### Defaults
118123

119124
To have e.g. `numbered` added to all toctrees, set it under a `defaults` top-level key:
@@ -132,6 +137,26 @@ main:
132137

133138
Available keys: `numbered`, `titlesonly`, `reversed`
134139

140+
## Add a ToC to a page's content
141+
142+
By default, the `toctree` generated per document (one per `part`) are appended to the end of the document and hidden (then, for example, most HTML themes show them in a side-bar).
143+
But if you would like them to be visible at a certain place within the document body, you may do so by using the `tableofcontents` directive:
144+
145+
ReStructuredText:
146+
147+
```restructuredtext
148+
.. tableofcontents::
149+
```
150+
151+
MyST Markdown:
152+
153+
````md
154+
```{tableofcontents}
155+
```
156+
````
157+
158+
Currently, only one `tableofcontents` should be used per page (all `toctree` will be added here), and only if it is a page with child/descendant documents.
159+
135160
## Excluding files not in ToC
136161

137162
By default, Sphinx will build all document files, regardless of whether they are specified in the Table of Contents, if they:
@@ -241,7 +266,7 @@ Questions / TODOs:
241266
it will add `doc.rst` to the excluded patterns but then, when looking for `doc.md`,
242267
will still select `doc.rst` (since it is first in `source_suffix`).
243268
Maybe open an issue on sphinx, that `doc2path` should respect exclude patterns.
244-
269+
- Intergrate https://github.com/executablebooks/sphinx-multitoc-numbering into this extension? (or upstream PR)
245270

246271
[github-ci]: https://github.com/executablebooks/sphinx-external-toc/workflows/continuous-integration/badge.svg?branch=main
247272
[github-link]: https://github.com/executablebooks/sphinx-external-toc

docs/_toc.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ defaults:
44
main:
55
file: intro
66
title: Introduction
7-
sections:
8-
- file: doc1
9-
- file: doc2
10-
sections:
11-
- file: subfolder/doc3
12-
- url: https://example.com
13-
title: Example Link
14-
- glob: subglobs/glob*
7+
parts:
8+
- sections:
9+
- file: doc1
10+
- file: doc2
11+
sections:
12+
- file: subfolder/doc3
13+
- url: https://example.com
14+
title: Example Link
15+
- sections:
16+
- glob: subglobs/glob*

docs/intro.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# sphinx-external-toc [IN-DEVELOPMENT]
22

3-
A sphinx extension that allows the documentation toctree to be defined in a single file.
3+
A sphinx extension that allows the documentation toctree to be defined in a single YAML file.
44

55
In normal Sphinx documentation, the documentation structure is defined *via* a bottom-up approach - adding [`toctree` directives](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#table-of-contents) within pages of the documentation.
66

77
This extension facilitates a **top-down** approach to defining the structure, within a single file that is external to the documentation.
8+
9+
```{tableofcontents}
10+
```

sphinx_external_toc/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111

1212
def setup(app: "Sphinx") -> dict:
1313
"""Initialize the Sphinx extension."""
14-
from .events import add_changed_toctrees, append_toctrees, parse_toc_to_env
14+
from .events import (
15+
add_changed_toctrees,
16+
insert_toctrees,
17+
parse_toc_to_env,
18+
TableofContents,
19+
)
1520

1621
# variables
1722
app.add_config_value("external_toc_path", "_toc.yml", "env")
@@ -22,7 +27,8 @@ def setup(app: "Sphinx") -> dict:
2227
# it will always mark the config as changed in the env setup and re-build everything
2328
app.connect("config-inited", parse_toc_to_env, priority=900)
2429
app.connect("env-get-outdated", add_changed_toctrees)
30+
app.add_directive("tableofcontents", TableofContents)
2531
# Note: this needs to occur before `TocTreeCollector.process_doc` (default priority 500)
26-
app.connect("doctree-read", append_toctrees, priority=100)
32+
app.connect("doctree-read", insert_toctrees, priority=100)
2733

2834
return {"version": __version__, "parallel_read_safe": True}

sphinx_external_toc/events.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
"""Sphinx event functions."""
1+
"""Sphinx event functions and directives."""
22
import glob
33
from pathlib import Path
4-
from typing import List, Optional, Set
4+
from typing import Any, List, Optional, Set
55

66
from docutils import nodes
77
from sphinx.addnodes import toctree as toctree_node
@@ -10,6 +10,7 @@
1010
from sphinx.environment import BuildEnvironment
1111
from sphinx.errors import ExtensionError
1212
from sphinx.util import docname_join, logging
13+
from sphinx.util.docutils import SphinxDirective
1314
from sphinx.util.matching import Matcher, patfilter, patmatch
1415

1516
from .api import DocItem, GlobItem, FileItem, SiteMap, UrlItem, parse_toc_file
@@ -48,7 +49,6 @@ def parse_toc_to_env(app: Sphinx, config: Config) -> None:
4849
4950
Also, change the ``master_doc`` and add to ``exclude_patterns`` if necessary.
5051
"""
51-
print(config["exclude_patterns"])
5252
try:
5353
site_map = parse_toc_file(Path(app.srcdir) / app.config["external_toc_path"])
5454
except Exception as exc:
@@ -118,7 +118,21 @@ def add_changed_toctrees(
118118
return changed_docs
119119

120120

121-
def append_toctrees(app: Sphinx, doctree: nodes.document) -> None:
121+
class TableOfContentsNode(nodes.Element):
122+
"""A placeholder for the insertion of a toctree (in ``append_toctrees``)."""
123+
124+
def __init__(self, **attributes: Any) -> None:
125+
super().__init__(rawsource="", **attributes)
126+
127+
128+
class TableofContents(SphinxDirective):
129+
# TODO allow for name option of tableofcontents (to reference it)
130+
def run(self) -> List[TableOfContentsNode]:
131+
"""Insert a ``TableOfContentsNode`` node."""
132+
return [TableOfContentsNode()]
133+
134+
135+
def insert_toctrees(app: Sphinx, doctree: nodes.document) -> None:
122136
"""Create the toctree nodes and add it to the document.
123137
124138
Adapted from `sphinx/directives/other.py::TocTree`
@@ -133,18 +147,44 @@ def append_toctrees(app: Sphinx, doctree: nodes.document) -> None:
133147
line=node.line,
134148
)
135149

150+
toc_placeholders: List[TableOfContentsNode] = list(
151+
doctree.traverse(TableOfContentsNode)
152+
)
153+
136154
site_map: SiteMap = app.env.external_site_map
137155
doc_item: Optional[DocItem] = site_map.get(app.env.docname)
138156

139157
if doc_item is None or not doc_item.parts:
158+
if toc_placeholders:
159+
create_warning(
160+
app,
161+
doctree,
162+
"tableofcontents",
163+
"tableofcontents directive in document with no descendants",
164+
)
165+
for node in toc_placeholders:
166+
node.replace_self([])
140167
return
141168

169+
# TODO allow for more than one tableofcontents, i.e. per part?
170+
for node in toc_placeholders[1:]:
171+
create_warning(
172+
app,
173+
doctree,
174+
"tableofcontents",
175+
"more than one tableofcontents directive in document",
176+
line=node.line,
177+
)
178+
node.replace_self([])
179+
142180
# initial variables
143181
suffixes = app.config.source_suffix
144182
all_docnames = app.env.found_docs.copy()
145183
all_docnames.remove(app.env.docname) # remove current document
146184
excluded = Matcher(app.config.exclude_patterns)
147185

186+
node_list: List[nodes.Element] = []
187+
148188
for toctree in doc_item.parts:
149189

150190
subnode = toctree_node()
@@ -158,15 +198,13 @@ def append_toctrees(app: Sphinx, doctree: nodes.document) -> None:
158198
# but alabaster theme intermittently raised `KeyError('rawcaption')`
159199
subnode["rawcaption"] = toctree.caption or ""
160200
subnode["glob"] = any(isinstance(entry, GlobItem) for entry in toctree.sections)
161-
subnode["hidden"] = True
201+
subnode["hidden"] = False if toc_placeholders else True
162202
subnode["includehidden"] = False
163203
subnode["numbered"] = toctree.numbered
164204
subnode["titlesonly"] = toctree.titlesonly
165205
wrappernode = nodes.compound(classes=["toctree-wrapper"])
166206
wrappernode.append(subnode)
167207

168-
node_list: List[nodes.Element] = []
169-
170208
for entry in toctree.sections:
171209

172210
if isinstance(entry, UrlItem):
@@ -217,7 +255,10 @@ def append_toctrees(app: Sphinx, doctree: nodes.document) -> None:
217255

218256
node_list.append(wrappernode)
219257

220-
# note here the toctree cannot not just be appended to the end of the document,
258+
if toc_placeholders:
259+
toc_placeholders[0].replace_self(node_list)
260+
else:
261+
# note here the toctree cannot not just be appended to the end of the doc,
221262
# since `TocTreeCollector.process_doc` expects it in a section
222263
# TODO check if there is this is always ok
223264
doctree.children[-1].extend(node_list)

tests/_toc_files/basic.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ main:
1313
- sections:
1414
- file: subfolder/doc4
1515
- url: https://example.com
16+
meta:
17+
regress: intro

tests/_toc_files/basic_compressed.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ main:
1010
sections:
1111
- file: doc4
1212
- url: https://example.com
13+
meta:
14+
regress: intro
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
main:
2+
file: intro
3+
parts:
4+
- sections:
5+
- file: doc1
6+
- sections:
7+
- file: doc2
8+
meta:
9+
create_append:
10+
intro: |
11+
.. tableofcontents::
12+
regress: intro
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
main:
2+
file: intro
3+
sections:
4+
- file: doc1
5+
meta:
6+
create_append:
7+
intro: |
8+
.. tableofcontents::
9+
10+
.. tableofcontents::
11+
expected_warning: more than one tableofcontents directive

0 commit comments

Comments
 (0)