Skip to content
Merged
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Composite figures: a bare `::: figure` container is a captionable host**
(carve#1122, PART 9 §4c). One figure holding ordered panels: the direct
`figure`/`table` children are the panels, a `^ ` caption after the CLOSING
fence - caption placement's sixth host, this kind only - captions and
numbers the whole group, and the AST gains a `figure_group` node type
(PART 12 §16). The group is one figure-sequence unit; a panel id resolves
`</#id>` as the group number plus a letter ("Figure 2a"); a `#` in a panel
caption stays literal. An opener carrying a quoted title or `[label]` stays
a generic container, as does a nested bare `::: figure`; both lint. HTML
renders `carve-figure-group` / `carve-figure-panels` / `carve-figure-panel`,
and PART 11 §10g fixes the writer spelling and the non-HTML degradations.
This is an observable parsing change inside 0.1.x for documents that already
hold a bare `::: figure` fence: the container reclassifies from a generic
div, and a previously dangling `^ ` line after its closer starts consuming a
figure number, which can renumber later figures.

- **A compact language attribute, `{:TAG}`** (carve#1114). `[Le Bon Usage]{:fr}`
is exact sugar for `{lang=fr}`, on inline spans and on block attribute lines
alike. The empty form `{:}` means the language is explicitly UNKNOWN and
Expand Down
28 changes: 28 additions & 0 deletions docs/ast-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,34 @@ reasons:
the same `<dl>`. A structure two producers disagree about, which no output
depends on, is an internal.

## Composite figures

A bare `::: figure` container (PART 9 section 4c) serializes as its own node type,
`figure_group` (§16):

```json
{"type": "figure_group",
"children": [ ... ],
"caption": [ ... ],
"attrs": { ... },
"pos": { ... }}
```

`children` are ordinary block nodes in source order; the panels are the
`figure` and `table` nodes among them, and non-panel stray content sits
between them in place. There is **no** `panels` array - repeating the children
under a second key would let the two disagree, so a consumer derives the panel
list the way the renderer does: by type, in order. `caption` is the group
caption (the `^ ` line after the closing fence); absent means uncaptioned, not
an empty array.

The node is discriminated by its `type`, deliberately: every `figure` carries
a `target`, the group does not, and a consumer probing for the missing field
instead of reading the type string would break silently the day either shape
grows a field. No `title`, no `label`, no `shortCaption`, no legend fields -
that design space belongs to carve#1118 and carve#1121 and is not claimed
here.

## Open question

**Citation items are plain objects**, carrying `key`, `prefix`, `locator` and
Expand Down
9 changes: 9 additions & 0 deletions docs/blocks-and-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ Header and label together - header first, label second.
:::
```

One container kind is reserved: a **bare `::: figure` opener** (no title, no
label) is a *composite figure* (PART 9 §4c), not an admonition. Its captioned
children become panels and a `^ ` caption after the closing fence captions the
whole group - the one `:::` closer that hosts a caption. Add a quoted title or
a `[label]` to the opener and the line falls back to a generic
`<div class="figure">` container (with a `figure-group-opener-metadata` lint
warning): the group form has no title or label slot, its caption is the
metadata channel.

Two strictness rules to know:

- The header must use **straight double quotes**. An unquoted trailing word (`::: note Custom Title`) or typographic quotes (`::: note “Custom”`, the kind word processors and CMS text filters substitute) make the line *not a fence at all* - the whole block degrades to a literal paragraph. If you see raw `:::` lines in your output, check the quotes first.
Expand Down
17 changes: 16 additions & 1 deletion docs/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ two
+ continuation cell | (+ = multi-line cell)
```

## Captions (images, quotes, tables, code listings, equations)
## Captions (images, quotes, tables, code listings, equations, figure groups)

```carve
![Photo](img.jpg)
Expand All @@ -150,6 +150,21 @@ two
A `^` caption after a fenced code block makes a numbered *listing*; after a
standalone `$$`-math block, a numbered *equation*.

A bare `::: figure` container is a *composite figure*: its captioned children
become lettered panels, and a `^` caption after the closing fence captions and
numbers the whole group (`</#panel-id>` then renders as "Figure 2a"):

```carve
::: figure
![one](a.png)
^ (a) One

![two](b.png)
^ (b) Two
:::
^ Figure #: The pair
```

A caption spans multiple lines like a paragraph — following lines fold in until
a blank line or a block that would interrupt a paragraph (a list marker folds
in, it does not end the caption):
Expand Down
9 changes: 9 additions & 0 deletions docs/graceful-degradation.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ The table reflects the reference engines' renderer behavior.
| Footnotes | jump links | print-native footnotes; `[^id]` preserved in Markdown | degrades natively |
| Links / autolinks | clickable | clickable in PDF; URL preserved in plain text | degrades natively |
| Cross-references / TOC | anchor links | internal PDF links; anchors preserved in Markdown | degrades natively |
| Composite figure (`::: figure`) | one `<figure>` with a panels `<div>` and a group `<figcaption>`; layout hints (`columns-2`) pass through as classes | Markdown: panels in order, panel captions as `*(...)*`, group caption last as `**...**`; plain/terminal: group caption first, then each panel's caption + host degradation | normative - PART 9 §4c + the writer/degradation clause in PART 11 |

The composite figure's contract is the floor applied with no exceptions: every
panel, every panel caption, any stray content between the panels, and the group
caption (its number resolved) are **content**, and no target may silently
discard or reorder any of them. Layout hints degrade to nothing - order is the
only layout a non-HTML target keeps - but the text never does. Panel letters
exist in cross-reference text only ("Figure 2a"); no target invents visible
`(a)` labels the author did not write.

Most constructs already degrade well because their distinguishing text is a
**title** (a quoted `"..."` node the renderer emits) or **source** (kept
Expand Down
3 changes: 2 additions & 1 deletion docs/implementation-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ under the same semantic-span rule,
`314-a-footnote-in-an-unresolved-reference-is-not-a-reference`,
`315-an-inline-note-s-content-resolves-after-the-note`,
`316-an-image-s-alt-text-closes-where-a-link-s-text-closes`,
`317-an-editorial-comment-s-bracket-is-content-not-the-close`.
`317-an-editorial-comment-s-bracket-is-content-not-the-close`,
`318-composite-figures`.

Those categories landed on hosts that could not retake the run above, so its
numbers describe the corpus WITHOUT them. The alternative was to edit the
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ features:
- title: Interactive Online, Readable Offline
details: "Built for the interactive web first — diagrams, math, charts and tabs hydrate into rich output online. With no JavaScript every block degrades to clean semantic HTML: a Mermaid fence still shows its source, <details> stays native, tables stay tables."
- title: Captions Everywhere
details: One ^ prefix captions images, blockquotes, tables, listings and equations — emitting semantic figure / figcaption / caption HTML.
details: One ^ prefix captions images, blockquotes, tables, listings, equations and composite figure groups — emitting semantic figure / figcaption / caption HTML.
- title: Friendly Tables
details: "|= for headers, ^ for rowspan, < for colspan, + for multi-line cells. No separator row required."
- title: Built-in Extensions
Expand Down Expand Up @@ -94,7 +94,7 @@ pinned to exact HTML in the [examples](./examples).

**Carve 0.1 is specified and shipping.** Tier-1 core and Tier-2 standard
extensions are normative and stable; Tier-3 app-level extensions ship but evolve
(see [Versioning](./versioning)). Conformance is pinned by 1006 corpus examples
(see [Versioning](./versioning)). Conformance is pinned by 1017 corpus examples
with exact HTML output, and the three reference engines - carve-js (TypeScript),
carve-php, and carve-rs - all run the same corpus. Where the corpus pins a rule
ahead of an engine, the window is declared on the
Expand Down
12 changes: 8 additions & 4 deletions docs/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ spelling.
`list_item`, `table`, `table_row`, `table_cell`, `thematic_break`, `div`,
`admonition`, `raw_block`, `footnote`, `frontmatter`, `definition_list`,
`definition_term`, `definition_description`, `section`, `line_block`,
`comment`, `figure`, `caption`, `abbreviation_def`,
`comment`, `figure`, `figure_group`, `caption`, `abbreviation_def`,
`link_reference_definition`.

**Inline:** `text`, `emphasis`, `strong`, `underline`, `strike`,
Expand Down Expand Up @@ -60,11 +60,15 @@ containers has no way to express that if the kind lives in a class string.

Which fences are callouts is the **Tier-1 canonical list** - `note`, `tip`,
`warning`, `danger`, `info`, `success`, `example`, `quote`. A fence opened with
any other word (`::: sidebar`, `::: figure-group`, a name your own extension
any other word (`::: sidebar`, `::: aside-note`, a name your own extension
claims) is a **generic container**: it renders as `<div class="name">` rather
than an `<aside class="admonition name">`, and it is classified as **`div`** for
profiles. So `denyBlock(['admonition'])` removes callouts and leaves those
containers standing, which is the capability the paragraph above promises:
profiles. The one reserved word is `figure`: a *bare* `::: figure` opener is a
composite figure (`figure_group`, PART 9 §4c), not an admonition - though a
`::: figure` opener carrying a quoted title or a `[label]` still falls back to
the generic container. So `denyBlock(['admonition'])` removes callouts and
leaves those containers standing, which is the capability the paragraph above
promises:

```js
const p = Profile.full()
Expand Down
34 changes: 34 additions & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,40 @@ To find affected documents, search for the seven names used as attributes on a
span, and for `:name[…]` with any of them. Where a value mattered, move it to an
attribute that survives - a `title`, or a link if it was a URL.

**A bare `::: figure` container is now a composite figure.** The kind word
`figure` is reserved (PART 9 section 4c, carve#1122): a bare opener produces a
`figure_group` - one numbered figure whose captioned children are panels - where
it used to produce a generic `<div class="figure">`, and the `^ ` line after its
closing fence attaches as the group caption where it used to stay a literal
paragraph:

```carve
{#ep}
::: figure
> To be
:::
^ Figure #: A pull quote
```

```html
<div class="figure" id="ep"> <!-- before -->
<blockquote><p>To be</p></blockquote>
</div>
<p>^ Figure #: A pull quote</p>

<figure class="carve-figure-group" id="ep"> <!-- after -->
<div class="carve-figure-panels">
<blockquote><p>To be</p></blockquote>
</div>
<figcaption>Figure 1: A pull quote</figcaption>
</figure>
```

An opener carrying a quoted title or a `[label]` keeps the old generic-container
shape. Documents that already hold a bare `::: figure` fence reclassify, and a
previously dangling caption starts consuming a figure number, which can renumber
later figures in the same document - `carve lint` reports the affected shapes.

### Checking documents mechanically

The marker is machine-readable, so this does not have to be done by eye. In
Expand Down
50 changes: 50 additions & 0 deletions resources/ast-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@
"definition_term",
"div",
"figure",
"figure_group",
"footnote",
"frontmatter",
"heading",
Expand Down Expand Up @@ -397,6 +398,21 @@
"$ref": "#/$defs/figure"
}
},
{
"if": {
"properties": {
"type": {
"const": "figure_group"
}
},
"required": [
"type"
]
},
"then": {
"$ref": "#/$defs/figure_group"
}
},
{
"if": {
"properties": {
Expand Down Expand Up @@ -1118,6 +1134,40 @@
},
"additionalProperties": false
},
"figure_group": {
"type": "object",
"title": "figure_group (block)",
"description": "A composite figure (PART 9 \u00a74c, carve#1122): one figure-numbering unit whose direct captionable children - ordinary `figure` and `table` nodes among `children`, in source order - are its panels. There is no `panels` array: a consumer derives the panel list the way the renderer does, by type, in order, and non-panel stray content is preserved in place between them. Discriminated by `type` rather than by shape: every `figure` carries a `target`, this node deliberately does not, and probing for the missing field instead of the type string would break the day either shape grows a field. No title, no label, no `shortCaption`, no legend fields - the group's one authored metadata channel is `caption`, and the rest is carve#1118/carve#1121 design space, not claimed here.",
"required": [
"type",
"children"
],
"properties": {
"type": {
"const": "figure_group"
},
"children": {
"type": "array",
"items": {
"$ref": "#/$defs/blockNode"
}
},
"caption": {
"type": "array",
"description": "The GROUP caption (the `^ ` line after the closing fence). Absent means the group is uncaptioned - no empty-array placeholder.",
"items": {
"$ref": "#/$defs/inlineNode"
}
},
"attrs": {
"$ref": "#/$defs/attrs"
},
"pos": {
"$ref": "#/$defs/pos"
}
},
"additionalProperties": false
},
"footnote": {
"type": "object",
"title": "footnote (block)",
Expand Down
14 changes: 14 additions & 0 deletions resources/engine-pin-drift.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@
306-a-captioned-quote-holds-more-than-one-block-3 A captioned quote renders `<figure><figcaption>`; the pin renders `<blockquote><footer>`.
306-a-captioned-quote-holds-more-than-one-block-4 A captioned quote renders `<figure><figcaption>`; the pin renders `<blockquote><footer>`.
306-a-captioned-quote-holds-more-than-one-block-5 A captioned quote renders `<figure><figcaption>`; the pin renders `<blockquote><footer>`.

# PART 9 §4c composite figures (carve#1122): the pinned carve-js predates the
# `::: figure` captionable host, so it renders these as a generic container
# with a literal `^ ` paragraph. The engine PRs ship the host; the next pin
# bump empties these entries.
318-composite-figures PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-2 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-3 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-4 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-5 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-6 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-9 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-10 PART 9 §4c composite figures; pinned carve-js predates the host
318-composite-figures-11 PART 9 §4c composite figures; pinned carve-js predates the host
1 change: 1 addition & 0 deletions resources/example-pages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ index: examples/edge-cases/index.md
a-container-a-lazy-line-folded-into-is-still-open
a-fence-keeps-the-blank-line-at-the-end-of-its-content
a-captioned-quote-holds-more-than-one-block
composite-figures
11-fenced-code-10
42-admonitions-5
42-admonitions-8
Expand Down
Loading