Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM node@sha256:0557ac14e0d45d02ed563067b82856ca5e7aa3437fa28d98d4350ea9c3d9494
RUN git clone \
https://github.com/markup-carve/pandoc-carve.git /opt/pandoc-carve \
&& cd /opt/pandoc-carve \
&& git checkout 60e219aa84db977f07e7c2d0b360512ae36e46c9 \
&& git checkout af285cc8edae3bcb1fe1abcfdcde221c5bbe3f1b \
&& git submodule update --init --recursive \
&& npm ci \
&& npm run build \
Expand Down
112 changes: 68 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ not required.

- SILE 0.15.13 or later
- resilient.sile 4.2.0 or later
- `pandoc-carve` on `PATH`
- `pandoc-carve` on `PATH`, built from a checkout that maps `figure_group`
(pandoc-carve `af285cc` or later, which is what `Dockerfile` pins)

`pandoc-carve` is not yet published in the npm registry. For now, install it
from its GitHub checkout:
Expand Down Expand Up @@ -59,8 +60,12 @@ inputter has been loaded.
./test/smoke.sh
```

The test checks conversion whenever `pandoc-carve` is installed and additionally
checks PDF generation when SILE is available.
The test runs the `carve.figuregroup` unit test whenever a Lua interpreter is
available, checks conversion whenever `pandoc-carve` is installed, and
additionally typesets `examples/smoke.crv`, `examples/composite-figure.crv` and
`examples/composite-figure-nested.crv` when SILE is available. For the two
composite examples it reads back the list entries SILE wrote and checks that
the group is one unit and that group content which is not a panel is not.

For a reproducible end-to-end test using SILE's official container image:

Expand All @@ -81,52 +86,71 @@ decided entirely by the two layers underneath it: the Carve engine pandoc-carve
depends on, and pandoc-carve's mapping to the Pandoc AST. A construct either
layer does not know about cannot be recovered here.

### Composite figures are not grouped floats yet
### Composite figures number as one unit

A bare `::: figure` container is one figure of ordered panels under a single
caption (Carve PART 9 section 4c). This pipeline does not typeset it as one
float today, and the reason is worth stating precisely, because two separate
layers have to move first.
caption (Carve PART 9 section 4c). The group is one numbering unit and only the
group produces a list-of-figures entry; its panels take neither a number nor an
entry, and a number placeholder in a panel caption stays literal.

Input:
Input, `examples/composite-figure.crv` in short:

```
{#fig-x .columns-2}
````
{#fig-mixed .columns-2}
::: figure
{#fig-x-a}
![one](a.png)
^ (a) One

{#fig-x-b}
![two](b.png)
^ (b) Two
:::
^ Figure #: Group caption
| Kind | N |
|------|---|
| a | 1 |
^ (a) A table panel

See </#fig-x> and </#fig-x-a>.
``` js
const x = 1
```
^ (b) A listing panel
:::
^ Figure #: Two panels, one figure
````

pandoc-carve maps that to a Pandoc `Figure` whose direct `Figure` and `Table`
children are the panels. Resilient's `pandocast` renderer turns each of those
into its own captioned float, so left alone a two-panel group consumes three
figure numbers, files three list-of-figures entries, and ends up numbered
`Figure 3` while the caption Carve resolved still reads `Figure 1:`.

`carve/figuregroup.lua` closes that gap. It walks the parsed tree, and on every
captioned figure it marks the direct captioned children `unnumbered` and
`notoc`, which are classes Resilient's `markdown:internal:captioned-figure` and
`markdown:internal:captioned-table` commands already read. The group is then
the only numbered element, its number agrees with the one Carve wrote into the
caption, and the list of figures has one entry per group.

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, which is what corpus documents `318-composite-figures-9`
and `-11` pin. An opener carrying a quoted title or a `[label]` is not this
production at all: it stays a generic container, and what it holds numbers on
its own.

#### What is still missing

The group numbers as one unit, but it is not yet laid out as one.

- Resilient's captioned elements are, in its own words, not floats. There is no
float mechanism to place a group into, so a composite figure sits in the text
flow where it was written.
- The `columns-N` layout hint arrives as a class on the group and nothing below
acts on it. Two panels stack vertically rather than sitting side by side.
Turning that into a real multi-column arrangement needs a renderer, which is
the one thing this adapter deliberately does not own.
- Carve resolves the caption placeholder before the text reaches SILE, and
Resilient prepends its own `Figure N.` to every caption. A caption written
`^ Figure #: ...` therefore renders its label twice. This is not specific to
composite figures; a single captioned image does the same.
- The two counters only agree in a document where every captioned figure
carries a placeholder. Resilient numbers every captioned figure it typesets;
Carve numbers only the captions that carry one. A caption written without a
placeholder still consumes a Resilient number, and everything after it is
numbered one higher than Carve thinks. `examples/composite-figure-nested.crv`
is deliberately such a document, which is why its check asserts entries
rather than numbers.

What reaches SILE today, with the published engine:

- the container is an ordinary `Div` carrying the `admonition`, `figure` and
`columns-2` classes, holding the two panels as separate Pandoc figures;
- the group caption is a PARAGRAPH whose text is the literal `^ Figure #: Group
caption`, caret and placeholder included, because a caption after a container
closer is section 4c's rule and the published engine predates it;
- both cross-references degrade to their bare target text, since nothing
numbered the group.

The order of the gate:

1. an `@markup-carve/carve` release containing the `figure_group` node - it is
implemented in carve-js but is not in 0.1.3, the newest published version and
the one pandoc-carve resolves;
2. pandoc-carve mapping `figure_group` to a Pandoc figure containing the panel
figures, so the group caption and the panel captions arrive as captions
rather than as text;
3. Resilient's `pandocast` renderer placing that as a float, at which point the
`columns-N` hint has something to act on.

Nothing in this repository sits between those steps, so there is no adapter-side
workaround: code here that recognized a grouped figure would be matching a shape
no layer below it emits.
1 change: 1 addition & 0 deletions carve-sile-dev-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ build = {
modules = {
["sile.inputters.carve"] = "inputters/carve.lua",
["sile.carve.bridge"] = "carve/bridge.lua",
["sile.carve.figuregroup"] = "carve/figuregroup.lua",
},
}
88 changes: 88 additions & 0 deletions carve/figuregroup.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
--- Make a Carve composite figure number as one unit in the Resilient renderer.
--
-- Carve PART 9 section 4c: a bare `::: figure` container is ONE figure of
-- ordered panels. Its direct `figure` and `table` children are the panels; the
-- group is one numbering unit, and only the group produces a list-of-figures
-- entry. A number placeholder in a panel caption stays literal.
--
-- pandoc-carve maps that node to a Pandoc `Figure` whose direct `Figure` and
-- `Table` children are the panels, and Resilient's `pandocast` renderer turns
-- every one of those into its own captioned float. Left alone, a group of two
-- panels therefore consumes three figure numbers and files three list-of-
-- figures entries, and Resilient's counter stops agreeing with the number
-- Carve already resolved into the group caption.
--
-- Resilient's `markdown:internal:captioned-*` commands read the `unnumbered`
-- and `notoc` classes off their options, so the whole correction is to put
-- those two classes on the panels. Nothing else about the group changes: the
-- panels keep their captions, their ids and their position, and group content
-- that is not a panel is left exactly where it was.
--
-- Only DIRECT children are panels. A captioned figure that sits inside group
-- content -- inside a `::: note`, or inside the generic div a nested bare
-- `::: figure` degrades to -- is not a panel and keeps its own number, which
-- is what corpus documents 318-composite-figures-9 and -11 pin.

local figuregroup = {}

-- The two commands `pandocast` produces for a captioned float. It has no path
-- to `markdown:internal:captioned-listing`, so that one is deliberately absent
-- rather than listed for symmetry: a panel can only arrive as one of these.
local CAPTIONED = {
["markdown:internal:captioned-figure"] = true,
["markdown:internal:captioned-table"] = true,
}

local PANEL_CLASSES = { "unnumbered", "notoc" }

local function hasClass (classes, name)
return string.find(" " .. classes .. " ", " " .. name .. " ", 1, true) ~= nil
end

local function markPanel (node)
node.options = node.options or {}
local classes = node.options.class or ""
for _, name in ipairs(PANEL_CLASSES) do
if not hasClass(classes, name) then
classes = classes == "" and name or (classes .. " " .. name)
end
end
node.options.class = classes
end

-- A captioned figure holds { <rendered blocks>, <caption> }. The first slot is
-- the list of the group's direct children, except that pandocast collapses a
-- one-element list to the element itself, so a single-panel group arrives as a
-- bare command node.
local function directChildren (node)
local slot = node[1]
if type(slot) ~= "table" then
return {}
end
if slot.command then
return { slot }
end
return slot
end

--- Mark the panels of every composite figure in a SILE AST, in place.
-- @tparam table tree SILE AST node, or a list of them
-- @treturn table the same tree
function figuregroup.mark (tree)
if type(tree) ~= "table" then
return tree
end
if tree.command == "markdown:internal:captioned-figure" then
for _, child in ipairs(directChildren(tree)) do
if type(child) == "table" and CAPTIONED[child.command] then
markPanel(child)
end
end
end
for _, child in ipairs(tree) do
figuregroup.mark(child)
end
return tree
end

return figuregroup
20 changes: 20 additions & 0 deletions examples/composite-figure-nested.crv
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Composite figures, nested content
author: Carve contributors
---

# Nested content is not a panel

Only the direct `figure` and `table` children of a bare `::: figure` container
are its panels (Carve PART 9 section 4c). A captioned figure that sits inside
group content is not a panel and keeps a number of its own.

:::: figure
::: note
``` js
const z = 3
```
^ A listing inside group content, still its own figure
:::
::::
^ Figure #: A group whose content is not a panel
34 changes: 34 additions & 0 deletions examples/composite-figure.crv
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: Composite figures
author: Carve contributors
---

# Composite figures

A bare `::: figure` opener is one figure of ordered panels (Carve PART 9
section 4c). The group takes one figure number and files one list-of-figures
entry; the panels take neither.

{#fig-mixed .columns-2}
::: figure
| Kind | N |
|------|---|
| a | 1 |
^ (a) A table panel

``` js
const x = 1
```
^ (b) A listing panel
:::
^ Figure #: Two panels, one figure

An opener that carries a quoted title is not that production. It stays a
generic container, and what it holds keeps numbering on its own.

::: figure "A titled container, not a group"
``` js
const y = 2
```
^ A listing that is still its own figure
:::
7 changes: 6 additions & 1 deletion inputters/carve.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

local base = require("inputters.base")
local bridge = require("carve.bridge")
local figuregroup = require("carve.figuregroup")
local pandocast = require("inputters.pandocast")

local inputter = pl.class(base)
Expand All @@ -25,7 +26,11 @@ function inputter:parse (doc)

-- Reuse resilient.sile's mature Pandoc AST-to-SILE renderer. This returns
-- the complete document AST, including the default markdown/resilient class.
return pandocast(self.options):parse(json)
local tree = pandocast(self.options):parse(json)

-- A Carve composite figure is one numbering unit (PART 9 section 4c), but
-- the renderer above numbers each panel separately. Suppress the panels.
return figuregroup.mark(tree)
end

return inputter
Loading
Loading