4
4
import os
5
5
6
6
from sphinx .errors import ExtensionError
7
+ from bs4 import BeautifulSoup as bs
7
8
8
9
from .bootstrap_html_translator import BootstrapHTML5Translator
9
- import docutils
10
10
11
11
__version__ = "0.4.2dev0"
12
12
13
13
14
14
def add_toctree_functions (app , pagename , templatename , context , doctree ):
15
- """Add functions so Jinja templates can add toctree objects.
15
+ """Add functions so Jinja templates can add toctree objects."""
16
16
17
- This converts the docutils nodes into a nested dictionary that Jinja can
18
- use in our templating.
19
- """
20
- from sphinx .environment .adapters .toctree import TocTree
17
+ def generate_nav_html (kind , ** kwargs ):
18
+ """
19
+ Return the navigation link structure in HTML. Arguments are passed
20
+ to Sphinx "toctree" function (context["toctree"] below).
21
+
22
+ We use beautifulsoup to add the right CSS classes / structure for bootstrap.
23
+
24
+ See https://www.sphinx-doc.org/en/master/templating.html#toctree.
21
25
22
- def get_nav_object (maxdepth = None , collapse = True , ** kwargs ):
26
+ Parameters
27
+ ----------
28
+ kind : ["navbar", "sidebar", "raw"]
29
+ The kind of UI element this toctree is generated for.
30
+ kwargs: passed to the Sphinx `toctree` template function.
31
+
32
+ Returns
33
+ -------
34
+ HTML string (if kind in ["navbar", "sidebar"])
35
+ or BeautifulSoup object (if kind == "raw")
36
+ """
37
+ toc_sphinx = context ["toctree" ](** kwargs )
38
+ soup = bs (toc_sphinx , "html.parser" )
39
+
40
+ # pair "current" with "active" since that's what we use w/ bootstrap
41
+ for li in soup ("li" , {"class" : "current" }):
42
+ li ["class" ].append ("active" )
43
+
44
+ if kind == "navbar" :
45
+ # Add CSS for bootstrap
46
+ for li in soup ("li" ):
47
+ li ["class" ].append ("nav-item" )
48
+ li .find ("a" )["class" ].append ("nav-link" )
49
+ out = "\n " .join ([ii .prettify () for ii in soup .find_all ("li" )])
50
+
51
+ elif kind == "sidebar" :
52
+ # Remove sidebar links to sub-headers on the page
53
+ for li in soup .select ("li.current ul li" ):
54
+ # Remove
55
+ if li .find ("a" ):
56
+ href = li .find ("a" )["href" ]
57
+ if "#" in href and href != "#" :
58
+ li .decompose ()
59
+
60
+ # Join all the top-level `li`s together for display
61
+ current_lis = soup .select ("li.current.toctree-l1 li.toctree-l2" )
62
+ out = "\n " .join ([ii .prettify () for ii in current_lis ])
63
+
64
+ elif kind == "raw" :
65
+ out = soup
66
+
67
+ return out
68
+
69
+ def generate_toc_html (kind = "html" ):
70
+ """Return the within-page TOC links in HTML."""
71
+
72
+ if "toc" not in context :
73
+ return ""
74
+
75
+ soup = bs (context ["toc" ], "html.parser" )
76
+
77
+ # Add toc-hN + visible classes
78
+ def add_header_level_recursive (ul , level ):
79
+ if level <= (context ["theme_show_toc_level" ] + 1 ):
80
+ ul ["class" ] = ul .get ("class" , []) + ["visible" ]
81
+ for li in ul ("li" , recursive = False ):
82
+ li ["class" ] = li .get ("class" , []) + [f"toc-h{ level } " ]
83
+ ul = li .find ("ul" , recursive = False )
84
+ if ul :
85
+ add_header_level_recursive (ul , level + 1 )
86
+
87
+ add_header_level_recursive (soup .find ("ul" ), 1 )
88
+
89
+ # Add in CSS classes for bootstrap
90
+ for ul in soup ("ul" ):
91
+ ul ["class" ] = ul .get ("class" , []) + ["nav" , "section-nav" , "flex-column" ]
92
+
93
+ for li in soup ("li" ):
94
+ li ["class" ] = li .get ("class" , []) + ["nav-item" , "toc-entry" ]
95
+ if li .find ("a" ):
96
+ a = li .find ("a" )
97
+ a ["class" ] = a .get ("class" , []) + ["nav-link" ]
98
+
99
+ # If we only have one h1 header, assume it's a title
100
+ h1_headers = soup .select (".toc-h1" )
101
+ if len (h1_headers ) == 1 :
102
+ title = h1_headers [0 ]
103
+ # If we have no sub-headers of a title then we won't have a TOC
104
+ if not title .select (".toc-h2" ):
105
+ out = ""
106
+ else :
107
+ out = title .find ("ul" ).prettify ()
108
+ # Else treat the h1 headers as sections
109
+ else :
110
+ out = soup .prettify ()
111
+
112
+ # Return the toctree object
113
+ if kind == "html" :
114
+ return out
115
+ else :
116
+ return soup
117
+
118
+ def get_nav_object (maxdepth = None , collapse = True , includehidden = True , ** kwargs ):
23
119
"""Return a list of nav links that can be accessed from Jinja.
24
120
25
121
Parameters
@@ -30,50 +126,39 @@ def get_nav_object(maxdepth=None, collapse=True, **kwargs):
30
126
Whether to only include sub-pages of the currently-active page,
31
127
instead of sub-pages of all top-level pages of the site.
32
128
kwargs: key/val pairs
33
- Passed to the `TocTree.get_toctree_for ` Sphinx method
129
+ Passed to the `toctree ` Sphinx method
34
130
"""
35
- # The TocTree will contain the full site TocTree including sub-pages.
36
- # "collapse=True" collapses sub-pages of non-active TOC pages.
37
- # maxdepth controls how many TOC levels are returned
38
- toctree = TocTree (app .env ).get_toctree_for (
39
- pagename , app .builder , collapse = collapse , maxdepth = maxdepth , ** kwargs
131
+ toc_sphinx = context ["toctree" ](
132
+ maxdepth = maxdepth , collapse = collapse , includehidden = includehidden , ** kwargs
40
133
)
41
- # If no toctree is defined (AKA a single-page site), skip this
42
- if toctree is None :
43
- return []
44
-
45
- # toctree has this structure
46
- # <caption>
47
- # <bullet_list>
48
- # <list_item classes="toctree-l1">
49
- # <list_item classes="toctree-l1">
50
- # `list_item`s are the actual TOC links and are the only thing we want
51
- toc_items = [
52
- item
53
- for child in toctree .children
54
- for item in child
55
- if isinstance (item , docutils .nodes .list_item )
56
- ]
134
+ soup = bs (toc_sphinx , "html.parser" )
57
135
58
- # Now convert our docutils nodes into dicts that Jinja can use
59
- nav = [docutils_node_to_jinja (child , only_pages = True ) for child in toc_items ]
136
+ # # If no toctree is defined (AKA a single-page site), skip this
137
+ # if toctree is None:
138
+ # return []
60
139
61
- return nav
140
+ nav_object = soup_to_python (soup , only_pages = True )
141
+ return nav_object
62
142
63
143
def get_page_toc_object ():
64
144
"""Return a list of within-page TOC links that can be accessed from Jinja."""
65
- self_toc = TocTree (app .env ).get_toc_for (pagename , app .builder )
145
+
146
+ if "toc" not in context :
147
+ return ""
148
+
149
+ soup = bs (context ["toc" ], "html.parser" )
66
150
67
151
try :
68
- # If there's only one child, assume we have a single "title" as top header
69
- # so start the TOC at the first item's children (AKA, level 2 headers)
70
- if len (self_toc .children ) == 1 :
71
- nav = docutils_node_to_jinja (self_toc .children [0 ]).get ("children" , [])
72
- else :
73
- nav = [docutils_node_to_jinja (item ) for item in self_toc .children ]
74
- return nav
152
+ toc_object = soup_to_python (soup , only_pages = False )
75
153
except Exception :
76
- return {}
154
+ return []
155
+
156
+ # If there's only one child, assume we have a single "title" as top header
157
+ # so start the TOC at the first item's children (AKA, level 2 headers)
158
+ if len (toc_object ) == 1 :
159
+ return toc_object [0 ]["children" ]
160
+ else :
161
+ return toc_object
77
162
78
163
def navbar_align_class ():
79
164
"""Return the class that aligns the navbar based on config."""
@@ -92,63 +177,67 @@ def navbar_align_class():
92
177
)
93
178
return align_options [align ]
94
179
180
+ context ["generate_nav_html" ] = generate_nav_html
181
+ context ["generate_toc_html" ] = generate_toc_html
95
182
context ["get_nav_object" ] = get_nav_object
96
183
context ["get_page_toc_object" ] = get_page_toc_object
97
184
context ["navbar_align_class" ] = navbar_align_class
98
185
99
186
100
- def docutils_node_to_jinja (list_item , only_pages = False ):
101
- """Convert a docutils node to a structure that can be read by Jinja.
187
+ def soup_to_python (soup , only_pages = False ):
188
+ """
189
+ Convert the toctree html structure to python objects which can be used in Jinja.
102
190
103
191
Parameters
104
192
----------
105
- list_item : docutils list_item node
106
- A parent item, potentially with children, corresponding to the level
107
- of a TocTree.
193
+ soup : BeautifulSoup object for the toctree
108
194
only_pages : bool
109
195
Only include items for full pages in the output dictionary. Exclude
110
196
anchor links (TOC items with a URL that starts with #)
111
197
112
198
Returns
113
199
-------
114
- nav : dict
115
- The TocTree , converted into a dictionary with key/values that work
200
+ nav : list of dicts
201
+ The toctree , converted into a dictionary with key/values that work
116
202
within Jinja.
117
203
"""
118
- if not list_item .children :
119
- return None
120
-
121
- # We assume this structure of a list item:
122
- # <list_item>
123
- # <compact_paragraph >
124
- # <reference> <-- the thing we want
125
- reference = list_item .children [0 ].children [0 ]
126
- title = reference .astext ()
127
- url = reference .attributes ["refuri" ]
128
- active = "current" in list_item .attributes ["classes" ]
129
-
130
- # If we've got an anchor link, skip it if we wish
131
- if only_pages and "#" in url :
132
- return None
133
-
134
- # Converting the docutils attributes into jinja-friendly objects
135
- nav = {}
136
- nav ["title" ] = title
137
- nav ["url" ] = url
138
- nav ["active" ] = active
139
-
140
- # Recursively convert children as well
141
- # If there are sub-pages for this list_item, there should be two children:
142
- # a paragraph, and a bullet_list.
143
- nav ["children" ] = []
144
- if len (list_item .children ) > 1 :
145
- # The `.children` of the bullet_list has the nodes of the sub-pages.
146
- subpage_list = list_item .children [1 ].children
147
- for sub_page in subpage_list :
148
- child_nav = docutils_node_to_jinja (sub_page , only_pages = only_pages )
149
- if child_nav is not None :
150
- nav ["children" ].append (child_nav )
151
- return nav
204
+ # toctree has this structure (caption only for toctree, not toc)
205
+ # <p class="caption">...</span></p>
206
+ # <ul>
207
+ # <li class="toctree-l1"><a href="..">..</a></li>
208
+ # <li class="toctree-l1"><a href="..">..</a></li>
209
+ # ...
210
+
211
+ def extract_level_recursive (ul , navs_list ):
212
+
213
+ for li in ul .find_all ("li" , recursive = False ):
214
+ ref = li .a
215
+ url = ref ["href" ]
216
+ title = "" .join (map (str , ref .contents ))
217
+ active = "current" in li .get ("class" , [])
218
+
219
+ # If we've got an anchor link, skip it if we wish
220
+ if only_pages and "#" in url and url != "#" :
221
+ continue
222
+
223
+ # Converting the docutils attributes into jinja-friendly objects
224
+ nav = {}
225
+ nav ["title" ] = title
226
+ nav ["url" ] = url
227
+ nav ["active" ] = active
228
+
229
+ navs_list .append (nav )
230
+
231
+ # Recursively convert children as well
232
+ nav ["children" ] = []
233
+ ul = li .find ("ul" , recursive = False )
234
+ if ul :
235
+ extract_level_recursive (ul , nav ["children" ])
236
+
237
+ navs = []
238
+ for ul in soup .find_all ("ul" , recursive = False ):
239
+ extract_level_recursive (ul , navs )
240
+ return navs
152
241
153
242
154
243
# -----------------------------------------------------------------------------
0 commit comments