Skip to content

♻️ Buttons: parse line-block text line by line, once - #315

Open
chrisjsewell wants to merge 11 commits into
mainfrom
maint/button-line-splitter
Open

chrisjsewell wants to merge 11 commits into
mainfrom
maint/button-line-splitter

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

This replaces the mechanism #308 merged for #156 (a line block in a button's text renders as separate lines) before it ships in 0.8.0. The behaviour on valid input stays the same; only the way the lines are found changes. Thanks to @Cherith1222 for #308: the sd_line_break node and its per-builder rendering, the per-line gettext messages, the docs and the tests all stay as they are.

#308 block-parsed the button text with nested_parse, kept the result if it held only line blocks, and otherwise parsed the text again as inline markup. A parse has side effects that a fallback cannot undo, so text that looked like a line block at its outermost indent but was not only one was parsed twice. A blank line followed by a deeper bar, paragraph, directive or substitution definition, or a first line deeper than the rest, gave a spurious "Too many autonumbered footnote references" (leaving the reference unresolved), a "Block quote ends without a blank line" warning, a duplicate explicit target, or a substitution definition leaking into the page.

The first commit finds the lines textually instead. Every non-blank line at the first line's indent must start with docutils' line-block marker, and a deeper line directly after a non-blank line continues the previous line. Each line is then parsed once with inline_text, into a translatable inline of its own carrying that line's source and line number. A line's text is what docutils gives it: its continuation lines keep their indent relative to each other, and a bare bar's empty first line is dropped (this part landed in a later commit, after review). Any other text is parsed once as inline markup, as before #308. Text can no longer be parsed twice, and the block-quote unwrap, the system-message filter, the node move and the docutils < 0.22 source-info fallback all go away.

What was measured, on Sphinx 7.4.7, 8.2.3 and 9.1.1:

  • On a page of valid line blocks, the HTML, LaTeX, text, man, texinfo and .pot output is byte-identical to main. The page covered the New line in button -- not working #156 markup, continuation lines (some indented deeper than the line before), a bare |, a blank line between blocks, a nested bar, inline markup and a footnote, button-ref, a button in a dropdown, and an included file. A second page added buttons in table cells and list items, nested sphinx-design directives, and a no-break space after the bar.
  • The one difference is the .pot location comments on docutils < 0.22 (Sphinx 7.4 and 8.2 here). Main gave every line the first line's number there; each line now names its own line, as main already did on docutils 0.22.
  • On the malformed bodies, the output matches the pre-🐛 Show button line blocks as separate lines #308 release exactly, with no extra messages.
  • All of 🐛 Show button line blocks as separate lines #308's tests pass unmodified. The malformed bodies are added as regressions; they fail on main and pass on the pre-🐛 Show button line blocks as separate lines #308 code. There are also unit tests for the splitter.

The second commit fixes an older line-number bug in the inline path. In RST, content_offset already counts the lines before the directive, and the path added it to the directive's line anyway. For a button at line 4 of a page, a role error in its text was reported at line 9 instead of 6. Inside a dropdown it named a line past the end of the file. For a button in an included file, it was blamed on the including page, at the included line plus the include's offset. The .pot locations had the same errors. The text is now located at its first content line through the state machine, which is where docutils reports a paragraph's inline errors. The gettext regression fixture had recorded the wrong line (19 for text at line 11) and is corrected. MyST's content_offset counts lines from the directive, so the MyST arithmetic is unchanged; its warnings and .pot output were measured identical.

The third commit adds a caveat to the docs about breaking button text with a MyST hard line break (a backslash at the end of the line). This works in HTML and LaTeX. The text builder, however, joins the lines ("Multi-linebutton text"), texinfo prints the raw break markup, and the gettext message contains that markup too. The note now says so, and points translated projects at the RST line block, which works from MyST inside an eval-rst block.

The fourth commit pins what a label that itself starts with a bar does. In RST, | Docs | API | is a one-line line block, so its leading | is consumed and the button reads "Docs | API |" (a bare | label is an empty line block, so an empty button, as on main). A literal leading bar is written \|. A test covers both forms, and the docs say so after the multi-line example.

The later commits come from review. One keeps a continuation line's indent relative to the lines before it, as docutils does: the first commit stripped each continuation line, and kept the empty first line of a bare bar, which changed whitespace inside such lines. One pins each line-block line's own .pot location and message line, which no test had checked, including in an included file. One rewords three comments in #308's tests that still described the block parse; their assertions are unchanged. One keeps the text after a line's bar as docutils does: the splitter stripped any Unicode whitespace there, and counted it as indentation before a bar, so a line starting | and a no-break or ideographic space lost that space; its tests also pin a bare bar's exact HTML. One pins that a line at the bar's indent without a bar is never a continuation, and that a line's message keeps its markup. The last says in the docs that the leading-bar rule is for a one-line label: a two-line label whose second line is plain text is not a line block, so its bar is shown.

Gates: the full test suite passes on Sphinx 7.4.7, 8.2.3 and 9.1.1 (321 passed, 1 skipped), and pre-commit run --all-files passes. sphinx-build -nW of the docs is clean for html and man. Locally it is also clean for latex, built without sphinx.ext.imgconverter because ImageMagick is not installed here.

…e once

#308 recognised a line block in button text by block-parsing the text with
nested_parse, checking that the result held only line blocks, and otherwise
parsing it again as inline text. A parse has side effects that a fallback
cannot undo, so a body that looked like a line block but was not only one
was parsed twice: a blank line followed by a deeper bar, paragraph,
directive or substitution definition, or a first line deeper than the rest,
gave a spurious "Too many autonumbered footnote references" (and an
unresolved reference), a "Block quote ends without a blank line" warning, a
duplicate explicit target, or a substitution definition leaking into the
page.

The lines are now found textually: every non-blank line at the first line's
indent must start with docutils' line-block marker, and a deeper line
directly after a non-blank line continues the previous line. Each line is
then parsed once with inline_text into a translatable inline of its own,
with the source and line of that content line. Any other text is parsed
once as inline text, as before #308, so the parse-twice class is gone by
construction, along with the block-quote unwrap, the system-message filter,
the node move and the docutils < 0.22 source-info fallback.

On valid line blocks the HTML, LaTeX, text, man, texinfo and .pot message
output is byte-identical to #308's, on Sphinx 7.4, 8.2 and 9.1; the .pot
locations on docutils < 0.22 now name each line instead of the first. All of
#308's tests pass unchanged; the malformed bodies are added as regressions
(they fail on #308's mechanism), with unit tests of the splitter.
In RST, a directive's content_offset is the offset of its content in the
whole input, so it already counts the lines before the directive. The
inline path of button-link and button-ref added it to the directive's line
anyway, both for the line it gave inline_text and for the gettext location
of the text, so the lines before the directive were counted twice. A role
error in the text of a button at line 4 of a page was reported at line 9
instead of 6, inside a dropdown it named a line past the end of the file,
and for a button in an included file it named the included file's line
plus the offset into the including page, attributed to the including page.

The text is now located at its first content line, through the state
machine, so an included file's text names that file. As for a paragraph,
docutils reports the messages of the inline markup at the text's first
line. MyST's content_offset counts the lines from the directive, so its
line is unchanged (measured identical warnings and .pot on Sphinx 7.4, 8.2
and 9.1).

The gettext regression fixture recorded the wrong line (19 for text at line
11) and is corrected.
The docs tell MyST users to break button text with a hard line break (a
backslash at the end of the line). That breaks the line in HTML and LaTeX,
but the text builder joins the lines ("Multi-linebutton text") and the
gettext message contains the raw break markup ("Multi-line<br /> \\ button
text"), the two defects the RST line block avoids. The note now says so,
and points translated projects at the RST line block, which works from MyST
inside an eval-rst block (separate lines in text output, one message per
line).
@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.93%. Comparing base (54cd807) to head (334c7bc).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #315      +/-   ##
==========================================
+ Coverage   92.60%   92.93%   +0.33%     
==========================================
  Files          13       13              
  Lines        1420     1430      +10     
==========================================
+ Hits         1315     1329      +14     
+ Misses        105      101       -4     
Flag Coverage Δ
pytests 92.93% <100.00%> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…cape

A single-line RST label written `| Docs | API |` is a one-line line block,
so it renders "Docs | API |"; a literal leading bar is written `\|`.
Test both, and say so in the button docs.
…ils does

The splitter stripped every continuation line, and kept the empty first
line of a bare bar. docutils (and so #308's block parse) strips only the
common indent of the continuation lines, so they keep their indent relative
to each other, and drops a bare bar's empty first line. A continuation line
indented deeper than the one before it therefore lost its leading spaces in
text, HTML, LaTeX, man and .pot output, and `|` followed by a continuation
line gained a leading newline. Both now match main and docutils.
Locating every line at the directive, or at the first line of the line
block, passed the whole suite. The gettext test now asserts each line's
.pot location, the message test asserts the unknown role is reported at
the second line, and a new test checks both for a line-block button in
an included file.
Three comments in the #308 tests still described the removed block parse
(nodes moved out of it, messages placed beside the line block, a source
fallback for docutils < 0.22). Their assertions are unchanged.
Texinfo prints the raw break markup of a MyST hard line break in button
text, rather than joining the lines as the text builder does. The note on
a leading bar also covers a bare `|` label, which is an empty line block.
…ls does

Only spaces indent an RST line, and docutils keeps a line block line's
text after the bar as is. The splitter stripped any Unicode whitespace in
both places, so `| \xa0text` (a no-break or ideographic space, the one way
to indent a line visibly in a button) lost its leading space, and a line
starting with a no-break space before a bar was read as an indented bar.

Pinned by four split cases and an exact-HTML test, which also pins that a
bare bar adds a break and no empty element.
A split case pins that a line at the bar's indent without a bar ends
the line block even directly after a bar, so `| Docs` then `More` is
not one line. A gettext test pins that a line's message is its source
text, markup included (`**hey** there`), not the text it renders.
A two-line label whose second line is plain text is not a line block,
so its leading bar is shown.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant