From 5bf434d81bc7e188c81288a463211fd77cab2d46 Mon Sep 17 00:00:00 2001 From: Yuzhong Zhang Date: Tue, 1 Sep 2026 18:29:56 +0000 Subject: [PATCH] Allow nested subtree entries without a file, glob, or url An entries item that is itself subtrees (no file/glob/url) raised MalformedError. Parse those nested toctrees onto the parent document. Fixes #89 --- sphinx_external_toc/parsing.py | 52 ++++++++++++++----- .../nested_subtree_entries_first.yml | 16 ++++++ .../nested_subtree_entries_not_first.yml | 14 +++++ ...toc_dict_nested_subtree_entries_first_.yml | 12 +++++ ...dict_nested_subtree_entries_not_first_.yml | 13 +++++ ..._sitemap_nested_subtree_entries_first_.yml | 35 +++++++++++++ ...emap_nested_subtree_entries_not_first_.yml | 35 +++++++++++++ ..._sitemap_nested_subtree_entries_first_.yml | 4 ++ ...emap_nested_subtree_entries_not_first_.yml | 4 ++ 9 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 tests/_toc_files/nested_subtree_entries_first.yml create mode 100644 tests/_toc_files/nested_subtree_entries_not_first.yml create mode 100644 tests/test_parsing/test_create_toc_dict_nested_subtree_entries_first_.yml create mode 100644 tests/test_parsing/test_create_toc_dict_nested_subtree_entries_not_first_.yml create mode 100644 tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_first_.yml create mode 100644 tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_not_first_.yml create mode 100644 tests/test_tools/test_file_to_sitemap_nested_subtree_entries_first_.yml create mode 100644 tests/test_tools/test_file_to_sitemap_nested_subtree_entries_not_first_.yml diff --git a/sphinx_external_toc/parsing.py b/sphinx_external_toc/parsing.py index 64bfa0f..5e5b9b1 100644 --- a/sphinx_external_toc/parsing.py +++ b/sphinx_external_toc/parsing.py @@ -195,6 +195,7 @@ def _parse_doc_item( _known_link_keys = {FILE_KEY, GLOB_KEY, URL_KEY} toctrees = [] + nested_docs_to_be_parsed: List[Tuple[str, Dict[str, Any]]] = [] for toc_idx, toc_data in enumerate(subtrees_data): toc_path = path if shorthand_used else f"{path}{toc_idx}/" @@ -208,8 +209,28 @@ def _parse_doc_item( if not (isinstance(items_data, Sequence) and items_data): raise MalformedError(f"'{items_key}' not a non-empty list @ '{toc_path}'") + # generate toc key-word arguments + keywords = {k: toc_data[k] for k in TOCTREE_OPTIONS if k in toc_data} + for key in defaults: + if key not in keywords: + keywords[key] = defaults[key] + # generate items list items: List[Union[GlobItem, FileItem, UrlItem]] = [] + + def _emit_toctree() -> None: + if not items: + return + try: + toc_item = TocTree(items=list(items), **keywords) + except (ValueError, TypeError) as exc: + exc_arg = exc.args[0] if exc.args else "" + raise MalformedError( + f"toctree validation @ '{toc_path}': {exc_arg}" + ) from exc + toctrees.append(toc_item) + items.clear() + for item_idx, item_data in enumerate(items_data): if not isinstance(item_data, Mapping): raise MalformedError( @@ -220,6 +241,21 @@ def _parse_doc_item( # validation checks if not link_keys: + # Nested subtrees without a file/glob/url attach to this document. + if subtrees_key in item_data or items_key in item_data: + _emit_toctree() + nested_path = f"{toc_path}{items_key}/{item_idx}/" + nested_data = {FILE_KEY: data[file_key], **item_data} + nested_doc, nested_docs_list = _parse_doc_item( + nested_data, + defaults, + nested_path, + depth=depth, + file_format=file_format, + ) + toctrees.extend(nested_doc.subtrees) + nested_docs_to_be_parsed.extend(nested_docs_list) + continue raise MalformedError( f"entry does not contain one of " f"{_known_link_keys!r} @ '{toc_path}{items_key}/{item_idx}'" @@ -250,20 +286,7 @@ def _parse_doc_item( f"entry validation @ '{toc_path}{items_key}/{item_idx}': {exc_arg}" ) from exc - # generate toc key-word arguments - keywords = {k: toc_data[k] for k in TOCTREE_OPTIONS if k in toc_data} - for key in defaults: - if key not in keywords: - keywords[key] = defaults[key] - - try: - toc_item = TocTree(items=items, **keywords) - except (ValueError, TypeError) as exc: - exc_arg = exc.args[0] if exc.args else "" - raise MalformedError( - f"toctree validation @ '{toc_path}': {exc_arg}" - ) from exc - toctrees.append(toc_item) + _emit_toctree() try: doc_item = Document( @@ -287,6 +310,7 @@ def _parse_doc_item( for ii, item_data in enumerate(toc_data[items_key]) if FILE_KEY in item_data ] + docs_to_be_parsed_list.extend(nested_docs_to_be_parsed) return ( doc_item, diff --git a/tests/_toc_files/nested_subtree_entries_first.yml b/tests/_toc_files/nested_subtree_entries_first.yml new file mode 100644 index 0000000..e6831fc --- /dev/null +++ b/tests/_toc_files/nested_subtree_entries_first.yml @@ -0,0 +1,16 @@ +root: intro +subtrees: +- maxdepth: 1 + entries: + - file: doc1 + title: Overview +- maxdepth: 2 + caption: Script shaping + entries: + - subtrees: + - numbered: 3 + maxdepth: 2 + caption: Indic + entries: + - file: doc2 + title: General model diff --git a/tests/_toc_files/nested_subtree_entries_not_first.yml b/tests/_toc_files/nested_subtree_entries_not_first.yml new file mode 100644 index 0000000..79a6c76 --- /dev/null +++ b/tests/_toc_files/nested_subtree_entries_not_first.yml @@ -0,0 +1,14 @@ +root: intro +subtrees: +- maxdepth: 2 + caption: Script shaping + entries: + - file: doc1 + title: Overview + - subtrees: + - numbered: 3 + maxdepth: 2 + caption: Indic + entries: + - file: doc2 + title: General model diff --git a/tests/test_parsing/test_create_toc_dict_nested_subtree_entries_first_.yml b/tests/test_parsing/test_create_toc_dict_nested_subtree_entries_first_.yml new file mode 100644 index 0000000..857c458 --- /dev/null +++ b/tests/test_parsing/test_create_toc_dict_nested_subtree_entries_first_.yml @@ -0,0 +1,12 @@ +root: intro +subtrees: +- entries: + - file: doc1 + title: Overview + maxdepth: 1 +- caption: Indic + entries: + - file: doc2 + title: General model + maxdepth: 2 + numbered: 3 diff --git a/tests/test_parsing/test_create_toc_dict_nested_subtree_entries_not_first_.yml b/tests/test_parsing/test_create_toc_dict_nested_subtree_entries_not_first_.yml new file mode 100644 index 0000000..c2027ed --- /dev/null +++ b/tests/test_parsing/test_create_toc_dict_nested_subtree_entries_not_first_.yml @@ -0,0 +1,13 @@ +root: intro +subtrees: +- caption: Script shaping + entries: + - file: doc1 + title: Overview + maxdepth: 2 +- caption: Indic + entries: + - file: doc2 + title: General model + maxdepth: 2 + numbered: 3 diff --git a/tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_first_.yml b/tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_first_.yml new file mode 100644 index 0000000..be05515 --- /dev/null +++ b/tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_first_.yml @@ -0,0 +1,35 @@ +documents: + doc1: + docname: doc1 + subtrees: [] + title: Overview + doc2: + docname: doc2 + subtrees: [] + title: General model + intro: + docname: intro + subtrees: + - caption: null + hidden: true + items: + - doc1 + maxdepth: 1 + numbered: false + restart_numbering: null + reversed: false + style: numerical + titlesonly: false + - caption: Indic + hidden: true + items: + - doc2 + maxdepth: 2 + numbered: 3 + restart_numbering: null + reversed: false + style: numerical + titlesonly: false + title: null +meta: {} +root: intro diff --git a/tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_not_first_.yml b/tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_not_first_.yml new file mode 100644 index 0000000..f5fdb51 --- /dev/null +++ b/tests/test_parsing/test_file_to_sitemap_nested_subtree_entries_not_first_.yml @@ -0,0 +1,35 @@ +documents: + doc1: + docname: doc1 + subtrees: [] + title: Overview + doc2: + docname: doc2 + subtrees: [] + title: General model + intro: + docname: intro + subtrees: + - caption: Script shaping + hidden: true + items: + - doc1 + maxdepth: 2 + numbered: false + restart_numbering: null + reversed: false + style: numerical + titlesonly: false + - caption: Indic + hidden: true + items: + - doc2 + maxdepth: 2 + numbered: 3 + restart_numbering: null + reversed: false + style: numerical + titlesonly: false + title: null +meta: {} +root: intro diff --git a/tests/test_tools/test_file_to_sitemap_nested_subtree_entries_first_.yml b/tests/test_tools/test_file_to_sitemap_nested_subtree_entries_first_.yml new file mode 100644 index 0000000..9c7c014 --- /dev/null +++ b/tests/test_tools/test_file_to_sitemap_nested_subtree_entries_first_.yml @@ -0,0 +1,4 @@ +- _toc.yml +- doc1.rst +- doc2.rst +- intro.rst diff --git a/tests/test_tools/test_file_to_sitemap_nested_subtree_entries_not_first_.yml b/tests/test_tools/test_file_to_sitemap_nested_subtree_entries_not_first_.yml new file mode 100644 index 0000000..9c7c014 --- /dev/null +++ b/tests/test_tools/test_file_to_sitemap_nested_subtree_entries_not_first_.yml @@ -0,0 +1,4 @@ +- _toc.yml +- doc1.rst +- doc2.rst +- intro.rst