@markup-carve/carve-grammars 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +67 -757
  2. package/diff/carve-diff.css +47 -0
  3. package/diff/index.d.ts +14 -0
  4. package/diff/index.js +58 -0
  5. package/highlightjs/carve.js +432 -184
  6. package/package.json +96 -6
  7. package/prism/carve.js +543 -98
  8. package/shiki/carve.css +36 -0
  9. package/shiki/diff.d.ts +9 -0
  10. package/shiki/diff.js +39 -0
  11. package/shiki/index.js +8 -6
  12. package/shiki/table-tokens.css +32 -0
  13. package/textmate/carve.tmLanguage.json +1200 -234
  14. package/tiptap/carve-editor.js +4 -1
  15. package/tiptap/carve-kit.js +96 -34
  16. package/tiptap/carve-to-pm.js +94 -11
  17. package/tiptap/editor.css +47 -1
  18. package/tiptap/extensions/carve-abbreviation.js +7 -1
  19. package/tiptap/extensions/carve-attribute-slots.js +53 -7
  20. package/tiptap/extensions/carve-block-extension.js +41 -0
  21. package/tiptap/extensions/carve-citation-definition.js +1 -1
  22. package/tiptap/extensions/carve-citation.js +6 -1
  23. package/tiptap/extensions/carve-comment.js +9 -3
  24. package/tiptap/extensions/carve-crossref.js +1 -1
  25. package/tiptap/extensions/carve-definition-list.js +3 -2
  26. package/tiptap/extensions/carve-directive.js +36 -0
  27. package/tiptap/extensions/carve-div.js +14 -10
  28. package/tiptap/extensions/carve-figure.js +3 -13
  29. package/tiptap/extensions/carve-footnote-definition.js +2 -2
  30. package/tiptap/extensions/carve-frontmatter.js +125 -12
  31. package/tiptap/extensions/carve-heading.js +2 -19
  32. package/tiptap/extensions/carve-link-ref-def.js +5 -2
  33. package/tiptap/extensions/carve-literal.js +4 -2
  34. package/tiptap/extensions/carve-math.js +2 -23
  35. package/tiptap/extensions/carve-raw-block.js +3 -1
  36. package/tiptap/extensions/carve-raw-inline.js +4 -2
  37. package/tiptap/extensions/carve-ruby.js +49 -0
  38. package/tiptap/extensions/carve-small-caps.js +42 -0
  39. package/tiptap/extensions/carve-source-preservation.js +18 -0
  40. package/tiptap/extensions/carve-substitution.js +29 -5
  41. package/tiptap/extensions/carve-symbol.js +3 -2
  42. package/tiptap/extensions/editable-atom-view.js +3 -1
  43. package/tiptap/extensions/index.js +4 -0
  44. package/tiptap/index.d.ts +7 -0
  45. package/tiptap/index.js +1 -1
  46. package/tiptap/schema-map.json +64 -12
  47. package/tiptap/serializer.js +321 -67
  48. package/tiptap/wire-fixtures.json +12 -2
package/README.md CHANGED
@@ -1,26 +1,20 @@
1
1
  # Carve Grammars
2
2
 
3
- Grammars for the [Carve](https://github.com/markup-carve/carve) markup language:
3
+ Editor integration and syntax-highlighting grammars for the
4
+ [Carve](https://github.com/markup-carve/carve) markup language.
4
5
 
5
- - a **Tiptap** integration (editor kit, Carve loader and serializer) that converts between Carve markup and Tiptap/ProseMirror JSON;
6
- - **Prism** and **highlight.js** syntax-highlighting grammars for rendering Carve source on the web;
7
- - a **TextMate** grammar (`textmate/carve.tmLanguage.json`) for TextMate-based highlighters such as Shiki (used by VitePress).
6
+ The package contains:
8
7
 
9
- Modeled on [djot-grammars](https://github.com/php-collective/djot-grammars), adapted to Carve's syntax. The Tiptap mark mapping mirrors `carve-php`'s `HtmlToCarve` converter; the highlighting grammars mirror the canonical token set in [`carve/resources/grammar.ebnf`](https://github.com/markup-carve/carve).
8
+ - a Tiptap kit, Carve loader, and serializer for Carve and ProseMirror JSON;
9
+ - Prism and highlight.js grammars for browser highlighting;
10
+ - a TextMate grammar for Shiki, VitePress, and other TextMate consumers.
10
11
 
11
- The TextMate grammar here is a **separate lineage** from the one in
12
- [vscode-carve](https://github.com/markup-carve/vscode-carve), not a copy of it. The two
13
- agree on the constructs they cover and differ in how they name scopes - this one carries
14
- 116 scope names against vscode-carve's 151, with `entity.name.tag.*` where that one uses
15
- `entity.name.type.*`, and so on. Neither is derived from the other, and a scope name
16
- present here is not a promise that the editor grammars use the same one.
17
-
18
- > **Status:** Tiptap integration, plus Prism, highlight.js and TextMate grammars.
19
- > Sibling editor grammars live in their own repos: editor-bundled **TextMate** copies in
20
- > [vscode-carve](https://github.com/markup-carve/vscode-carve) and
21
- > [intellij-carve](https://github.com/markup-carve/intellij-carve);
22
- > **Tree-sitter** in [tree-sitter-carve](https://github.com/markup-carve/tree-sitter-carve)
23
- > and [zed-carve](https://github.com/markup-carve/zed-carve).
12
+ Tree-sitter and editor-bundled grammars live in
13
+ [tree-sitter-carve](https://github.com/markup-carve/tree-sitter-carve),
14
+ [vscode-carve](https://github.com/markup-carve/vscode-carve), and the other
15
+ editor repositories. This package's TextMate grammar has a separate lineage
16
+ from the VS Code grammar. The [complete reference](docs/reference.md) explains
17
+ their scope and naming differences.
24
18
 
25
19
  ## Install
26
20
 
@@ -28,30 +22,26 @@ present here is not a promise that the editor grammars use the same one.
28
22
  npm install @markup-carve/carve-grammars
29
23
  ```
30
24
 
31
- All peer dependencies are optional - install only what you use:
32
- `@tiptap/core` + `@tiptap/starter-kit` (v2 or v3) for the editor, `prismjs` (v1)
33
- for Prism, `highlight.js` (v11) for highlight.js. CI runs the suite against both
34
- Tiptap majors.
35
-
36
- On Tiptap 3, `CarveKit` disables StarterKit's bundled Underline and Link, since
37
- it registers its own (underline carries Carve's `_text_` mapping). Pass
38
- `starterKit: { underline: true }` to opt back in, at the cost of a duplicate
39
- mark name.
40
-
41
- `CarveKit` also pulls in several standalone Tiptap marks/extensions (highlight,
42
- subscript, superscript, underline, link, image, table, task-list); install the
43
- `@tiptap/extension-*` packages you use, or disable them via
44
- `CarveKit.configure({ underline: false, ... })`.
25
+ Peer dependencies are optional. Install only those needed by the selected
26
+ entry point. For Tiptap, install `@tiptap/core`, `@tiptap/pm`,
27
+ `@tiptap/starter-kit`, and the extensions used by `CarveKit`. The package is
28
+ tested with Tiptap 2 and 3. The complete peer-dependency command is in the
29
+ [installation reference](docs/reference.md#install).
45
30
 
46
- ## Usage
31
+ ## Tiptap
47
32
 
48
33
  ```js
49
34
  import { Editor } from '@tiptap/core'
50
- import { CarveKit, serializeToCarve } from '@markup-carve/carve-grammars/tiptap'
35
+ import {
36
+ CarveKit,
37
+ carveToProseMirror,
38
+ serializeToCarve,
39
+ } from '@markup-carve/carve-grammars/tiptap'
51
40
 
52
41
  const editor = new Editor({
53
42
  element: document.getElementById('editor'),
54
43
  extensions: [CarveKit],
44
+ content: carveToProseMirror(source, { unsupported: 'preserve' }),
55
45
  onUpdate: ({ editor }) => {
56
46
  const carve = serializeToCarve(editor.getJSON())
57
47
  console.log(carve)
@@ -59,762 +49,82 @@ const editor = new Editor({
59
49
  })
60
50
  ```
61
51
 
62
- ### Individual extensions
63
-
64
- ```js
65
- import StarterKit from '@tiptap/starter-kit'
66
- import { CarveInsert, CarveDelete, CarveDiv, serializeToCarve } from '@markup-carve/carve-grammars/tiptap'
67
-
68
- const editor = new Editor({
69
- extensions: [StarterKit, CarveInsert, CarveDelete, CarveDiv],
70
- })
71
- ```
72
-
73
- ## Mark mapping
74
-
75
- | Tiptap mark | Carve token | Renders as |
76
- |-------------|-------------|------------|
77
- | bold | `*text*` / `{*text*}` | `<strong>` |
78
- | italic | `/text/` / `{/text/}` | `<em>` |
79
- | underline | `_text_` / `{_text_}` | `<u>` |
80
- | code | `` `text` `` | `<code>` |
81
- | highlight | `=text=` / `{=text=}` | `<mark>` |
82
- | strike | `~text~` / `{~text~}` | `<s>` |
83
- | subscript | `{,text,}` (braced only) | `<sub>` |
84
- | superscript | `{^text^}` (braced only) | `<sup>` |
85
- | insert | `{+text+}` | `<ins>` |
86
- | delete | `{-text-}` | `<del>` |
87
- | link | `[text](url)` / `[text](url "title")` | `<a>` |
88
- | image | `![alt](src)` / `![alt](src "title")` | `<img>` |
89
- | span | `[text]{.class}` | `<span class>` |
90
- | abbreviation | `[text]{abbr="..."}` | `<abbr title>` \*\*\* |
91
-
92
- \*\*\* `[text]{abbr="..."}` renders a real `<abbr title>` only when carve's
93
- `SemanticSpanExtension` is enabled (the same opt-in extension also maps `{kbd}`
94
- -> `<kbd>`, `{dfn}` -> `<dfn>`, `{samp}` -> `<samp>`, `{var}` -> `<var>`).
95
- Without it, the attribute stays literal: `<span abbr="...">`. The mark's
96
- `parseHTML` reads back the `<abbr title>` form.
97
-
98
- The tokens target carve-php's **parser** (the contract: serialized Carve must parse
99
- back to the same elements). Carve's inline syntax differs notably from Djot's:
100
- emphasis is `/text/` (Djot uses `_`), `_text_` is underline, `~text~` is
101
- strikethrough, highlight is `=text=`, and subscript/superscript are the
102
- braced `{,text,}` / `{^text^}` only (a bare `,` or `^` is literal text since
103
- carve #259).
104
-
105
- Each single-char delimiter has two equivalent forms: a **bare** form
106
- (`=text=`) and a **forced brace** form (`{=text=}`) that also works intraword;
107
- both parse to the same element. The two columns above list bare / forced.
108
- `serializeToCarve` emits the bare form for `* / _ ~` and the forced `{…}` form
109
- for `= , ^` (round-trip-safe — those delimiters are likelier to be inert bare);
110
- `{+…+}` / `{-…-}` (insert / delete) have only the brace form, since `+` / `-`
111
- are not emphasis delimiters.
112
-
113
- ### Escaping
114
-
115
- To honor that round-trip contract, `serializeToCarve` escapes literal Carve
116
- syntax in plain text so it parses back as text rather than markup - inline code,
117
- links, footnotes, CriticMarkup, mentions/tags/emoji, and an emphasis delimiter
118
- appearing inside its own span. Escaping is **contextual**: Carve's flanking rules
119
- already make most lone delimiters inert (`price * 2`, intraword `x_1`,
120
- `comma,, two`, `C:\path`, `a@b.com`), so those stay clean. The same logic is
121
- exposed as `escapeCarve(text)`.
52
+ `unsupported: 'throw'` is the default. Preservation mode retains authored
53
+ source details that ProseMirror cannot model directly and merges edits back
54
+ into the original Carve source.
122
55
 
123
- ## Block elements
124
-
125
- Headings (`#`), bullet / ordered / task lists, blockquotes (`>`), fenced code
126
- blocks (`` ``` lang ``), horizontal rules (`---`), tables (with `|=` header
127
- cells and `^` / `<` row / column spans), container divs (`::: class`), and
128
- definition lists.
129
-
130
- ## Loading Carve Into Tiptap
131
-
132
- Use the AST loader when opening Carve source in an editor. It parses Carve with
133
- `@markup-carve/carve` and builds the ProseMirror JSON shape consumed by
134
- `CarveKit`, avoiding the lossy HTML pivot where attributes disappear unless a
135
- Tiptap extension happens to claim them during `parseHTML`.
56
+ Individual extensions are also exported for hosts that do not want
57
+ `CarveKit`:
136
58
 
137
59
  ```js
60
+ import StarterKit from '@tiptap/starter-kit'
138
61
  import {
139
- CarveKit,
140
- carveToProseMirror,
62
+ CarveDelete,
63
+ CarveDiv,
64
+ CarveInsert,
141
65
  serializeToCarve,
142
66
  } from '@markup-carve/carve-grammars/tiptap'
143
67
 
144
- const content = carveToProseMirror(source, { unsupported: 'preserve' })
145
-
146
68
  const editor = new Editor({
147
- extensions: [CarveKit],
148
- content,
69
+ extensions: [StarterKit, CarveInsert, CarveDelete, CarveDiv],
149
70
  })
150
-
151
- const saved = serializeToCarve(editor.getJSON())
152
71
  ```
153
72
 
154
- Entry points:
155
-
156
- - `carveToProseMirror(source, options?)` parses Carve source and returns a
157
- ProseMirror `doc`.
158
- - `astToProseMirror(ast, options?)` converts an already parsed Carve
159
- `document` AST.
160
-
161
- Unsupported handling:
162
-
163
- - `unsupported: 'throw'` is the default. The loader throws `UnsupportedNodeError`
164
- instead of silently dropping content.
165
- - `unsupported: 'preserve'` builds the richest available document and verifies
166
- its canonical serialization against the parsed AST. When authored columns,
167
- delimiter choices, blank ownership, or other source layout cannot be held in
168
- ProseMirror attributes, the document carries both the authored source and its
169
- canonical projection. `serializeToCarve` performs a three-way merge after an
170
- edit, preserving untouched authored layout while giving changed content the
171
- canonical Carve spelling.
172
-
173
- All 1,538 documents and 440 categories in the pinned corpus are load/save
174
- lossless in preservation mode, with no whole-document fallback. Abbreviation
175
- definitions and uses, figures and captions, advanced tables, comments, raw
176
- passthrough, references, and footnotes all have structured editor mappings.
177
- `carveToProseMirrorWithReport()` identifies any future construct that still has
178
- to use a local opaque atom; `tiptap/schema-map.json` is the public mapping
179
- authority.
180
-
181
- ## Tab sets and code groups in the editor
182
-
183
- A tab set and a code group are the same thing to a reader - a strip of labels,
184
- one panel visible. In the editor they were not. A `:::: tabs` container had a
185
- bar that could only switch panels; a `:::: code-group` had no bar at all, so it
186
- rendered as a plain vertical stack of code blocks with its `[one.js]` labels
187
- invisible and no way to tell it apart from two adjacent code blocks. Neither
188
- could be edited as a widget: adding, removing, renaming or reordering a panel
189
- meant leaving the visual editor and editing source.
190
-
191
- Both now render an interactive bar:
192
-
193
- | Action | How |
194
- | --- | --- |
195
- | switch panel | click a label |
196
- | rename | double-click a label, then Enter (Escape abandons) |
197
- | add | `+` |
198
- | remove | `×` - refuses on the last panel |
199
- | reorder | `‹` / `›` |
200
-
201
- **Switching dispatches nothing.** It sets `data-active` on the wrapper and the
202
- stylesheet does the rest, so moving between tabs never marks the document dirty
203
- or reaches the serializer. The other four change the document and are undoable
204
- like any other edit.
205
-
206
- **The stylesheet is required, not decoration.** Because switching is only an
207
- attribute, without these rules every panel is visible at once and clicking a
208
- label appears to do nothing:
209
-
210
- ```js
211
- import '@markup-carve/carve-grammars/tiptap/editor.css'
212
- ```
213
-
214
- It reads carve-css custom properties when they are present and falls back to
215
- literals otherwise, so it composes with that package without depending on it.
216
-
217
- Two things worth knowing:
218
-
219
- - **A code group is still a plain `carveDiv`.** It arrives as a div with
220
- `class: "code-group"` whose children carry their own `carveLabel`, and giving
221
- it dedicated node types to mirror the tab-set shape would change what the
222
- serializer sees for a change that is entirely about presentation. So the bar
223
- attaches to the existing node: the document shape, the serializer and the
224
- round trip are untouched. The cost is that the nodeView is called for every
225
- div, so every other kind - admonitions, figures, plain containers - is handed
226
- straight back to the schema's own `toDOM`.
227
- - **A code group mounted from HTML shows languages, not labels.** carve-js's
228
- HTML for a code group emits bare `<pre>` children and drops the per-block
229
- `[label]`, so `one.js` is not recoverable from that seed and the bar falls
230
- back to `js`. Mounted from the AST (`carveToProseMirror`) the labels survive
231
- and are shown. Renaming writes `carveLabel` and never touches the language, so
232
- changing a tab's caption cannot silently restyle the code.
233
-
234
- Disable the code-group bar with `CarveKit.configure({ carveCodeGroup: false })`,
235
- or keep it read-only with
236
- `CarveKit.configure({ carveCodeGroup: { editable: false } })`.
237
-
238
- ## Framework-independent editor element
239
-
240
- Applications that do not otherwise use Tiptap can mount the same lossless
241
- bridge through a Web Component:
242
-
243
- ```js
244
- import { defineCarveEditor } from '@markup-carve/carve-grammars/editor'
245
-
246
- defineCarveEditor()
247
- const editor = document.querySelector('carve-editor')
248
- editor.value = '# Hello'
249
- editor.addEventListener('input', event => save(event.detail.value))
250
- ```
251
-
252
- ```html
253
- <carve-editor></carve-editor>
254
- ```
255
-
256
- The element exposes a string `value`, emits bubbling and composed `input`
257
- events, and uses `unsupported: 'preserve'` internally. Its editable surface is
258
- available as the `editor` CSS part (`carve-editor::part(editor)`). Tiptap stays
259
- an implementation detail of the element, although its peer packages must be
260
- installed with `carve-grammars`.
261
-
262
- Document front matter appears as a collapsed **Document metadata** card. Expand
263
- it to edit the common `title`, `lang`, `author`, and `description` fields, or use
264
- the raw YAML/TOML field for custom metadata. Both paths update the document
265
- through the editor, so they participate in undo/redo and preserve unknown keys.
266
- Unsupported-source atoms use the same compact/editable pattern for their exact
267
- Carve payload. Inline footnotes, cross-references, and citations open target
268
- pickers, while abbreviation and link-reference definitions use collapsible
269
- forms. Import `@markup-carve/carve-grammars/tiptap/editor.css` when mounting
270
- `CarveKit` directly to receive the associated editor chrome and table styling.
271
-
272
73
  ## Syntax highlighting
273
74
 
274
- Render Carve source as highlighted HTML on the web. Both grammars cover the full
275
- Carve token set: headings, lists, tables, blockquotes, fenced/raw blocks,
276
- container divs, front matter and comments, plus inline emphasis
277
- (`*bold*` `/italic/` `_underline_` `~strike~` `=highlight=`, braced
278
- `{^sup^}` `{,sub,}`),
279
- code, links, images, spans, attributes, footnotes, math (`` $`x` ``),
280
- CriticMarkup (`{+ins+}` `{-del-}`), mentions, tags and emoji.
281
-
282
- ### Where the three grammars deliberately differ
283
-
284
- The TextMate grammar is stricter than the Prism and highlight.js grammars about
285
- **indented block openers at document level**, and that difference is a decision
286
- rather than drift.
287
-
288
- Carve opens a block at column 0, or at an enclosing container's content column -
289
- nowhere in between. So at document level these are all ordinary paragraphs:
290
-
291
- ````
292
- # H
293
- > q
294
- *[HTML]: HyperText
295
- ```js
296
- x
297
- ```
298
- ````
299
-
300
- while the same four openers at a list item's content column are real blocks:
301
-
302
- ````
303
- - item
304
-
305
- # H
306
-
307
- > quoted
308
-
309
- ```js
310
- x
311
- ```
312
- ````
313
-
314
- Telling those two apart needs block context. Only the TextMate grammar has it:
315
- its list-item rules track the item's actual content column, so a document-level
316
- rule can be anchored at column 0 while an `_in_container` twin stays permissive
317
- and is reachable only from inside a container. Its `heading`, `fenced_code`,
318
- `blockquote` and `abbreviation` rules are therefore anchored at column 0, and
319
- `heading_in_container`, `fenced_code_in_container`, `blockquote_in_container`
320
- and `abbreviation_in_container` carry the indented forms.
321
-
322
- Prism and highlight.js are line-based and have no container model, so they
323
- cannot make that distinction. Anchoring their block rules at column 0 would not
324
- buy accuracy - it would stop highlighting **every** legitimately indented
325
- construct inside a list item or a block quote, which is a common valid shape,
326
- in exchange for correcting a rare invalid one. So both keep their `^[ \t]*`
327
- anchors and knowingly over-colour the indented-at-document-level case.
328
-
329
- The practical consequence: a document that indents a heading, fence, blockquote
330
- or abbreviation definition by one or two columns at top level is highlighted by
331
- Prism and highlight.js and left as plain text by the TextMate grammar (Shiki,
332
- VS Code). The TextMate answer is the one that agrees with the engines.
333
-
334
- `tests/lib/constructs.js` is the shared construct inventory all three sweeps
335
- read, and the same asymmetry is written down there as `skip` entries on the
336
- column-sensitive cases; the TextMate-only column cases live in the `NEGATIVE`
337
- list in `tests/textmate-sweep-test.js`.
338
-
339
- ### One rule, three spellings: a leading byte order mark
340
-
341
- A byte order mark at the **start of a document** is not content. The spec says
342
- so ("Line endings and a byte order mark"), and carve-js, carve-rs and carve-php
343
- all strip it before the block scanner runs. It is neither a space nor a tab, so
344
- without an explicit allowance it sits between the line start and the marker and
345
- defeats every line-anchored opener - a mark in front of a heading left the title
346
- unscoped, and a mark in front of a fence handed the line to the inline code rule
347
- instead.
348
-
349
- All three grammars now allow it, and the restriction to the document's start is
350
- load-bearing rather than pedantry. A mark anywhere else is an ordinary
351
- zero-width character that opens nothing:
352
-
353
- ```
354
- # T
355
-
356
- <a byte order mark here>- item
357
- ```
358
-
359
- renders as a paragraph holding literal text in carve-rs and in carve-php, and as
360
- a list only in carve-js, whose own `\s` class is Unicode White_Space plus U+FEFF
361
- (markup-carve/carve#806). Every rule here anchors with `^` under a multiline
362
- flag, which matches at *every* line start, so the allowance has to carry its own
363
- document-start assertion - and the three grammars do not share one:
364
-
365
- | grammar | spelling | mechanism |
366
- | --- | --- | --- |
367
- | prism | `(?:(?<![\s\S])\uFEFF)?` | JavaScript lookbehind: nothing precedes offset 0 |
368
- | highlightjs | `(?:(?<![\s\S])\uFEFF)?` | the same, and it survives highlight.js compilation |
369
- | textmate | `(?:\A\x{FEFF})?` | Oniguruma `\A`, which vscode-textmate resolves against the first line only |
370
-
371
- The codepoint is always written as an escape. No file in this repo holds a
372
- literal byte order mark: it is invisible, and an editor or a normalizing filter
373
- can drop the one character a rule is about. The spec corpus is the exception and
374
- can afford to be - it marks `tests/corpus/**` as `-text`, so
375
- `250-line-endings-and-a-byte-order-mark-3.crv` really does begin `ef bb bf`.
376
-
377
- ### Fence words
378
-
379
- All three surfaces answer `carve` and `crv`. `.crv` is the canonical file
380
- extension, so a ` ```crv ` fence highlights wherever a ` ```carve ` one does,
381
- whichever highlighter a site runs.
382
-
383
- | Surface | Answers | Extra |
384
- |---|---|---|
385
- | Prism | `carve`, `crv` | `carvemd`, the embedded form |
386
- | highlight.js | `carve`, `crv` | any casing: `getLanguage` lowercases its argument |
387
- | Shiki | `carve`, `crv` | `Carve`, because Shiki matches a name by exact string |
388
-
389
- The extras differ because the lookups do. Shiki is the only surface where a
390
- capitalized spelling is a distinct alias worth listing; Prism keys must be
391
- lowercase, since `Prism.util.getLanguage` lowercases the `language-xxx` class
392
- before resolving it. `tests/lib/aliases.js` holds the required set and
393
- `tests/alias-parity-test.js` asserts it on each surface through that surface's
394
- own registration API.
395
-
396
- ### Prism
397
-
398
- The grammar registers itself against the global `Prism`, so `Prism` must be
399
- global before the grammar module runs. Because static `import` statements are
400
- hoisted (they all evaluate before any top-level assignment), load the grammar
401
- with a dynamic `import` after assigning `globalThis.Prism`:
402
-
403
75
  ```js
404
76
  import Prism from 'prismjs'
405
77
 
406
- globalThis.Prism = Prism // grammar reads the global Prism
407
- await import('@markup-carve/carve-grammars/prism/carve.js') // registers Prism.languages.carve
408
-
409
- const html = Prism.highlight(source, Prism.languages.carve, 'carve')
78
+ globalThis.Prism = Prism
79
+ await import('@markup-carve/carve-grammars/prism/carve.js')
410
80
  ```
411
81
 
412
- In the browser, load `prismjs` first (it sets the global `Prism`), then load
413
- `@markup-carve/carve-grammars/prism/carve.js`.
414
-
415
- ### highlight.js
416
-
417
82
  ```js
418
- import hljs from 'highlight.js'
83
+ import hljs from 'highlight.js/lib/core'
419
84
  import carve from '@markup-carve/carve-grammars/highlightjs/carve.js'
420
85
 
421
86
  hljs.registerLanguage('carve', carve)
422
- const { value } = hljs.highlight(source, { language: 'carve' })
423
- ```
424
-
425
- Loaded as a classic `<script>` after highlight.js, it self-registers against
426
- the global `hljs`:
427
-
428
- ```html
429
- <script src="highlight.min.js"></script>
430
- <script src="node_modules/@markup-carve/carve-grammars/highlightjs/carve.js"></script>
431
- <script>hljs.highlightAll();</script>
432
- ```
433
-
434
- ### Shiki / VitePress
435
-
436
- `@markup-carve/carve-grammars/shiki` is the shared kit every Carve docs site uses, so
437
- highlighting stays identical across them: the TextMate grammar, GitHub
438
- light/dark themes extended with Carve scope colors, and a transformer + CSS
439
- pair that bridges what Shiki's HTML emitter cannot express (strikethrough,
440
- sub/superscript positioning, highlight background).
441
-
442
- ```ts
443
- // .vitepress/config.ts
444
- import { defineConfig } from 'vitepress'
445
- import { carveMarkdown } from '@markup-carve/carve-grammars/shiki'
446
-
447
- export default defineConfig({
448
- markdown: {
449
- ...carveMarkdown(),
450
- // carveMarkdown({ light, dark, languages }) to override base themes
451
- // or register extra grammars
452
- },
453
- })
454
- ```
455
-
456
- ```ts
457
- // .vitepress/theme/index.ts
458
- import '@markup-carve/carve-grammars/shiki/carve.css'
459
- ```
460
-
461
- Named exports for other setups: `carveGrammar`, `carveLightExtras` /
462
- `carveDarkExtras`, `carveLightTheme` / `carveDarkTheme`, `extendTheme`,
463
- `carveStylingTransformer`.
464
-
465
- ## Diagram rendering
466
-
467
- Carve's `FencedRenderExtension` presets emit a `<pre class="LANG">source</pre>`
468
- hydration element; something on the client turns it into a diagram. Mermaid,
469
- WaveDrom, Vega-Lite and Chart each render once **you** load their browser
470
- library. For the rest, `@markup-carve/carve-grammars/diagrams` ships renderers:
471
-
472
- | Type | Renderer | Engine | Network |
473
- |------|----------|--------|---------|
474
- | `graphviz` (`dot`) | `renderGraphvizDiagrams` | `@viz-js/viz` (WASM) | **offline** |
475
- | `d2` | `renderD2Diagrams` | `@terrastruct/d2` (WASM) | **offline** |
476
- | `plantuml` (`puml`) | `renderKrokiDiagrams` | a Kroki server | **network** |
477
-
478
- Graphviz and D2 render **entirely in the browser** - no server, no external
479
- call, works offline (in an IDE, behind a firewall, ...). The rendered SVG is
480
- placed in an inert `<img>` data URI (like the Kroki path), so even untrusted
481
- diagram source cannot run script or expose a `javascript:` link. The WASM
482
- libraries are optional peer dependencies, imported lazily only when a matching
483
- block is on the page:
484
-
485
- ```js
486
- import { renderGraphvizDiagrams } from '@markup-carve/carve-grammars/diagrams/graphviz'
487
- import { renderD2Diagrams } from '@markup-carve/carve-grammars/diagrams/d2'
488
-
489
- await renderGraphvizDiagrams(container)
490
- await renderD2Diagrams(container)
491
- ```
492
-
493
- `renderDiagrams` runs both (and PlantUML, when you opt in) in one call; each
494
- no-ops when its blocks are absent, so you pay nothing for the types not present:
495
-
496
- ```js
497
- import { renderDiagrams } from '@markup-carve/carve-grammars/diagrams'
498
-
499
- await renderDiagrams(container) // graphviz + d2, offline
500
- await renderDiagrams(container, { kroki: {} }) // + PlantUML via kroki.io
501
- await renderDiagrams(container, { kroki: { server: 'https://kroki.internal' } })
502
- ```
503
-
504
- ### PlantUML (Kroki)
505
-
506
- PlantUML is the one preset with no practical in-browser renderer - its only
507
- pure-JS build is a multi-megabyte JVM-in-WASM. `renderKrokiDiagrams` renders it
508
- by POSTing the source to a [Kroki](https://kroki.io) server; the returned SVG
509
- rides in an `<img>` data URI (which cannot execute script). Idempotent, and
510
- dependency-free (plain-text POST, no deflate/base64).
511
-
512
- > ⚠️ **Privacy / GDPR.** The default server is the **public `https://kroki.io`**,
513
- > so the diagram source is sent to a **third party outside your domain**. For
514
- > anything sensitive, or to stay offline, point `server` at a **self-hosted or
515
- > localhost Kroki** so no data leaves your control - and disclose the external
516
- > call to end users where required. Because of this, `renderDiagrams` leaves the
517
- > Kroki step **off unless you pass `kroki`**.
518
-
519
- ```js
520
- import { renderKrokiDiagrams } from '@markup-carve/carve-grammars/diagrams/kroki'
521
-
522
- await renderKrokiDiagrams(container, { server: 'https://kroki.internal' })
523
87
  ```
524
88
 
525
- Options: `server` (default `https://kroki.io`), `types` (class → Kroki-type map,
526
- default `KROKI_DIAGRAM_TYPES` = `plantuml`/`puml` only; extend it to Kroki-render
527
- graphviz/d2 against a self-hosted server), `onError`, `fetch`.
528
-
529
- > When the diagram is rendered at build time (SSG) rather than in the browser,
530
- > prefer the engine's static-render hook (carve-js `renderers.plantuml`,
531
- > carve-php's own render pipeline) so the page ships finished SVG and needs no
532
- > client JS at all.
533
-
534
- ## API
535
-
536
- - `renderDiagrams(container, options?)` - render Graphviz + D2 (offline), and
537
- PlantUML via Kroki when `options.kroki` is set. See
538
- [Diagram rendering](#diagram-rendering).
539
- - `renderGraphvizDiagrams(container, options?)` / `renderD2Diagrams(container, options?)` -
540
- render `graphviz`/`d2` blocks with the offline WASM engines.
541
- - `renderKrokiDiagrams(container, options?)` - render PlantUML (and any opted-in
542
- type) via a Kroki server; `KROKI_DIAGRAM_TYPES` is the default class→type map.
543
- - `carveToProseMirror(source, options?)` - parse Carve source and convert it to
544
- ProseMirror JSON. `options.unsupported` is `'throw'` by default or
545
- `'preserve'` for opaque source-preserving blocks.
546
- - `astToProseMirror(ast, options?)` - convert an existing `@markup-carve/carve`
547
- AST to ProseMirror JSON.
548
- - `serializeToCarve(doc)` - serialize an `editor.getJSON()` document to Carve markup.
549
- - `escapeCarve(text)` - contextually escape literal Carve syntax in a plain-text run so it round-trips as text (used internally by `serializeToCarve`).
550
- - `CarveKit` - the bundled Tiptap extension set.
551
- - Individual extensions: `CarveInsert`, `CarveDelete`, `CarveCriticComment`, `CarveDiv`, `CarveSpan`, `CarveFootnote`, `CarveFootnoteDefinition`, `CarveMath`, `CarveEmbed`, `CarveAbbreviation`, `CarveDefinitionList`, `CarveUnsupported`.
552
-
553
- ## Schema map (for other engines)
554
-
555
- `tiptap/schema-map.json` publishes the Carve-to-ProseMirror vocabulary as data, so
556
- an engine building a bridge in another language reads it instead of restating it:
89
+ For Prism or highlight.js, load the optional table token colors after the
90
+ highlighter's theme stylesheet:
557
91
 
558
92
  ```js
559
- import map from '@markup-carve/carve-grammars/tiptap/schema-map.json'
560
-
561
- map.types.strong // { kind: 'mark', pm: 'bold' }
562
- map.types.list // { kind: 'node', pm: ['bulletList', 'orderedList', 'taskList'], ... }
563
- map.unmapped.figure // 'figure / caption blocks are not modeled'
93
+ import '@markup-carve/carve-grammars/shiki/table-tokens.css'
564
94
  ```
565
95
 
566
- Every Carve node type appears exactly once, either in `types` with its
567
- ProseMirror name(s) or in `unmapped` with the reason it has none - the negative
568
- space is part of the contract, because a bridge that silently drops table
569
- alignment or figure captions is worse than one that says it cannot carry them.
96
+ Plain table pipes use a muted border color; header and span markers use a
97
+ stronger operator color. Shiki's included light and dark themes carry the same
98
+ palette without this stylesheet.
99
+ For a dark Prism or highlight.js theme, set `.dark` or
100
+ `data-theme="dark"` on an ancestor.
570
101
 
571
- `tests/schema-map-test.js` keeps it honest: every ProseMirror name must exist in
572
- the `CarveKit` schema with the declared node/mark kind, and every type in the
573
- pinned spec vocabulary must have a decision. Types the map covers ahead of the
574
- `spec/` pin are declared explicitly and must be removed once the pin catches up.
102
+ TextMate consumers can load `textmate/carve.tmLanguage.json`. Shiki and
103
+ VitePress users can call `carveMarkdown()` from
104
+ `@markup-carve/carve-grammars/shiki`; it registers both `carve` and `crv`.
575
105
 
576
- Two sections are keyed by ProseMirror name rather than by Carve type, because
577
- neither names a Carve construct: `preservationNodes` (`carveUnsupported` and
578
- `carveUnsupportedInline`, the atoms holding a construct's exact source) and
579
- `markCarrierNodes` (`carveEmptyMark`, the atom a mark with no content rides on).
580
- Both are part of the wire - an unknown ProseMirror name is an error rather than a
581
- skip - so a bridge has to read them alongside `types`.
106
+ The implementations deliberately differ where their host engines have
107
+ different capabilities. The [syntax-highlighting reference](docs/reference.md#syntax-highlighting)
108
+ covers substitutions, attributes, fence words, byte order marks, and engine
109
+ limits.
582
110
 
583
- Restating this mapping per engine is what the spec's own node-vocabulary test was
584
- written to prevent: carve-php once emitted `citation-group` while every other
585
- implementation spelled it with underscores.
111
+ ## Diagrams
586
112
 
587
- ## Attributes, math and footnotes
113
+ The `/diagrams` entry point renders Graphviz and D2 offline. PlantUML rendering
114
+ is available through an opt-in Kroki renderer, which sends diagram source to
115
+ the configured Kroki service. Mermaid, WaveDrom, Vega-Lite, and Chart blocks
116
+ use libraries loaded by the host page. Configuration and security details are
117
+ in the [diagram reference](docs/reference.md#diagram-rendering).
588
118
 
589
- - **Attributes** - spans, headings and images serialize an `id` and `class`
590
- (and any extra non-structural attrs) as a `{#id .class key="val"}` block, e.g.
591
- `[text]{#me .note}`, `![alt](src){.wide}`. Inline attrs trail their target;
592
- block attrs (headings) sit on the **preceding** line (strict djot), e.g.
593
- `{#slug}` then `# Title`.
594
- - **Attribute order** - a run is written back in the order it was AUTHORED, not
595
- in a canonical one. ProseMirror attributes are an unordered map, so the order
596
- travels as its own attribute, `carveAttrOrder`: the AST's `order` field
597
- verbatim, an array whose entries are `#id`, `.class` and each key by name.
119
+ ## Reference
598
120
 
599
- ```js
600
- carveToProseMirror('[x]{key=c .a #b}')
601
- // the carveSpan mark carries: { id: 'b', class: 'a',
602
- // carveKeyValues: { key: 'c' }, carveAttrOrder: ['key', '.class', '#id'] }
603
- ```
604
-
605
- All of a node's classes stay contiguous at the position of the first one,
606
- which is what the AST records. A document with no `carveAttrOrder` - anything
607
- an editor builds from scratch - writes the canonical `#id .class key="val"`
608
- order, and a slot the order names but the document no longer has is skipped.
609
- - **Math** - `CarveMath` (inline atom) serializes to `` $`x` `` and, with
610
- `display: true`, `` $$`x` ``. Math has no closing `$` sentinel (grammar.ebnf
611
- PART 9 §18): the `$` / `$$` prefix opens a verbatim span and the backtick run
612
- ends it, which is what keeps currency like `$5` literal.
613
- - **Footnotes** - `CarveFootnote` is the inline `[^label]` reference;
614
- `CarveFootnoteDefinition` is the matching body block, serialized as
615
- `[^label]: body`.
616
-
617
- ## Tests
618
-
619
- ```bash
620
- npm test
621
- ```
622
-
623
- The suite holds all three grammars to one source of truth: the shared corpus
624
- from the [`markup-carve/carve`](https://github.com/markup-carve/carve) spec,
625
- vendored as the `spec/` git submodule (`git submodule update --init`).
626
-
627
- - `npm run test:coverage` - the coverage matrix. Each grammar (prism,
628
- highlightjs, tiptap) declares a covered-category set and a skip set (with a
629
- reason per skip); the test fails if the two do not partition every corpus
630
- category, so a new spec category forces a deliberate decision.
631
- - `npm run test:snapshot` - golden token snapshots. Each covered `.crv` is
632
- tokenized with Prism's and highlight.js's own tokenizers and the token stream
633
- (type + text) is compared against a committed golden in `tests/snapshots/`.
634
- Refresh intended changes with `npm run snapshots:update`.
635
- - `npm run test:roundtrip` - the Tiptap serializer round-trip. Each covered
636
- `.crv` runs `parse -> ProseMirror JSON -> serializeToCarve -> parse` and the
637
- two parsed ASTs must be identical, catching serializer drift. Categories the
638
- serializer cannot represent are skipped with a reason.
639
-
640
- `npm test` runs all of the above plus the structural grammar and serializer
641
- unit tests. CI runs the same on Node 18, 20 and 22.
642
-
643
- ### The construct ledger: ten surfaces, one derived list
644
-
645
- Carve's syntax lives on ten grammar surfaces - `tree-sitter-carve`, the four
646
- here (TextMate, Prism, highlight.js, Tiptap), `vscode-carve`, `intellij-carve`,
647
- `sublime-carve`, `vim-carve` and `emacs-carve` - and nothing used to measure
648
- them against the same construct list. `{%` comments landed on five of them and
649
- had no rule at all on the other five, with nothing going red
650
- (markup-carve/carve#1239, #284).
651
-
652
- `npm test` now runs `tests/construct-ledger-test.js`, which holds
653
- `tests/lib/construct-ledger.json` to three rules:
654
-
655
- - **The construct list is derived, never written.** `scripts/spec-constructs.mjs`
656
- reads the alternatives of the `block` and `inline_element` productions out of
657
- the spec's normative `spec/resources/grammar.ebnf`. A clause that adds a
658
- construct makes every surface unclassified until someone says what it did
659
- about it. A hand-maintained checklist would go stale in exactly that case,
660
- because whoever forgot the grammar rule also forgot the checklist row.
661
- - **Three columns, not two.** Each construct on each surface is `IMPLEMENTED`
662
- (with the name that surface gives it), `UNSUPPORTED` (with a reason - an empty
663
- reason fails), or `GAP` (with a ticket). A construct in none of them fails the
664
- test. Without the third column a legitimate gap - a Prism tokenizer cannot do
665
- what a tree-sitter grammar does - reads as a defect, and a check with a high
666
- noise floor gets muted.
667
- - **Two axes per construct.** Recognizing a construct and keeping its payload
668
- inert are different questions, and a surface can be right about the first and
669
- wrong about the second: on four editor surfaces `{% *not bold* %}` colored a
670
- bold run inside a comment. Every entry declares which, and the answer is
671
- measured on every run - by tokenizing a sample with a live marker inside the
672
- payload - for Prism, highlight.js and every TextMate grammar whose checkout is
673
- in front of the seeder, and by converting that sample through the bridge for
674
- Tiptap. Those rows cannot rot. The rest are recorded, which is what
675
- `payload: "unmeasured"` says out loud - a state no cell is in any more,
676
- since intellij-carve's thirteen were the last of them (#329).
677
-
678
- One sample per construct is a re-measurement, not a measurement.
679
- `tests/opaque-payload-test.js` generates every payload up to three characters
680
- over each construct's own delimiter alphabet - about ten thousand documents -
681
- and it is what finds this class: of 1325 corpus documents exactly ONE exposed
682
- the fenced-code leak in #309, and a comment leaking only when it spans a line
683
- break is invisible to a single-line sample (#320).
684
-
685
- #### A red row is a question, not a defect
686
-
687
- Three surfaces have now been worked row by row, and the ratio is stable enough
688
- to plan against rather than be surprised by:
689
-
690
- | pass | rows | the instrument | genuinely absent |
691
- | --- | --- | --- | --- |
692
- | `tree-sitter-carve` (#245) | 14 | 9 | 3 |
693
- | `vim-carve` + `sublime-carve` (#318) | 20 | 11 | 6 |
694
- | `textmate`, `prism`, `tiptap`, `vscode-carve` (#307, #308, #310) | 47 | 31 | 9 |
695
-
696
- **Two rows in three are a name the probe cannot reach**, and the third pass also
697
- turned up six rows that read `IMPLEMENTED` and were not. So the first job on a
698
- per-surface ticket is deciding which rows are real, and the answer is expected
699
- to be "most of them are not". A GAP row means *the probe did not find a rule it
700
- recognizes*, which is a different claim from *this surface does not implement
701
- the construct*.
702
-
703
- A fold belongs in `SIGNATURE_OVERRIDES`, which the file calls a per-surface
704
- NAMING table - never in a rule renamed on a surface to satisfy the probe. Where
705
- a surface genuinely cannot express a construct, `UNSUPPORTED` **with a reason**
706
- is the entry; a wrong rule is worse than a missing one.
707
-
708
- #### The instrument can be wrong in the direction that looks green
709
-
710
- Five ways the probe has mis-read a surface, each found by opening the file:
711
-
712
- 1. **It read too little.** `Object.keys(grammar.json.rules)` is 196 of
713
- tree-sitter's 346 names; a `.sublime-syntax` scopes a capture as well as a
714
- match (#315).
715
- 2. **The signature list had holes** - `code_span` is `verbatim` on one surface
716
- and `code_inline` on another (#315, #307).
717
- 3. **One rule implements several constructs**, and one name cannot carry four
718
- through a shared table (#318).
719
- 4. **The evidence named a rule about a different construct.** `carveCommentInline`
720
- is the braced comment and was cited for the trailing one (#318). The same
721
- pair beat the word-boundary rank on the TextMate family, where both
722
- candidates are whole-name hits and *length* picked the wrong one (#307).
723
- 5. **The re-check read the file's prose as evidence.** `every IMPLEMENTED row
724
- cites a name the shipped grammar really carries` asked whether the evidence
725
- was a SUBSTRING of the source. `prism/carve.js` carries the comment "Prism
726
- has no cross-reference token at all" beside the lookbehind that worked
727
- around the absence - so a row citing `cross-ref` re-checked **green** for as
728
- long as the rule was missing (#307). That is the trap the docblock at the
729
- top of `scripts/surface-probe.mjs` describes, and that its extractors were
730
- built to avoid, surviving in the check that verifies their output. It reads
731
- `vocabulary()` now, so the seed and the re-check agree on what a name is.
732
-
733
- The lesson generalizes past this file: **a check that treats a file's prose as
734
- evidence that the file implements what the prose says it does NOT implement
735
- cannot fail.** Match against what a grammar DECLARES, never against its text.
736
-
737
- Re-measure after changing a grammar:
738
-
739
- ```bash
740
- node scripts/seed-construct-ledger.mjs
741
- ```
742
-
743
- That reads the four surfaces here directly. The six in other repositories are
744
- read from checkouts named by environment variables, and any that is not given
745
- keeps the row already recorded, with the commit it was read at:
746
-
747
- ```bash
748
- CARVE_SURFACE_VIM_CARVE=../vim-carve \
749
- CARVE_SURFACE_EMACS_CARVE=../emacs-carve \
750
- node scripts/seed-construct-ledger.mjs
751
- ```
752
-
753
- Statuses are measured; reasons, per-construct notes, tickets and a surface's
754
- `note` are written by hand and carried across a re-measurement, so a re-run
755
- never drops a stated reason. A surface-level `note` is for what the rows cannot
756
- say - why a surface needed no work to reach zero, or what its measured payload
757
- column does NOT cover.
758
-
759
- One thing a re-run does NOT carry: the payload axis of a surface the seeder
760
- cannot tokenize. It is only carried over when the recorded value is already
761
- `inert` or `leaks`, so a construct that moves from `GAP` to `IMPLEMENTED` in the
762
- same run arrives with `payload: "unmeasured"` and a ticket, even when the person
763
- doing the run has just measured it. That is deliberate: a seeder cannot tokenize
764
- a Vim syntax file or an emacs font-lock table, so the alternative is inventing an
765
- answer. Measure the payload as part of the same pass and write `inert` (or
766
- `leaks`, with a note) by hand on those rows.
767
-
768
- It applies to fewer surfaces than it used to. `vscode-carve` and
769
- `intellij-carve` are TextMate grammars, and the seeder loads any of those through
770
- Shiki, so naming their checkout measures the recognition axis and the payload
771
- sample in the same run:
772
-
773
- ```bash
774
- CARVE_SURFACE_VSCODE_CARVE=../vscode-carve node scripts/seed-construct-ledger.mjs
775
- CARVE_SURFACE_INTELLIJ_CARVE=../intellij-carve node scripts/seed-construct-ledger.mjs
776
- ```
777
-
778
- **A `leaks` cell is still hand-written on those surfaces, and the seeder carries
779
- it rather than overwriting it.** The seeder tokenizes ONE sample per construct
780
- and the sweep below generates hundreds, and on all three surfaces measured for
781
- the first time since #320 the sample said every row was inert while the sweep
782
- disagreed - five rows on intellij-carve alone (#329). So a run whose sample says
783
- `inert` leaves a recorded `leaks` alone. That cannot hide a fix: the sweep
784
- asserts every recorded leak STILL leaks, so a fix fails that file, its entry
785
- comes out, and the ledger's own orphan check then fails until the cell is
786
- corrected too.
787
-
788
- ### The payload sweep reaches nine of the ten surfaces
789
-
790
- `tests/opaque-payload-test.js` is the second axis measured over a GENERATED space
791
- rather than one sample per construct, and one sample is not enough. Every surface
792
- measured for the first time so far has had leaks the sample could not see:
793
-
794
- | surface | leaking rows | the sample found |
795
- | --- | --- | --- |
796
- | tree-sitter-carve (#328) | 3 | none of them |
797
- | emacs-carve (#328) | 2 | the `%%%` fence, not the verbatim run |
798
- | intellij-carve (#329) | 4 | none of them |
799
-
800
- Name a checkout and the sweep drives it:
801
-
802
- ```bash
803
- CARVE_SURFACE_TREE_SITTER_CARVE=../tree-sitter-carve \
804
- CARVE_SURFACE_EMACS_CARVE=../emacs-carve \
805
- CARVE_SURFACE_VSCODE_CARVE=../vscode-carve \
806
- CARVE_SURFACE_INTELLIJ_CARVE=../intellij-carve \
807
- node tests/opaque-payload-test.js
808
- ```
121
+ The [complete reference](docs/reference.md) covers the Carve-to-Tiptap mapping,
122
+ loading and preservation behavior, tab sets, code groups, and the
123
+ framework-independent `<carve-editor>` element. It also documents highlighting
124
+ compatibility, diagram configuration, API exports, and the schema map.
809
125
 
810
- tree-sitter needs its native addon built (`npm install` in that checkout), and
811
- emacs-carve needs an `emacs` on PATH - the mode is fontified in one batch Emacs
812
- per sweep row, which is what `prime` on that tokenizer is for. A surface whose
813
- checkout is not named, or not built, is simply not swept.
126
+ ## Development
814
127
 
815
- A leak on a grammar in ANOTHER repository is a defect this suite can measure and
816
- not fix, so it is recorded in `KNOWN_LEAKS` with the ticket it lives on and
817
- asserted to STILL leak. A fix therefore fails this file and the entry comes out
818
- with the fix, the same arrangement the residual table at the end of it uses for
819
- one document at a time. A surface in THIS repository is deliberately absent from
820
- that table: a leak here is fixable here, so it stays red.
128
+ Contributor setup, tests, and maintenance commands are in the
129
+ [development guide](docs/development.md). The [September 30 audit](https://github.com/markup-carve/carve-grammars/blob/main/docs/spec-engine-audit-20260930.md)
130
+ records current editor behavior and remaining highlighting gaps.