Skip to content

Commit 18ac58b

Browse files
committed
Fix possibly-invalid HTML when a rubric node is manually created
1 parent ffd7b78 commit 18ac58b

File tree

7 files changed

+46
-25
lines changed

7 files changed

+46
-25
lines changed

CHANGES.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Features added
1616
Bugs fixed
1717
----------
1818

19+
* Fix invalid HTML when a rubric node with invalid ``heading-level`` is used.
20+
Patch by Adam Turner.
21+
1922
Testing
2023
-------
2124

@@ -120,7 +123,7 @@ Features added
120123
* #11773: Display :py:class:`~typing.Annotated` annotations
121124
with their metadata in the Python domain.
122125
Patch by Adam Turner and David Stansby.
123-
* #12506: Add ``level`` option to :rst:dir:`rubric` directive.
126+
* #12506: Add ``heading-level`` option to :rst:dir:`rubric` directive.
124127
Patch by Chris Sewell.
125128
* #12567: Add the :event:`write-started` event.
126129
Patch by Chris Sewell.

doc/usage/restructuredtext/directives.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,10 @@ Presentational
544544
A rubric is like an informal heading that doesn't correspond to the document's structure,
545545
i.e. it does not create a table of contents node.
546546

547-
.. rst:directive:option:: level: n
547+
.. rst:directive:option:: heading-level: n
548548
:type: number from 1 to 6
549549
550-
.. versionadded:: 7.4
550+
.. versionadded:: 7.4.1
551551
552552
Use this option to specify the heading level of the rubric.
553553
In this case the rubric will be rendered as ``<h1>`` to ``<h6>`` for HTML output,

sphinx/directives/patches.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,17 @@ class Rubric(SphinxDirective):
187187
option_spec = {
188188
'class': directives.class_option,
189189
'name': directives.unchanged,
190-
'level': lambda c: directives.choice(c, ('1', '2', '3', '4', '5', '6')),
190+
'heading-level': lambda c: directives.choice(c, ('1', '2', '3', '4', '5', '6')),
191191
}
192192

193-
def run(self) -> list[Node]:
193+
def run(self) -> list[nodes.rubric | nodes.system_message]:
194194
set_classes(self.options)
195195
rubric_text = self.arguments[0]
196196
textnodes, messages = self.parse_inline(rubric_text, lineno=self.lineno)
197+
if 'heading-level' in self.options:
198+
self.options['heading-level'] = int(self.options['heading-level'])
197199
rubric = nodes.rubric(rubric_text, '', *textnodes, **self.options)
198200
self.add_name(rubric)
199-
if 'level' in self.options:
200-
rubric['level'] = int(self.options['level'])
201201
return [rubric, *messages]
202202

203203

sphinx/writers/html5.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -510,10 +510,10 @@ def depart_title(self, node: Element) -> None:
510510
super().depart_title(node)
511511

512512
# overwritten
513-
def visit_rubric(self, node: Element) -> None:
514-
if "level" in node:
515-
level = node["level"]
516-
if level in (1, 2, 3, 4, 5, 6):
513+
def visit_rubric(self, node: nodes.rubric) -> None:
514+
if 'heading-level' in node:
515+
level = node['heading-level']
516+
if level in {1, 2, 3, 4, 5, 6}:
517517
self.body.append(self.starttag(node, f'h{level}', '', CLASS='rubric'))
518518
else:
519519
logger.warning(
@@ -527,8 +527,8 @@ def visit_rubric(self, node: Element) -> None:
527527
super().visit_rubric(node)
528528

529529
# overwritten
530-
def depart_rubric(self, node: Element) -> None:
531-
if level := node.get("level"):
530+
def depart_rubric(self, node: nodes.rubric) -> None:
531+
if (level := node.get('heading-level')) in {1, 2, 3, 4, 5, 6}:
532532
self.body.append(f'</h{level}>\n')
533533
else:
534534
super().depart_rubric(node)

sphinx/writers/latex.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -972,12 +972,12 @@ def depart_seealso(self, node: Element) -> None:
972972
self.body.append(r'\end{sphinxseealso}')
973973
self.body.append(BLANKLINE)
974974

975-
def visit_rubric(self, node: Element) -> None:
975+
def visit_rubric(self, node: nodes.rubric) -> None:
976976
if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
977977
raise nodes.SkipNode
978978
tag = 'subsubsection'
979-
if "level" in node:
980-
level = node["level"]
979+
if 'heading-level' in node:
980+
level = node['heading-level']
981981
try:
982982
tag = self.sectionnames[self.top_sectionlevel - 1 + level]
983983
except Exception:
@@ -992,7 +992,7 @@ def visit_rubric(self, node: Element) -> None:
992992
self.context.append('}' + CR)
993993
self.in_title = 1
994994

995-
def depart_rubric(self, node: Element) -> None:
995+
def depart_rubric(self, node: nodes.rubric) -> None:
996996
self.in_title = 0
997997
self.body.append(self.context.pop())
998998

tests/roots/test-markup-rubric/index.rst

+7-7
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@ test-markup-rubric
1010
:class: myclass
1111

1212
.. rubric:: A rubric with a heading level 1
13-
:level: 1
13+
:heading-level: 1
1414
:class: myclass
1515

1616
.. rubric:: A rubric with a heading level 2
17-
:level: 2
17+
:heading-level: 2
1818
:class: myclass
1919

2020
.. rubric:: A rubric with a heading level 3
21-
:level: 3
21+
:heading-level: 3
2222
:class: myclass
2323

2424
.. rubric:: A rubric with a heading level 4
25-
:level: 4
25+
:heading-level: 4
2626
:class: myclass
2727

2828
.. rubric:: A rubric with a heading level 5
29-
:level: 5
29+
:heading-level: 5
3030
:class: myclass
3131

3232
.. rubric:: A rubric with a heading level 6
33-
:level: 6
33+
:heading-level: 6
3434
:class: myclass
3535

3636
.. rubric:: A rubric with a heading level 7
37-
:level: 7
37+
:heading-level: 7
3838
:class: myclass
3939

tests/test_builders/test_build_html_5_output.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44

55
import pytest
6+
from docutils import nodes
67

78
from tests.test_builders.xpath_util import check_xpath
89

@@ -282,8 +283,25 @@ def test_html5_output(app, cached_etree_parse, fname, path, check):
282283

283284
@pytest.mark.sphinx('html', testroot='markup-rubric')
284285
def test_html5_rubric(app):
286+
def insert_invalid_rubric_heading_level(app, doctree, docname):
287+
if docname != 'index':
288+
return
289+
new_node = nodes.rubric('', 'INSERTED RUBRIC')
290+
new_node['heading-level'] = 7
291+
doctree[0].append(new_node)
292+
293+
app.connect('doctree-resolved', insert_invalid_rubric_heading_level)
285294
app.build()
286-
assert '"7" unknown' in app.warning.getvalue()
295+
296+
warnings = app.warning.getvalue()
287297
content = (app.outdir / 'index.html').read_text(encoding='utf8')
288298
assert '<p class="rubric">This is a rubric</p>' in content
289299
assert '<h2 class="myclass rubric">A rubric with a heading level 2</h2>' in content
300+
301+
# directive warning
302+
assert '"7" unknown' in warnings
303+
304+
# html writer warning
305+
assert 'WARNING: unsupported rubric heading level: 7' in warnings
306+
assert '</h7>' not in content
307+
assert '<p class="rubric">INSERTED RUBRIC</p>' in content

0 commit comments

Comments
 (0)