Skip to content

Number a Carve composite figure as one unit - #4

Closed
dereuromark wants to merge 4 commits into
mainfrom
feat/2-grouped-floats-composite-sat-6436a4ec
Closed

Number a Carve composite figure as one unit#4
dereuromark wants to merge 4 commits into
mainfrom
feat/2-grouped-floats-composite-sat-6436a4ec

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

This is a draft and is not meant to be merged yet. It lands ahead of the
dependency that would complete it, so it is staged and visible rather than
merged. Please do not mark it ready or merge it without the decision below.

Closes nothing. #2 stays open.

Where the three gates stand

#2 and the docs section this replaces named three layers that had to move
before anything here could be real. Two of them have.

  1. An engine carrying figure_group - cleared, through a git pin. The
    newest published build of the carve-js engine on npm is 0.1.3 and does
    not have the node, but pandoc-carve's lockfile no longer resolves a
    published version: it pins a carve-js commit that does. Parsing
    tests/corpus/318-composite-figures.crv through that pin yields a
    figure_group node with two figure children.

  2. A pandoc-carve mapping - cleared. Map figure_group to pandoc's subfigure model, both directions pandoc-carve#70 maps
    figure_group in both directions. A bare ::: figure opener becomes a
    Pandoc Figure whose direct Figure and Table children are the panels
    and whose other content stays in place. An opener carrying a quoted title
    stays a Div. Measured on this PR's examples/composite-figure.crv:

    Figure id="fig-mixed" cls=["columns-2"]
      Table
      Figure
        CodeBlock
    ...
    Div cls=["admonition","figure"]
      Para
      Figure
        CodeBlock
    

    Crossrefs come through the same way. See </#fig-x> and </#fig-x-a>.
    arrives as two links already reading Figure 1 and Figure 1a, resolved
    upstream, with no adapter code involved.

  3. Resilient placing the group as a float - still open, and partly
    unreachable.
    classes/resilient/book.lua says of its captioned elements,
    in a comment above them, that despite the similar naming to LaTeX these are
    not floats. There is no float mechanism in resilient 4.2.0 to place a
    composite figure into, and nothing below this adapter turns a columns-N
    class into a column arrangement. What resilient does have is numbering, and
    that half is reachable.

What this lands

carve/figuregroup.lua makes a composite figure one numbering unit, which is
what Carve PART 9 section 4c asks for and what the paged-output contract in
#2 lists as one line item: one list-of-figures entry per group.

Resilient's pandocast renders every nested Figure and Table as its own
captioned float. Left alone, the two-panel example gives its listing panel a
figure number and a list-of-figures entry of its own, gives its table panel a
table number and a list-of-tables entry, and leaves the group numbered
Figure 2 while the caption Carve already resolved still reads Figure 1:. The module walks the parsed tree and marks the direct
captioned children of a captioned figure unnumbered and notoc, two classes
markdown:internal:captioned-figure and markdown:internal:captioned-table
already read. The group then takes exactly one number, that number agrees with
the one Carve wrote into the caption, and the list of figures gains one entry
per group.

Two details are worth flagging for review:

  • The transform runs on the SILE AST, not on the Pandoc JSON. That is
    deliberate. Renderer:Table in pandocast discards a table's Pandoc
    attributes outright (function Renderer:Table (_, caption, ...)), so a class
    put on a table panel any earlier would never arrive.
  • Only DIRECT children are panels. A captioned figure inside group content, in
    a ::: note or in the generic div a nested bare ::: figure degrades to,
    keeps its own number. That is what corpus documents
    318-composite-figures-9 and -11 pin.

Dockerfile pinned pandoc-carve at 60e219a, which predates the mapping in
markup-carve/pandoc-carve#70. The container could not have exercised a group at
all. It now pins af285cc.

What this deliberately does not do

  • It does not place the group as a float, because there is nothing to place it
    into. A composite figure sits in the text flow where it was written, which is
    the degradation floor Render composite figures as grouped floats #2 names: vertical stack in source order.
  • It does not act on columns-N. The class arrives on the group and no layer
    below acts on it. Making two panels sit side by side means emitting layout
    commands, which means this adapter owning a renderer, and it deliberately
    does not. Left as it is rather than written against a shape nothing produces.
  • It does not deduplicate the caption label. Carve resolves the placeholder
    before the text reaches SILE and Resilient prepends its own Figure N., so a
    caption written ^ Figure #: ... renders its label twice. That is not
    specific to composite figures. A single captioned image does the same, and
    fixing it is a separate decision about which layer owns figure numbers.

What the tests pin, and what they do not

test/figuregroup.lua is a pure-Lua unit test over the shape pandocast
actually produces, read off a parsed tree rather than imagined.
test/toccheck.lua reads back the list entries a SILE run writes and checks
them, so the numbering is asserted through the real renderer rather than
through a model of it.

The control is the titled opener, which must stay a generic container. It does
discriminate: markup-carve/pandoc-carve#70 makes the bare and titled spellings
produce different Pandoc nodes, so the same panel content under a title keeps
numbering on its own, and the check fails if that stops being true. This is
worth stating explicitly because in a pipeline whose upstream lacked section 4c
the two spellings render identically and that control would pass while testing
nothing.

The first example did NOT pin the direct-children rule, and it took a mutation
to notice. Rewriting the module to mark every captioned DESCENDANT rather than
the direct children left the whole suite green, because the only nested figure
in that example is a sibling of the group rather than part of it.
examples/composite-figure-nested.crv was added for exactly that case, and the
mutation now fails both the unit test and the end-to-end check.

One thing is knowingly unpinned. The nested example's check asserts entries,
not numbers, because Resilient numbers every captioned figure it typesets while
Carve numbers only the captions that carry a placeholder. The two counters
agree only in a document where every caption carries one. The README says so,
and so does the comment on the check.

To take this out of draft

A reviewer would need to decide two things.

  1. Whether the numbering half is worth having while the layout half is not
    reachable. If a composite figure numbering as one unit but stacking
    vertically is an acceptable intermediate state, this is complete as it
    stands and only needs the pin question below settled.
  2. What pandoc-carve a consumer is expected to have. Both gate 1 and gate 2
    are cleared through unpublished commits: pandoc-carve is not on npm, and its
    own engine dependency is a git pin rather than a release. The Dockerfile
    pin and the README requirement line say af285cc for now. If this repo
    should instead wait for a published carve-js engine release carrying
    figure_group and a published pandoc-carve on top of it, that is the reason
    to hold the draft.

A bare `::: figure` container is one figure of ordered panels (Carve PART 9
section 4c): the group is one numbering unit and only the group produces a
list-of-figures entry. Two of the three layers that have to know about it now
do.

pandoc-carve maps `figure_group` to a Pandoc `Figure` whose direct `Figure` and
`Table` children are the panels, and its lockfile pins a carve-js commit that
carries the node, so the shape reaches this adapter. Measured on
examples/composite-figure.crv:

```
Figure id="fig-mixed" cls=["columns-2"]
  Table
  Figure
    CodeBlock
```

Resilient's pandocast renderer turns every one of those into its own captioned
float. Left alone the two panels each take a figure number and file a
list-of-figures entry, the group is numbered 3, and the caption Carve already
resolved still reads "Figure 1:". The two numbering schemes disagree.

carve/figuregroup.lua walks the parsed tree and marks the direct captioned
children of a captioned figure `unnumbered` and `notoc`. Resilient's
`markdown:internal:captioned-figure` and `markdown:internal:captioned-table`
commands already read both classes, so nothing new has to be taught to the
renderer. Doing this on the SILE AST rather than on the Pandoc JSON is
deliberate: `Renderer:Table` discards the Pandoc attributes of a table
outright, so a class put on a table panel earlier would never arrive.

Only direct children are panels. A captioned figure sitting inside group
content, in a `::: note` or in the generic div a nested bare `::: figure`
degrades to, keeps its own number, which is what corpus documents
318-composite-figures-9 and -11 pin.

The Dockerfile pinned pandoc-carve at 60e219a, which predates that mapping, so
the container test could not have exercised a group at all. It now pins
af285cc.

What this does NOT do: place the group as a float, or act on the `columns-N`
hint. Resilient's captioned elements are not floats and nothing below this
adapter turns a `columns-N` class into a column arrangement, so panels stack in
the text flow. Writing that here would mean owning a renderer. The README says
so in place.
A table panel files its entry in the list of TABLES, at toclevel 6, not in the
list of figures. Counting level-5 entries therefore said nothing about it: the
example passed the check whether or not the captioned-table branch of
carve/figuregroup.lua did anything.

Assert that the example files no list-of-tables entry either. Without the
suppression the run files one, and the panel takes table number 1.
Only the direct figure and table children of a bare `::: figure` container are
its panels. A captioned figure inside group content, in a `::: note` or in the
generic div a nested bare `::: figure` degrades to, is not a panel and keeps a
number of its own, which corpus documents 318-composite-figures-9 and -11 pin.

Nothing here tested that. Rewriting carve/figuregroup.lua to mark every
captioned DESCENDANT of a captioned figure rather than its direct children left
both the unit test and the end-to-end run green, because the only figure the
first example nests inside a container is a sibling of the group, not part of
it. The rule the module is built on was free to be wrong.

examples/composite-figure-nested.crv puts a captioned listing inside a
`::: note` inside a group, which pandoc-carve maps to a Figure holding a Div
holding a Figure. test/toccheck.lua grows a mode for it: two list-of-figures
entries, one for the group and one for the listing. Under the descendant
rewrite the listing is suppressed and only one entry is left.

The check asserts no numbers, deliberately. Resilient numbers every captioned
figure, Carve numbers only the captions carrying a placeholder, so the two
counters agree only in a document where every caption carries one, and this
example is not that document.
The nested example is now part of the test run, and the reason its check
asserts entries rather than numbers is a property of the pipeline worth stating
where a reader will look for it: Resilient numbers every captioned figure it
typesets, Carve numbers only the captions carrying a placeholder, so the two
counters agree only in a document where every caption carries one.
@dereuromark

Copy link
Copy Markdown
Contributor Author

Superseded: PR #1 replaced the pandoc-carve pipeline with the native exchange-AST renderer this branch predates, and #5 landed composite figures on that renderer (one numbered unit, panels as unnumbered captioned children, engine pin bumped). The numbering insight from this branch's figuregroup transform is covered by #5's direct-children discrimination test.

@dereuromark
dereuromark deleted the feat/2-grouped-floats-composite-sat-6436a4ec branch August 16, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant