diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f31abd0..3c746b17 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
+ ({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
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.