feat: a bare figure fence is one figure of ordered panels - #1073
Merged
Conversation
dereuromark
force-pushed
the
feat/1122-composite-figures
branch
from
August 15, 2026 02:42
7ab71ff to
e4957d0
Compare
This was referenced Aug 15, 2026
Closed
dereuromark
force-pushed
the
feat/1122-composite-figures
branch
2 times, most recently
from
August 15, 2026 09:17
a6c23da to
9e9a9f7
Compare
PART 9 §4c (markup-carve/carve#1122). A bare `::: figure` opener - the fence and the kind word, no quoted title, no `[label]` - parses as the new `figure_group` node: ONE figure whose captionable direct 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, quote, code listing or display math, a promoted reference image, or a table captioned or not - and everything else is plain group content, preserved in place. The `^ ` line after the CLOSING fence is §4's sixth caption host, this kind only, using the same slot idiom as the five parse-time hosts (adjacent or across one blank line attaches, two detach), and reachable only when a closer exists, so an EOF-closed group has no slot by construction. An opener carrying a title or label never matches the production and stays a generic container; a bare opener anywhere inside an open group's body stays one too - the flag rides the sub-lexer chain, so the no-nesting rule follows the recursion rather than guarding direct children only. Panels are DERIVED, not stored: one predicate (figureGroupPanels), shared by the numbering pass and the lint, so the two cannot drift. The group is one sequence unit (PART 9R R5 through §4c): its caption's `#` draws one number from the label's own sequence, the group id registers as "Label N", and each panel id registers with a letter by panel order (a..z, then aa) - but only when the group itself drew a number. Panels draw nothing, and a `#` in a panel caption stays a typed caption_number node WITHOUT `n`, un-numbered on the wire, exactly as carve-php and carve-rs publish it. That literal-`#` rule is why the HTML caption_number arm now renders an unresolved placeholder as its authored `#` instead of the empty string - the Markdown, plain-text and ANSI arms already did, and the empty string was unreachable before this change because every placeholder the walk found got a number. The HTML shape is the corpus's byte shape: a class-first, deduplicated `figure.carve-figure-group` wrapper, an UNCONDITIONAL panels div (zero panels still wrap the preserved content), panel figures leading with `carve-figure-panel`, a table panel behind an explicit bare wrapper keeping its own caption and attrs, and a trailing figcaption only when captioned. The canonical writer emits the authored form back with the group caption unescaped - and, because the group now hosts a caption, a DETACHED `^ ` paragraph after one is caret-escaped, with exactly ONE structural escape: 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, writing escaped parens and hashes where the other engines write the characters bare. Markdown degrades panels in place, each host separated from its emphasized caption by a blank line, with the bold group caption last; plain text and ANSI lead with the group caption and write caption-over-host per panel. Five lint codes name the shapes that parse fine and do less than they look like they do: figure-group-nested, figure-group-opener-metadata, figure-group-panel-number, figure-group-empty and figure-group-single-panel, and a numbered group registers its id and its panels' ids as valid crossref targets. The HTML importer reads the rendered carve-figure-group shape back to the node, own output only, unwrapping the bare table-panel wrapper to its table. Every existing traversal that switches on block types grew the new arm - the Markdown prepass and the lint heading index were caught missing it by review (a heading inside a group lost its anchor and its crossrefs degraded or falsely linted broken), and a sweep over the block_quote arms every typed walker shares confirms the rest carry it or traverse generically by field name, which covers it by construction. The spec submodule pins the merged spec commit that carries the §4c corpus, schema and docs; src/wire-fields.ts is regenerated from it, and the eleven 318-composite-figures corpus documents run as real tests.
dereuromark
force-pushed
the
feat/1122-composite-figures
branch
from
August 15, 2026 10:46
9e9a9f7 to
e768c28
Compare
dereuromark
marked this pull request as ready for review
August 15, 2026 10:50
This was referenced Aug 15, 2026
Merged
Document why a composite figure is not a grouped float in this adapter yet
markup-carve/carve-sile#3
Merged
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.
Implements composite figures (PART 9 §4c): a bare
::: figurefence is one numbered figure of ordered, independently captionable panels.Part of markup-carve/carve#1122; spec: markup-carve/carve#1215 (merged).
What
::: figureopener (kind only; no quoted title, no[label]) produces the newfigure_groupnode. The closing fence is §4's sixth caption host: a^line after it attaches as the group caption through the same slot idiom the five parse-time hosts use (adjacent or one blank line attaches, two detach; an EOF-closed group has no slot). An opener carrying a title or label stays a generic container, and a bare opener anywhere inside an open group's body is demoted the same way (groups do not nest) - the flag rides the sub-lexer chain, so the rule follows the recursion.FigureGroup { children, caption?, attrs?, pos }, discriminated bytype. Panels are derived, not stored: thefigureandtablechildren in source order - one predicate (figureGroupPanels) shared by numbering and lint.src/wire-fields.tsregenerated from the pinned schema; encode/decode and validation ride the generated tables.a..z, thenaa, by order among panels), so</#panel-id>resolves asFigure 2a. A#in a panel caption stays a typedcaption_numbernode withoutnon the wire (carve-php / carve-rs parity; the unresolved-reference precedent), and the HTML arm now renders an unresolved placeholder as its authored#, matching the Markdown/plain/ANSI arms.renderFigureGroupemits the corpus byte shape -figure.carve-figure-groupwrapper (class-first, deduplicated merge), unconditionaldiv.carve-figure-panels, panels asfigure.carve-figure-panel(a table panel gets an explicit bare wrapper and keeps its own<caption>), stray content preserved in place, trailing<figcaption>only when captioned.::: figure, body, closer, unescaped^group caption. A detached^paragraph after a group comes back with exactly one structural escape (\^); the caption-opening caret now escapes in the minimal pass too, so such documents no longer escalate to conservative escaping (\(a\),\#).figure-group-nested,figure-group-opener-metadata,figure-group-panel-number,figure-group-empty,figure-group-single-panel; a numbered group registers its id and its panels' ids as valid crossref targets. (This engine's lint carries a single severity, so the two advisory codes emit as ordinary warnings like every other rule.)figure.carve-figure-groupcomes back asfigure_group(own-output round trip); the bare table-panel wrapper unwraps to its table so the table keeps its caption and attrs.figure_grouparm (heading ids/numbers/level shift, footnote numbering, heading references, block-image promotion, profile filter/vocabulary, the Markdown prepass, the lint heading index); the generic field-name walkers cover it by construction.Fixtures
318-composite-figurescases (basic group, numbering + crossrefs, captionless, one-panel, stray content, two-blank-line detach, non-host closers, opener metadata, nested demotion, mixed panels, group-before-children numbering order) run as real tests via thecomposite-figuresIMPLEMENTED entry.Cross-engine parity
A parity pass against carve-php and carve-rs settled three divergences (Markdown panel-caption paragraph separation, minimal-escape fmt output for the detached-caption shape, typed un-numbered
caption_numberon the wire for panel placeholders), and a branch review caught two traversals missing the new arm (Markdown prepass, lint heading index) - all fixed here with regression tests.Spec pin
specis pinned to9cd27d8, the merged spec commit on carve main that carries the §4c grammar, corpus, schema and docs. Satellites (pandoc-carve, carve-lsp, carve-hexapdf, carve-sile, wysiwyg) are follow-up tickets, filed not implemented.