Composite figures: a bare figure fence is one figure of ordered panels - #1265
Merged
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
dereuromark
force-pushed
the
feat/1122-composite-figures
branch
from
August 15, 2026 09:00
94c4091 to
794c8b9
Compare
This was referenced Aug 15, 2026
dereuromark
force-pushed
the
feat/1122-composite-figures
branch
from
August 15, 2026 09:20
794c8b9 to
48404b8
Compare
PART 9 section 4c (markup-carve/carve#1122). A bare `::: figure` container - the fence, its separator, and the kind word, nothing else - parses as the new `figure_group` node: ONE figure whose direct captionable children are its PANELS, in source order. The body parses under the unchanged inner rules, so a panel is simply the `figure` or `table` node those rules already produce - a captioned image paragraph, a captioned quote, a captioned code listing, captioned display math, or a table captioned or not - and everything else is plain group content, preserved in place. The `^ ` line after the CLOSING fence is section 4's sixth caption host, this kind only, and it reaches the group through the same tryParseCaption slot every other host uses, so the one-blank-line allowance, the two-blank detachment, the invisible-interrupter rule and the second-caption rule all hold without a second spelling. An opener carrying a quoted title or a `[label]` does not match the production and stays a generic Tier-2 container, title and label preserved; a bare figure opener anywhere inside an open group's body stays one too, because groups do not nest. NUMBERING (PART 9R R5 through section 4c): the group is one sequence unit. Its caption's `#` draws one number from the label's own sequence, the group id registers as "Label N", and each panel id registers as "Label N" plus a letter by panel order - a..z, then aa - but only when the group itself drew a number. Panels draw nothing: the resolver skips them, and a `#` in a panel caption stays LITERAL. That last word is why renderCaptionNumber() now prints `#` for an unresolved placeholder instead of the empty string - the Markdown, plain-text and ANSI targets already printed the literal, and the empty string was unreachable before this change because every placeholder the walk found got a number. The wire shape is PART 12 section 16: `type`, `children`, an optional `caption` holding inline content - the same flattening a table's caption already gets - `attrs` and `pos`, and deliberately no target, title or label. The vendored resources/ast-schema.json moves to the spec pin that names it, and the ProseMirror bridge lists the type as unmapped beside `figure`. The canonical writer emits the authored form and does not escape the group caption's caret (it is the caption the closer hosts, not text in that position) while a paragraph that merely sits where the allowance would reach the closer keeps its `\^ ` escape; Markdown, plain text and ANSI degrade per PART 11 section 10g; the HTML importer reads the rendered carve-figure-group shape back to `::: figure` source, own output only. The tests/spec submodule pin moves to 9cd27d8 on spec main, the merged markup-carve/carve#1215, which carries the corpus category 318-composite-figures on top of the quote-attribution withdrawal this engine already made. A cross-engine parity pass against carve-php and carve-rs settled three divergences before this landed. Markdown separates a panel host from its emphasized caption with a blank line - the caption is its own paragraph. The canonical writer escapes ONLY the structural caret of a detached caption paragraph: the caret sat outside the minimal escape class, so the minimal form always failed the redundancy check and the whole document escalated to conservative escaping, writing escaped parens and hashes where the other engines write the characters bare. And a panel caption's placeholder stays a TYPED caption_number node on the wire, un-numbered, rather than flattening to text - the unresolved-reference precedent: keep the node, render its authored spelling, which the HTML arm now does for an unresolved placeholder instead of rendering nothing. A branch review then caught the class of miss that produces most cross-engine drift here: an existing traversal that switches on block types and lacks the new arm. The Markdown prepass never entered a group, so a heading inside one lost its anchor stamp and every crossref to it degraded to plain text; the lint heading index had the same hole, reporting valid references as broken and missing duplicate ids inside groups. Both descend now, and a sweep over every block-type switch in src/ (via the block_quote arms they all share) confirms the remaining walkers either carry the figure_group arm or traverse generically by field name, which covers it by construction.
dereuromark
force-pushed
the
feat/1122-composite-figures
branch
from
August 15, 2026 10:53
48404b8 to
9d7ba5f
Compare
dereuromark
marked this pull request as ready for review
August 15, 2026 10:58
This was referenced Aug 16, 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.
Part of markup-carve/carve#1122; spec: markup-carve/carve#1215.
A bare
::: figurecontainer parses as the newfigure_groupnode: one figure of ordered panels, per PART 9 §4c of the spec. All eleven318-composite-figurescorpus documents pass byte for byte.FigureGroup; the^line after the closing fence is the group caption, reached through the sharedtryParseCaptionslot (adjacent or one blank line attaches, two detach, second caption stays a paragraph). An opener with a quoted title or[label]stays a generic container; a bare figure opener anywhere inside an open group's body stays generic too (groups do not nest).figurenodes (captioned image, quote, code listing, display math, promoted reference image) andtablenodes, captioned or not - in source order; stray content is preserved in place.</#id>as "Figure Na" (letter by panel order, a..z then aa) when the group drew a number; a#in a panel caption stays literal, andrenderCaptionNumber()now prints the literal#for an unresolved placeholder, matching the other three targets (previously unreachable on the HTML path).carve-figure-group/carve-figure-panels/carve-figure-panelshape - class-first with deduplicated merge, panels div unconditional, no trailingfigcaptionwithout a group caption.figure_group=type,children, optional inlinecaption,attrs,pos(PART 12 §16); vendoredresources/ast-schema.jsonsynced from the spec pin; ProseMirror bridge lists the type as unmapped besidefigure.\^paragraph keeps its escape. Fmt corpus (semantic + idempotence) green with no exclusions.<figure class="carve-figure-group">back to::: figuresource (own-output round trip).Out of scope, per the rollout plan: the D7b lint codes (
figure-group-nested,figure-group-opener-metadata,figure-group-panel-number,figure-group-empty,figure-group-single-panel) have no home in this engine's current lint surface (a line-based Markdown-habit linter plus one attribute pass, no container diagnostics) and follow separately.Spec pin
tests/specis pinned to9cd27d8on spec main - the merged markup-carve/carve#1215 - which carries the eleven318-composite-figurescorpus documents on top of the quote-attribution withdrawal this engine already carries (#1264).Satellites (pandoc-carve, carve-lsp, carve-hexapdf, carve-sile, wysiwyg) are follow-up tickets, filed not implemented.
Cross-engine parity pass
A parity run against carve-php and carve-rs (which agreed with each other and the spec) settled three divergences, fixed here with regression tests:
318-composite-figures-6) now writes exactly one structural escape (\^on the detached line). The caption-opening caret sat outside the minimal escape class, so the minimal pass always failed the writer's redundancy check and the whole document escalated to conservative escaping (\(a\),\#where the other engines write the characters bare).#placeholder in a PANEL caption stays a typedcaption_numbernode withoutn, matching the carve-php/carve-rs serialization (the unresolved-reference precedent: keep the node, render its authored spelling). The HTML arm now renders an unresolved placeholder as the literal#it spells, like the Markdown/plain/ANSI arms already did.A branch review then caught two existing traversals missing the new arm: the Markdown prepass (heading-id index / reference scan) and the lint heading index did not descend into groups, so a heading inside one lost its anchor and its crossrefs degraded (or falsely linted broken). Both fixed with regression tests, and a sweep over every block-type switch in
src/confirms the remaining walkers carry the arm or traverse generically by field name.