@markup-carve/carve-grammars 0.1.4 → 0.1.6
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/LICENSE +1 -0
- package/README.md +278 -12
- package/highlightjs/carve.js +497 -35
- package/highlightjs/carve.mjs +2 -2
- package/package.json +8 -5
- package/prism/carve.js +425 -37
- package/shiki/index.js +13 -4
- package/textmate/carve.tmLanguage.json +79 -25
- package/tiptap/carve-kit.js +15 -2
- package/tiptap/carve-to-pm.js +76 -8
- package/tiptap/editor.css +193 -0
- package/tiptap/extensions/carve-abbreviation-definition.js +22 -0
- package/tiptap/extensions/carve-abbreviation.js +3 -0
- package/tiptap/extensions/carve-code-group.js +141 -0
- package/tiptap/extensions/carve-critic-comment.js +1 -1
- package/tiptap/extensions/carve-definition-list.js +8 -0
- package/tiptap/extensions/carve-delete.js +1 -1
- package/tiptap/extensions/carve-div.js +1 -1
- package/tiptap/extensions/carve-embed.js +1 -1
- package/tiptap/extensions/carve-footnote-definition.js +1 -1
- package/tiptap/extensions/carve-footnote.js +1 -1
- package/tiptap/extensions/carve-insert.js +1 -1
- package/tiptap/extensions/carve-math.js +1 -1
- package/tiptap/extensions/carve-source-preservation.js +5 -4
- package/tiptap/extensions/carve-span.js +1 -1
- package/tiptap/extensions/carve-tabs.js +45 -61
- package/tiptap/extensions/index.js +2 -0
- package/tiptap/extensions/panel-bar.js +296 -0
- package/tiptap/index.d.ts +2 -0
- package/tiptap/index.js +5 -3
- package/tiptap/schema-map.json +15 -2
- package/tiptap/serializer.js +190 -19
- package/tiptap/wire-fixtures.json +3 -23
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -6,7 +6,14 @@ Grammars for the [Carve](https://github.com/markup-carve/carve) markup language:
|
|
|
6
6
|
- **Prism** and **highlight.js** syntax-highlighting grammars for rendering Carve source on the web;
|
|
7
7
|
- a **TextMate** grammar (`textmate/carve.tmLanguage.json`) for TextMate-based highlighters such as Shiki (used by VitePress).
|
|
8
8
|
|
|
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)
|
|
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).
|
|
10
|
+
|
|
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.
|
|
10
17
|
|
|
11
18
|
> **Status:** Tiptap integration, plus Prism, highlight.js and TextMate grammars.
|
|
12
19
|
> Sibling editor grammars live in their own repos: editor-bundled **TextMate** copies in
|
|
@@ -155,17 +162,78 @@ Unsupported handling:
|
|
|
155
162
|
|
|
156
163
|
- `unsupported: 'throw'` is the default. The loader throws `UnsupportedNodeError`
|
|
157
164
|
instead of silently dropping content.
|
|
158
|
-
- `unsupported: 'preserve'`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
the
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 } })`.
|
|
169
237
|
|
|
170
238
|
## Framework-independent editor element
|
|
171
239
|
|
|
@@ -296,6 +364,25 @@ can drop the one character a rule is about. The spec corpus is the exception and
|
|
|
296
364
|
can afford to be - it marks `tests/corpus/**` as `-text`, so
|
|
297
365
|
`250-line-endings-and-a-byte-order-mark-3.crv` really does begin `ef bb bf`.
|
|
298
366
|
|
|
367
|
+
### Fence words
|
|
368
|
+
|
|
369
|
+
All three surfaces answer `carve` and `crv`. `.crv` is the canonical file
|
|
370
|
+
extension, so a ` ```crv ` fence highlights wherever a ` ```carve ` one does,
|
|
371
|
+
whichever highlighter a site runs.
|
|
372
|
+
|
|
373
|
+
| Surface | Answers | Extra |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| Prism | `carve`, `crv` | `carvemd`, the embedded form |
|
|
376
|
+
| highlight.js | `carve`, `crv` | any casing: `getLanguage` lowercases its argument |
|
|
377
|
+
| Shiki | `carve`, `crv` | `Carve`, because Shiki matches a name by exact string |
|
|
378
|
+
|
|
379
|
+
The extras differ because the lookups do. Shiki is the only surface where a
|
|
380
|
+
capitalized spelling is a distinct alias worth listing; Prism keys must be
|
|
381
|
+
lowercase, since `Prism.util.getLanguage` lowercases the `language-xxx` class
|
|
382
|
+
before resolving it. `tests/lib/aliases.js` holds the required set and
|
|
383
|
+
`tests/alias-parity-test.js` asserts it on each surface through that surface's
|
|
384
|
+
own registration API.
|
|
385
|
+
|
|
299
386
|
### Prism
|
|
300
387
|
|
|
301
388
|
The grammar registers itself against the global `Prism`, so `Prism` must be
|
|
@@ -542,3 +629,182 @@ vendored as the `spec/` git submodule (`git submodule update --init`).
|
|
|
542
629
|
|
|
543
630
|
`npm test` runs all of the above plus the structural grammar and serializer
|
|
544
631
|
unit tests. CI runs the same on Node 18, 20 and 22.
|
|
632
|
+
|
|
633
|
+
### The construct ledger: ten surfaces, one derived list
|
|
634
|
+
|
|
635
|
+
Carve's syntax lives on ten grammar surfaces - `tree-sitter-carve`, the four
|
|
636
|
+
here (TextMate, Prism, highlight.js, Tiptap), `vscode-carve`, `intellij-carve`,
|
|
637
|
+
`sublime-carve`, `vim-carve` and `emacs-carve` - and nothing used to measure
|
|
638
|
+
them against the same construct list. `{%` comments landed on five of them and
|
|
639
|
+
had no rule at all on the other five, with nothing going red
|
|
640
|
+
(markup-carve/carve#1239, #284).
|
|
641
|
+
|
|
642
|
+
`npm test` now runs `tests/construct-ledger-test.js`, which holds
|
|
643
|
+
`tests/lib/construct-ledger.json` to three rules:
|
|
644
|
+
|
|
645
|
+
- **The construct list is derived, never written.** `scripts/spec-constructs.mjs`
|
|
646
|
+
reads the alternatives of the `block` and `inline_element` productions out of
|
|
647
|
+
the spec's normative `spec/resources/grammar.ebnf`. A clause that adds a
|
|
648
|
+
construct makes every surface unclassified until someone says what it did
|
|
649
|
+
about it. A hand-maintained checklist would go stale in exactly that case,
|
|
650
|
+
because whoever forgot the grammar rule also forgot the checklist row.
|
|
651
|
+
- **Three columns, not two.** Each construct on each surface is `IMPLEMENTED`
|
|
652
|
+
(with the name that surface gives it), `UNSUPPORTED` (with a reason - an empty
|
|
653
|
+
reason fails), or `GAP` (with a ticket). A construct in none of them fails the
|
|
654
|
+
test. Without the third column a legitimate gap - a Prism tokenizer cannot do
|
|
655
|
+
what a tree-sitter grammar does - reads as a defect, and a check with a high
|
|
656
|
+
noise floor gets muted.
|
|
657
|
+
- **Two axes per construct.** Recognizing a construct and keeping its payload
|
|
658
|
+
inert are different questions, and a surface can be right about the first and
|
|
659
|
+
wrong about the second: on four editor surfaces `{% *not bold* %}` colored a
|
|
660
|
+
bold run inside a comment. Every entry declares which, and the answer is
|
|
661
|
+
measured on every run - by tokenizing a sample with a live marker inside the
|
|
662
|
+
payload - for Prism, highlight.js and every TextMate grammar whose checkout is
|
|
663
|
+
in front of the seeder, and by converting that sample through the bridge for
|
|
664
|
+
Tiptap. Those rows cannot rot. The rest are recorded, which is what
|
|
665
|
+
`payload: "unmeasured"` says out loud - a state no cell is in any more,
|
|
666
|
+
since intellij-carve's thirteen were the last of them (#329).
|
|
667
|
+
|
|
668
|
+
One sample per construct is a re-measurement, not a measurement.
|
|
669
|
+
`tests/opaque-payload-test.js` generates every payload up to three characters
|
|
670
|
+
over each construct's own delimiter alphabet - about ten thousand documents -
|
|
671
|
+
and it is what finds this class: of 1325 corpus documents exactly ONE exposed
|
|
672
|
+
the fenced-code leak in #309, and a comment leaking only when it spans a line
|
|
673
|
+
break is invisible to a single-line sample (#320).
|
|
674
|
+
|
|
675
|
+
#### A red row is a question, not a defect
|
|
676
|
+
|
|
677
|
+
Three surfaces have now been worked row by row, and the ratio is stable enough
|
|
678
|
+
to plan against rather than be surprised by:
|
|
679
|
+
|
|
680
|
+
| pass | rows | the instrument | genuinely absent |
|
|
681
|
+
| --- | --- | --- | --- |
|
|
682
|
+
| `tree-sitter-carve` (#245) | 14 | 9 | 3 |
|
|
683
|
+
| `vim-carve` + `sublime-carve` (#318) | 20 | 11 | 6 |
|
|
684
|
+
| `textmate`, `prism`, `tiptap`, `vscode-carve` (#307, #308, #310) | 47 | 31 | 9 |
|
|
685
|
+
|
|
686
|
+
**Two rows in three are a name the probe cannot reach**, and the third pass also
|
|
687
|
+
turned up six rows that read `IMPLEMENTED` and were not. So the first job on a
|
|
688
|
+
per-surface ticket is deciding which rows are real, and the answer is expected
|
|
689
|
+
to be "most of them are not". A GAP row means *the probe did not find a rule it
|
|
690
|
+
recognizes*, which is a different claim from *this surface does not implement
|
|
691
|
+
the construct*.
|
|
692
|
+
|
|
693
|
+
A fold belongs in `SIGNATURE_OVERRIDES`, which the file calls a per-surface
|
|
694
|
+
NAMING table - never in a rule renamed on a surface to satisfy the probe. Where
|
|
695
|
+
a surface genuinely cannot express a construct, `UNSUPPORTED` **with a reason**
|
|
696
|
+
is the entry; a wrong rule is worse than a missing one.
|
|
697
|
+
|
|
698
|
+
#### The instrument can be wrong in the direction that looks green
|
|
699
|
+
|
|
700
|
+
Five ways the probe has mis-read a surface, each found by opening the file:
|
|
701
|
+
|
|
702
|
+
1. **It read too little.** `Object.keys(grammar.json.rules)` is 196 of
|
|
703
|
+
tree-sitter's 346 names; a `.sublime-syntax` scopes a capture as well as a
|
|
704
|
+
match (#315).
|
|
705
|
+
2. **The signature list had holes** - `code_span` is `verbatim` on one surface
|
|
706
|
+
and `code_inline` on another (#315, #307).
|
|
707
|
+
3. **One rule implements several constructs**, and one name cannot carry four
|
|
708
|
+
through a shared table (#318).
|
|
709
|
+
4. **The evidence named a rule about a different construct.** `carveCommentInline`
|
|
710
|
+
is the braced comment and was cited for the trailing one (#318). The same
|
|
711
|
+
pair beat the word-boundary rank on the TextMate family, where both
|
|
712
|
+
candidates are whole-name hits and *length* picked the wrong one (#307).
|
|
713
|
+
5. **The re-check read the file's prose as evidence.** `every IMPLEMENTED row
|
|
714
|
+
cites a name the shipped grammar really carries` asked whether the evidence
|
|
715
|
+
was a SUBSTRING of the source. `prism/carve.js` carries the comment "Prism
|
|
716
|
+
has no cross-reference token at all" beside the lookbehind that worked
|
|
717
|
+
around the absence - so a row citing `cross-ref` re-checked **green** for as
|
|
718
|
+
long as the rule was missing (#307). That is the trap the docblock at the
|
|
719
|
+
top of `scripts/surface-probe.mjs` describes, and that its extractors were
|
|
720
|
+
built to avoid, surviving in the check that verifies their output. It reads
|
|
721
|
+
`vocabulary()` now, so the seed and the re-check agree on what a name is.
|
|
722
|
+
|
|
723
|
+
The lesson generalizes past this file: **a check that treats a file's prose as
|
|
724
|
+
evidence that the file implements what the prose says it does NOT implement
|
|
725
|
+
cannot fail.** Match against what a grammar DECLARES, never against its text.
|
|
726
|
+
|
|
727
|
+
Re-measure after changing a grammar:
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
node scripts/seed-construct-ledger.mjs
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
That reads the four surfaces here directly. The six in other repositories are
|
|
734
|
+
read from checkouts named by environment variables, and any that is not given
|
|
735
|
+
keeps the row already recorded, with the commit it was read at:
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
CARVE_SURFACE_VIM_CARVE=../vim-carve \
|
|
739
|
+
CARVE_SURFACE_EMACS_CARVE=../emacs-carve \
|
|
740
|
+
node scripts/seed-construct-ledger.mjs
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Statuses are measured; reasons, per-construct notes, tickets and a surface's
|
|
744
|
+
`note` are written by hand and carried across a re-measurement, so a re-run
|
|
745
|
+
never drops a stated reason. A surface-level `note` is for what the rows cannot
|
|
746
|
+
say - why a surface needed no work to reach zero, or what its measured payload
|
|
747
|
+
column does NOT cover.
|
|
748
|
+
|
|
749
|
+
One thing a re-run does NOT carry: the payload axis of a surface the seeder
|
|
750
|
+
cannot tokenize. It is only carried over when the recorded value is already
|
|
751
|
+
`inert` or `leaks`, so a construct that moves from `GAP` to `IMPLEMENTED` in the
|
|
752
|
+
same run arrives with `payload: "unmeasured"` and a ticket, even when the person
|
|
753
|
+
doing the run has just measured it. That is deliberate: a seeder cannot tokenize
|
|
754
|
+
a Vim syntax file or an emacs font-lock table, so the alternative is inventing an
|
|
755
|
+
answer. Measure the payload as part of the same pass and write `inert` (or
|
|
756
|
+
`leaks`, with a note) by hand on those rows.
|
|
757
|
+
|
|
758
|
+
It applies to fewer surfaces than it used to. `vscode-carve` and
|
|
759
|
+
`intellij-carve` are TextMate grammars, and the seeder loads any of those through
|
|
760
|
+
Shiki, so naming their checkout measures the recognition axis and the payload
|
|
761
|
+
sample in the same run:
|
|
762
|
+
|
|
763
|
+
```bash
|
|
764
|
+
CARVE_SURFACE_VSCODE_CARVE=../vscode-carve node scripts/seed-construct-ledger.mjs
|
|
765
|
+
CARVE_SURFACE_INTELLIJ_CARVE=../intellij-carve node scripts/seed-construct-ledger.mjs
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**A `leaks` cell is still hand-written on those surfaces, and the seeder carries
|
|
769
|
+
it rather than overwriting it.** The seeder tokenizes ONE sample per construct
|
|
770
|
+
and the sweep below generates hundreds, and on all three surfaces measured for
|
|
771
|
+
the first time since #320 the sample said every row was inert while the sweep
|
|
772
|
+
disagreed - five rows on intellij-carve alone (#329). So a run whose sample says
|
|
773
|
+
`inert` leaves a recorded `leaks` alone. That cannot hide a fix: the sweep
|
|
774
|
+
asserts every recorded leak STILL leaks, so a fix fails that file, its entry
|
|
775
|
+
comes out, and the ledger's own orphan check then fails until the cell is
|
|
776
|
+
corrected too.
|
|
777
|
+
|
|
778
|
+
### The payload sweep reaches nine of the ten surfaces
|
|
779
|
+
|
|
780
|
+
`tests/opaque-payload-test.js` is the second axis measured over a GENERATED space
|
|
781
|
+
rather than one sample per construct, and one sample is not enough. Every surface
|
|
782
|
+
measured for the first time so far has had leaks the sample could not see:
|
|
783
|
+
|
|
784
|
+
| surface | leaking rows | the sample found |
|
|
785
|
+
| --- | --- | --- |
|
|
786
|
+
| tree-sitter-carve (#328) | 3 | none of them |
|
|
787
|
+
| emacs-carve (#328) | 2 | the `%%%` fence, not the verbatim run |
|
|
788
|
+
| intellij-carve (#329) | 4 | none of them |
|
|
789
|
+
|
|
790
|
+
Name a checkout and the sweep drives it:
|
|
791
|
+
|
|
792
|
+
```bash
|
|
793
|
+
CARVE_SURFACE_TREE_SITTER_CARVE=../tree-sitter-carve \
|
|
794
|
+
CARVE_SURFACE_EMACS_CARVE=../emacs-carve \
|
|
795
|
+
CARVE_SURFACE_VSCODE_CARVE=../vscode-carve \
|
|
796
|
+
CARVE_SURFACE_INTELLIJ_CARVE=../intellij-carve \
|
|
797
|
+
node tests/opaque-payload-test.js
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
tree-sitter needs its native addon built (`npm install` in that checkout), and
|
|
801
|
+
emacs-carve needs an `emacs` on PATH - the mode is fontified in one batch Emacs
|
|
802
|
+
per sweep row, which is what `prime` on that tokenizer is for. A surface whose
|
|
803
|
+
checkout is not named, or not built, is simply not swept.
|
|
804
|
+
|
|
805
|
+
A leak on a grammar in ANOTHER repository is a defect this suite can measure and
|
|
806
|
+
not fix, so it is recorded in `KNOWN_LEAKS` with the ticket it lives on and
|
|
807
|
+
asserted to STILL leak. A fix therefore fails this file and the entry comes out
|
|
808
|
+
with the fix, the same arrangement the residual table at the end of it uses for
|
|
809
|
+
one document at a time. A surface in THIS repository is deliberately absent from
|
|
810
|
+
that table: a leak here is fixable here, so it stays red.
|