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
4 changes: 2 additions & 2 deletions sphinx/builders/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class I18nTags(Tags):
always returns True value even if no tags are defined.
"""

def eval_condition(self, condition: Any) -> bool:
def eval_condition(self, condition: str) -> bool:
return True


Expand All @@ -140,7 +140,7 @@ def init(self) -> None:
super().init()
self.env.set_versioning_method(self.versioning_method,
self.env.config.gettext_uuid)
self.tags = I18nTags()
self.tags = self.app.tags = I18nTags()
self.catalogs: defaultdict[str, Catalog] = defaultdict(Catalog)

def get_target_uri(self, docname: str, typ: str | None = None) -> str:
Expand Down
111 changes: 66 additions & 45 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 abspath, relpath
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, cast
from typing import TYPE_CHECKING, ClassVar

from docutils import nodes
from docutils.parsers.rst import directives
Expand Down Expand Up @@ -319,6 +319,23 @@ def run(self) -> list[Node]:
class Only(SphinxDirective):
"""
Directive to only include text if the given tag(s) are enabled.

This directive functions somewhat akin to a pre-processor,
as tag expressions are constant throughout the build.
The ``only`` directive is the only one that is able to 'hoist'
content in the section hierarchy, as the expected usage includes
conditional inclusion of sections.

At present, there is no supported mechanism to parse content that
may contain section headings at a level equal to or greater than
the section containing the ``only`` directive. Implementation of
such a mechanism is possible, though complex. Prior to Sphinx 7.4,
this approach was used to implement the ``only`` directive.

The current implementation makes use of the ability to modify the
input lines of the document being parsed, whilst parsing it. This
is not encouraged. Given the nature of ``only`` as both a special
case and akin to a pre-processor, this is considered acceptable.
"""

has_content = True
Expand All @@ -328,51 +345,55 @@ 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
tags = self.env.app.builder.tags
expr = self.arguments[0]

try:
self.state.nested_parse(self.content, self.content_offset,
node, match_titles=True)
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
keep_content = tags.eval_condition(expr)
except Exception as err:
logger.warning(
__('exception while evaluating only directive expression: %s'),
err,
location=self.get_location())
keep_content = True

# Does the directive content end with a newline?
trailing_newline = self.block_text[-1] == '\n'

# Calculate line counts for the entire block, the content, and the preamble.
total_line_count = self.block_text.count('\n') + 1
content_line_count = len(self.content.data) + (1 * trailing_newline)
preamble_line_count = total_line_count - content_line_count

# Calculate the location of the directive content in the input lines.
offset_end = self.state_machine.line_offset
offset_start = offset_end - total_line_count + 1

# Every copy of ``input_lines`` must be updated, so we propagate up
# through the parent hierarchy.
input_lines = self.state_machine.input_lines
while input_lines is not None:
if keep_content:
blank_lines = [''] * preamble_line_count
content = self.content.data + ([''] * trailing_newline)
# Blank out the initial lines
input_lines.data[offset_start:offset_start + preamble_line_count] = blank_lines
# Replace the remaining lines with the unindented content
input_lines.data[offset_start + preamble_line_count:offset_end + 1] = content
else:
blank_lines = [''] * total_line_count
# Blank out every line
input_lines.data[offset_start:offset_end + 1] = blank_lines

# Update the offsets for the parent
if input_lines.parent_offset is not None:
offset_start += input_lines.parent_offset
offset_end += input_lines.parent_offset
input_lines = input_lines.parent

# ``1 - directive_lines`` is from the offset_start calculation
self.state_machine.next_line(1 - total_line_count)
return []


class Include(BaseInclude, SphinxDirective):
Expand Down
63 changes: 63 additions & 0 deletions sphinx/util/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,69 @@ def nested_parse_with_titles(state: Any, content: StringList, node: Node,
state.memo.section_level = surrounding_section_level


def _lift_sections(
node: Node,
*,
state_parent: Element | None,
inner_title_styles: list[str | tuple[str, str]],
surrounding_title_styles: list[str | tuple[str, str]],
) -> None:
"""Try to handle nested sections which should be raised higher up the doctree.

For example:

.. code-block:: rst

1. Sections in only directives
==============================

1.1. Section
-------------

.. only:: not nonexisting_tag

1.2. Section
-------------
Should be lifted one level.

1.3. Section
-------------
Should be here.

.. only:: not nonexisting_tag

2. Included document level heading
==================================
Should be lifted two levels.
"""
if state_parent is None:
return
extant_nested_sections = (
surrounding_title_styles
and inner_title_styles
and inner_title_styles[0] in surrounding_title_styles
)
if not extant_nested_sections:
# No nested sections so no special handling needed.
return
# Calculate the depths of the current and nested sections.
current_depth = 0
parent = state_parent
while not isinstance(parent.parent, nodes.document):
current_depth += 1
parent = parent.parent
nested_depth = surrounding_title_styles.index(inner_title_styles[0])
# Use these depths to determine where the nested sections should
# be placed in the doctree.
num_sections_to_raise = current_depth - nested_depth + 1
parent = state_parent
for _ in range(num_sections_to_raise):
if parent.parent:
parent = parent.parent
parent.append(node)
return


def clean_astext(node: Element) -> str:
"""Like node.astext(), but ignore images."""
node = node.deepcopy()
Expand Down
1 change: 1 addition & 0 deletions tests/roots/test-intl-only-directive/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exclude_patterns = ['_build']
14 changes: 14 additions & 0 deletions tests/roots/test-intl-only-directive/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Only directive
--------------

.. only:: html

In HTML.

.. only:: latex

In LaTeX.

.. only:: html or latex

In both.
29 changes: 29 additions & 0 deletions tests/roots/test-intl-only-directive/xx/LC_MESSAGES/index.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2010, Georg Brandl & Team
# This file is distributed under the same license as the Sphinx <Tests> package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Sphinx <Tests> 0.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-02-04 13:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "Only directive"
msgstr "ONLY DIRECTIVE"

msgid "In HTML."
msgstr "IN HTML."

msgid "In LaTeX."
msgstr "IN LATEX."

msgid "In both."
msgstr "IN BOTH."
58 changes: 28 additions & 30 deletions tests/test_directives/test_directive_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,37 @@

@pytest.mark.sphinx('text', testroot='directive-only')
def test_sectioning(app, status, warning):

def getsects(section):
if not isinstance(section, nodes.section):
return [getsects(n) for n in section.children]
title = section.next_node(nodes.title).astext().strip()
subsects = []
children = section.children[:]
while children:
node = children.pop(0)
if isinstance(node, nodes.section):
subsects.append(node)
continue
children = list(node.children) + children
return [title, [getsects(subsect) for subsect in subsects]]

def testsects(prefix, sects, indent=0):
title = sects[0]
parent_num = title.split()[0]
assert prefix == parent_num, \
'Section out of place: %r' % title
for i, subsect in enumerate(sects[1]):
num = subsect[0].split()[0]
assert re.match('[0-9]+[.0-9]*[.]', num), \
'Unnumbered section: %r' % subsect[0]
testsects(prefix + str(i + 1) + '.', subsect, indent + 4)

app.build(filenames=[app.srcdir / 'only.rst'])
doctree = app.env.get_doctree('only')
app.env.apply_post_transforms(doctree, 'only')

parts = [getsects(n)
for n in [_n for _n in doctree.children if isinstance(_n, nodes.section)]]
for i, s in enumerate(parts):
testsects(str(i + 1) + '.', s, 4)
parts = _get_sections(doctree)
for i, section in enumerate(parts):
_test_sections(f'{i + 1}.', section, 4)
assert len(parts) == 4, 'Expected 4 document level headings, got:\n%s' % \
'\n'.join(p[0] for p in parts)


def _get_sections(section: nodes.Node):
if not isinstance(section, nodes.section):
return list(map(_get_sections, section.children))
title = section.next_node(nodes.title).astext().strip()
subsections = []
children = section.children.copy()
while children:
node = children.pop(0)
if isinstance(node, nodes.section):
subsections.append(node)
continue
children = list(node.children) + children
return [title, list(map(_get_sections, subsections))]


def _test_sections(prefix: str, sections, indent=0):
title = sections[0]
parent_num = title.split()[0]
assert prefix == parent_num, f'Section out of place: {title!r}'
for i, subsection in enumerate(sections[1]):
num = subsection[0].split()[0]
assert re.match('[0-9]+[.0-9]*[.]', num), f'Unnumbered section: {subsection[0]!r}'
_test_sections(f'{prefix}{i + 1}.', subsection, indent + 4)
Loading