From 02660a20956e207b8be74a478b96bc960aa47f89 Mon Sep 17 00:00:00 2001
From: cherith <315844584+Cherith1222@users.noreply.github.com>
Date: Thu, 8 Oct 2026 06:34:03 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Show=20button=20line?=
=?UTF-8?q?=20blocks=20as=20separate=20lines?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
button-link parsed its body as inline text, so an RST line-block bar stayed on the page. Bars indented under an option were parsed as a block quote and still kept the bar.
AI-assisted (Cursor). The account owner confirmed this can be submitted.
---
CHANGELOG.md | 4 ++
sphinx_design/badges_buttons.py | 71 ++++++++++++++++++++++++++++--
tests/test_buttons.py | 76 +++++++++++++++++++++++++++++++++
3 files changed, 148 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f31abd0..0c6dc7b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
+ ({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
diff --git a/sphinx_design/badges_buttons.py b/sphinx_design/badges_buttons.py
index ceb2e67a..05f08283 100644
--- a/sphinx_design/badges_buttons.py
+++ b/sphinx_design/badges_buttons.py
@@ -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 ``
``."""
+ pieces: list[nodes.Node] = []
+ for child in block:
+ if pieces:
+ pieces.append(nodes.raw("", "
", 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("", "
", format="html"))
+ pieces.extend(flattened)
+ return pieces
+
+
class _ButtonDirective(SdDirective):
"""A base button directive."""
@@ -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)
@@ -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)
diff --git a/tests/test_buttons.py b/tests/test_buttons.py
index 24318c3c..42c5a987 100644
--- a/tests/test_buttons.py
+++ b/tests/test_buttons.py
@@ -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
@@ -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
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.
From 8adb50eb830074c9dffdfca757f7a4b1abf38530 Mon Sep 17 00:00:00 2001
From: cherith <315844584+Cherith1222@users.noreply.github.com>
Date: Thu, 8 Oct 2026 06:35:38 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Link=20the=20button?=
=?UTF-8?q?=20line-block=20note=20to=20its=20pull=20request?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
AI-assisted (Cursor). The account owner confirmed this can be submitted.
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c6dc7b9..3c746b17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
- 🐛 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
- ({issue}`156`)
+ ({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