1
1
from pathlib import Path
2
- from typing import Any , List
2
+ from typing import Any , Dict , List , Optional , Tuple , Type
3
3
4
4
import docutils
5
5
import yaml
6
6
from sphinx import addnodes
7
7
from sphinx .application import Sphinx
8
8
from sphinx .builders .latex .nodes import thebibliography
9
+ from sphinx .environment import BuildEnvironment
9
10
from sphinx .transforms import SphinxTransform
10
11
from sphinx .transforms .post_transforms import SphinxPostTransform
11
12
from sphinx .util import logging
15
16
logger = logging .getLogger (__name__ )
16
17
17
18
18
- def replaceWithNode (srcNode , toReplace , copyChildren ):
19
- node = toReplace ("" )
20
- node ["classes" ] = srcNode ["classes" ]
21
- if copyChildren :
22
- node .children = srcNode .children
23
- srcNode .replace_self ([node ])
19
+ def replace_node_cls (
20
+ src_node : docutils .nodes .Element ,
21
+ node_cls : Type [docutils .nodes .Element ],
22
+ copy_children : bool ,
23
+ ) -> None :
24
+ """Replace the class of a node."""
25
+ node = node_cls ("" )
26
+ node ["classes" ] = src_node ["classes" ]
27
+ if copy_children :
28
+ node .children = src_node .children
29
+ src_node .replace_self ([node ])
24
30
25
31
26
- def depth (node , parentId ):
32
+ def get_section_depth (node : docutils .nodes .section , root_ids : Tuple [str , ...]) -> int :
33
+ """Get the nesting depth of the section."""
27
34
d = 0
28
- while node .attributes ["ids" ][ 0 ] != parentId :
35
+ while tuple ( node .attributes ["ids" ]) != root_ids :
29
36
d += 1
30
- node = node .parent
37
+ node = getattr (node , "parent" , None )
38
+ if not node :
39
+ break
31
40
return d
32
41
33
42
34
- def find_parent (env , node , parentTag ):
35
- while 1 :
43
+ def find_parent (
44
+ env : BuildEnvironment , node : docutils .nodes .Element , parent_tag : str
45
+ ) -> Optional [docutils .nodes .Element ]:
46
+ """Find the parent node."""
47
+ while True :
36
48
node = node .parent
37
49
if node is None :
38
- return
50
+ return None
39
51
# parent should be a document in toc
40
52
if (
41
53
"docname" in node .attributes
@@ -44,7 +56,7 @@ def find_parent(env, node, parentTag):
44
56
):
45
57
return node
46
58
47
- if node .tagname == parentTag :
59
+ if node .tagname == parent_tag :
48
60
return node
49
61
50
62
return None
@@ -63,54 +75,55 @@ def is_root_document(document: docutils.nodes.document, app: Sphinx) -> bool:
63
75
return app .project .path2doc (document ["source" ]) == app .config .master_doc
64
76
65
77
66
- # Transforms and postTransforms
78
+ class LatexRootDocTransforms (SphinxTransform ):
79
+ """Arrange the toctrees and sections in the required structure."""
67
80
68
-
69
- class LatexMasterDocTransforms (SphinxTransform ):
70
81
default_priority = 500
71
82
72
83
def apply (self , ** kwargs : Any ) -> None :
73
- tocwrrapper = []
74
-
75
- def getSectionDepth ():
76
- levelDict = dict ()
77
- for count , sect in enumerate (
78
- self .document .traverse (docutils .nodes .section )
79
- ):
80
- if count == 0 :
81
- parentSect = sect
82
- parentId = sect .attributes ["ids" ][0 ]
83
- levelDict [sect .attributes ["ids" ][0 ]] = depth (sect , parentId )
84
- return levelDict , parentSect
85
-
86
- def alterNodes (sectLevelsDict , parentSect ):
87
- hNode = None
88
- for sect in self .document .traverse (docutils .nodes .section ):
89
- if parentSect != sect :
90
- for child in sect .children :
91
- if isinstance (child , docutils .nodes .title ):
92
- if sectLevelsDict [sect .attributes ["ids" ][0 ]] == 1 :
93
- hNode = H2Node ("" )
94
- elif sectLevelsDict [sect .attributes ["ids" ][0 ]] == 2 :
95
- hNode = H3Node ("" )
96
- if hNode is not None :
97
- hNode .children = child .children
98
- child = hNode
99
- parentSect .append (child )
100
- replaceWithNode (sect , HiddenCellNode , False )
101
-
102
- if is_root_document (self .document , self .app ):
103
- # pull the toctree-wrapper and append it later to the topmost document level
104
- for node in self .document .traverse (docutils .nodes .compound ):
105
- if "toctree-wrapper" in node ["classes" ]:
106
- tocwrrapper .append (node )
107
- replaceWithNode (node , HiddenCellNode , False )
108
-
109
- sectLevelsDict , parentSect = getSectionDepth ()
110
- alterNodes (sectLevelsDict , parentSect )
111
- # append the toctreewrapper to the topmost level of document
112
- if tocwrrapper :
113
- self .document .extend (tocwrrapper )
84
+
85
+ if not is_root_document (self .document , self .app ):
86
+ return
87
+
88
+ # pop the toctree-wrappers and append them to the topmost document level
89
+ for node in self .document .traverse (docutils .nodes .compound ):
90
+ if "toctree-wrapper" in node ["classes" ]:
91
+ replace_node_cls (node , HiddenCellNode , False )
92
+ self .document .append (node )
93
+
94
+ # map the section ids to their depth in the tree (h1 == 0)
95
+ section_levels : Dict [Tuple [str , ...], int ] = {}
96
+ for count , sect in enumerate (self .document .traverse (docutils .nodes .section )):
97
+ sect_id = tuple (sect .attributes ["ids" ])
98
+ if count == 0 :
99
+ top_level_section = sect
100
+ top_level_id = sect_id
101
+ section_levels [sect_id ] = get_section_depth (sect , top_level_id )
102
+
103
+ if not section_levels :
104
+ return
105
+
106
+ # flatten the AST sections under the top-level section
107
+ for sect in self .document .traverse (docutils .nodes .section ):
108
+ if sect == top_level_section :
109
+ continue
110
+ # move section children to the top-level
111
+ for child in sect .children :
112
+ # replace H2 and H3 titles with nodes that can be custom rendered
113
+ if isinstance (child , docutils .nodes .title ):
114
+ sect_id = tuple (sect .attributes ["ids" ])
115
+ header_node = None
116
+ if section_levels [sect_id ] == 1 :
117
+ header_node = H2Node ("" )
118
+ elif section_levels [sect_id ] == 2 :
119
+ header_node = H3Node ("" )
120
+ if header_node is not None :
121
+ header_node .children = child .children
122
+ child = header_node
123
+ top_level_section .append (child )
124
+
125
+ # remove the section node
126
+ replace_node_cls (sect , HiddenCellNode , False )
114
127
115
128
116
129
class MystNbPostTransform (SphinxPostTransform ):
@@ -140,80 +153,76 @@ def apply(self, **kwargs: Any) -> None:
140
153
141
154
for node in self .document .traverse (CellNode ):
142
155
if "tag_hide-cell" in node ["classes" ]:
143
- replaceWithNode (node , HiddenCellNode , True )
156
+ replace_node_cls (node , HiddenCellNode , True )
144
157
if "tag_hide-input" in node ["classes" ]:
145
- inputNode = node .traverse (CellInputNode )
146
- for node in inputNode :
147
- replaceWithNode (node , HiddenCellNode , True )
158
+ for input_node in node .traverse (CellInputNode ):
159
+ replace_node_cls (input_node , HiddenCellNode , True )
148
160
if "tag_hide-output" in node ["classes" ]:
149
- outputNode = node .traverse (CellOutputNode )
150
- for node in outputNode :
151
- replaceWithNode (node , HiddenCellNode , True )
161
+ for output_node in node .traverse (CellOutputNode ):
162
+ replace_node_cls (output_node , HiddenCellNode , True )
152
163
153
164
154
- class handleSubSections (SphinxPostTransform ):
155
- default_priority = 700
165
+ def check_node_in_part (part , node , app : Sphinx ) -> bool :
166
+ """ """
167
+ if "chapters" in part :
168
+ nodefile = node .children [0 ].attributes ["docname" ]
169
+ chapfiles = part ["chapters" ]
170
+ for chap in chapfiles :
171
+ chapname = remove_suffix (list (chap .values ())[0 ], app .config .source_suffix )
172
+ if nodefile in chapname :
173
+ return True
174
+ return False
156
175
157
- def apply (self , ** kwargs : Any ) -> None :
158
- if is_root_document (self .document , self .app ):
159
- for compound in self .document .traverse (docutils .nodes .compound ):
160
- if "toctree-wrapper" in compound ["classes" ]:
161
- nodecopy = compound
162
- node = find_parent (self .app .env , nodecopy , "section" )
163
- if node :
164
- replaceWithNode (compound , HiddenCellNode , False )
165
- node .append (nodecopy )
166
176
177
+ class LatexRootDocPostTransforms (SphinxPostTransform ):
178
+ """Arrange the toctrees and bibliographies into the required structure."""
167
179
168
- class ToctreeTransforms (SphinxPostTransform ):
169
- default_priority = 800
180
+ default_priority = 700
170
181
171
182
def apply (self , ** kwargs : Any ) -> None :
172
- def checkNodeIsInPart (part , node ):
173
- if "chapters" in part :
174
- nodefile = node .children [0 ].attributes ["docname" ]
175
- chapfiles = part ["chapters" ]
176
- for chap in chapfiles :
177
- chapname = remove_suffix (
178
- list (chap .values ())[0 ], self .app .config .source_suffix
179
- )
180
- if nodefile in chapname :
181
- return True
182
- return False
183
+ if not is_root_document (self .document , self .app ):
184
+ return
183
185
184
- if is_root_document (self .document , self .app ):
185
- TOC_PATH = Path (self .app .confdir or self .app .srcdir ).joinpath ("_toc.yml" )
186
- tocfile = yaml .safe_load (TOC_PATH .read_text ("utf8" ))
187
-
188
- # store bibliography nodes to append it at the end of doc
189
- bibNodes = []
190
- for bibnode in self .document .traverse (thebibliography ):
191
- bibNodes .append (bibnode )
192
- replaceWithNode (bibnode , HiddenCellNode , False )
193
-
194
- for f in tocfile :
195
- if "part" in f :
196
- self .app .config ["latex_toplevel_sectioning" ] = "part"
197
- partname = f ["part" ]
198
- compoundParent = docutils .nodes .compound ("" )
199
- compoundParent ["classes" ] = "toctree-wrapper"
200
- startOfFile = addnodes .start_of_file ("" )
201
- startOfFile ["docname" ] = partname
202
- title = docutils .nodes .title (text = partname )
203
- sectionName = docutils .nodes .section ("" )
204
- sectionName ["docname" ] = partname
205
- startOfFile .append (sectionName )
206
- sectionName .append (title )
207
- compoundParent .append (startOfFile )
208
- for node in self .document .traverse (docutils .nodes .compound ):
209
- if "toctree-wrapper" in node ["classes" ]:
210
- flag = checkNodeIsInPart (f , node )
211
- if flag :
212
- nodecopy = node
213
- replaceWithNode (node , HiddenCellNode , False )
214
- sectionName .append (nodecopy )
215
- self .document .append (compoundParent )
216
-
217
- # append bib at the end
218
- if len (bibNodes ):
219
- self .document .extend (bibNodes )
186
+ # move toctrees to the end of their parent section
187
+ for original_node in self .document .traverse (docutils .nodes .compound ):
188
+ if "toctree-wrapper" in original_node ["classes" ]:
189
+ parent_node = find_parent (self .app .env , original_node , "section" )
190
+ if parent_node :
191
+ replace_node_cls (original_node , HiddenCellNode , False )
192
+ parent_node .append (original_node )
193
+
194
+ # store bibliography nodes to append it at the end of doc
195
+ bib_nodes = []
196
+ for bib_node in self .document .traverse (thebibliography ):
197
+ bib_nodes .append (bib_node )
198
+ replace_node_cls (bib_node , HiddenCellNode , False )
199
+
200
+ toc_path = Path (self .app .confdir or self .app .srcdir ).joinpath ("_toc.yml" )
201
+ tocfile = yaml .safe_load (toc_path .read_text ("utf8" ))
202
+
203
+ for f in tocfile :
204
+ if "part" in f :
205
+ self .app .config ["latex_toplevel_sectioning" ] = "part"
206
+ partname = f ["part" ]
207
+ compound_parent = docutils .nodes .compound ("" )
208
+ compound_parent ["classes" ] = "toctree-wrapper"
209
+ start_of_file = addnodes .start_of_file ("" )
210
+ start_of_file ["docname" ] = partname
211
+ title = docutils .nodes .title (text = partname )
212
+ section_node = docutils .nodes .section ("" )
213
+ section_node ["docname" ] = partname
214
+ start_of_file .append (section_node )
215
+ section_node .append (title )
216
+ compound_parent .append (start_of_file )
217
+ for node in self .document .traverse (docutils .nodes .compound ):
218
+ if "toctree-wrapper" in node ["classes" ]:
219
+ flag = check_node_in_part (f , node , self .app )
220
+ if flag :
221
+ original_node = node
222
+ replace_node_cls (node , HiddenCellNode , False )
223
+ section_node .append (original_node )
224
+ self .document .append (compound_parent )
225
+
226
+ # append bib at the end
227
+ if bib_nodes :
228
+ self .document .extend (bib_nodes )
0 commit comments