77from attr .validators import instance_of , deep_iterable , optional
88import yaml
99
10+ FILE_KEY = "file"
11+ GLOB_KEY = "glob"
12+ URL_KEY = "url"
13+
1014
1115class FileItem (str ):
1216 """A document path in a toctree list.
@@ -147,7 +151,7 @@ class MalformedError(Exception):
147151 """Raised if toc file is malformed."""
148152
149153
150- def parse_toc_file (path : Union [str , Path ], encoding : str = "utf8" ) -> SiteMap :
154+ def parse_toc_yaml (path : Union [str , Path ], encoding : str = "utf8" ) -> SiteMap :
151155 """Parse the ToC file."""
152156 with Path (path ).open (encoding = encoding ) as handle :
153157 data = yaml .safe_load (handle )
@@ -168,7 +172,7 @@ def parse_toc_data(data: Dict[str, Any]) -> SiteMap:
168172
169173
170174def _parse_doc_item (
171- data : Dict [str , Any ], defaults : Dict [str , Any ], path : str , file_key : str = "file"
175+ data : Dict [str , Any ], defaults : Dict [str , Any ], path : str , file_key : str = FILE_KEY
172176) -> Tuple [DocItem , Sequence [Dict [str , Any ]]]:
173177 """Parse a single doc item."""
174178 if file_key not in data :
@@ -184,7 +188,7 @@ def _parse_doc_item(
184188 if not isinstance (parts_data , Sequence ):
185189 raise MalformedError (f"'parts' not a sequence: '{ path } '" )
186190
187- _known_link_keys = {"file" , "glob" , "url" }
191+ _known_link_keys = {FILE_KEY , GLOB_KEY , URL_KEY }
188192
189193 parts = []
190194 for part_idx , part in enumerate (parts_data ):
@@ -203,12 +207,12 @@ def _parse_doc_item(
203207 "toctree section contains incompatible keys "
204208 f"{ link_keys !r} : { path } { part_idx } /{ sect_idx } "
205209 )
206- if link_keys == {"file" }:
207- sections .append (FileItem (section ["file" ]))
208- elif link_keys == {"glob" }:
209- sections .append (GlobItem (section ["glob" ]))
210- elif link_keys == {"url" }:
211- sections .append (UrlItem (section ["url" ], section .get ("title" )))
210+ if link_keys == {FILE_KEY }:
211+ sections .append (FileItem (section [FILE_KEY ]))
212+ elif link_keys == {GLOB_KEY }:
213+ sections .append (GlobItem (section [GLOB_KEY ]))
214+ elif link_keys == {URL_KEY }:
215+ sections .append (UrlItem (section [URL_KEY ], section .get ("title" )))
212216
213217 # generate toc key-word arguments
214218 keywords = {}
@@ -239,7 +243,7 @@ def _parse_doc_item(
239243 section
240244 for part in parts_data
241245 for section in part ["sections" ]
242- if "file" in section
246+ if FILE_KEY in section
243247 ]
244248
245249 return (
@@ -264,3 +268,72 @@ def _parse_docs_list(
264268 site_map [docname ] = child_item
265269
266270 _parse_docs_list (child_docs_list , site_map , defaults , child_path )
271+
272+
273+ def create_toc_dict (site_map : SiteMap , * , skip_defaults : bool = True ) -> Dict [str , Any ]:
274+ """Create the Toc dictionary from a site-map."""
275+ data = _docitem_to_dict (
276+ site_map .root , site_map , skip_defaults = skip_defaults , file_key = "root"
277+ )
278+ if site_map .meta :
279+ data ["meta" ] = site_map .meta .copy ()
280+ return data
281+
282+
283+ def _docitem_to_dict (
284+ doc_item : DocItem ,
285+ site_map : SiteMap ,
286+ * ,
287+ skip_defaults : bool = True ,
288+ file_key : str = FILE_KEY ,
289+ parsed_docnames : Optional [Set [str ]] = None ,
290+ ) -> Dict [str , Any ]:
291+
292+ # protect against infinite recursion
293+ parsed_docnames = parsed_docnames or set ()
294+ if doc_item .docname in parsed_docnames :
295+ raise RecursionError (f"{ doc_item .docname !r} in site-map multiple times" )
296+ parsed_docnames .add (doc_item .docname )
297+
298+ data : Dict [str , Any ] = {}
299+
300+ data [file_key ] = doc_item .docname
301+ if doc_item .title is not None :
302+ data ["title" ] = doc_item .title
303+
304+ if not doc_item .parts :
305+ return data
306+
307+ def _parse_section (item ):
308+ if isinstance (item , FileItem ):
309+ return _docitem_to_dict (
310+ site_map [item ],
311+ site_map ,
312+ skip_defaults = skip_defaults ,
313+ parsed_docnames = parsed_docnames ,
314+ )
315+ if isinstance (item , GlobItem ):
316+ return {GLOB_KEY : str (item )}
317+ if isinstance (item , UrlItem ):
318+ if item .title is not None :
319+ return {URL_KEY : item .url , "title" : item .title }
320+ return {URL_KEY : item .url }
321+ raise TypeError (item )
322+
323+ data ["parts" ] = []
324+ fields = attr .fields_dict (TocItem )
325+ for part in doc_item .parts :
326+ # only add these keys if their value is not the default
327+ part_data = {
328+ key : getattr (part , key )
329+ for key in ("caption" , "numbered" , "reversed" , "titlesonly" )
330+ if (not skip_defaults ) or getattr (part , key ) != fields [key ].default
331+ }
332+ part_data ["sections" ] = [_parse_section (s ) for s in part .sections ]
333+ data ["parts" ].append (part_data )
334+
335+ # apply shorthand if possible
336+ if len (data ["parts" ]) == 1 and list (data ["parts" ][0 ]) == ["sections" ]:
337+ data ["sections" ] = data .pop ("parts" )[0 ]["sections" ]
338+
339+ return data
0 commit comments