Skip to content

Fix wrapped {table} captions becoming body paragraphs - #1195

Open
BetterAndBetterII wants to merge 10 commits into
executablebooks:masterfrom
BetterAndBetterII:fix/table-wrapped-caption
Open

BetterAndBetterII wants to merge 10 commits into
executablebooks:masterfrom
BetterAndBetterII:fix/table-wrapped-caption

Conversation

@BetterAndBetterII

Copy link
Copy Markdown

Summary

MyST takes directive arguments from the opening fence line only. A hard-wrapped {table} caption (common at ~70 columns) therefore becomes a leading paragraph in the body, and RSTTable fails with "exactly one table expected" even though the Markdown is otherwise valid.

Fold leading non-table body lines into the table caption argument when a Markdown/rST table follows. Single-line captions and unrelated invalid body content are unchanged.

Test plan

  • Unit tests for wrapped and single-line captions (tests/test_renderers/test_parse_directives.py)
  • Sphinx fixture case in sphinx_directives.md
  • pytest tests/test_renderers/test_parse_directives.py tests/test_renderers/test_fixtures_sphinx.py

Fixes #1104

BetterAndBetterII and others added 10 commits September 1, 2026 20:55
MyST only takes the directive argument from the opening fence line, so a
hard-wrapped table caption leaked into the body and RSTTable failed with
"exactly one table expected". Fold leading non-table body lines into the
caption when a table follows.

Fixes executablebooks#1104
…aption

# Conflicts:
#	tests/test_renderers/test_parse_directives.py
Follow-ups to the wrapped table caption fix:

- Fold only lines that directly follow the opening line (or the options
  block): run the fold before the leading blank body line is stripped, so
  a paragraph separated by a blank line is not taken as the caption, and
  still gives "exactly one table expected".
- Do not fold an option-looking line (`:key:` or `---`) into the caption,
  where its option would be silently lost: leave the body unchanged and
  warn that options must directly follow the opening line.
- Do not fold lines that start a MyST block construct (fences, link and
  footnote definitions, comments, block breaks, targets, HTML comments);
  a link definition or a directive that renders nothing can already
  precede a valid table.
- Detect the table start as a GFM header line followed by a delimiter
  row, so tables without leading pipes are recognised; drop the rST grid
  and simple table checks, which a MyST body never reaches.
- Tests for each of these, the body offset of the wrapped case, a colon
  fence fixture, and a docs section in syntax/tables.md.
With attrs_block, a `{#id .class}` line directly after the opening line
renders nothing and applies its attributes to the table, so that body was
already a valid table. Treat such a line as not caption text, so the table
keeps its id and classes instead of the attribute text ending up in the
caption.
The delimiter-row regex had whitespace runs at both ends of the line next to
only optional tokens, so a line of spaces around a "-" that follows a
"|"-line in a {table} body backtracked in cubic time (a 3 KB document took
10 seconds to parse). Match the stripped line instead, so every whitespace
run is followed by a required "|", "-" or the end of the line. The set of
lines that match is unchanged.

Also test that the options-warning line counts an options block before
the wrapped caption line.
A link reference definition's label may contain escaped brackets
(`[a\]b]: url`) or continue on the next line (`[foo` / `bar]: url`).
Such a definition renders nothing, so a {table} body that starts with one
was already a valid table, but it was folded into the caption,
and links using the definition no longer resolved.
The label pattern is now escape-aware, and is also checked against the
joined caption lines.

Also test the delimiter-row timing with a line that has no leading spaces,
so restoring the old pattern while keeping the strip is caught.
Replace the text-level fold (a list of lines that are not caption text,
and a delimiter-row regex) with a structural one: the table body is
parsed exactly as before, and only changed where docutils' table
directive would fail with "exactly one table expected", namely when it
parses to a paragraph that starts directly after the opening line (or
the options block), then a table (system messages aside). The paragraph
is then taken out, and its inline content ends the table's caption.

Any body that renders a table without that error is unchanged by
construction, whatever syntax extensions are enabled. The one exception
kept from before: a paragraph with a line MyST reads as an option line
(`:key: value`) is not joined, since the option would be lost in the
caption; it still fails as before.
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.

Long captions break table directive (in HTML output by Sphinx)

2 participants