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