Skip to content

Opt-in composite figures: a figure div is one figure of ordered panels - #270

Merged
dereuromark merged 1 commit into
masterfrom
feature/figure-group-extension
Aug 20, 2026
Merged

Opt-in composite figures: a figure div is one figure of ordered panels#270
dereuromark merged 1 commit into
masterfrom
feature/figure-group-extension

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Composite figures, as an opt-in extension.

Upstream djot has no composite figure. The open discussion is jgm/djot#31, where the objection to the ^ caption syntax this library already ships (jgm/djot#37) was that it cannot spell a subfigure: "Figure 1a", "Figure 1b", one caption over the whole plate. This adds that, without new syntax and without touching the parser.

A bare ::: figure div becomes one figure of ordered panels:

{#fig-x .columns-2}
::: figure
![one](a.png)
^ (a) One

![two](b.png)
^ (b) Two
:::
^ Group caption
<figure class="figure-group columns-2" id="fig-x">
<div class="figure-panels">
<figure class="figure-panel">
<img alt="one" src="a.png"><figcaption>(a) One</figcaption>
</figure>
<figure class="figure-panel">
<img alt="two" src="b.png"><figcaption>(b) Two</figcaption>
</figure>
</div>
<figcaption>Group caption</figcaption>
</figure>

Why no parser change

Both halves already parse: ::: figure is a div with class figure, and a ^ line that cannot attach to anything is a paragraph whose first inline is Text("^ ..."). So FigureGroupExtension implements BeforeRenderExtensionInterface and rewrites the tree once, before rendering.

That is also why there are no renderer changes in this diff. beforeRender runs on the single render() path, so Markdown, plain text and ANSI degrade for free through the Figure, Div and Caption arms they already have.

Rules

  • Panels are the children this library already makes captionable: image and blockquote Figure nodes are promoted in place, a Table is wrapped in a panel figure and keeps its own <caption>.
  • Stray content stays put. A note between two panels renders where it was written, inside the panels container; it is simply not a panel.
  • The group caption is the caret paragraph immediately after the closer. Escaping it (\^ ...) leaves an ordinary paragraph. That works because an escaped caret parses as EscapedText, a sibling of Text, not a subclass, so the check is sound rather than accidental.
  • Groups do not nest. A figure div inside an open group stays a plain div.
  • Opt-in. Without the extension a figure div still renders <div class="figure">; there is a test for exactly that.

The four class names (figure, figure-group, figure-panels, figure-panel) are constructor options.

Deliberately out of scope

  • Numbering. The panel letters and the group number are what make subfigures useful in prose, and this library has no figure numbering at all to hook into. Nothing here fakes one.
  • Short and long captions. The other half of the Figures jgm/djot#31 objection, and a separate decision (see Captions jgm/djot#28).
  • Code and math panels. Only images, blockquotes and tables are captionable here today, so only those can be panels. Widening that is a core caption question, not an extension one.
  • HtmlToDjot round trip.

Noted while working, not fixed here

PlainTextRenderer has no Figure arm, so a captioned image already renders as aPanel caption, the alt text glued to the caption. That predates this branch (reproduces on master with a plain captioned image) and the new test pins the current behavior rather than papering over it. Worth its own fix.

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.38710% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.49%. Comparing base (b9c8bac) to head (2e1d021).

Files with missing lines Patch % Lines
src/Extension/FigureGroupExtension.php 98.38% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #270      +/-   ##
============================================
+ Coverage     92.42%   92.49%   +0.07%     
- Complexity     3683     3708      +25     
============================================
  Files           109      110       +1     
  Lines         10439    10501      +62     
============================================
+ Hits           9648     9713      +65     
+ Misses          791      788       -3     

☔ 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.

@dereuromark

Copy link
Copy Markdown
Contributor Author

Ordering note: #271 fixes the plain-text gap this PR's degradation test currently pins (PlainTextRenderer has no Figure or Caption arm, so a captioned image renders as aPanel caption - a bug that predates this branch). Once 271 lands I will rebase this branch and update the assertion to the fixed output.

@dereuromark
dereuromark marked this pull request as ready for review August 15, 2026 11:40
@dereuromark dereuromark added the enhancement New feature or request label Aug 15, 2026
A bare `::: figure` div becomes one figure made of ordered panels, so a
document can spell the subfigure case (Figure 1a, 1b) that a single
captioned image cannot. Opt-in only: composite figures are not djot, and
without the extension a figure div still renders as a plain div.

No parser change is needed. The div and the `^` caption line already
parse; the extension rewrites the tree in beforeRender, which every
renderer shares, so HTML, Markdown, plain text and ANSI all degrade
through the figure, div and caption paths they already have.

Rules:

- Captioned children (image and blockquote figures) are promoted to
  panels; a table is wrapped in a panel figure and keeps its own
  caption element.
- Content that is not captionable stays where it was written, inside
  the panels container, and is not a panel.
- A caret paragraph immediately after the closing fence becomes the
  group caption. Escaping the caret keeps it an ordinary paragraph,
  which is how an unattachable caption line already behaves.
- Groups do not nest: a figure div inside an open group stays a plain
  div.

The four class names are constructor options, so a project mirroring
another vocabulary can rename them without a subclass.

Numbering is deliberately absent: this engine has no figure numbering
to hook into, so the panel letters and the group number that make
subfigures useful in prose are left for whenever numbering lands.
@dereuromark
dereuromark force-pushed the feature/figure-group-extension branch from 8e59185 to 2e1d021 Compare August 15, 2026 23:59
@dereuromark

Copy link
Copy Markdown
Contributor Author

Rebased onto master (b9c8bac). The degradation test asserted the pre-php-collective/djot-php#271 plain-text output (aPanel captionGroup caption); with that fix in, a composite figure now degrades to

a
Panel caption
Group caption

which is the point of #271. Assertion updated to the fixed output, nothing else changed. Full suite, phpstan and phpcs green on the rebased commit.

@dereuromark
dereuromark merged commit 1d5e7ce into master Aug 20, 2026
6 checks passed
@dereuromark
dereuromark deleted the feature/figure-group-extension branch August 20, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant