Skip to content

Render a composite figure as one numbered unit - #5

Merged
dereuromark merged 1 commit into
mainfrom
feat/2-figure-group-renderer-6f32b250
Aug 16, 2026
Merged

Render a composite figure as one numbered unit#5
dereuromark merged 1 commit into
mainfrom
feat/2-figure-group-renderer-6f32b250

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Fixes #2.

What this is

The direct-AST renderer gains the figure_group handler that #2 and the README's gate list named as the step this repository owns. A bare ::: figure fence (Carve PART 9 section 4c) now typesets as ONE numbered figure of ordered panels instead of stopping the pipeline with Unsupported Carve AST node type 'figure_group' the moment the engine pin moved past the node.

The engine gate is cleared the same way the rest of this pipeline is built: the Dockerfile pin moves from carve-js 8815874 to bde69f85 (current carve-js main), the first pin that emits the node. The newest npm release (0.1.3) still predates it, and the README's requirements section now says so.

What renders how

  • The group maps to one markdown:internal:captioned-figure carrying the group's attributes. Its caption - already resolved by the engine, caption_number included - closes the figure, and the list of figures gains exactly one entry per group.
  • Panels (the direct figure and table children) keep their own captions but carry unnumbered and notoc, the two classes Resilient's captioned commands already read. Left unmarked, a two-panel group would consume three figure numbers and file three list entries (Resilient numbers every captioned element it typesets).
  • Stray group content typesets in place between the panels; a hint decides arrangement, never content.
  • Only DIRECT children are panels: a captioned figure inside a ::: note in the group keeps its own number, which is the engine's rule too.
  • A group without a caption numbers nothing, for itself or its panels, matching the engine.

For examples/composite-figure.crv (table panel + listing panel under ^ Figure #: Two panels, one figure) that comes out as: table with its (a) caption, listing with its (b) caption, group caption Figure 1: Two panels, one figure last, one list-of-figures entry.

Where this sits against the paged-output contract

The contract in #2 mirrors markup-carve/carve-hexapdf#18, whose merged implementation (markup-carve/carve-hexapdf#19) owns its whole layout stack and could deliver the float, the split rule and columns-N. This adapter deliberately does not own a layout engine, and Resilient's captioned elements are not floats (its book class says so of itself), so:

  • one list-of-figures entry per group: delivered;
  • panel captions kept, group caption attached: delivered;
  • group as one float, columns-N honored when width suffices: NOT expressible below this adapter today. The group typesets as the contract's degradation floor - a vertical stack of panels with captions in source order, group caption last - and the columns-N class survives on the emitted options for the day Resilient grows a float or column mechanism.

The README states all of this, including the one caveat shared with every captioned element here rather than specific to groups: Resilient prefixes its own Figure N. label from its own counter next to the label Carve resolved into the caption text.

Relation to #4

Draft #4 solved the numbering half of this ticket for the pandoc-carve pipeline, and #1 replaced that pipeline with the direct-AST renderer after #4 was drafted, which is why that branch is CONFLICTING. This PR delivers the same semantics (panels unnumbered + notoc, direct-children rule, titled-opener control living engine-side now) natively in carve/renderer.lua. If this merges, #4 can close as superseded.

Tests

test/renderer.lua grows a group fixture in the broad invariant document plus four checks: the group as the one numbered element with its caption last and the engine's number resolved; an uncaptioned group emitting no caption command; the direct-children depth, pinned by a nested ::: note figure (marking every captioned DESCENDANT leaves the other checks green, so this is the discriminating one); and a standalone captioned figure staying numbered, so the panel classes cannot be fixed at the figure or table handler layer. test/smoke.sh now also asserts the installed engine emits figure_group at all - on a pre-4c engine the construct arrives as an admonition and every composite check would pass while testing nothing - and typesets the new example to PDF.

PART 9 section 4c gives a bare ::: figure fence its own exchange node,
figure_group: one figure of ordered panels, where the panels are the figure
and table children in source order, any other child is stray group content
kept in place, and the caption after the closing fence belongs to the whole
group. The pinned engine predated the node, so the renderer, whose unknown
node types are hard errors, would stop on the first composite figure the
day the pin moved. The pin moves here, and the handler arrives with it.

The group maps to one markdown:internal:captioned-figure. Resilient numbers
every captioned figure and table it typesets and files each into a list, so
unmarked panels would take a number and a list entry each: a two-panel group
would consume three figure-sequence draws and file three entries. The panels
therefore carry unnumbered and notoc, the two classes Resilient's captioned
commands already read. The group takes the only number, the list of figures
gains exactly one entry per group, each panel keeps its own caption, and a
group without a caption numbers nothing, matching the engine. Only DIRECT
children are panels: a captioned figure nested inside stray group content
keeps its own number, which is the engine's rule as well.

Resilient's captioned elements are not floats, and no layer below acts on a
columns-N class, so the group typesets as the paged-output contract's
degradation floor: a vertical stack of panels with their captions in source
order, group caption last. The class survives on the emitted options, so a
future Resilient float or column mechanism finds it; a hint decides
arrangement, never content.

The renderer test grows a group fixture in the broad document plus four
checks: the group as the one numbered element with caption last and number
resolved, the uncaptioned group emitting no caption command, the
direct-children depth (marking every captioned descendant instead leaves the
other checks green), and a standalone captioned figure staying numbered so
the classes cannot be fixed at the wrong layer. The smoke test now also
asserts that the installed engine emits figure_group at all, because on an
older engine the construct arrives as an admonition and every composite
check would pass while testing nothing.
@dereuromark dereuromark added the enhancement New feature or request label Aug 16, 2026
@dereuromark
dereuromark merged commit df5287f into main Aug 16, 2026
2 checks passed
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.

Render composite figures as grouped floats

1 participant