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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- 馃悰 FIX: `button-link` and `button-ref` render an RST line block as separate
lines, including when the bars are indented under an option such as
`:expand:`, instead of keeping the `|` characters in the button
({pr}`308`, {issue}`156`)
- 鈾伙笍 IMPROVE: Replace the Sass/Node build with a dependency-free Python CSS
generator (`tools/generate_css.py` driven by `style/design.toml` and
hand-authored `style/*.css`); `package.json` is gone. The compiled
Expand Down
71 changes: 68 additions & 3 deletions sphinx_design/badges_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,60 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
return [node], []


_LINE_BLOCK_LINE = re.compile(r"^[ \t]*\|(.*)$")


def _is_button_line_block(content: Sequence[str]) -> bool:
"""Return whether ``content`` is only an RST line block.

``inline_text`` does not understand block markup, so a button whose body is
``| hey`` / ``| new line`` would otherwise keep the bar characters (#156).
"""
nonempty = [line for line in content if line.strip()]
return bool(nonempty) and all(_LINE_BLOCK_LINE.match(line) for line in nonempty)


def _flatten_line_block(block: nodes.line_block) -> list[nodes.Node]:
"""Turn a parsed line block into inline nodes separated by ``<br>``."""
pieces: list[nodes.Node] = []
for child in block:
if pieces:
pieces.append(nodes.raw("", "<br>", format="html"))
if isinstance(child, nodes.line):
pieces.extend(item.deepcopy() for item in child.children)
elif isinstance(child, nodes.line_block):
pieces.extend(_flatten_line_block(child))
return pieces


def _line_blocks_in(node: nodes.Element) -> list[nodes.line_block] | None:
"""Return line blocks when ``node`` contains nothing else.

A bar indented past a directive option (``:expand:``) is a block quote
whose only child is the line block. A blank line between bars is two
sibling line blocks. Both are the markup from #156; anything else is not.
"""
children = list(node)
while len(children) == 1 and isinstance(children[0], nodes.block_quote):
children = list(children[0])
if children and all(isinstance(child, nodes.line_block) for child in children):
return children
return None


def _flatten_line_blocks(blocks: list[nodes.line_block]) -> list[nodes.Node]:
"""Join line blocks with a break where a blank line split them."""
pieces: list[nodes.Node] = []
for block in blocks:
flattened = _flatten_line_block(block)
if not flattened:
continue
if pieces:
pieces.append(nodes.raw("", "<br>", format="html"))
pieces.extend(flattened)
return pieces


class _ButtonDirective(SdDirective):
"""A base button directive."""

Expand Down Expand Up @@ -368,6 +422,19 @@ def create_ref_node(
"""Create the reference node."""
raise NotImplementedError

def _button_content_nodes(self) -> list[nodes.Node]:
"""Parse button body, expanding an RST line block into separate lines."""
if _is_button_line_block(self.content):
container = nodes.Element()
self.state.nested_parse(self.content, self.content_offset, container)
blocks = _line_blocks_in(container)
if blocks is not None:
return _flatten_line_blocks(blocks)
textnodes, _ = self.state.inline_text(
"\n".join(self.content), self.lineno + self.content_offset
)
return textnodes

def run_with_defaults(self) -> list[nodes.Node]:
rawtext = self.arguments[0]
target = self.get_target(rawtext)
Expand All @@ -392,9 +459,7 @@ def run_with_defaults(self) -> list[nodes.Node]:
node["reftitle"] = self.options["tooltip"]

if self.content:
textnodes, _ = self.state.inline_text(
"\n".join(self.content), self.lineno + self.content_offset
)
textnodes = self._button_content_nodes()
# make link text translatable -
# target gettext to the content lines, not the outer directive
translatable = nodes.inline("", "", *textnodes, translatable=True)
Expand Down
76 changes: 76 additions & 0 deletions tests/test_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
``button-link``, rather than being flattened to plain text.
* An unresolved ``button-ref`` target must emit a normal missing-reference
warning and still build (no traceback).
* #156 - a ``button-link`` whose content is an RST line block must show the
lines, not the literal ``|`` markers.
"""

from docutils import nodes
Expand Down Expand Up @@ -325,6 +327,80 @@ def test_button_ref_nested_xref_no_crash(fmt, sphinx_builder):
assert _BUTTON_REF_MARKER_PREFIX not in html


LINE_BLOCK_RST = """
Heading
=======

.. button-link:: https://example.com
:expand:

| hey
| new line
"""

# The report indents the bars one column past ``:expand:``. Docutils then
# parses a block quote around the line block, and the bars used to survive.
ISSUE_LINE_BLOCK_RST = """
Heading
=======

.. button-link:: example.com
:expand:

| hey
| new line
"""

BLANK_LINE_BLOCK_RST = """
Heading
=======

.. button-link:: https://example.com

| hey

| new line
"""


def _assert_button_lines(html: str, href: str) -> None:
assert f'href="{href}"' in html
assert "| hey" not in html
assert "| new line" not in html
assert "hey<br>new line" in html


def test_button_link_line_block_is_not_literal(sphinx_builder):
"""RST line blocks inside ``button-link`` become separate lines (#156)."""
builder = sphinx_builder(conf_kwargs={"extensions": ["sphinx_design"]})
builder.src_path.joinpath("index.rst").write_text(LINE_BLOCK_RST, encoding="utf8")
builder.build()
html = (builder.out_path / "index.html").read_text(encoding="utf8")
_assert_button_lines(html, "https://example.com")


def test_button_link_line_block_indented_under_option(sphinx_builder):
"""Bars indented past an option still render as lines (#156)."""
builder = sphinx_builder(conf_kwargs={"extensions": ["sphinx_design"]})
builder.src_path.joinpath("index.rst").write_text(
ISSUE_LINE_BLOCK_RST, encoding="utf8"
)
builder.build()
html = (builder.out_path / "index.html").read_text(encoding="utf8")
_assert_button_lines(html, "example.com")


def test_button_link_line_block_blank_line(sphinx_builder):
"""A blank line between bars does not leave the markers in the button."""
builder = sphinx_builder(conf_kwargs={"extensions": ["sphinx_design"]})
builder.src_path.joinpath("index.rst").write_text(
BLANK_LINE_BLOCK_RST, encoding="utf8"
)
builder.build()
html = (builder.out_path / "index.html").read_text(encoding="utf8")
_assert_button_lines(html, "https://example.com")


def test_button_ref_titlecase_ref_type(sphinx_builder):
"""A Title-Case target with ``ref-type: ref`` resolves like ``:ref:`` does.

Expand Down