Skip to content

Commit d5dcbd8

Browse files
authored
✨ NEW: Improve option parsing, add jupyter-book migration (#18)
1 parent e0df890 commit d5dcbd8

39 files changed

+658
-63
lines changed

README.md

+67-28
Original file line numberDiff line numberDiff line change
@@ -84,60 +84,100 @@ sections:
8484
- glob: subfolder/other*
8585
```
8686

87-
### Titles and Captions
87+
### File and URL titles
8888

89-
By default, ToCs will use the initial header within a document as its title.
90-
91-
With the `title` key you can set an alternative title for a document or URL in the ToC.
92-
Each part can also have a `caption`, e.g. for use in ToC side-bars:
89+
By default, the initial header within a `file` document will be used as its title in generated Table of Contents.
90+
With the `title` key you can set an alternative title for a document. and also for `url`:
9391

9492
```yaml
9593
root: intro
9694
parts:
97-
- caption: Part Caption
98-
sections:
95+
- sections:
9996
- file: doc1
100-
title: Document 1
97+
title: Document 1 Title
10198
- url: https://example.com
102-
title: Example Site
99+
title: Example URL Title
103100
```
104101

105-
### Numbering
102+
### ToC tree (part) options
106103

107-
You can automatically add numbers to all docs with a part by adding the `numbered: true` flag to it:
104+
Each part can be configured with a number of options (see also [sphinx `toctree` options](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-toctree)):
105+
106+
- `caption` (string): A title for the whole the part, e.g. shown above the part in ToCs
107+
- `hidden` (boolean): Whether to show the ToC within (inline of) the document (default `False`).
108+
By default it is appended to the end of the document, but see also the `tableofcontents` directive for positioning of the ToC.
109+
- `maxdepth` (integer): A maximum nesting depth to use when showing the ToC within the document.
110+
- `numbered` (boolean or integer): Automatically add numbers to all documents within a part (default `False`).
111+
If set to `True`, all sub-parts will also be numbered based on nesting (e.g. with `1.1` or `1.1.1`),
112+
or if set to an integer then the numbering will only be applied to that depth.
113+
- `reversed` (boolean): If `True` then the entries in the part will be listed in reverse order.
114+
This can be useful when using `glob` sections.
115+
- `titlesonly` (boolean): If `True` then only the first heading in the document will be shown in the ToC, not other headings of the same level.
116+
117+
These options can be set at the level of the part:
108118

109119
```yaml
110120
root: intro
111121
parts:
112-
- numbered: true
122+
- caption: Part Caption
123+
hidden: False
124+
maxdepth: 1
125+
numbered: True
126+
reversed: False
127+
titlesonly: True
113128
sections:
114129
- file: doc1
115-
- file: doc2
130+
parts:
131+
- titlesonly: True
132+
sections:
133+
- file: doc2
116134
```
117135

118-
You can also **limit the TOC numbering depth** by setting the `numbered` flag to an integer instead of `true`, e.g., `numbered: 3`.
119-
120-
:::{note}
121-
By default, section numbering restarts for each `part`.
122-
If you want want this numbering to be continuous, check-out the [sphinx-multitoc-numbering extension](https://github.com/executablebooks/sphinx-multitoc-numbering).
123-
:::
136+
or, if you are using the shorthand for a single part, set options under an `options` key:
124137

125-
### Defaults
138+
```yaml
139+
root: intro
140+
options:
141+
caption: Part Caption
142+
hidden: False
143+
maxdepth: 1
144+
numbered: True
145+
reversed: False
146+
titlesonly: True
147+
sections:
148+
- file: doc1
149+
options:
150+
titlesonly: True
151+
sections:
152+
- file: doc2
153+
```
126154

127-
To have e.g. `numbered` added to all toctrees, set it under a `defaults` top-level key:
155+
You can also use the top-level `defaults` key, to set default options for all parts:
128156

129157
```yaml
130-
defaults:
131-
numbered: true
132158
root: intro
159+
defaults:
160+
titlesonly: True
161+
options:
162+
caption: Part Caption
163+
hidden: False
164+
maxdepth: 1
165+
numbered: True
166+
reversed: False
133167
sections:
134168
- file: doc1
135169
sections:
136170
- file: doc2
137-
- url: https://example.com
138171
```
139172

140-
Available keys: `numbered`, `titlesonly`, `reversed`
173+
:::{warning}
174+
`numbered` should not generally be used as a default, since numbering cannot be changed by nested parts, and sphinx will log a warning.
175+
:::
176+
177+
:::{note}
178+
By default, section numbering restarts for each `part`.
179+
If you want want this numbering to be continuous, check-out the [sphinx-multitoc-numbering extension](https://github.com/executablebooks/sphinx-multitoc-numbering).
180+
:::
141181

142182
## Add a ToC to a page's content
143183

@@ -159,6 +199,8 @@ MyST Markdown:
159199

160200
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.
161201

202+
Note, this will override the `hidden` option set for a part.
203+
162204
## Excluding files not in ToC
163205

164206
By default, Sphinx will build all document files, regardless of whether they are specified in the Table of Contents, if they:
@@ -313,9 +355,6 @@ intro:
313355

314356
Questions / TODOs:
315357

316-
- add migration CLI command for old jupyter-book format
317-
- Should `titlesonly` default to `True` (as in jupyter-book)?
318-
- nested numbered toctree not allowed (logs warning), so should be handled if `numbered: true` is in defaults
319358
- Add additional top-level keys, e.g. `appendices` (see https://github.com/sphinx-doc/sphinx/issues/2502) and `bibliography`
320359
- Using `external_toc_exclude_missing` to exclude a certain file suffix:
321360
currently if you had files `doc.md` and `doc.rst`, and put `doc.md` in your ToC,

codecov.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ coverage:
22
status:
33
project:
44
default:
5-
target: 85%
5+
target: 90%
66
threshold: 0.2%
77
patch:
88
default:

docs/_toc.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
root: intro
12
defaults:
2-
numbered: 3
33
titlesonly: false
4-
root: intro
54
parts:
65
- caption: Part 1
6+
numbered: true
77
sections:
88
- file: doc1
99
- file: doc2
@@ -12,5 +12,6 @@ parts:
1212
- url: https://example.com
1313
title: Example Link
1414
- caption: Part 2
15+
numbered: true
1516
sections:
1617
- glob: subglobs/glob*

sphinx_external_toc/api.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ class TocItem:
3737
instance_of((GlobItem, FileItem, UrlItem)), instance_of(list)
3838
)
3939
)
40-
caption: Optional[str] = attr.ib(None, validator=optional(instance_of(str)))
41-
numbered: Union[bool, int] = attr.ib(False, validator=instance_of((bool, int)))
42-
# TODO in jupyter-book titlesonly default is True, but why
43-
titlesonly: bool = attr.ib(True, validator=instance_of(bool))
44-
reversed: bool = attr.ib(False, validator=instance_of(bool))
40+
caption: Optional[str] = attr.ib(
41+
None, kw_only=True, validator=optional(instance_of(str))
42+
)
43+
hidden: bool = attr.ib(True, kw_only=True, validator=instance_of(bool))
44+
maxdepth: int = attr.ib(-1, kw_only=True, validator=instance_of(int))
45+
numbered: Union[bool, int] = attr.ib(
46+
False, kw_only=True, validator=instance_of((bool, int))
47+
)
48+
reversed: bool = attr.ib(False, kw_only=True, validator=instance_of(bool))
49+
titlesonly: bool = attr.ib(True, kw_only=True, validator=instance_of(bool))
4550

4651
def files(self) -> List[str]:
4752
return [

sphinx_external_toc/cli.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
from pathlib import PurePosixPath
1+
from pathlib import Path, PurePosixPath
22

33
import click
44
import yaml
55

66
from sphinx_external_toc import __version__
77
from sphinx_external_toc.parsing import create_toc_dict, parse_toc_yaml
8-
from sphinx_external_toc.tools import create_site_from_toc, create_site_map_from_path
8+
from sphinx_external_toc.tools import (
9+
create_site_from_toc,
10+
create_site_map_from_path,
11+
migrate_jupyter_book,
12+
)
913

1014

1115
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@@ -105,3 +109,17 @@ def create_toc(site_folder, extension, index, skip_match, guess_titles):
105109
site_map[docname].title = " ".join(words).capitalize()
106110
data = create_toc_dict(site_map)
107111
click.echo(yaml.dump(data, sort_keys=False, default_flow_style=False))
112+
113+
114+
@main.command("migrate")
115+
@click.argument("toc_file", type=click.Path(exists=True, file_okay=True))
116+
@click.option(
117+
"-f",
118+
"--format",
119+
type=click.Choice(["jb-v0.10"]),
120+
help="The format to migrate from.",
121+
)
122+
def migrate_toc(toc_file, format):
123+
"""Migrate a ToC from a previous revision."""
124+
toc = migrate_jupyter_book(Path(toc_file))
125+
click.echo(yaml.dump(toc, sort_keys=False, default_flow_style=False))

sphinx_external_toc/events.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,26 @@ def parse_toc_to_env(app: Sphinx, config: Config) -> None:
7676
# we do not use `Path.glob` here, since it does not ignore hidden files:
7777
# https://stackoverflow.com/questions/49862648/why-do-glob-glob-and-pathlib-path-glob-treat-hidden-files-differently
7878
for path_str in glob.iglob(
79-
str(Path(app.srcdir) / "**" / f"*[{suffix}]"), recursive=True
79+
str(Path(app.srcdir) / "**" / f"*{suffix}"), recursive=True
8080
):
8181
path = Path(path_str)
8282
if not path.is_file():
8383
continue
8484
posix = path.relative_to(app.srcdir).as_posix()
85-
possix_no_suffix = posix[: -len(suffix)]
85+
posix_no_suffix = posix[: -len(suffix)]
86+
components = posix.split("/")
8687
if not (
8788
# files can be stored with or without suffixes
8889
posix in site_map
89-
or possix_no_suffix in site_map
90-
# ignore anything already excluded
91-
or already_excluded(posix)
90+
or posix_no_suffix in site_map
91+
# ignore anything already excluded, we have to check against
92+
# the file path and all its sub-directory paths
93+
or any(
94+
already_excluded("/".join(components[: i + 1]))
95+
for i in range(len(components))
96+
)
9297
# don't exclude docnames matching globs
93-
or any(patmatch(possix_no_suffix, pat) for pat in site_map.globs())
98+
or any(patmatch(posix_no_suffix, pat) for pat in site_map.globs())
9499
):
95100
new_excluded.append(posix)
96101
if new_excluded:
@@ -201,13 +206,13 @@ def insert_toctrees(app: Sphinx, doctree: nodes.document) -> None:
201206
subnode.source = doctree.source
202207
subnode["entries"] = []
203208
subnode["includefiles"] = []
204-
subnode["maxdepth"] = -1
209+
subnode["maxdepth"] = toctree.maxdepth
205210
subnode["caption"] = toctree.caption
206211
# TODO this wasn't in the original code,
207212
# but alabaster theme intermittently raised `KeyError('rawcaption')`
208213
subnode["rawcaption"] = toctree.caption or ""
209214
subnode["glob"] = any(isinstance(entry, GlobItem) for entry in toctree.sections)
210-
subnode["hidden"] = False if toc_placeholders else True
215+
subnode["hidden"] = False if toc_placeholders else toctree.hidden
211216
subnode["includehidden"] = False
212217
subnode["numbered"] = toctree.numbered
213218
subnode["titlesonly"] = toctree.titlesonly

sphinx_external_toc/parsing.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
GLOB_KEY = "glob"
1313
URL_KEY = "url"
1414

15+
TOCTREE_OPTIONS = (
16+
"caption",
17+
"hidden",
18+
"maxdepth",
19+
"numbered",
20+
"reversed",
21+
"titlesonly",
22+
)
23+
1524

1625
class MalformedError(Exception):
1726
"""Raised if toc file is malformed."""
@@ -50,7 +59,7 @@ def _parse_doc_item(
5059
# this is a shorthand for defining a single part
5160
if "parts" in data:
5261
raise MalformedError(f"Both 'sections' and 'parts' found: '{path}'")
53-
parts_data = [{"sections": data["sections"]}]
62+
parts_data = [{"sections": data["sections"], **data.get("options", {})}]
5463
elif "parts" in data:
5564
parts_data = data["parts"]
5665
if not (isinstance(parts_data, Sequence) and parts_data):
@@ -112,19 +121,11 @@ def _parse_doc_item(
112121
sections.append(UrlItem(section[URL_KEY], section.get("title")))
113122

114123
# generate toc key-word arguments
115-
keywords = {}
116-
for key in ("caption", "numbered", "titlesonly", "reversed"):
117-
if key in part:
118-
keywords[key] = part[key]
119-
elif key in defaults:
124+
keywords = {k: part[k] for k in TOCTREE_OPTIONS if k in part}
125+
for key in defaults:
126+
if key not in keywords:
120127
keywords[key] = defaults[key]
121128

122-
# TODO this is a hacky fix for the fact that sphinx logs a warning
123-
# for nested toctrees, see:
124-
# sphinx/environment/collectors/toctree.py::TocTreeCollector::assign_section_numbers::_walk_toctree
125-
if keywords.get("numbered") and path != "/":
126-
keywords.pop("numbered")
127-
128129
try:
129130
toc_item = TocItem(sections=sections, **keywords)
130131
except TypeError as exc:

0 commit comments

Comments
 (0)