Fix possibly-invalid HTML when a rubric node is manually created

This commit is contained in:
Adam Turner committed 2024-07-15 10:04:48 +01:00
1 parent ffd7b786d5
commit 18ac58bd53
7 files changed
+46 -25

No files matched your search

+4 -1
View File
@@ -16,6 +16,9 @@ Features added
Bugs fixed
----------
* Fix invalid HTML when a rubric node with invalid ``heading-level`` is used.
Patch by Adam Turner.
Testing
-------
@@ -120,7 +123,7 @@ Features added
* #11773: Display :py:class:`~typing.Annotated` annotations
with their metadata in the Python domain.
Patch by Adam Turner and David Stansby.
* #12506: Add ``level`` option to :rst:dir:`rubric` directive.
* #12506: Add ``heading-level`` option to :rst:dir:`rubric` directive.
Patch by Chris Sewell.
* #12567: Add the :event:`write-started` event.
Patch by Chris Sewell.
+2 -2
View File
@@ -544,10 +544,10 @@ Presentational
A rubric is like an informal heading that doesn't correspond to the document's structure,
i.e. it does not create a table of contents node.
.. rst:directive:option:: level: n
.. rst:directive:option:: heading-level: n
:type: number from 1 to 6
.. versionadded:: 7.4
.. versionadded:: 7.4.1
Use this option to specify the heading level of the rubric.
In this case the rubric will be rendered as ``<h1>`` to ``<h6>`` for HTML output,
+4 -4
View File
@@ -187,17 +187,17 @@ class Rubric(SphinxDirective):
option_spec = {
'class': directives.class_option,
'name': directives.unchanged,
'level': lambda c: directives.choice(c, ('1', '2', '3', '4', '5', '6')),
'heading-level': lambda c: directives.choice(c, ('1', '2', '3', '4', '5', '6')),
}
def run(self) -> list[Node]:
def run(self) -> list[nodes.rubric | nodes.system_message]:
set_classes(self.options)
rubric_text = self.arguments[0]
textnodes, messages = self.parse_inline(rubric_text, lineno=self.lineno)
if 'heading-level' in self.options:
self.options['heading-level'] = int(self.options['heading-level'])
rubric = nodes.rubric(rubric_text, '', *textnodes, **self.options)
self.add_name(rubric)
if 'level' in self.options:
rubric['level'] = int(self.options['level'])
return [rubric, *messages]
+6 -6
View File
@@ -510,10 +510,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
super().depart_title(node)
# overwritten
def visit_rubric(self, node: Element) -> None:
if "level" in node:
level = node["level"]
if level in (1, 2, 3, 4, 5, 6):
def visit_rubric(self, node: nodes.rubric) -> None:
if 'heading-level' in node:
level = node['heading-level']
if level in {1, 2, 3, 4, 5, 6}:
self.body.append(self.starttag(node, f'h{level}', '', CLASS='rubric'))
else:
logger.warning(
@@ -527,8 +527,8 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
super().visit_rubric(node)
# overwritten
def depart_rubric(self, node: Element) -> None:
if level := node.get("level"):
def depart_rubric(self, node: nodes.rubric) -> None:
if (level := node.get('heading-level')) in {1, 2, 3, 4, 5, 6}:
self.body.append(f'</h{level}>\n')
else:
super().depart_rubric(node)
+4 -4
View File
@@ -972,12 +972,12 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append(r'\end{sphinxseealso}')
self.body.append(BLANKLINE)
def visit_rubric(self, node: Element) -> None:
def visit_rubric(self, node: nodes.rubric) -> None:
if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
raise nodes.SkipNode
tag = 'subsubsection'
if "level" in node:
level = node["level"]
if 'heading-level' in node:
level = node['heading-level']
try:
tag = self.sectionnames[self.top_sectionlevel - 1 + level]
except Exception:
@@ -992,7 +992,7 @@ class LaTeXTranslator(SphinxTranslator):
self.context.append('}' + CR)
self.in_title = 1
def depart_rubric(self, node: Element) -> None:
def depart_rubric(self, node: nodes.rubric) -> None:
self.in_title = 0
self.body.append(self.context.pop())
+7 -7
View File
@@ -10,30 +10,30 @@ test-markup-rubric
:class: myclass
.. rubric:: A rubric with a heading level 1
:level: 1
:heading-level: 1
:class: myclass
.. rubric:: A rubric with a heading level 2
:level: 2
:heading-level: 2
:class: myclass
.. rubric:: A rubric with a heading level 3
:level: 3
:heading-level: 3
:class: myclass
.. rubric:: A rubric with a heading level 4
:level: 4
:heading-level: 4
:class: myclass
.. rubric:: A rubric with a heading level 5
:level: 5
:heading-level: 5
:class: myclass
.. rubric:: A rubric with a heading level 6
:level: 6
:heading-level: 6
:class: myclass
.. rubric:: A rubric with a heading level 7
:level: 7
:heading-level: 7
:class: myclass
@@ -3,6 +3,7 @@
import re
import pytest
from docutils import nodes
from tests.test_builders.xpath_util import check_xpath
@@ -282,8 +283,25 @@ def test_html5_output(app, cached_etree_parse, fname, path, check):
@pytest.mark.sphinx('html', testroot='markup-rubric')
def test_html5_rubric(app):
def insert_invalid_rubric_heading_level(app, doctree, docname):
if docname != 'index':
return
new_node = nodes.rubric('', 'INSERTED RUBRIC')
new_node['heading-level'] = 7
doctree[0].append(new_node)
app.connect('doctree-resolved', insert_invalid_rubric_heading_level)
app.build()
assert '"7" unknown' in app.warning.getvalue()
warnings = app.warning.getvalue()
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<p class="rubric">This is a rubric</p>' in content
assert '<h2 class="myclass rubric">A rubric with a heading level 2</h2>' in content
# directive warning
assert '"7" unknown' in warnings
# html writer warning
assert 'WARNING: unsupported rubric heading level: 7' in warnings
assert '</h7>' not in content
assert '<p class="rubric">INSERTED RUBRIC</p>' in content