Map figure_group to pandoc's subfigure model, both directions - #70
Merged
Conversation
The published 0.1.3 predates PART 9 4c, so `::: figure` parses as a generic admonition there and no arm for the composite node can be written against it. A git pin is the correct pin mid-development; it moves back to a range at the next engine release. The lockfile's `resolved` url is rewritten to the git+https form by hand: this host rewrites HTTPS to SSH via insteadOf, npm records the rewritten url, and `npm ci` follows the LOCKED url - which fails in CI with a publickey error on a public repository.
PART 9 4c gives a bare `::: figure` container the `figure_group` node: one figure of ordered panels, its `^ ` line after the closing fence the caption of the whole group. Pandoc has that model natively - a `Figure` whose blocks are themselves `Figure`s is its subfigure shape - so the mapping is structural in both directions rather than a degradation. Carve to pandoc: the group becomes the outer `Figure`, its caption the outer caption, its children the blocks. The panels among them - the direct `figure` and `table` children - lower to nested `Figure`s and `Table`s; everything else stays in place between them, unmoved and unwrapped, because the group is one sequence of children and not a panel array. Pandoc to carve: a `Figure` holding nested `Figure`/`Table` blocks used to hit the "general figure content unwrapped" path, which dropped the grouping and left the caption as a trailing paragraph. It now becomes a `figure_group`. The single-block Figures keep their existing mapping: a lone captioned image or table is one figure, not a group of one. Numbering follows 4c rather than the source order the caption line suggests. The group draws its number at the OPENING fence, so its caption converts before its children - corpus 318-composite-figures-11 numbers the group "Figure 1" and a captioned figure nested in its stray content "Figure 2". A panel draws nothing, and neither does anything a panel CONTAINS; its `#` stays literal, the visible failure the language prefers. Suppressing the draw matters as much as suppressing the digit, since a panel that consumed a number would shift every later caption in the document by one. A panel id resolves `</#id>` as the group's number plus a bijective base-26 letter by panel order - "Figure 2a", corpus 318-composite-figures-2 - and a group with no numbered caption lends its panels nothing. One asymmetry is deliberate and 4c states it: inside a group a captioned quote is a PANEL, not a 4a attribution, "the quote is not a special host inside the group either". The 4a reroute still governs everywhere else, including deeper inside the group's own stray content. An opener carrying a quoted title or a `[label]` is NOT this production and is untouched: it stays a generic container, and crosses as the same `Div ["admonition","figure"]` it always did.
Every case carries the CONTROL alongside it, because the two spellings are one character apart: a bare `::: figure` is the group, an opener with a quoted title or a `[label]` is a generic container and must stay the `Div ["admonition","figure"]` it always was. That control is not a formality - an engine without 4c renders the two identically, so a group test can pass against an implementation that never discriminated at all. What is pinned: the nested-Figure shape both ways, the number drawn at the opening fence rather than at the caption line, a panel drawing nothing and neither does what a panel contains, the panel letters on `</#id>`, the captioned quote's split between panel and 4a attribution, stray content kept in place, and the group surviving the loop as a group over two laps. The one asymmetry recorded rather than smoothed over: a panel's `#` returns as the escaped character, not as the placeholder node. Resolution answers the group's `#` on the way out and nothing answers the panel's, so the panel's is still the literal text it printed - reconstructing the placeholder would mean guessing it back out of a rendered `#` and misreading an author's genuine one.
The table row that mattered was missing the discrimination, not the mapping: which `::: figure` opener is the group and which one stays a Div is what a reader has to know before either row is useful. The changelog entry states the replacement plainly - a filter keyed on `Div ["admonition","figure"]` for a bare opener stops matching - and says the engine pin is a git pin until an engine release carries the node.
A pandoc `Figure` around a single `Table` becomes one Carve `table`, and the wrapper's Attr was going nowhere. Pandoc's readers put the label on the Figure rather than on the Table it wraps, so the id a cross-reference resolves against was being dropped with the wrapper - visible as a reference that resolves to nothing. The two nodes collapse into one, so their attrs merge instead: the outer id wins because it was the referenceable one, classes union, key/values merge with the outer taking precedence. That is the rule the 4a quote branch already applies to the same kind of collapse, and it is now shared. It bites hardest on a 4c table PANEL, whose id resolves as the group's number plus a letter, which is what put the case in front of a reviewer.
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #62.
Carve PART 9 section 4c gives a bare
::: figurecontainer thefigure_groupnode: one figure of ordered panels, whose^line after the closing fence captions the whole group. Pandoc has that model natively - aFigurewhose blocks are themselvesFigures is its subfigure shape - so this maps structurally in both directions rather than degrading.Carve to pandoc
The group becomes the outer
Figure, its caption the outer caption, its children the blocks. The panels - the directfigureandtablechildren - lower to nestedFigures andTables, and everything else stays in place between them.Input:
Through
pandoc -t latex:This replaces the
Div ["admonition","figure"]a bare::: figureused to cross as. A filter keyed on that Div has to key on the Figure nesting instead; the changelog says so.Pandoc to Carve
A
Figureholding nestedFigure/Tableblocks used to hit the "general figure content unwrapped" path, which dropped the grouping and left the caption as a trailing paragraph. It imports as afigure_groupnow. Single-block Figures keep their existing mapping on purpose: a lone captioned image or table is one figure, not a group of one, and nothing in the document distinguishes the two if it were.Numbering follows section 4c, not source order
318-composite-figures-11numbers the group "Figure 1" and a captioned figure nested in its stray content "Figure 2", even though the group's^line is the construct's last.#prints as written - the visible failure the language prefers to a silent one. Suppressing the draw matters as much as suppressing the digit: a panel that consumed a number would shift every later caption in the document by one.</#id>as the group's number plus a bijective base-26 letter by panel order - "Figure 2a", corpus318-composite-figures-2. A group with no numbered caption lends its panels nothing.One asymmetry is deliberate and section 4c states it: inside a group a captioned quote is a PANEL, not a section 4a attribution, because "the quote is not a special host inside the group either" (corpus
318-composite-figures-10). The 4a reroute still governs everywhere else, including deeper inside the group's own stray content.Engine pin
No published
@markup-carve/carvecarries the node -0.1.3parses::: figureas a generic container, so no arm for the composite node compiles against it. The dependency is a git pin at carve-js3f5dd8c(markup-carve/carve-js#1073) until an engine release carries it.The lockfile's
resolvedurl is written in thegit+https://form by hand. Hosts with a gitinsteadOfrule rewriting HTTPS to SSH makenpm installrecordgit+ssh://git@github.com/..., andnpm cifollows the LOCKED url - which fails in CI with a publickey error on a public repository. Verified by deletingnode_modulesand runningnpm ci.Also fixed
A pandoc
Figurewrapping a singleTablewas dropping the wrapper's attributes when the two collapse into one Carvetable. Pandoc's readers put the label on the Figure rather than on the Table it wraps, so the id a cross-reference resolves against went with the wrapper. They merge now - outer id wins, classes union, key/values merge with the outer taking precedence - which is the rule the 4a quote branch already applied to the same kind of collapse. It bites hardest on a section 4c table panel, whose id is what resolves with a letter.Note on the ticket's numbering line
The ticket asked the round trip to pin "a panel caption's
#placeholder survives as the typed node withoutn". It does not, and the test records what actually happens instead: a caption's#is a request and the rendered number is the answer, this bridge resolves on the way out for every figure, and the reverse importer never rebuilds acaption_numberfrom printed text for anything. So the group's#comes back as "1" and a panel's comes back as the escaped character it printed - visually identical, still drawing nothing. Reconstructing the placeholder would mean guessing it back out of a rendered#and misreading an author's genuine one.Every case has a CONTROL beside it, because the two spellings are one character apart and an engine without section 4c renders them identically - a group test can pass against an implementation that never discriminated at all. Both controls were mutated to feed them the group spelling and both then failed, so they discriminate rather than pass on anything.