Skip to content

fix: a captioned quote is a figure in both directions again - #69

Closed
dereuromark wants to merge 1 commit into
mainfrom
p4-quote-figure-40aebb82
Closed

fix: a captioned quote is a figure in both directions again#69
dereuromark wants to merge 1 commit into
mainfrom
p4-quote-figure-40aebb82

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Clears the P4 rows of markup-carve/carve#1210 (cross-repo, so it does not auto-close anything there).

markup-carve/carve#1213 withdrew the quote attribution model: a captioned quote is a figure whose target is a block_quote again. This bridge still implemented the withdrawn model, which was not stale bookkeeping but two live defects against the pinned engine.

What was broken

The exchange AST did not validate. pandocToCarveAst put an attribution field on a block_quote, and resources/ast-schema.json refuses it - the node declares additionalProperties: false. Reversing a Figure[BlockQuote] produced:

{"keyword":"additionalProperties","params":{"additionalProperty":"attribution"}}

Caption numbering disagreed with the engine. For

> To be
^ Figure #: Hamlet

![a](a.png)
^ Figure #: Second

the engine renders Figure 1: Hamlet then Figure 2: Second. The bridge kept the quote's # literal and started the sequence at the image, so pandoc's HTML writer printed:

<blockquote>
<p>To be</p>
<p><span class="attribution">Figure #: Hamlet</span></p>
</blockquote>
<figure>
<img src="a.png" alt="a" />
<figcaption>Figure 1: Second</figcaption>
</figure>

A placeholder that never resolves, and an off-by-one on every later figure and cross-reference.

What changed

A quote figure lowers through the ordinary figure path, and a Figure[BlockQuote] reverses to figure{target: block_quote}. The runtime probe of the engine's serializer and the shape-rewriting lowering go with the model they served.

The reason the old lowering existed still holds, and is not quote-specific

Pandoc's plain and rst writers do drop a Figure's caption. Measured on pandoc 3.5:

writer caption on Figure[BlockQuote]
plain dropped
rst dropped (.. container:: float)
latex kept
markdown, html, gfm kept
docbook kept
org kept

The plain writer drops the caption of every figure, image figures included; rst keeps one only through the .. figure:: directive, which is the image case. A figure wrapping a code block loses it identically. So the loss is pandoc writer behavior for non-image figures. Moving the caption inside the quote to dodge it puts the attribution in the one place the HTML Standard forbids, which is what the spec withdrew. The roundtrip test now asserts both halves of that table, so the day pandoc changes it, the test says so.

No transitional shape is accepted

block_quote.attribution never appeared in a published engine (the carve npm package is at 0.1.3 and has no such field), and this bridge has no tag and no npm release. No document produced by the withdrawn model exists outside a working tree.

Reading the [X]{.attribution} span back in was therefore dropped rather than kept. A review pass argued for retaining it for documents produced by the previous forward converter; declining, because nothing emits it anymore and re-importing it would move content out of a blockquote on the strength of a class name this bridge invented. Such a document still round-trips with its text intact, spelled as what it literally says.

A crash found while measuring, fixed here too

Pandoc's own HTML reader emits a Figure with an EMPTY caption for <figure><img src="a.png"></figure>. Both figure branches built a figure node with no caption field from it - refused by the schema, which requires the field, and fatal in the writer:

TypeError: Cannot read properties of undefined (reading 'forEach')
    at renderInlines (.../render-carve.js:1265:15)
    at renderFigure (.../render-carve.js:1113:28)

It predates this change on the image branch; the quote branch would have joined it. Emitting caption: [] is not the fix - renderCarve writes a lone ^ line for it, and that line re-parses as a lazy continuation, so > q comes back as the two-line paragraph q\n^ inside the quote. An uncaptioned figure is emitted as its host instead, which is a shape Carve source can spell, and the dropped wrapper is reported.

The spec withdrew the quote attribution model in markup-carve/carve#1213, so a
captioned quote is a `figure` whose target is a `block_quote` and whose caption
is the `^ ` line. This bridge still carried the withdrawn model, and that was
not merely stale bookkeeping - it was two live defects against the pinned
engine, both measured rather than argued.

The exchange AST did not validate. `pandocToCarveAst` put an `attribution`
field on a `block_quote`, and `resources/ast-schema.json` refuses it: the node
declares `additionalProperties: false`, so reversing a `Figure[BlockQuote]`
produced a document the published schema rejects with
`{"keyword":"additionalProperties","params":{"additionalProperty":"attribution"}}`.
The one thing an exchange AST has to be is the exchange AST.

Caption numbering disagreed with the engine. For a captioned quote followed by
a captioned image, both spelling `Figure #`, the engine renders `Figure 1` then
`Figure 2`. The bridge kept the quote's `#` literal and started the sequence at
the image, so pandoc's HTML writer printed `Figure #: Hamlet` and
`Figure 1: Second` - a placeholder that never resolves and an off-by-one on
every later figure and cross-reference.

A quote figure now lowers through the ordinary figure path, and a
`Figure[BlockQuote]` reverses to `figure{target: block_quote}`. The runtime
probe of the engine's serializer and the lowering that rewrote the shape on the
way out are both gone with the model they served.

The attribution lowering existed for a real reason and the reason still holds:
pandoc's plain and rst writers drop a `Figure`'s caption. Measured on pandoc
3.5, that is not quote-specific. The plain writer drops the caption of EVERY
figure, image figures included; the rst writer keeps one only through the
`.. figure::` directive, which is the image case. A figure wrapping a code block
loses it identically. So the loss belongs to pandoc's writers for non-image
figures, and routing around it by moving the caption inside the quote puts the
attribution in the one place the HTML Standard forbids - which is what the spec
withdrew. latex, html, markdown, docbook and org all keep it, and the roundtrip
test now asserts both halves of that table so the day pandoc changes, the test
says so.

Nothing had to be kept working: `block_quote.attribution` never appeared in any
published engine (the carve npm package is at 0.1.3, which has no such field),
and this bridge has no tag and no npm release, so no document produced by the
withdrawn model exists outside a working tree. Reading the
`[X]{.attribution}` span back in was therefore dropped rather than kept: nothing
emits it anymore, and re-importing it would move content out of a blockquote on
the strength of a class name this bridge itself invented.

Measuring the new path surfaced a separate crash that predates it. Pandoc's own
HTML reader emits a `Figure` with an EMPTY caption for
`<figure><img src="a.png"></figure>`, and both figure branches built a `figure`
node with no `caption` field from it - refused by the schema, which requires the
field, and fatal in the writer with
`Cannot read properties of undefined (reading 'forEach')`. Emitting `caption: []`
instead is not the fix: `renderCarve` writes a lone `^` line for it, and that
line re-parses as a lazy continuation, so `> q` comes back as the two-line
paragraph `q\n^` inside the quote. An uncaptioned figure is emitted as its host
instead - a shape Carve source can spell - and the dropped wrapper is reported.
@dereuromark

Copy link
Copy Markdown
Contributor Author

Superseded. Closing rather than rebasing, because the behavior this asks for is already on main.

It was opened at 12:29 and never rebased. Three PRs landed after it, all touching the same files:

  • markup-carve/pandoc-carve#70 mapped figure_group in both directions (src/convert.ts, src/reverse.ts, src/index.ts)
  • markup-carve/pandoc-carve#71 bumped the spec corpus submodule
  • markup-carve/pandoc-carve#72 removed the withdrawn §4a quote-attribution apparatus outright: the quote reroute, attributionInlines, the attr-merge, the numbering exception, attributionFromBlocks, and the renderCarve probe

That is why it reports CONFLICTING / DIRTY. The conflict is not a merge accident to be resolved; it is two branches doing the same job, and the other one landed with the §4c work built on top of it.

Verified on current main rather than inferred. Input:

> To be, or not to be.
^ Hamlet

Forward, the top-level block is a Figure whose caption is the ^ line and whose content is the BlockQuote:

{"t":"Figure","c":[["",[],[]],[null,[{"t":"Plain","c":[{"t":"Str","c":"Hamlet"}]}]],[{"t":"BlockQuote","c":[{"t":"Para","c":[...]}]}]]}

Reverse, it comes back byte-identical:

> To be, or not to be.
^ Hamlet

And git grep for attributionInlines, attributionFromBlocks and 'attribution' under src/ on main returns nothing.

Rebasing this branch would replay a removal against a tree the removal already happened in. Nothing is lost by closing it; the branch is preserved.

@dereuromark
dereuromark deleted the p4-quote-figure-40aebb82 branch August 17, 2026 10: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