Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Release 9.1.1 (in development)
Bugs fixed
----------

* #13861: Fix section hierarchy and parser state after parsing sections inside
``only`` directives.

* #14465: LaTeX: PDF build crash since LaTeX June 2026 release if tables are
styled with ``'colorrows'`` (which is the default).
Patch by Jean-François B.
Expand Down
6 changes: 4 additions & 2 deletions doc/usage/restructuredtext/directives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1466,8 +1466,10 @@ Including content based on tags

.. warning::

This directive is designed to control only content of document. It could
not control sections, labels and so on.
The content of this directive is parsed during the read phase when its
expression evaluates to true. Sections in the content then participate
in the document's normal section hierarchy. If the active tags change,
Sphinx reparses the documents.

.. _table-directives:

Expand Down
16 changes: 15 additions & 1 deletion sphinx/builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,19 @@ def read(self) -> list[str]:

self.env.find_files(self.config, self)
updated = self.env.config_status != CONFIG_OK
added, changed, removed = self.env.get_outdated_files(updated)
current_tags = frozenset(self.tags)
previous_tags = getattr(self.env, '_parsed_tags', None)
tags_changed = (
bool(self.env.all_docs)
if previous_tags is None
else previous_tags != current_tags
)
added, changed, removed = self.env.get_outdated_files(
updated or (tags_changed and previous_tags is None)
)
if tags_changed and previous_tags is not None:
changed.update(self.env._only_docs & self.env.found_docs)
self.env._parsed_tags = current_tags

# allow user intervention as well
for docs in self.events.emit(
Expand All @@ -495,6 +507,8 @@ def read(self) -> list[str]:
self.env.config_status_extra or ''
)
logger.info('[%s] ', reason, nonl=True)
elif tags_changed:
logger.info('[%s] ', __('tags changed'), nonl=True)

logger.info(
__('%s added, %s changed, %s removed'),
Expand Down
92 changes: 44 additions & 48 deletions sphinx/directives/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
from os.path import relpath
from pathlib import Path
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING

from docutils import nodes
from docutils.parsers.rst import directives
Expand All @@ -21,7 +21,7 @@

if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Any, ClassVar
from typing import ClassVar

from docutils.nodes import Element, Node

Expand Down Expand Up @@ -318,54 +318,50 @@ class Only(SphinxDirective):
option_spec: ClassVar[OptionSpec] = {}

def run(self) -> list[Node]:
node = addnodes.only()
node.document = self.state.document
self.set_source_info(node)
node['expr'] = self.arguments[0]

# Same as util.nested_parse_with_titles but try to handle nested
# sections which should be raised higher up the doctree.
memo: Any = self.state.memo
surrounding_title_styles = memo.title_styles
surrounding_section_level = memo.section_level
memo.title_styles = []
memo.section_level = 0
self.env._only_docs.add(self.env.docname)
try:
self.state.nested_parse(
self.content, self.content_offset, node, match_titles=True
include_content = self.env._tags.eval_condition(self.arguments[0])
except Exception as err:
logger.warning(
__('exception while evaluating only directive expression: %s'),
err,
location=self.get_location(),
)
title_styles = memo.title_styles
if (
not surrounding_title_styles
or not title_styles
or title_styles[0] not in surrounding_title_styles
or not self.state.parent
):
# No nested sections so no special handling needed.
return [node]
# Calculate the depths of the current and nested sections.
current_depth = 0
parent = self.state.parent
while parent:
current_depth += 1
parent = parent.parent
current_depth -= 2
title_style = title_styles[0]
nested_depth = len(surrounding_title_styles)
if title_style in surrounding_title_styles:
nested_depth = surrounding_title_styles.index(title_style)
# Use these depths to determine where the nested sections should
# be placed in the doctree.
n_sects_to_raise = current_depth - nested_depth + 1
parent = cast('nodes.Element', self.state.parent)
for _i in range(n_sects_to_raise):
if parent.parent:
parent = parent.parent
parent.append(node)
return []
finally:
memo.title_styles = surrounding_title_styles
memo.section_level = surrounding_section_level
include_content = True

# Reparse enabled content in the document's active section context.
# Keep the block's line count unchanged so the parser can reread it at
# the original offset without shifting source locations.
total_line_count = self.block_text.count('\n') + 1
offset_end = self.state_machine.line_offset
offset_start = offset_end - total_line_count + 1
input_lines = self.state_machine.input_lines

content_start = 0
content_view = self.content
while content_view is not input_lines:
if content_view.parent is None or content_view.parent_offset is None:
msg = 'only directive content is detached from parser input'
raise RuntimeError(msg)
content_start += content_view.parent_offset
content_view = content_view.parent

# Included and nested input may be a ViewList slice; update each parent.
while input_lines is not None:
input_lines.data[offset_start : offset_end + 1] = [''] * total_line_count
if include_content:
input_lines.data[content_start : content_start + len(self.content)] = (
self.content.data
)

if input_lines.parent_offset is not None:
offset_start += input_lines.parent_offset
offset_end += input_lines.parent_offset
content_start += input_lines.parent_offset
input_lines = input_lines.parent

self.state_machine.next_line(1 - total_line_count)
return []


class Include(BaseInclude, SphinxDirective):
Expand Down
9 changes: 8 additions & 1 deletion sphinx/environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@

# This is increased every time an environment attribute is added
# or changed to properly invalidate pickle files.
ENV_VERSION = 66
ENV_VERSION = 67

# config status
CONFIG_UNSET = -1
Expand Down Expand Up @@ -122,6 +122,8 @@ def __init__(self, app: Sphinx) -> None:
self.events: EventManager = app.events
self.project: Project = app.project
self.version: Mapping[str, int] = _get_env_version(app.extensions)
self._parsed_tags: frozenset[str] | None = None
self._only_docs: set[str] = set()

# the method of doctree versioning; see set_versioning_method
self.versioning_condition: Literal[False] | Callable[[Node], bool] | None = None
Expand Down Expand Up @@ -411,6 +413,7 @@ def set_versioning_method(

def clear_doc(self, docname: str) -> None:
"""Remove all traces of a source file in the inventory."""
self._only_docs.discard(docname)
if docname in self.all_docs:
self.all_docs.pop(docname, None)
self.included.pop(docname, None)
Expand All @@ -430,6 +433,10 @@ def merge_info_from(
for docname in docnames:
self.all_docs[docname] = other.all_docs[docname]
self.included[docname] = other.included[docname]
if docname in other._only_docs:
self._only_docs.add(docname)
else:
self._only_docs.discard(docname)
if docname in other.reread_always:
self.reread_always.add(docname)

Expand Down
2 changes: 2 additions & 0 deletions sphinx/util/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ def has(self, tag: str) -> bool:

def add(self, tag: str) -> None:
self._tags.add(tag)
self._condition_cache.clear()

def remove(self, tag: str) -> None:
self._tags.discard(tag)
self._condition_cache.clear()

def eval_condition(self, condition: str) -> bool:
"""Evaluate a boolean condition.
Expand Down
2 changes: 1 addition & 1 deletion tests/js/fixtures/cpp/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/js/fixtures/multiterm/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/js/fixtures/partial/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/js/fixtures/titles/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading