Skip to content

Commit

Permalink
[rst] Add level option to rubric directive (#12506)
Browse files Browse the repository at this point in the history
This commit adds a `level` option to the `rubric` directive, which propagates a `level` attribute to the `rubric` node,
and allows renderers to select a specific heading level.

Logic for this attribute has been added to the HTML5 and LaTeX builder.
  • Loading branch information
chrisjsewell committed Jul 14, 2024
1 parent 35e7bfc commit 41b363d
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Features added

.. rst-class:: compact

* Add ``level`` option to :rst:dir:`rubric` directive.
Patch by Chris Sewell.
* Add optional ``description`` argument to
:meth:`~sphinx.application.Sphinx.add_config_value`.
Patch by Chris Sewell.
Expand Down
14 changes: 12 additions & 2 deletions doc/usage/restructuredtext/directives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,18 @@ units as well as normal text.

.. rst:directive:: .. rubric:: title
This directive creates a paragraph heading that is not used to create a
table of contents node.
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
:type: number from 1 to 6
.. versionadded:: 7.4
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,
or as the corresponding non-numbered sectioning command for LaTeX
(see :confval:`latex_toplevel_sectioning`).
.. note::

Expand Down
26 changes: 26 additions & 0 deletions sphinx/directives/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,38 @@ def add_target(self, ret: list[Node]) -> None:
ret.insert(0, target)


class Rubric(SphinxDirective):
"""A patch of the docutils' :rst:dir:`rubric` directive,
which adds a level option to specify the heading level of the rubric.
"""

required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {
'class': directives.class_option,
'name': directives.unchanged,
'level': lambda c: directives.choice(c, ('1', '2', '3', '4', '5', '6')),
}

def run(self) -> list[Node]:
set_classes(self.options)
rubric_text = self.arguments[0]
textnodes, messages = self.parse_inline(rubric_text, lineno=self.lineno)
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]


def setup(app: Sphinx) -> ExtensionMetadata:
directives.register_directive('figure', Figure)
directives.register_directive('meta', Meta)
directives.register_directive('csv-table', CSVTable)
directives.register_directive('code', Code)
directives.register_directive('math', MathDirective)
directives.register_directive('rubric', Rubric)

return {
'version': 'builtin',
Expand Down
24 changes: 24 additions & 0 deletions sphinx/writers/html5.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,30 @@ def depart_title(self, node: Element) -> None:

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):
self.body.append(self.starttag(node, f'h{level}', '', CLASS='rubric'))
else:
logger.warning(
__('unsupported rubric heading level: %s'),
level,
type='html',
location=node
)
super().visit_rubric(node)
else:
super().visit_rubric(node)

# overwritten
def depart_rubric(self, node: Element) -> None:
if level := node.get("level"):
self.body.append(f'</h{level}>\n')
else:
super().depart_rubric(node)

# overwritten
def visit_literal_block(self, node: Element) -> None:
if node.rawsource != node.astext():
Expand Down
15 changes: 14 additions & 1 deletion sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,20 @@ def depart_seealso(self, node: Element) -> None:
def visit_rubric(self, node: Element) -> None:
if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
raise nodes.SkipNode
self.body.append(r'\subsubsection*{')
tag = 'subsubsection'
if "level" in node:
level = node["level"]
try:
tag = self.sectionnames[self.top_sectionlevel - 1 + level]
except Exception:
logger.warning(
__('unsupported rubric heading level: %s'),
level,
type='latex',
location=node
)

self.body.append(rf'\{tag}*{{')
self.context.append('}' + CR)
self.in_title = 1

Expand Down
1 change: 1 addition & 0 deletions tests/roots/test-markup-rubric/conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
latex_documents = [
('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
]
latex_toplevel_sectioning = 'section'
32 changes: 32 additions & 0 deletions tests/roots/test-markup-rubric/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,35 @@ test-markup-rubric

.. rubric:: This is
a multiline rubric

.. rubric:: A rubric with a class
:class: myclass

.. rubric:: A rubric with a heading level 1
:level: 1
:class: myclass

.. rubric:: A rubric with a heading level 2
:level: 2
:class: myclass

.. rubric:: A rubric with a heading level 3
:level: 3
:class: myclass

.. rubric:: A rubric with a heading level 4
:level: 4
:class: myclass

.. rubric:: A rubric with a heading level 5
:level: 5
:class: myclass

.. rubric:: A rubric with a heading level 6
:level: 6
:class: myclass

.. rubric:: A rubric with a heading level 7
:level: 7
:class: myclass

9 changes: 9 additions & 0 deletions tests/test_builders/test_build_html_5_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,12 @@ def checker(nodes):
def test_html5_output(app, cached_etree_parse, fname, path, check):
app.build()
check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check)


@pytest.mark.sphinx('html', testroot='markup-rubric')
def test_html5_rubric(app):
app.build()
assert '"7" unknown' in 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
8 changes: 8 additions & 0 deletions tests/test_builders/test_build_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -1759,3 +1759,11 @@ def test_one_parameter_per_line(app, status, warning):
assert ('\\pysiglinewithargsret{\\sphinxbfcode{\\sphinxupquote{hello}}}' in result)

assert ('\\pysigwithonelineperarg{\\sphinxbfcode{\\sphinxupquote{foo}}}' in result)


@pytest.mark.sphinx('latex', testroot='markup-rubric')
def test_latex_rubric(app):
app.build()
content = (app.outdir / 'test.tex').read_text(encoding='utf8')
assert r'\subsubsection*{This is a rubric}' in content
assert r'\subsection*{A rubric with a heading level 2}' in content
1 change: 1 addition & 0 deletions tests/test_builders/test_build_texinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def test_texinfo_rubric(app, status, warning):
output = (app.outdir / 'projectnamenotset.texi').read_text(encoding='utf8')
assert '@heading This is a rubric' in output
assert '@heading This is a multiline rubric' in output
assert '@heading A rubric with a heading level' in output


@pytest.mark.sphinx('texinfo', testroot='markup-citation')
Expand Down

0 comments on commit 41b363d

Please sign in to comment.