@markup-carve/carve-grammars 0.1.9 → 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.
- package/README.md +66 -723
- package/highlightjs/carve.js +67 -15
- package/package.json +6 -4
- package/prism/carve.js +105 -8
- package/shiki/index.js +6 -6
- package/shiki/table-tokens.css +32 -0
- package/textmate/carve.tmLanguage.json +7 -2
- package/tiptap/carve-kit.js +22 -0
- package/tiptap/carve-to-pm.js +72 -1
- package/tiptap/editor.css +25 -0
- package/tiptap/extensions/carve-block-extension.js +41 -0
- package/tiptap/extensions/carve-comment.js +7 -2
- package/tiptap/extensions/carve-directive.js +36 -0
- package/tiptap/extensions/carve-ruby.js +49 -0
- package/tiptap/extensions/carve-small-caps.js +42 -0
- package/tiptap/extensions/carve-source-preservation.js +1 -0
- package/tiptap/extensions/index.js +4 -0
- package/tiptap/schema-map.json +60 -9
- package/tiptap/serializer.js +151 -30
package/README.md
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
# Carve Grammars
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Editor integration and syntax-highlighting grammars for the
|
|
4
|
+
[Carve](https://github.com/markup-carve/carve) markup language.
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
[
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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,34 +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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`@markup-carve/carve-grammars/tiptap` loads `CarveKit`, which imports several
|
|
37
|
-
standalone Tiptap extensions, so the editor entry needs all of these installed.
|
|
38
|
-
Disabling one through `CarveKit.configure()` does not remove its import:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit \
|
|
42
|
-
@tiptap/extension-{bullet-list,code,code-block,hard-break,heading,highlight,image,link,list-item,ordered-list,subscript,superscript,table,table-cell,table-header,table-row,task-item,task-list,underline}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
On Tiptap 3, `CarveKit` disables StarterKit's bundled Underline and Link, since
|
|
46
|
-
it registers its own (underline carries Carve's `_text_` mapping). Pass
|
|
47
|
-
`starterKit: { underline: true }` to opt back in, at the cost of a duplicate
|
|
48
|
-
mark name.
|
|
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).
|
|
49
30
|
|
|
50
|
-
##
|
|
31
|
+
## Tiptap
|
|
51
32
|
|
|
52
33
|
```js
|
|
53
34
|
import { Editor } from '@tiptap/core'
|
|
54
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
CarveKit,
|
|
37
|
+
carveToProseMirror,
|
|
38
|
+
serializeToCarve,
|
|
39
|
+
} from '@markup-carve/carve-grammars/tiptap'
|
|
55
40
|
|
|
56
41
|
const editor = new Editor({
|
|
57
42
|
element: document.getElementById('editor'),
|
|
58
43
|
extensions: [CarveKit],
|
|
44
|
+
content: carveToProseMirror(source, { unsupported: 'preserve' }),
|
|
59
45
|
onUpdate: ({ editor }) => {
|
|
60
46
|
const carve = serializeToCarve(editor.getJSON())
|
|
61
47
|
console.log(carve)
|
|
@@ -63,725 +49,82 @@ const editor = new Editor({
|
|
|
63
49
|
})
|
|
64
50
|
```
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
import StarterKit from '@tiptap/starter-kit'
|
|
70
|
-
import { CarveInsert, CarveDelete, CarveDiv, serializeToCarve } from '@markup-carve/carve-grammars/tiptap'
|
|
71
|
-
|
|
72
|
-
const editor = new Editor({
|
|
73
|
-
extensions: [StarterKit, CarveInsert, CarveDelete, CarveDiv],
|
|
74
|
-
})
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Mark mapping
|
|
78
|
-
|
|
79
|
-
| Tiptap mark | Carve token | Renders as |
|
|
80
|
-
|-------------|-------------|------------|
|
|
81
|
-
| bold | `*text*` / `{*text*}` | `<strong>` |
|
|
82
|
-
| italic | `/text/` / `{/text/}` | `<em>` |
|
|
83
|
-
| underline | `_text_` / `{_text_}` | `<u>` |
|
|
84
|
-
| code | `` `text` `` | `<code>` |
|
|
85
|
-
| highlight | `=text=` / `{=text=}` | `<mark>` |
|
|
86
|
-
| strike | `~text~` / `{~text~}` | `<s>` |
|
|
87
|
-
| subscript | `{,text,}` (braced only) | `<sub>` |
|
|
88
|
-
| superscript | `{^text^}` (braced only) | `<sup>` |
|
|
89
|
-
| insert | `{+text+}` | `<ins>` |
|
|
90
|
-
| delete | `{-text-}` | `<del>` |
|
|
91
|
-
| link | `[text](url)` / `[text](url "title")` | `<a>` |
|
|
92
|
-
| image | `` / `` | `<img>` |
|
|
93
|
-
| span | `[text]{.class}` | `<span class>` |
|
|
94
|
-
| abbreviation | `[text]{abbr="..."}` | `<abbr title>` \*\*\* |
|
|
95
|
-
|
|
96
|
-
\*\*\* `[text]{abbr="..."}` renders a real `<abbr title>` only when carve's
|
|
97
|
-
`SemanticSpanExtension` is enabled (the same opt-in extension also maps `{kbd}`
|
|
98
|
-
-> `<kbd>`, `{dfn}` -> `<dfn>`, `{samp}` -> `<samp>`, `{var}` -> `<var>`).
|
|
99
|
-
Without it, the attribute stays literal: `<span abbr="...">`. The mark's
|
|
100
|
-
`parseHTML` reads back the `<abbr title>` form.
|
|
101
|
-
|
|
102
|
-
The tokens target carve-php's **parser** (the contract: serialized Carve must parse
|
|
103
|
-
back to the same elements). Carve's inline syntax differs from Djot's:
|
|
104
|
-
emphasis is `/text/` (Djot uses `_`), `_text_` is underline, `~text~` is
|
|
105
|
-
strikethrough, highlight is `=text=`, and subscript/superscript are the
|
|
106
|
-
braced `{,text,}` / `{^text^}` only (a bare `,` or `^` is literal text since
|
|
107
|
-
carve #259).
|
|
108
|
-
|
|
109
|
-
Each single-char delimiter has two equivalent forms: a **bare** form
|
|
110
|
-
(`=text=`) and a **forced brace** form (`{=text=}`) that also works intraword;
|
|
111
|
-
both parse to the same element. The two columns above list bare / forced.
|
|
112
|
-
`serializeToCarve` emits the bare form for `* / _ ~`, or the forced form where a
|
|
113
|
-
letter or digit touches the delimiter, and the forced `{…}` form
|
|
114
|
-
for `= , ^` (round-trip-safe: those delimiters are likelier to be inert bare);
|
|
115
|
-
`{+…+}` / `{-…-}` (insert / delete) have only the brace form, since `+` / `-`
|
|
116
|
-
are not emphasis delimiters.
|
|
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.
|
|
117
55
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
To honor that round-trip contract, `serializeToCarve` escapes literal Carve
|
|
121
|
-
syntax in plain text so it parses back as text rather than markup - inline code,
|
|
122
|
-
links, footnotes, CriticMarkup, mentions/tags/emoji, and an emphasis delimiter
|
|
123
|
-
appearing inside its own span. Escaping is **contextual**: Carve's flanking rules
|
|
124
|
-
already make most lone delimiters inert (`price * 2`, intraword `x_1`,
|
|
125
|
-
`comma,, two`, `C:\path`, `a@b.com`), so those stay clean. The same logic is
|
|
126
|
-
exposed as `escapeCarve(text)`.
|
|
127
|
-
|
|
128
|
-
## Block elements
|
|
129
|
-
|
|
130
|
-
Headings (`#`), bullet / ordered / task lists, blockquotes (`>`), fenced code
|
|
131
|
-
blocks (`` ``` lang ``), horizontal rules (`---`), tables (with `|=` header
|
|
132
|
-
cells and `^` / `<` row / column spans), container divs (`::: class`), and
|
|
133
|
-
definition lists.
|
|
134
|
-
|
|
135
|
-
## Loading Carve Into Tiptap
|
|
136
|
-
|
|
137
|
-
Use the AST loader when opening Carve source in an editor. It parses Carve with
|
|
138
|
-
`@markup-carve/carve` and builds the ProseMirror JSON shape consumed by
|
|
139
|
-
`CarveKit`, avoiding the lossy HTML pivot where attributes disappear unless a
|
|
140
|
-
Tiptap extension happens to claim them during `parseHTML`.
|
|
56
|
+
Individual extensions are also exported for hosts that do not want
|
|
57
|
+
`CarveKit`:
|
|
141
58
|
|
|
142
59
|
```js
|
|
60
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
143
61
|
import {
|
|
144
|
-
|
|
145
|
-
|
|
62
|
+
CarveDelete,
|
|
63
|
+
CarveDiv,
|
|
64
|
+
CarveInsert,
|
|
146
65
|
serializeToCarve,
|
|
147
66
|
} from '@markup-carve/carve-grammars/tiptap'
|
|
148
67
|
|
|
149
|
-
const content = carveToProseMirror(source, { unsupported: 'preserve' })
|
|
150
|
-
|
|
151
68
|
const editor = new Editor({
|
|
152
|
-
extensions: [
|
|
153
|
-
content,
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
const saved = serializeToCarve(editor.getJSON())
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Entry points:
|
|
160
|
-
|
|
161
|
-
- `carveToProseMirror(source, options?)` parses Carve source and returns a
|
|
162
|
-
ProseMirror `doc`.
|
|
163
|
-
- `astToProseMirror(ast, options?)` converts an already parsed Carve
|
|
164
|
-
`document` AST.
|
|
165
|
-
|
|
166
|
-
Unsupported handling:
|
|
167
|
-
|
|
168
|
-
- `unsupported: 'throw'` is the default. The loader throws `UnsupportedNodeError`
|
|
169
|
-
instead of silently dropping content.
|
|
170
|
-
- `unsupported: 'preserve'` builds the richest available document and verifies
|
|
171
|
-
its canonical serialization against the parsed AST. When authored columns,
|
|
172
|
-
delimiter choices, blank ownership, or other source layout cannot be held in
|
|
173
|
-
ProseMirror attributes, the document carries both the authored source and its
|
|
174
|
-
canonical projection. `serializeToCarve` performs a three-way merge after an
|
|
175
|
-
edit, preserving untouched authored layout while giving changed content the
|
|
176
|
-
canonical Carve spelling.
|
|
177
|
-
|
|
178
|
-
All 1,538 documents and 440 categories in the pinned corpus are load/save
|
|
179
|
-
lossless in preservation mode, with no whole-document fallback. Abbreviation
|
|
180
|
-
definitions and uses, figures and captions, advanced tables, comments, raw
|
|
181
|
-
passthrough, references, and footnotes all have structured editor mappings.
|
|
182
|
-
`carveToProseMirrorWithReport()` identifies any future construct that still has
|
|
183
|
-
to use a local opaque atom; `tiptap/schema-map.json` is the public mapping
|
|
184
|
-
authority.
|
|
185
|
-
|
|
186
|
-
## Tab sets and code groups in the editor
|
|
187
|
-
|
|
188
|
-
A tab set and a code group are the same thing to a reader - a strip of labels,
|
|
189
|
-
one panel visible. In the editor they were not. A `:::: tabs` container had a
|
|
190
|
-
bar that could only switch panels; a `:::: code-group` had no bar at all, so it
|
|
191
|
-
rendered as a plain vertical stack of code blocks with its `[one.js]` labels
|
|
192
|
-
invisible and no way to tell it apart from two adjacent code blocks. Neither
|
|
193
|
-
could be edited as a widget: adding, removing, renaming or reordering a panel
|
|
194
|
-
meant leaving the visual editor and editing source.
|
|
195
|
-
|
|
196
|
-
Both now render an interactive bar:
|
|
197
|
-
|
|
198
|
-
| Action | How |
|
|
199
|
-
| --- | --- |
|
|
200
|
-
| switch panel | click a label |
|
|
201
|
-
| rename | double-click a label, then Enter (Escape abandons) |
|
|
202
|
-
| add | `+` |
|
|
203
|
-
| remove | `×` - refuses on the last panel |
|
|
204
|
-
| reorder | `‹` / `›` |
|
|
205
|
-
|
|
206
|
-
**Switching dispatches nothing.** It sets `data-active` on the wrapper and the
|
|
207
|
-
stylesheet does the rest, so moving between tabs never marks the document dirty
|
|
208
|
-
or reaches the serializer. The other four change the document and are undoable
|
|
209
|
-
like any other edit.
|
|
210
|
-
|
|
211
|
-
**The stylesheet is required, not decoration.** Because switching is only an
|
|
212
|
-
attribute, without these rules every panel is visible at once and clicking a
|
|
213
|
-
label appears to do nothing:
|
|
214
|
-
|
|
215
|
-
```js
|
|
216
|
-
import '@markup-carve/carve-grammars/tiptap/editor.css'
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
It reads carve-css custom properties when they are present and falls back to
|
|
220
|
-
literals otherwise, so it composes with that package without depending on it.
|
|
221
|
-
|
|
222
|
-
Two things worth knowing:
|
|
223
|
-
|
|
224
|
-
- **A code group is still a plain `carveDiv`.** It arrives as a div with
|
|
225
|
-
`class: "code-group"` whose children carry their own `carveLabel`, and giving
|
|
226
|
-
it dedicated node types to mirror the tab-set shape would change what the
|
|
227
|
-
serializer sees for a change that is entirely about presentation. So the bar
|
|
228
|
-
attaches to the existing node: the document shape, the serializer and the
|
|
229
|
-
round trip are untouched. The cost is that the nodeView is called for every
|
|
230
|
-
div, so every other kind - admonitions, figures, plain containers - is handed
|
|
231
|
-
straight back to the schema's own `toDOM`.
|
|
232
|
-
- **A code group mounted from HTML shows languages, not labels.** carve-js's
|
|
233
|
-
HTML for a code group emits bare `<pre>` children and drops the per-block
|
|
234
|
-
`[label]`, so `one.js` is not recoverable from that seed and the bar falls
|
|
235
|
-
back to `js`. Mounted from the AST (`carveToProseMirror`) the labels survive
|
|
236
|
-
and are shown. Renaming writes `carveLabel` and never touches the language, so
|
|
237
|
-
changing a tab's caption cannot silently restyle the code.
|
|
238
|
-
|
|
239
|
-
Disable the code-group bar with `CarveKit.configure({ carveCodeGroup: false })`,
|
|
240
|
-
or keep it read-only with
|
|
241
|
-
`CarveKit.configure({ carveCodeGroup: { editable: false } })`.
|
|
242
|
-
|
|
243
|
-
## Framework-independent editor element
|
|
244
|
-
|
|
245
|
-
Applications that do not otherwise use Tiptap can mount the same lossless
|
|
246
|
-
bridge through a Web Component:
|
|
247
|
-
|
|
248
|
-
```js
|
|
249
|
-
import { defineCarveEditor } from '@markup-carve/carve-grammars/editor'
|
|
250
|
-
|
|
251
|
-
defineCarveEditor()
|
|
252
|
-
const editor = document.querySelector('carve-editor')
|
|
253
|
-
editor.value = '# Hello'
|
|
254
|
-
editor.addEventListener('input', event => save(event.detail.value))
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
```html
|
|
258
|
-
<carve-editor></carve-editor>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
The element exposes a string `value`, emits bubbling and composed `input`
|
|
262
|
-
events, and uses `unsupported: 'preserve'` internally. Its editable surface is
|
|
263
|
-
available as the `editor` CSS part (`carve-editor::part(editor)`). Tiptap stays
|
|
264
|
-
an implementation detail of the element, although its peer packages must be
|
|
265
|
-
installed with `carve-grammars`.
|
|
266
|
-
|
|
267
|
-
Document front matter appears as a collapsed **Document metadata** card. Expand
|
|
268
|
-
it to edit the common `title`, `lang`, `author`, and `description` fields, or use
|
|
269
|
-
the raw YAML/TOML field for custom metadata. Both paths update the document
|
|
270
|
-
through the editor, so they participate in undo/redo and preserve unknown keys.
|
|
271
|
-
|
|
272
|
-
### Quick fields
|
|
273
|
-
|
|
274
|
-
The four quick fields are a default, not the contract. A product whose front
|
|
275
|
-
matter means something else describes its own:
|
|
276
|
-
|
|
277
|
-
```js
|
|
278
|
-
CarveKit.configure({
|
|
279
|
-
carveFrontmatter: {
|
|
280
|
-
fields: [
|
|
281
|
-
{ key: 'title', label: 'Note title', placeholder: 'Optional title', inputAttributes: { required: true, maxlength: 120 } },
|
|
282
|
-
{ key: 'summary', label: 'Summary', multiline: true },
|
|
283
|
-
],
|
|
284
|
-
},
|
|
69
|
+
extensions: [StarterKit, CarveInsert, CarveDelete, CarveDiv],
|
|
285
70
|
})
|
|
286
71
|
```
|
|
287
72
|
|
|
288
|
-
A descriptor needs a `key` - the front matter key it reads and writes. `label`
|
|
289
|
-
defaults to the capitalized key, `placeholder` is optional, and `multiline`
|
|
290
|
-
picks a `textarea` instead of an `input`. The list replaces the default one
|
|
291
|
-
entirely, so an empty array renders the raw front matter editor with no quick
|
|
292
|
-
fields at all:
|
|
293
|
-
|
|
294
|
-
```js
|
|
295
|
-
CarveKit.configure({ carveFrontmatter: { fields: [] } })
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
A key may appear only once; a second descriptor for it is refused, because the
|
|
299
|
-
controls are held by key and the earlier one would render blank and write
|
|
300
|
-
nowhere.
|
|
301
|
-
|
|
302
|
-
`inputAttributes` carries native validation and input hints: `required`,
|
|
303
|
-
`autocomplete`, `inputmode`, `minlength`, `maxlength`, `pattern` and
|
|
304
|
-
`aria-describedby`. These are enforced: a change that fails the control's own
|
|
305
|
-
`checkValidity()` is reported to the author and never reaches the document.
|
|
306
|
-
Anything else is **refused with a `TypeError`** rather than ignored - the node view owns `name`, `type`, `value`, `disabled` and `readonly`,
|
|
307
|
-
and an event handler set here would bypass the format-aware update path. A
|
|
308
|
-
constraint that is silently dropped reads like a working one that never fires,
|
|
309
|
-
which is why the rejection is loud.
|
|
310
|
-
|
|
311
|
-
The collapsed summary still reads `title` and `lang` out of the document
|
|
312
|
-
whatever the field list says, and writes still go through an editor transaction,
|
|
313
|
-
so undo/redo and unknown-key preservation are unchanged.
|
|
314
|
-
Unsupported-source atoms use the same compact/editable pattern for their exact
|
|
315
|
-
Carve payload. Inline footnotes, cross-references, and citations open target
|
|
316
|
-
pickers, while abbreviation and link-reference definitions use collapsible
|
|
317
|
-
forms. Import `@markup-carve/carve-grammars/tiptap/editor.css` when mounting
|
|
318
|
-
`CarveKit` directly to receive the associated editor chrome and table styling.
|
|
319
|
-
|
|
320
73
|
## Syntax highlighting
|
|
321
74
|
|
|
322
|
-
Render Carve source as highlighted HTML on the web. Both grammars cover the full
|
|
323
|
-
Carve token set: headings, lists, tables, blockquotes, fenced/raw blocks,
|
|
324
|
-
container divs, front matter and comments, plus inline emphasis
|
|
325
|
-
(`*bold*` `/italic/` `_underline_` `~strike~` `=highlight=`, braced
|
|
326
|
-
`{^sup^}` `{,sub,}`),
|
|
327
|
-
code, links, images, spans, attributes, footnotes, math (`` $`x` ``),
|
|
328
|
-
CriticMarkup (`{+ins+}` `{-del-}`), mentions, tags and emoji.
|
|
329
|
-
|
|
330
|
-
### Where the three grammars deliberately differ
|
|
331
|
-
|
|
332
|
-
The TextMate grammar is stricter than the Prism and highlight.js grammars about
|
|
333
|
-
**indented block openers at document level**, and that difference is a decision
|
|
334
|
-
rather than drift.
|
|
335
|
-
|
|
336
|
-
Carve opens a block at column 0, or at an enclosing container's content column -
|
|
337
|
-
nowhere in between. So at document level these are all ordinary paragraphs:
|
|
338
|
-
|
|
339
|
-
````
|
|
340
|
-
# H
|
|
341
|
-
> q
|
|
342
|
-
*[HTML]: HyperText
|
|
343
|
-
```js
|
|
344
|
-
x
|
|
345
|
-
```
|
|
346
|
-
````
|
|
347
|
-
|
|
348
|
-
while the same four openers at a list item's content column are real blocks:
|
|
349
|
-
|
|
350
|
-
````
|
|
351
|
-
- item
|
|
352
|
-
|
|
353
|
-
# H
|
|
354
|
-
|
|
355
|
-
> quoted
|
|
356
|
-
|
|
357
|
-
```js
|
|
358
|
-
x
|
|
359
|
-
```
|
|
360
|
-
````
|
|
361
|
-
|
|
362
|
-
Telling those two apart needs block context. Only the TextMate grammar has it:
|
|
363
|
-
its list-item rules track the item's actual content column, so a document-level
|
|
364
|
-
rule can be anchored at column 0 while an `_in_container` twin stays permissive
|
|
365
|
-
and is reachable only from inside a container. Its `heading`, `fenced_code`,
|
|
366
|
-
`blockquote` and `abbreviation` rules are therefore anchored at column 0, and
|
|
367
|
-
`heading_in_container`, `fenced_code_in_container`, `blockquote_in_container`
|
|
368
|
-
and `abbreviation_in_container` carry the indented forms.
|
|
369
|
-
|
|
370
|
-
Prism and highlight.js are line-based and have no container model, so they
|
|
371
|
-
cannot make that distinction. Anchoring their block rules at column 0 would not
|
|
372
|
-
buy accuracy - it would stop highlighting **every** legitimately indented
|
|
373
|
-
construct inside a list item or a block quote, which is a common valid shape,
|
|
374
|
-
in exchange for correcting a rare invalid one. So both keep their `^[ \t]*`
|
|
375
|
-
anchors and knowingly over-colour the indented-at-document-level case.
|
|
376
|
-
|
|
377
|
-
The practical consequence: a document that indents a heading, fence, blockquote
|
|
378
|
-
or abbreviation definition by one or two columns at top level is highlighted by
|
|
379
|
-
Prism and highlight.js and left as plain text by the TextMate grammar (Shiki,
|
|
380
|
-
VS Code). The TextMate answer is the one that agrees with the engines.
|
|
381
|
-
|
|
382
|
-
`tests/lib/constructs.js` is the shared construct inventory all three sweeps
|
|
383
|
-
read, and the same asymmetry is written down there as `skip` entries on the
|
|
384
|
-
column-sensitive cases; the TextMate-only column cases live in the `NEGATIVE`
|
|
385
|
-
list in `tests/textmate-sweep-test.js`.
|
|
386
|
-
|
|
387
|
-
### One rule, three spellings: a leading byte order mark
|
|
388
|
-
|
|
389
|
-
A byte order mark at the **start of a document** is not content. The spec says
|
|
390
|
-
so ("Line endings and a byte order mark"), and carve-js, carve-rs and carve-php
|
|
391
|
-
all strip it before the block scanner runs. It is neither a space nor a tab, so
|
|
392
|
-
without an explicit allowance it sits between the line start and the marker and
|
|
393
|
-
defeats every line-anchored opener - a mark in front of a heading left the title
|
|
394
|
-
unscoped, and a mark in front of a fence handed the line to the inline code rule
|
|
395
|
-
instead.
|
|
396
|
-
|
|
397
|
-
All three grammars now allow it, and the restriction to the document's start is
|
|
398
|
-
load-bearing rather than pedantry. A mark anywhere else is an ordinary
|
|
399
|
-
zero-width character that opens nothing:
|
|
400
|
-
|
|
401
|
-
```
|
|
402
|
-
# T
|
|
403
|
-
|
|
404
|
-
<a byte order mark here>- item
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
renders as a paragraph holding literal text in carve-rs and in carve-php, and as
|
|
408
|
-
a list only in carve-js, whose own `\s` class is Unicode White_Space plus U+FEFF
|
|
409
|
-
(markup-carve/carve#806). Every rule here anchors with `^` under a multiline
|
|
410
|
-
flag, which matches at *every* line start, so the allowance has to carry its own
|
|
411
|
-
document-start assertion - and the three grammars do not share one:
|
|
412
|
-
|
|
413
|
-
| grammar | spelling | mechanism |
|
|
414
|
-
| --- | --- | --- |
|
|
415
|
-
| prism | `(?:(?<![\s\S])\uFEFF)?` | JavaScript lookbehind: nothing precedes offset 0 |
|
|
416
|
-
| highlightjs | `(?:(?<![\s\S])\uFEFF)?` | the same, and it survives highlight.js compilation |
|
|
417
|
-
| textmate | `(?:\A\x{FEFF})?` | Oniguruma `\A`, which vscode-textmate resolves against the first line only |
|
|
418
|
-
|
|
419
|
-
The codepoint is always written as an escape. No file in this repo holds a
|
|
420
|
-
literal byte order mark: it is invisible, and an editor or a normalizing filter
|
|
421
|
-
can drop the one character a rule is about. The spec corpus is the exception and
|
|
422
|
-
can afford to be - it marks `tests/corpus/**` as `-text`, so
|
|
423
|
-
`250-line-endings-and-a-byte-order-mark-3.crv` really does begin `ef bb bf`.
|
|
424
|
-
|
|
425
|
-
### Link destinations inside a bare run
|
|
426
|
-
|
|
427
|
-
A delimiter inside a link destination, its title or an autolink does not close
|
|
428
|
-
the bare run around it, so `/see [x](http://a.b/c) now/` stays one italic run.
|
|
429
|
-
The three grammars follow the spec's productions for what counts: a `](` needs a
|
|
430
|
-
complete label before it (`link_text`), a destination and a title take only
|
|
431
|
-
their own escapes, a title follows exactly one space (`link_title`), a URL
|
|
432
|
-
autolink holds only `url_char`, and an email autolink needs a dotted domain
|
|
433
|
-
ending in letters (`email_autolink`). Anything else closes the run where the
|
|
434
|
-
spec closes it.
|
|
435
|
-
|
|
436
|
-
Known limits, shared by all three unless noted:
|
|
437
|
-
|
|
438
|
-
- A label nested more than four brackets deep, or a destination holding three
|
|
439
|
-
levels of parentheses, is not recognized. A regex cannot count.
|
|
440
|
-
- Prism and highlight.js compile without the `u` flag, so an email autolink
|
|
441
|
-
accepts any non-ASCII character other than whitespace where the spec asks for
|
|
442
|
-
a letter. The TextMate grammar uses `\p{L}`.
|
|
443
|
-
|
|
444
|
-
### Substitutions
|
|
445
|
-
|
|
446
|
-
A substitution splits at its first arrow outside code, math, a literal, a
|
|
447
|
-
comment or an escape, and each half holds inline content. With no such arrow,
|
|
448
|
-
`{~ ~}` is a forced strikethrough.
|
|
449
|
-
|
|
450
|
-
Known limit: in highlight.js and TextMate, a link or autolink that spans the
|
|
451
|
-
arrow stays whole in the deleted half. `{~[x](u~>v)~>c~}` splits after `u`.
|
|
452
|
-
|
|
453
|
-
### Fence words
|
|
454
|
-
|
|
455
|
-
All three surfaces answer `carve` and `crv`. `.crv` is the canonical file
|
|
456
|
-
extension, so a ` ```crv ` fence highlights wherever a ` ```carve ` one does,
|
|
457
|
-
whichever highlighter a site runs.
|
|
458
|
-
|
|
459
|
-
| Surface | Answers | Extra |
|
|
460
|
-
|---|---|---|
|
|
461
|
-
| Prism | `carve`, `crv` | `carvemd`, the embedded form |
|
|
462
|
-
| highlight.js | `carve`, `crv` | any casing: `getLanguage` lowercases its argument |
|
|
463
|
-
| Shiki | `carve`, `crv` | `Carve`, because Shiki matches a name by exact string |
|
|
464
|
-
|
|
465
|
-
The extras differ because the lookups do. Shiki is the only surface where a
|
|
466
|
-
capitalized spelling is a distinct alias worth listing; Prism keys must be
|
|
467
|
-
lowercase, since `Prism.util.getLanguage` lowercases the `language-xxx` class
|
|
468
|
-
before resolving it. `tests/lib/aliases.js` holds the required set and
|
|
469
|
-
`tests/alias-parity-test.js` asserts it on each surface through that surface's
|
|
470
|
-
own registration API.
|
|
471
|
-
|
|
472
|
-
### Attribute blocks
|
|
473
|
-
|
|
474
|
-
Known limit, shared by all three: a block glued to a bare delimiter is read as
|
|
475
|
-
attached even where the delimiter closes no run. In `` x*{title="`"} y `` the
|
|
476
|
-
braces are text, so the backtick opens a code span, but all three scope an
|
|
477
|
-
attribute block.
|
|
478
|
-
|
|
479
|
-
### TextMate limits
|
|
480
|
-
|
|
481
|
-
A TextMate rule sees one line at a time, so a few shapes color differently from
|
|
482
|
-
how Carve reads them. vscode-carve declares the same three.
|
|
483
|
-
|
|
484
|
-
- A bare bold run can cross a soft line break, so its opener can't check for a
|
|
485
|
-
closer first. When the only closer-shaped `*` sits inside a code span, or
|
|
486
|
-
there is none, the run colors to the end of its paragraph:
|
|
487
|
-
`` x *a `b* c` d `` renders as text but shows as bold.
|
|
488
|
-
- A braced span whose closer is on a later line is not scoped as a span.
|
|
489
|
-
- A bare italic, underline, strikethrough or highlight run is scoped only when
|
|
490
|
-
its closer is on the same line.
|
|
491
|
-
|
|
492
|
-
### Prism and highlight.js limits
|
|
493
|
-
|
|
494
|
-
- Prism scopes a bare run only when its closer is on the same line.
|
|
495
|
-
- highlight.js compiles without the `u` flag, so it reads any non-ASCII
|
|
496
|
-
character after a bare closer as a letter. `/a/« b` renders `a` in italics
|
|
497
|
-
but shows as text.
|
|
498
|
-
|
|
499
|
-
### Prism
|
|
500
|
-
|
|
501
|
-
The grammar registers itself against the global `Prism`, so `Prism` must be
|
|
502
|
-
global before the grammar module runs. Because static `import` statements are
|
|
503
|
-
hoisted (they all evaluate before any top-level assignment), load the grammar
|
|
504
|
-
with a dynamic `import` after assigning `globalThis.Prism`:
|
|
505
|
-
|
|
506
75
|
```js
|
|
507
76
|
import Prism from 'prismjs'
|
|
508
77
|
|
|
509
|
-
globalThis.Prism = Prism
|
|
510
|
-
await import('@markup-carve/carve-grammars/prism/carve.js')
|
|
511
|
-
|
|
512
|
-
const html = Prism.highlight(source, Prism.languages.carve, 'carve')
|
|
78
|
+
globalThis.Prism = Prism
|
|
79
|
+
await import('@markup-carve/carve-grammars/prism/carve.js')
|
|
513
80
|
```
|
|
514
81
|
|
|
515
|
-
In the browser, load `prismjs` first (it sets the global `Prism`), then load
|
|
516
|
-
`@markup-carve/carve-grammars/prism/carve.js`.
|
|
517
|
-
|
|
518
|
-
### highlight.js
|
|
519
|
-
|
|
520
82
|
```js
|
|
521
|
-
import hljs from 'highlight.js'
|
|
83
|
+
import hljs from 'highlight.js/lib/core'
|
|
522
84
|
import carve from '@markup-carve/carve-grammars/highlightjs/carve.js'
|
|
523
85
|
|
|
524
86
|
hljs.registerLanguage('carve', carve)
|
|
525
|
-
const { value } = hljs.highlight(source, { language: 'carve' })
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
Loaded as a classic `<script>` after highlight.js, it self-registers against
|
|
529
|
-
the global `hljs`:
|
|
530
|
-
|
|
531
|
-
```html
|
|
532
|
-
<script src="highlight.min.js"></script>
|
|
533
|
-
<script src="node_modules/@markup-carve/carve-grammars/highlightjs/carve.js"></script>
|
|
534
|
-
<script>hljs.highlightAll();</script>
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
### Shiki / VitePress
|
|
538
|
-
|
|
539
|
-
`@markup-carve/carve-grammars/shiki` is the shared kit every Carve docs site uses, so
|
|
540
|
-
highlighting stays identical across them: the TextMate grammar, GitHub
|
|
541
|
-
light/dark themes extended with Carve scope colors, and a transformer + CSS
|
|
542
|
-
pair that bridges what Shiki's HTML emitter cannot express (strikethrough,
|
|
543
|
-
sub/superscript positioning, highlight background).
|
|
544
|
-
|
|
545
|
-
```ts
|
|
546
|
-
// .vitepress/config.ts
|
|
547
|
-
import { defineConfig } from 'vitepress'
|
|
548
|
-
import { carveMarkdown } from '@markup-carve/carve-grammars/shiki'
|
|
549
|
-
|
|
550
|
-
export default defineConfig({
|
|
551
|
-
markdown: {
|
|
552
|
-
...carveMarkdown(),
|
|
553
|
-
// carveMarkdown({ light, dark, languages }) to override base themes
|
|
554
|
-
// or register extra grammars
|
|
555
|
-
},
|
|
556
|
-
})
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
```ts
|
|
560
|
-
// .vitepress/theme/index.ts
|
|
561
|
-
import '@markup-carve/carve-grammars/shiki/carve.css'
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
Named exports for other setups: `carveGrammar`, `carveLightExtras` /
|
|
565
|
-
`carveDarkExtras`, `carveLightTheme` / `carveDarkTheme`, `extendTheme`,
|
|
566
|
-
`carveStylingTransformer`.
|
|
567
|
-
|
|
568
|
-
#### Diff presentation with an underlying language
|
|
569
|
-
|
|
570
|
-
Carve keeps a code block's presentation hooks separate from its language. The
|
|
571
|
-
portable source convention for an instructional diff is a `.diff` block
|
|
572
|
-
attribute above a language-tagged fence:
|
|
573
|
-
|
|
574
|
-
````carve
|
|
575
|
-
{.diff}
|
|
576
|
-
```js
|
|
577
|
-
- fileIcon.classList.add("icon-file-text");
|
|
578
|
-
+ fileIcon.classList.remove("icon-file-text");
|
|
579
|
-
```
|
|
580
|
-
````
|
|
581
|
-
|
|
582
|
-
Core HTML preserves both channels as
|
|
583
|
-
`<pre class="diff"><code class="language-js">`. A host can detect that shape
|
|
584
|
-
and invoke the opt-in `diffCodeTransformer()` with the fence language:
|
|
585
|
-
|
|
586
|
-
```js
|
|
587
|
-
import { diffCodeTransformer } from '@markup-carve/carve-grammars/shiki/diff'
|
|
588
|
-
import '@markup-carve/carve-grammars/shiki/carve.css'
|
|
589
|
-
|
|
590
|
-
const transformers = pre.classList.contains('diff')
|
|
591
|
-
? [diffCodeTransformer()]
|
|
592
|
-
: []
|
|
593
|
-
|
|
594
|
-
const highlighted = highlighter.codeToHtml(code.textContent, {
|
|
595
|
-
lang: 'javascript',
|
|
596
|
-
theme: 'github-light',
|
|
597
|
-
transformers,
|
|
598
|
-
})
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
Create a fresh transformer for every code block. It treats `+`, `-`, and a
|
|
602
|
-
space as structural first characters, removes that character while Shiki
|
|
603
|
-
tokenizes the underlying language, then restores it and marks added/removed
|
|
604
|
-
lines. This first version targets compact instructional changes, not complete
|
|
605
|
-
patch files with `@@` hunks or `---` / `+++` file headers. It is deliberately
|
|
606
|
-
not enabled by `carveMarkdown()`, because ordinary code blocks must not lose
|
|
607
|
-
their first character.
|
|
608
|
-
|
|
609
|
-
A host that highlights with **highlight.js, Prism, or nothing** - where Shiki's
|
|
610
|
-
per-line token model is not available - uses the highlighter-agnostic helper
|
|
611
|
-
instead. It takes a per-line highlight callback (or defaults to HTML-escaping)
|
|
612
|
-
and produces the same `line` / `diff add` / `diff remove` / `diff-marker`
|
|
613
|
-
classes:
|
|
614
|
-
|
|
615
|
-
```js
|
|
616
|
-
import { applyLanguageDiff } from '@markup-carve/carve-grammars/diff'
|
|
617
|
-
import '@markup-carve/carve-grammars/diff/carve-diff.css'
|
|
618
|
-
|
|
619
|
-
document.querySelectorAll('pre.diff > code').forEach((code) => {
|
|
620
|
-
const language = [...code.classList]
|
|
621
|
-
.find((name) => name.startsWith('language-'))
|
|
622
|
-
?.slice('language-'.length)
|
|
623
|
-
applyLanguageDiff(code, (body) => hljs.highlight(body, { language }).value)
|
|
624
|
-
})
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
`renderLanguageDiff(code, highlightLine)` returns the HTML string if the host
|
|
628
|
-
manages the DOM itself (a webview building a document string, a server writing
|
|
629
|
-
markup). The marker is stripped before `highlightLine` runs, so the underlying
|
|
630
|
-
language tokenizes the line body without its `+`/`-`/space.
|
|
631
|
-
|
|
632
|
-
## Diagram rendering
|
|
633
|
-
|
|
634
|
-
Carve's `FencedRenderExtension` presets emit a `<pre class="LANG">source</pre>`
|
|
635
|
-
hydration element; something on the client turns it into a diagram. Mermaid,
|
|
636
|
-
WaveDrom, Vega-Lite and Chart each render once **you** load their browser
|
|
637
|
-
library. For the rest, `@markup-carve/carve-grammars/diagrams` ships renderers:
|
|
638
|
-
|
|
639
|
-
| Type | Renderer | Engine | Network |
|
|
640
|
-
|------|----------|--------|---------|
|
|
641
|
-
| `graphviz` (`dot`) | `renderGraphvizDiagrams` | `@viz-js/viz` (WASM) | **offline** |
|
|
642
|
-
| `d2` | `renderD2Diagrams` | `@terrastruct/d2` (WASM) | **offline** |
|
|
643
|
-
| `plantuml` (`puml`) | `renderKrokiDiagrams` | a Kroki server | **network** |
|
|
644
|
-
|
|
645
|
-
Graphviz and D2 render **entirely in the browser** - no server, no external
|
|
646
|
-
call, works offline (in an IDE, behind a firewall, ...). The rendered SVG is
|
|
647
|
-
placed in an inert `<img>` data URI (like the Kroki path), so even untrusted
|
|
648
|
-
diagram source cannot run script or expose a `javascript:` link. The WASM
|
|
649
|
-
libraries are optional peer dependencies, imported lazily only when a matching
|
|
650
|
-
block is on the page:
|
|
651
|
-
|
|
652
|
-
```js
|
|
653
|
-
import { renderGraphvizDiagrams } from '@markup-carve/carve-grammars/diagrams/graphviz'
|
|
654
|
-
import { renderD2Diagrams } from '@markup-carve/carve-grammars/diagrams/d2'
|
|
655
|
-
|
|
656
|
-
await renderGraphvizDiagrams(container)
|
|
657
|
-
await renderD2Diagrams(container)
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
`renderDiagrams` runs both (and PlantUML, when you opt in) in one call; each
|
|
661
|
-
no-ops when its blocks are absent, so you pay nothing for the types not present:
|
|
662
|
-
|
|
663
|
-
```js
|
|
664
|
-
import { renderDiagrams } from '@markup-carve/carve-grammars/diagrams'
|
|
665
|
-
|
|
666
|
-
await renderDiagrams(container) // graphviz + d2, offline
|
|
667
|
-
await renderDiagrams(container, { kroki: {} }) // + PlantUML via kroki.io
|
|
668
|
-
await renderDiagrams(container, { kroki: { server: 'https://kroki.internal' } })
|
|
669
87
|
```
|
|
670
88
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
PlantUML is the one preset with no practical in-browser renderer - its only
|
|
674
|
-
pure-JS build is a multi-megabyte JVM-in-WASM. `renderKrokiDiagrams` renders it
|
|
675
|
-
by POSTing the source to a [Kroki](https://kroki.io) server; the returned SVG
|
|
676
|
-
rides in an `<img>` data URI (which cannot execute script). Idempotent, and
|
|
677
|
-
dependency-free (plain-text POST, no deflate/base64).
|
|
678
|
-
|
|
679
|
-
> ⚠️ **Privacy / GDPR.** The default server is the **public `https://kroki.io`**,
|
|
680
|
-
> so the diagram source is sent to a **third party outside your domain**. For
|
|
681
|
-
> anything sensitive, or to stay offline, point `server` at a **self-hosted or
|
|
682
|
-
> localhost Kroki** so no data leaves your control - and disclose the external
|
|
683
|
-
> call to end users where required. Because of this, `renderDiagrams` leaves the
|
|
684
|
-
> Kroki step **off unless you pass `kroki`**.
|
|
89
|
+
For Prism or highlight.js, load the optional table token colors after the
|
|
90
|
+
highlighter's theme stylesheet:
|
|
685
91
|
|
|
686
92
|
```js
|
|
687
|
-
import
|
|
688
|
-
|
|
689
|
-
await renderKrokiDiagrams(container, { server: 'https://kroki.internal' })
|
|
93
|
+
import '@markup-carve/carve-grammars/shiki/table-tokens.css'
|
|
690
94
|
```
|
|
691
95
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
> prefer the engine's static-render hook (carve-js `renderers.plantuml`,
|
|
698
|
-
> carve-php's own render pipeline) so the page ships finished SVG and needs no
|
|
699
|
-
> client JS at all.
|
|
700
|
-
|
|
701
|
-
## API
|
|
702
|
-
|
|
703
|
-
- `renderDiagrams(container, options?)` - render Graphviz + D2 (offline), and
|
|
704
|
-
PlantUML via Kroki when `options.kroki` is set. See
|
|
705
|
-
[Diagram rendering](#diagram-rendering).
|
|
706
|
-
- `renderGraphvizDiagrams(container, options?)` / `renderD2Diagrams(container, options?)` -
|
|
707
|
-
render `graphviz`/`d2` blocks with the offline WASM engines.
|
|
708
|
-
- `renderKrokiDiagrams(container, options?)` - render PlantUML (and any opted-in
|
|
709
|
-
type) via a Kroki server; `KROKI_DIAGRAM_TYPES` is the default class→type map.
|
|
710
|
-
- `carveToProseMirror(source, options?)` - parse Carve source and convert it to
|
|
711
|
-
ProseMirror JSON. `options.unsupported` is `'throw'` by default or
|
|
712
|
-
`'preserve'` for opaque source-preserving blocks.
|
|
713
|
-
- `astToProseMirror(ast, options?)` - convert an existing `@markup-carve/carve`
|
|
714
|
-
AST to ProseMirror JSON.
|
|
715
|
-
- `serializeToCarve(doc)` - serialize an `editor.getJSON()` document to Carve markup.
|
|
716
|
-
- `serializeToCarveWithReport(doc)` - the same, returning `{ source, dropped, degraded }`. `dropped` names what the source could not carry (a mention's display `label` that differs from its `id`), `degraded` a node written as literal text (a mention name with no Carve spelling, such as `Lea Thompson`, becomes `\@Lea Thompson`). A stock Tiptap `mention` node is written like `carveMention`: `id` is the name, a `null` label counts as absent, and `mentionSuggestionChar` is never written.
|
|
717
|
-
- `escapeCarve(text)` - contextually escape literal Carve syntax in a plain-text run so it round-trips as text (used internally by `serializeToCarve`).
|
|
718
|
-
- `CarveKit` - the bundled Tiptap extension set.
|
|
719
|
-
- Individual extensions: `CarveInsert`, `CarveDelete`, `CarveCriticComment`, `CarveDiv`, `CarveSpan`, `CarveFootnote`, `CarveFootnoteDefinition`, `CarveMath`, `CarveEmbed`, `CarveAbbreviation`, `CarveDefinitionList`, `CarveUnsupported`.
|
|
720
|
-
|
|
721
|
-
## Schema map (for other engines)
|
|
722
|
-
|
|
723
|
-
`tiptap/schema-map.json` publishes the Carve-to-ProseMirror vocabulary as data, so
|
|
724
|
-
an engine building a bridge in another language reads it instead of restating it:
|
|
725
|
-
|
|
726
|
-
```js
|
|
727
|
-
import map from '@markup-carve/carve-grammars/tiptap/schema-map.json'
|
|
728
|
-
|
|
729
|
-
map.types.strong // { kind: 'mark', pm: 'bold' }
|
|
730
|
-
map.types.list // { kind: 'node', pm: ['bulletList', 'orderedList', 'taskList'], ... }
|
|
731
|
-
map.unmapped.figure // 'figure / caption blocks are not modeled'
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
Every Carve node type appears exactly once, either in `types` with its
|
|
735
|
-
ProseMirror name(s) or in `unmapped` with the reason it has none - the negative
|
|
736
|
-
space is part of the contract, because a bridge that silently drops table
|
|
737
|
-
alignment or figure captions is worse than one that says it cannot carry them.
|
|
738
|
-
|
|
739
|
-
`tests/schema-map-test.js` keeps it honest: every ProseMirror name must exist in
|
|
740
|
-
the `CarveKit` schema with the declared node/mark kind, and every type in the
|
|
741
|
-
pinned spec vocabulary must have a decision. Types the map covers ahead of the
|
|
742
|
-
`spec/` pin are declared explicitly and must be removed once the pin catches up.
|
|
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.
|
|
743
101
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
`markCarrierNodes` (`carveEmptyMark`, the atom a mark with no content rides on).
|
|
748
|
-
Both are part of the wire - an unknown ProseMirror name is an error rather than a
|
|
749
|
-
skip - so a bridge has to read them alongside `types`.
|
|
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`.
|
|
750
105
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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.
|
|
754
110
|
|
|
755
|
-
##
|
|
111
|
+
## Diagrams
|
|
756
112
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
- **Attribute order** - a run is written back in the order it was AUTHORED, not
|
|
763
|
-
in a canonical one. ProseMirror attributes are an unordered map, so the order
|
|
764
|
-
travels as its own attribute, `carveAttrOrder`: the AST's `order` field
|
|
765
|
-
verbatim, an array whose entries are `#id`, `.class` and each key by name.
|
|
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).
|
|
766
118
|
|
|
767
|
-
|
|
768
|
-
carveToProseMirror('[x]{key=c .a #b}')
|
|
769
|
-
// the carveSpan mark carries: { id: 'b', class: 'a',
|
|
770
|
-
// carveKeyValues: { key: 'c' }, carveAttrOrder: ['key', '.class', '#id'] }
|
|
771
|
-
```
|
|
119
|
+
## Reference
|
|
772
120
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
- **Math** - `CarveMath` (inline atom) serializes to `` $`x` `` and, with
|
|
778
|
-
`display: true`, `` $$`x` ``. Math has no closing `$` sentinel (grammar.ebnf
|
|
779
|
-
PART 9 §18): the `$` / `$$` prefix opens a verbatim span and the backtick run
|
|
780
|
-
ends it, which is what keeps currency like `$5` literal.
|
|
781
|
-
- **Footnotes** - `CarveFootnote` is the inline `[^label]` reference;
|
|
782
|
-
`CarveFootnoteDefinition` is the matching body block, serialized as
|
|
783
|
-
`[^label]: body`.
|
|
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.
|
|
784
125
|
|
|
785
126
|
## Development
|
|
786
127
|
|
|
787
|
-
Contributor setup,
|
|
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.
|