7
7
8
8
import sphinx .builders .html
9
9
from sphinx .errors import ExtensionError
10
+ from sphinx .environment .adapters .toctree import TocTree
11
+ from sphinx import addnodes
12
+ from docutils import nodes
13
+ from pathlib import Path
10
14
11
15
from .bootstrap_html_translator import BootstrapHTML5Translator
12
16
import docutils
13
17
14
18
__version__ = "0.1.1"
15
19
16
20
21
+ class MyTocTree (TocTree ):
22
+ def get_toctree_for_subpage (
23
+ self , pagename , builder , collapse = True , maxdepth = - 1 , ** kwargs
24
+ ):
25
+ """Return the global TOC nodetree."""
26
+ if pagename in ["genindex" , "search" ]:
27
+ return
28
+ doctree = self .env .get_doctree (pagename )
29
+ toctrees = [] # type: List[Element]
30
+ if "includehidden" not in kwargs :
31
+ kwargs ["includehidden" ] = True
32
+ if "maxdepth" not in kwargs :
33
+ kwargs ["maxdepth" ] = 0
34
+ kwargs ["collapse" ] = collapse
35
+ for toctreenode in doctree .traverse (addnodes .toctree ):
36
+ toctree = self .resolve (pagename , builder , toctreenode , prune = True , ** kwargs )
37
+ if toctree :
38
+ toctrees .append (toctree )
39
+ if not toctrees :
40
+ return None
41
+ result = toctrees [0 ]
42
+ for toctree in toctrees [1 :]:
43
+ result .extend (toctree .children )
44
+ return result
45
+
46
+
17
47
def add_toctree_functions (app , pagename , templatename , context , doctree ):
18
48
"""Add functions so Jinja templates can add toctree objects.
19
49
20
50
This converts the docutils nodes into a nested dictionary that Jinja can
21
51
use in our templating.
22
52
"""
23
- from sphinx .environment .adapters .toctree import TocTree
24
53
25
- def get_nav_object (maxdepth = None , collapse = True , ** kwargs ):
54
+ def get_nav_object (maxdepth = None , collapse = True , subpage_caption = False , ** kwargs ):
26
55
"""Return a list of nav links that can be accessed from Jinja.
27
56
28
57
Parameters
@@ -38,26 +67,59 @@ def get_nav_object(maxdepth=None, collapse=True, **kwargs):
38
67
# The TocTree will contain the full site TocTree including sub-pages.
39
68
# "collapse=True" collapses sub-pages of non-active TOC pages.
40
69
# maxdepth controls how many TOC levels are returned
41
- toctree = TocTree (app .env ).get_toctree_for (
70
+ toc = MyTocTree (app .env )
71
+ toctree = toc .get_toctree_for (
42
72
pagename , app .builder , collapse = collapse , maxdepth = maxdepth , ** kwargs
43
73
)
74
+
44
75
# If no toctree is defined (AKA a single-page site), skip this
45
76
if toctree is None :
46
77
return []
47
78
79
+ if subpage_caption :
80
+ if pagename not in [app .env .config .master_doc , "genindex" , "search" ]:
81
+ def is_first_active_page (node ):
82
+ return isinstance (node , nodes .bullet_list ) and node .attributes .get ("iscurrent" )
83
+
84
+ active_first_page = list (toctree .traverse (is_first_active_page ))[0 ]
85
+ # A path to the active TOC item's first page, relative to the current page
86
+ first_page_path = list (active_first_page .traverse (nodes .reference ))[0 ].attributes .get ("refuri" )
87
+ if first_page_path == "" :
88
+ # First TOC item's first page *is* the active page
89
+ first_page_path = Path (pagename ).name
90
+ else :
91
+ first_page_path = Path (first_page_path ).with_suffix ("" )
92
+ rel_first_page_path = str (Path (pagename ).parent .joinpath (first_page_path ))
93
+
94
+ # We only wish to show a single page's descendants, so we'll keep their captions
95
+ subpage_toctree = toc .get_toctree_for_subpage (
96
+ rel_first_page_path , app .builder , collapse = collapse , maxdepth = maxdepth , ** kwargs
97
+ )
98
+ if subpage_toctree is not None :
99
+ # Find the current page in the top-level children
100
+ for item in toctree .children :
101
+ if isinstance (item , nodes .bullet_list ) and item .attributes .get ("iscurrent" , []):
102
+ # Append that pages' toctree so we get captions
103
+ subpage_list = item .children [0 ]
104
+ subpage_list .children = [subpage_list .children [0 ]] + subpage_toctree .children
105
+
48
106
# toctree has this structure
49
107
# <caption>
50
108
# <bullet_list>
51
109
# <list_item classes="toctree-l1">
52
110
# <list_item classes="toctree-l1">
53
111
# `list_item`s are the actual TOC links and are the only thing we want
54
- toc_items = [item for child in toctree .children for item in child
55
- if isinstance (item , docutils .nodes .list_item )]
112
+ toc_items = []
113
+ for child in toctree .children :
114
+ if isinstance (child , docutils .nodes .caption ):
115
+ toc_items .append (child )
116
+ elif isinstance (child , docutils .nodes .bullet_list ):
117
+ for list_entry in child :
118
+ if isinstance (list_entry , docutils .nodes .list_item ):
119
+ toc_items .append (list_entry )
56
120
57
121
# Now convert our docutils nodes into dicts that Jinja can use
58
- nav = [docutils_node_to_jinja (child , only_pages = True )
59
- for child in toc_items ]
60
-
122
+ nav = [docutils_node_to_jinja (child , only_pages = True ) for child in toc_items ]
61
123
return nav
62
124
63
125
def get_page_toc_object ():
@@ -73,6 +135,7 @@ def get_page_toc_object():
73
135
context ["get_nav_object" ] = get_nav_object
74
136
context ["get_page_toc_object" ] = get_page_toc_object
75
137
138
+
76
139
def docutils_node_to_jinja (list_item , only_pages = False ):
77
140
"""Convert a docutils node to a structure that can be read by Jinja.
78
141
@@ -91,6 +154,12 @@ def docutils_node_to_jinja(list_item, only_pages=False):
91
154
The TocTree, converted into a dictionary with key/values that work
92
155
within Jinja.
93
156
"""
157
+ # If a caption, pass it through
158
+ if isinstance (list_item , docutils .nodes .caption ):
159
+ nav = {"text" : list_item .astext (), "type" : "caption" }
160
+ return nav
161
+
162
+ # Else, we assume it's a list item and need to parse the item content
94
163
if not list_item .children :
95
164
return None
96
165
@@ -100,48 +169,58 @@ def docutils_node_to_jinja(list_item, only_pages=False):
100
169
# <reference> <-- the thing we want
101
170
reference = list_item .children [0 ].children [0 ]
102
171
title = reference .astext ()
103
- url = reference .attributes ["refuri" ]
172
+
173
+ url = reference .attributes .get ("refuri" , "" )
104
174
active = "current" in list_item .attributes ["classes" ]
105
175
106
176
# If we've got an anchor link, skip it if we wish
107
- if only_pages and '#' in url :
177
+ if only_pages and "#" in url :
108
178
return None
109
179
110
180
# Converting the docutils attributes into jinja-friendly objects
111
181
nav = {}
112
182
nav ["title" ] = title
113
183
nav ["url" ] = url
114
184
nav ["active" ] = active
185
+ nav ["type" ] = "ref"
115
186
116
187
# Recursively convert children as well
117
188
# If there are sub-pages for this list_item, there should be two children:
118
189
# a paragraph, and a bullet_list.
119
190
nav ["children" ] = []
120
191
if len (list_item .children ) > 1 :
121
- # The `.children` of the bullet_list has the nodes of the sub-pages.
122
- subpage_list = list_item .children [1 ].children
123
- for sub_page in subpage_list :
124
- child_nav = docutils_node_to_jinja (sub_page , only_pages = only_pages )
125
- if child_nav is not None :
126
- nav ["children" ].append (child_nav )
192
+ child_pages = list_item .children [1 :]
193
+ for child in child_pages :
194
+ # Will either be a caption or another bullet list
195
+ if isinstance (child , nodes .bullet_list ):
196
+ for subpage in child .children :
197
+ this_nav = docutils_node_to_jinja (subpage , only_pages = only_pages )
198
+ nav ["children" ].append (this_nav )
199
+ else :
200
+ this_nav = docutils_node_to_jinja (child , only_pages = only_pages )
201
+ nav ["children" ].append (this_nav )
127
202
return nav
128
203
129
204
130
205
# -----------------------------------------------------------------------------
131
206
207
+
132
208
def setup_edit_url (app , pagename , templatename , context , doctree ):
133
209
"""Add a function that jinja can access for returning the edit URL of a page."""
210
+
134
211
def get_edit_url ():
135
212
"""Return a URL for an "edit this page" link."""
136
213
required_values = ["github_user" , "github_repo" , "github_version" ]
137
214
for val in required_values :
138
215
if not context .get (val ):
139
- raise ExtensionError ("Missing required value for `edit this page` button. "
140
- "Add %s to your `html_context` configuration" % val )
141
-
142
- github_user = context ['github_user' ]
143
- github_repo = context ['github_repo' ]
144
- github_version = context ['github_version' ]
216
+ raise ExtensionError (
217
+ "Missing required value for `edit this page` button. "
218
+ "Add %s to your `html_context` configuration" % val
219
+ )
220
+
221
+ github_user = context ["github_user" ]
222
+ github_repo = context ["github_repo" ]
223
+ github_version = context ["github_version" ]
145
224
file_name = f"{ pagename } { context ['page_source_suffix' ]} "
146
225
147
226
# Make sure that doc_path has a path separator only if it exists (to avoid //)
@@ -153,7 +232,7 @@ def get_edit_url():
153
232
url_edit = f"https://github.com/{ github_user } /{ github_repo } /edit/{ github_version } /{ doc_path } { file_name } "
154
233
return url_edit
155
234
156
- context [' get_edit_url' ] = get_edit_url
235
+ context [" get_edit_url" ] = get_edit_url
157
236
158
237
159
238
# -----------------------------------------------------------------------------
@@ -174,6 +253,6 @@ def setup(app):
174
253
# uses a special "dirhtml" builder so we need to replace these both with
175
254
# our custom HTML builder
176
255
app .set_translator ("readthedocs" , BootstrapHTML5Translator , override = True )
177
- app .set_translator (' readthedocsdirhtml' , BootstrapHTML5Translator , override = True )
256
+ app .set_translator (" readthedocsdirhtml" , BootstrapHTML5Translator , override = True )
178
257
app .connect ("html-page-context" , setup_edit_url )
179
258
app .connect ("html-page-context" , add_toctree_functions )
0 commit comments