Skip to content

feat(syntax): a bare figure fence is a composite figure, and the pin moves with it - #22

Merged
dereuromark merged 2 commits into
mainfrom
feat/composite-figures-composite-sat-6436a4ec
Aug 15, 2026
Merged

feat(syntax): a bare figure fence is a composite figure, and the pin moves with it#22
dereuromark merged 2 commits into
mainfrom
feat/composite-figures-composite-sat-6436a4ec

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Carries the composite-figures vocabulary into this repo, both halves of what it ships. Part of markup-carve/carve-grammars#222; spec PART 9 §4c, landed as markup-carve/carve#1215, design markup-carve/carve#1122.

Three things move together

They are one fact about this repository: queries/carve/*.scm are copies, install_revision pins the grammar they were copied from, and tools/check-query-drift.sh compares them against THAT rev rather than a moving main. So the pin and the seven query files move in one commit, and the drift check is what proves the copy is still a copy.

The pin moves to tree-sitter-carve at composite figures (17362de, markup-carve/tree-sitter-carve#197), where a bare ::: figure opener's class word captures @type.builtin rather than the generic @type.

The syntax file, for users who never install the grammar

Input:

::: figure
![one](a.png)
^ (a) One
:::
^ Figure #: Group caption

The fence lines are carveFigureGroupFence; the ^ line below the closer is carveCaption, claimed at the top level exactly as every other caption is.

The control, which must not change:

::: figure "A titled figure div"
Body.
:::

Still carveDivFence with carveDivTitle.

And the degradation:

::: figure
:::: figure
Nested is a generic container.
::::
:::

The inner opener is carveDivFence.

Why a region and not a match

The first commit here used a syntax match, which has no notion of being inside anything, so every nested ::: figure coloured as another group. A region says it: contains=ALLBUT,carveFigureGroup means a bare opener inside the group cannot match the group rule again and falls through to carveDivFence. That holds through a quote or a list item as readily as directly, because containment is structural rather than a pattern per shape.

\z( / \z1 is the colon-fence depth rule (PART 9 §12) - the region closes on a fence of the SAME length that opened it, which is what lets :::: nest inside :::.

Two details found by watching the assertions fail rather than by reading:

  • Order is precedence. Vim gives a later syntax match priority at the same position. Written before carveDivFence - which reads more naturally - the div rule won every line and the new group was unreachable.
  • The separator is a space run, never a tab (grammar PART 7, MARKER SEPARATORS). A tab-separated opener is left to carveDivFence, which over-colours it exactly as it does today.

On one codex finding, deliberately not acted on

Codex flagged that queries/carve/highlights.scm enforces the no-nesting rule only to a fixed depth. That is true, and it is upstream's residual, stated at the rule there. It must not be fixed here: that file is a verbatim copy and tools/check-query-drift.sh fails the moment it diverges from the pinned tree-sitter-carve commit. A deeper reach belongs in markup-carve/tree-sitter-carve and arrives here with the next pin bump.

…moves with it

PART 9 section 4c reserves the kind word `figure` among the `:::` types: a
BARE opener - the fence, its separator, the word, and nothing else - is ONE
figure of ordered panels, not an admonition. An opener carrying a quoted
title or a `[label]` is a generic container and must stay one.

Three things move together here, because they are one fact about this
repository: the tree-sitter queries are copies, `install_revision` pins the
grammar they were copied from, and `tools/check-query-drift.sh` compares
them against THAT rev rather than a moving main. So the pin, the seven query
files and the syntax file are bumped in one commit, and the drift check is
what proves the copy is still a copy.

The pin moves to tree-sitter-carve at composite figures, which is where a
bare `::: figure` opener's class word captures `@type.builtin` rather than
the generic `@type`.

syntax/carve.vim gets the same distinction for users who do not install the
tree-sitter grammar at all. Two details worth naming:

  - ORDER IS PRECEDENCE. Vim gives a later `syntax match` priority at the
    same position, so carveFigureGroup sits AFTER carveDivFence. Written
    before it - which reads more naturally - the div rule won every line and
    the new group was unreachable; the highlight assertions caught it.
  - The separator is a SPACE run, never a tab (grammar PART 7, MARKER
    SEPARATORS). A tab-separated opener is left to carveDivFence, which
    over-colours it exactly as it does today.

tests/highlight.crv carries the assertions, both halves in one document: the
bare opener's fence and kind word, the group caption on the line after the
closing fence, and the titled opener whose fence and title must stay
carveDivFence and carveDivTitle.
The first pass spelled the composite figure as a `syntax match`, which has
no notion of being inside anything: every nested `::: figure` was coloured
as another group, where PART 9 section 4c says a bare opener inside an open
group is a generic container at ANY depth.

A region says it. `contains=ALLBUT,carveFigureGroup` is the whole mechanism -
a bare opener inside the group cannot match the group rule again, so it falls
through to carveDivFence, which is the generic reading. It holds through a
quote or a list item as readily as directly, because containment is
structural rather than a pattern per shape.

`\z(` / `\z1` is the colon-fence depth rule (PART 9 12): the region closes on
a fence of the SAME length that opened it, which is what lets `::::` nest
inside `:::`. `keepend` stops an inner item from carrying the region past its
own closer, and the region ending AT the closing fence is what leaves the
group caption to carveCaption at the top level.

The word-level group is gone with the match: `matchgroup` colours the whole
delimiter line as carveFigureGroupFence, and the distinction that matters -
a bare opener reading differently from `::: figure "T"` - is carried by that
group against carveDivFence.

Assertions cover the opener, the closer, the caption below it, the titled
opener that must stay carveDivFence with its carveDivTitle, and the nested
opener that must degrade.
@dereuromark
dereuromark merged commit 828bb0e into main Aug 15, 2026
1 check passed
@dereuromark
dereuromark deleted the feat/composite-figures-composite-sat-6436a4ec branch August 15, 2026 12:44
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