diff --git a/README.md b/README.md index a9363f3..45fc880 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ silently. |---------------------|---------------------------------------------|------------------------------------------------------| | `parser_path` | `nil` | Register a pre-compiled parser directly. | | `install_url` | tree-sitter-carve repo | URL for `:TSInstall carve`. | -| `install_revision` | `b6877de1af5f6d1dde55395ebeee32a0483b1946` | Revision to install (post-0.1.2 main; pinned to the bundled queries). | +| `install_revision` | `17362de88d2c3177e7c6b4d5f83841f38a42ae4d` | Revision to install (post-0.1.2 main, at composite figures; pinned to the bundled queries). | | `register_filetype` | `true` | Map `carve` filetype to `carve` lang. | ## License diff --git a/lua/carve/init.lua b/lua/carve/init.lua index 380a23c..441acbd 100644 --- a/lua/carve/init.lua +++ b/lua/carve/init.lua @@ -33,7 +33,7 @@ local DEFAULTS = { -- (see README, "Bundled queries"), so an unpinned branch could compile a -- grammar newer or older than the queries and silently miss captures. -- Bump this alongside every query re-copy so the two stay paired. - install_revision = 'b6877de1af5f6d1dde55395ebeee32a0483b1946', + install_revision = '17362de88d2c3177e7c6b4d5f83841f38a42ae4d', -- Map the `carve` filetype to the `carve` language. register_filetype = true, } diff --git a/queries/carve/highlights.scm b/queries/carve/highlights.scm index 746f0ed..caa50f3 100644 --- a/queries/carve/highlights.scm +++ b/queries/carve/highlights.scm @@ -267,6 +267,87 @@ (class_name) ] @type +; Composite figures (PART 9 4c, markup-carve/carve#1215). The kind word `figure` +; is RESERVED among the `:::` types: a BARE opener - the fence, its separator, +; the word, and nothing else - is ONE figure of ordered panels, not an +; admonition. `!title !label` IS the distinction, and it is the whole reason this +; belongs in a query rather than in the grammar: the parse tree already tells the +; two apart by which fields the opener carries, so a reserved kind word needs a +; reserved capture rather than a new node. An opener carrying a quoted title or a +; `[label]` matches nothing here and keeps `@type` above, which is the generic +; Tier-2 container the clause says it stays. +; +; The group caption needs no rule: it is an ordinary `^ ` line one line below the +; closing fence, and the parser already places it as a sibling of the container +; rather than inside it, where the existing `(caption)` patterns claim it. +((div + class: (class_name) @type.builtin + !title + !label) + (#eq? @type.builtin "figure") + (#set! priority 105)) + +; GROUPS DO NOT NEST: a bare `::: figure` inside an open group is a generic +; container, not an inner group, at ANY depth. A query has no transitive +; closure - there is no "any descendant" - so the reach is spelled as wildcard +; chains rooted at the group, one per intervening level, each restoring `@type` +; on the inner opener at a higher priority than the pattern above gives it. +; +; Three levels covers every shape the language actually produces: a direct child +; of the group's content; one intervening container (`div` > `content` > `div`, +; and `block_quote` > `content` > `div`); and a list item +; (`list` > `list_item` > `list_item_content` > `div`). The wildcards are +; deliberate - naming the container types would have to be revisited every time +; a new block gains a content field, and the chain LENGTH is the real constraint. +; +; RESIDUAL, written down rather than left to be rediscovered: a bare opener +; reached through MORE than three levels - a quote inside a list item inside the +; group, say - keeps the group capture. The parse tree is right either way; only +; the colour is not. +((div + class: (class_name) @_group.class + !title + !label + content: (content + (div + class: (class_name) @type + !title + !label))) + (#eq? @_group.class "figure") + (#eq? @type "figure") + (#set! priority 110)) + +((div + class: (class_name) @_group.class + !title + !label + content: (content + (_ + (_ + (div + class: (class_name) @type + !title + !label))))) + (#eq? @_group.class "figure") + (#eq? @type "figure") + (#set! priority 110)) + +((div + class: (class_name) @_group.class + !title + !label + content: (content + (_ + (_ + (_ + (div + class: (class_name) @type + !title + !label)))))) + (#eq? @_group.class "figure") + (#eq? @type "figure") + (#set! priority 110)) + (identifier) @tag (key_value diff --git a/syntax/carve.vim b/syntax/carve.vim index 4323a46..2f20a67 100644 --- a/syntax/carve.vim +++ b/syntax/carve.vim @@ -146,6 +146,39 @@ syntax keyword carveAdmonition contained note tip warning danger info success ex syntax match carveDivTitle /"[^"]*"/ contained syntax match carveDivLabel /\[[^]]*\]/ contained +" A BARE `::: figure` opener - the fence, its separator, the kind word, and +" NOTHING else - is a composite figure (PART 9 4c, markup-carve/carve#1215): one +" figure of ordered panels, not an admonition. `\s*$` is the whole distinction; +" `::: figure "T"` and `::: figure [g]` match nothing here and stay the generic +" container the clause says they stay. +" +" A REGION rather than a match, and the region is what carries the rule that +" GROUPS DO NOT NEST. Its `contains` omits carveFigureGroup, so a bare +" `::: figure` anywhere inside an open group - at any depth, through a quote or +" a list item as readily as directly - falls through to carveDivFence, which is +" the generic reading the clause degrades it to. A plain `syntax match` cannot +" say that: it has no notion of being inside anything, and it coloured every +" nested opener as another group. +" +" `\z(` / `\z1` is the colon-fence depth rule (PART 9 12): the region closes on +" a fence of the SAME length as the one that opened it, which is what lets +" `::::` nest inside `:::`. `keepend` stops an inner item from carrying the +" region past its own closer. +" +" The separator is a SPACE run, never a tab (grammar PART 7, MARKER SEPARATORS): +" `:::` + TAB + `figure` opens nothing, so it is left to carveDivFence, which +" over-colours it exactly as it does today. +" +" The group caption needs no rule: it is an ordinary `^ ` line below the closing +" fence, and the region ends AT that fence, so carveCaption claims it at the top +" level exactly as it claims every other caption. +syntax region carveFigureGroup + \ matchgroup=carveFigureGroupFence + \ start=/^\s*\z(:\{3,}\) \+figure\s*$/ + \ end=/^\s*\z1\s*$/ + \ keepend + \ contains=ALLBUT,carveFigureGroup + " --------------------------------------------------------------------------- " Tables: | cell | with |= headers, alignment and span markers. " --------------------------------------------------------------------------- @@ -298,6 +331,8 @@ highlight default link carveMathDelim Delimiter highlight default link carveLiteralInline String highlight default link carveLiteralDelim Delimiter +highlight default link carveFigureGroup NONE +highlight default link carveFigureGroupFence Type highlight default link carveDivFence Delimiter highlight default link carveAdmonition Keyword highlight default link carveDivTitle String diff --git a/tests/highlight.crv b/tests/highlight.crv index 058ebae..cf3a531 100644 --- a/tests/highlight.crv +++ b/tests/highlight.crv @@ -36,3 +36,25 @@ An over-long tag [u]{:toolongtag} is prose. A colon-leading value [v]{key=:fr} is a value. %% ^^^ carveAttrKey %% ^^^ carveSpan + +::: figure +%%^^^^^^^^^^ carveFigureGroupFence +![one](a.png) +^ (a) One +::: +%%^^^ carveFigureGroupFence +^ Figure #: Group caption +%%^^^^^^^^^^^^^^^^^^^^^^^^^ carveCaption + +::: figure "A titled figure div" +%%^^^^ carveDivFence +%% ^^^^^^^^^^^^^^^^^^^^^ carveDivTitle +Body. +::: + +::: figure +:::: figure +%%^^^^ carveDivFence +Nested is a generic container. +:::: +:::