mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2026-10-15 01:42:12 -05:00
Restore support for nested `only` nodes in toctrees (#13663)
This commit is contained in:
1 parent
88f7fa95fe
commit
c188e3f243
5 files changed
+192
-48
No files matched your search
@@ -482,56 +482,84 @@ def _toctree_add_classes(node: Element, depth: int, docname: str) -> None:
|
||||
subnode = subnode.parent
|
||||
|
||||
|
||||
ET = TypeVar('ET', bound=Element)
|
||||
_ET = TypeVar('_ET', bound=Element)
|
||||
|
||||
|
||||
def _toctree_copy(
|
||||
node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tags
|
||||
) -> ET:
|
||||
node: _ET, depth: int, maxdepth: int, collapse: bool, tags: Tags
|
||||
) -> _ET:
|
||||
"""Utility: Cut and deep-copy a TOC at a specified depth."""
|
||||
keep_bullet_list_sub_nodes = depth <= 1 or (
|
||||
(depth <= maxdepth or maxdepth <= 0) and (not collapse or 'iscurrent' in node)
|
||||
)
|
||||
assert not isinstance(node, addnodes.only)
|
||||
depth = max(depth - 1, 1)
|
||||
copied = _toctree_copy_seq(node, depth, maxdepth, collapse, tags, initial_call=True)
|
||||
assert len(copied) == 1
|
||||
return copied[0] # type: ignore[return-value]
|
||||
|
||||
copy = node.copy()
|
||||
for subnode in node.children:
|
||||
if isinstance(subnode, addnodes.compact_paragraph | nodes.list_item):
|
||||
# for <p> and <li>, just recurse
|
||||
copy.append(_toctree_copy(subnode, depth, maxdepth, collapse, tags))
|
||||
elif isinstance(subnode, nodes.bullet_list):
|
||||
# for <ul>, copy if the entry is top-level
|
||||
# or, copy if the depth is within bounds and;
|
||||
# collapsing is disabled or the sub-entry's parent is 'current'.
|
||||
# The boolean is constant so is calculated outwith the loop.
|
||||
if keep_bullet_list_sub_nodes:
|
||||
copy.append(_toctree_copy(subnode, depth + 1, maxdepth, collapse, tags))
|
||||
elif isinstance(subnode, addnodes.toctree):
|
||||
# copy sub toctree nodes for later processing
|
||||
copy.append(subnode.copy())
|
||||
elif isinstance(subnode, addnodes.only):
|
||||
# only keep children if the only node matches the tags
|
||||
if _only_node_keep_children(subnode, tags):
|
||||
for child in subnode.children:
|
||||
copy.append(
|
||||
_toctree_copy(
|
||||
child,
|
||||
depth,
|
||||
maxdepth,
|
||||
collapse,
|
||||
tags, # type: ignore[type-var]
|
||||
)
|
||||
)
|
||||
elif isinstance(subnode, nodes.reference | nodes.title):
|
||||
# deep copy references and captions
|
||||
sub_node_copy = subnode.copy()
|
||||
sub_node_copy.children = [child.deepcopy() for child in subnode.children]
|
||||
for child in sub_node_copy.children:
|
||||
child.parent = sub_node_copy
|
||||
copy.append(sub_node_copy)
|
||||
else:
|
||||
msg = f'Unexpected node type {subnode.__class__.__name__!r}!'
|
||||
raise ValueError(msg) # NoQA: TRY004
|
||||
return copy
|
||||
|
||||
def _toctree_copy_seq(
|
||||
node: Node,
|
||||
depth: int,
|
||||
maxdepth: int,
|
||||
collapse: bool,
|
||||
tags: Tags,
|
||||
*,
|
||||
initial_call: bool = False,
|
||||
is_current: bool = False,
|
||||
) -> list[Element]:
|
||||
copy: Element
|
||||
if isinstance(node, addnodes.compact_paragraph | nodes.list_item):
|
||||
# for <p> and <li>, just recurse
|
||||
copy = node.copy()
|
||||
for subnode in node.children:
|
||||
copy += _toctree_copy_seq( # type: ignore[assignment,operator]
|
||||
subnode, depth, maxdepth, collapse, tags, is_current='iscurrent' in node
|
||||
)
|
||||
return [copy]
|
||||
|
||||
if isinstance(node, nodes.bullet_list):
|
||||
# for <ul>, copy if the entry is top-level
|
||||
# or, copy if the depth is within bounds and;
|
||||
# collapsing is disabled or the sub-entry's parent is 'current'.
|
||||
# The boolean is constant so is calculated outwith the loop.
|
||||
keep_bullet_list_sub_nodes = depth <= 1 or (
|
||||
(depth <= maxdepth or maxdepth <= 0)
|
||||
and (not collapse or is_current or 'iscurrent' in node)
|
||||
)
|
||||
if not keep_bullet_list_sub_nodes and not initial_call:
|
||||
return []
|
||||
depth += 1
|
||||
copy = node.copy()
|
||||
for subnode in node.children:
|
||||
copy += _toctree_copy_seq(
|
||||
subnode, depth, maxdepth, collapse, tags, is_current='iscurrent' in node
|
||||
)
|
||||
return [copy]
|
||||
|
||||
if isinstance(node, addnodes.toctree):
|
||||
# copy sub toctree nodes for later processing
|
||||
return [node.copy()]
|
||||
|
||||
if isinstance(node, addnodes.only):
|
||||
# only keep children if the only node matches the tags
|
||||
if not _only_node_keep_children(node, tags):
|
||||
return []
|
||||
copied: list[Element] = []
|
||||
for subnode in node.children:
|
||||
copied += _toctree_copy_seq(
|
||||
subnode, depth, maxdepth, collapse, tags, is_current='iscurrent' in node
|
||||
)
|
||||
return copied
|
||||
|
||||
if isinstance(node, nodes.reference | nodes.title):
|
||||
# deep copy references and captions
|
||||
sub_node_copy = node.copy()
|
||||
sub_node_copy.children = [child.deepcopy() for child in node.children]
|
||||
for child in sub_node_copy.children:
|
||||
child.parent = sub_node_copy
|
||||
return [sub_node_copy]
|
||||
|
||||
msg = f'Unexpected node type {node.__class__.__name__!r}!'
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _get_toctree_ancestors(
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import jinja2.parser
|
||||
from sphinx.deprecation import RemovedInSphinx90Warning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator, Sequence
|
||||
from collections.abc import Collection, Iterator
|
||||
from typing import Literal
|
||||
|
||||
_ENV = jinja2.environment.Environment()
|
||||
@@ -42,7 +42,7 @@ class BooleanParser(jinja2.parser.Parser):
|
||||
|
||||
|
||||
class Tags:
|
||||
def __init__(self, tags: Sequence[str] = ()) -> None:
|
||||
def __init__(self, tags: Collection[str] = ()) -> None:
|
||||
self._tags = set(tags or ())
|
||||
self._condition_cache: dict[str, bool] = {}
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,26 @@
|
||||
test-toctree-only
|
||||
=================
|
||||
|
||||
.. only:: not nonexistent
|
||||
|
||||
hello world
|
||||
|
||||
.. only:: text or not text
|
||||
|
||||
.. js:data:: test_toctree_only1
|
||||
|
||||
lorem ipsum dolor sit amet...
|
||||
|
||||
.. only:: not lorem
|
||||
|
||||
.. only:: not ipsum
|
||||
|
||||
.. js:data:: test_toctree_only2
|
||||
|
||||
lorem ipsum dolor sit amet...
|
||||
|
||||
after ``only:: not ipsum``
|
||||
|
||||
.. js:data:: test_toctree_only2
|
||||
|
||||
we're just normal men; we're just innocent men
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import docutils
|
||||
import pytest
|
||||
from docutils import nodes
|
||||
from docutils.nodes import bullet_list, list_item, literal, reference, title
|
||||
@@ -11,8 +12,13 @@ from docutils.nodes import bullet_list, list_item, literal, reference, title
|
||||
from sphinx import addnodes
|
||||
from sphinx.addnodes import compact_paragraph, only
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.environment.adapters.toctree import document_toc, global_toctree_for_doc
|
||||
from sphinx.environment.adapters.toctree import (
|
||||
_toctree_copy,
|
||||
document_toc,
|
||||
global_toctree_for_doc,
|
||||
)
|
||||
from sphinx.testing.util import assert_node
|
||||
from sphinx.util.tags import Tags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sphinx.testing.util import SphinxTestApp
|
||||
@@ -916,3 +922,87 @@ def test_toctree_index(app):
|
||||
numbered=0,
|
||||
entries=[(None, 'genindex'), (None, 'modindex'), (None, 'search')],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.sphinx('dummy', testroot='toctree-only')
|
||||
def test_toctree_only(app):
|
||||
# regression test for https://github.com/sphinx-doc/sphinx/issues/13022
|
||||
# we mainly care that this doesn't fail
|
||||
|
||||
if docutils.__version_info__[:2] >= (0, 22):
|
||||
true = '1'
|
||||
else:
|
||||
true = 'True'
|
||||
expected_pformat = f"""\
|
||||
<bullet_list>
|
||||
<list_item>
|
||||
<compact_paragraph>
|
||||
<reference anchorname="" internal="{true}" refuri="#">
|
||||
test-toctree-only
|
||||
<bullet_list>
|
||||
<list_item>
|
||||
<compact_paragraph skip_section_number="{true}">
|
||||
<reference anchorname="#test_toctree_only1" internal="{true}" refuri="#test_toctree_only1">
|
||||
<literal>
|
||||
test_toctree_only1
|
||||
<list_item>
|
||||
<compact_paragraph skip_section_number="{true}">
|
||||
<reference anchorname="#test_toctree_only2" internal="{true}" refuri="#test_toctree_only2">
|
||||
<literal>
|
||||
test_toctree_only2
|
||||
<list_item>
|
||||
<compact_paragraph skip_section_number="{true}">
|
||||
<reference anchorname="#id0" internal="{true}" refuri="#id0">
|
||||
<literal>
|
||||
test_toctree_only2
|
||||
"""
|
||||
app.build()
|
||||
toc = document_toc(app.env, 'index', app.tags)
|
||||
assert toc.pformat(' ') == expected_pformat
|
||||
|
||||
|
||||
def test_toctree_copy_only():
|
||||
# regression test for https://github.com/sphinx-doc/sphinx/issues/13022
|
||||
# ensure ``_toctree_copy()`` properly filters out ``only`` nodes,
|
||||
# including nested nodes.
|
||||
node = nodes.literal('lobster!', 'lobster!')
|
||||
node = nodes.reference('', '', node, anchorname='', internal=True, refuri='index')
|
||||
node = addnodes.only('', node, expr='lobster')
|
||||
node = addnodes.compact_paragraph('', '', node, skip_section_number=True)
|
||||
node = nodes.list_item('', node)
|
||||
node = addnodes.only('', node, expr='not spam')
|
||||
node = addnodes.only('', node, expr='lobster')
|
||||
node = addnodes.only('', node, expr='not ham')
|
||||
node = nodes.bullet_list('', node)
|
||||
# this is a tree of the shape:
|
||||
# <bullet_list>
|
||||
# <only expr="not ham">
|
||||
# <only expr="lobster">
|
||||
# <only expr="not spam">
|
||||
# <list_item>
|
||||
# <compact_paragraph skip_section_number="True">
|
||||
# <only expr="lobster">
|
||||
# <reference anchorname="" internal="True" refuri="index">
|
||||
# <literal>
|
||||
# lobster!
|
||||
|
||||
tags = Tags({'lobster'})
|
||||
toc = _toctree_copy(node, 2, 0, False, tags)
|
||||
# the filtered ToC should look like:
|
||||
# <bullet_list>
|
||||
# <list_item>
|
||||
# <compact_paragraph skip_section_number="True">
|
||||
# <reference anchorname="" internal="True" refuri="index">
|
||||
# <literal>
|
||||
# lobster!
|
||||
|
||||
# no only nodes should remain
|
||||
assert list(toc.findall(addnodes.only)) == []
|
||||
|
||||
# the tree is preserved
|
||||
assert isinstance(toc, nodes.bullet_list)
|
||||
assert isinstance(toc[0], nodes.list_item)
|
||||
assert isinstance(toc[0][0], addnodes.compact_paragraph)
|
||||
assert isinstance(toc[0][0][0], nodes.reference)
|
||||
assert isinstance(toc[0][0][0][0], nodes.literal)
|
||||
assert toc[0][0][0][0][0] == nodes.Text('lobster!')
|
||||
Reference in new issue
Block a user