Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rst] Add level option to rubric directive #12506

Merged
merged 6 commits into from
Jul 14, 2024
Merged
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
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.
Comment on lines +31 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go at the end (chronological ordering)

* 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
Comment on lines +379 to +380
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think level is a little vague, perhaps heading-level or section-level?


.. 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:
Comment on lines +531 to +533
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an assert on the allowed level (to avoid people injecting invalid attributes). And make sure that it's also of type int.

The level should not be at most 6 since otherwise it's not a valid HTML tag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a warning for unsupported level values has been added

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say node['level'] = 9, the warning would be emitted on visit_ with no opening tag, but the closing </h9> tag would still be added. This isn't possible using the directive but we should guard against it (and test appropriately).

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check levels 1 to 6 and invalid levels as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

: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
Comment on lines +36 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only tests that the directive is rejecting numbers outwith 1-6, we need to test the writers independently.


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