@markup-carve/carve-grammars 0.1.6 → 0.1.8
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 +10 -0
- package/highlightjs/carve.js +140 -22
- package/package.json +3 -3
- package/prism/carve.js +116 -7
- package/textmate/carve.tmLanguage.json +39 -16
- package/tiptap/carve-editor.js +70 -0
- package/tiptap/carve-kit.js +125 -8
- package/tiptap/carve-to-pm.js +28 -8
- package/tiptap/editor.css +433 -1
- package/tiptap/extensions/carve-abbreviation-definition.js +7 -0
- package/tiptap/extensions/carve-citation.js +14 -0
- package/tiptap/extensions/carve-comment.js +7 -3
- package/tiptap/extensions/carve-crossref.js +13 -0
- package/tiptap/extensions/carve-embed.js +35 -4
- package/tiptap/extensions/carve-footnote.js +18 -0
- package/tiptap/extensions/carve-frontmatter.js +122 -0
- package/tiptap/extensions/carve-link-ref-def.js +7 -0
- package/tiptap/extensions/carve-literal.js +4 -3
- package/tiptap/extensions/carve-math.js +12 -0
- package/tiptap/extensions/carve-mention.js +9 -0
- package/tiptap/extensions/carve-raw-inline.js +4 -3
- package/tiptap/extensions/carve-substitution.js +11 -0
- package/tiptap/extensions/carve-symbol.js +8 -0
- package/tiptap/extensions/carve-unsupported-inline.js +3 -0
- package/tiptap/extensions/carve-unsupported.js +3 -0
- package/tiptap/extensions/editable-atom-view.js +356 -0
- package/tiptap/extensions/panel-bar.js +24 -3
- package/tiptap/schema-map.json +7 -5
- package/tiptap/serializer.js +21 -8
- package/tiptap/wire-fixtures.json +16 -7
package/README.md
CHANGED
|
@@ -259,6 +259,16 @@ available as the `editor` CSS part (`carve-editor::part(editor)`). Tiptap stays
|
|
|
259
259
|
an implementation detail of the element, although its peer packages must be
|
|
260
260
|
installed with `carve-grammars`.
|
|
261
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
|
+
|
|
262
272
|
## Syntax highlighting
|
|
263
273
|
|
|
264
274
|
Render Carve source as highlighted HTML on the web. Both grammars cover the full
|
package/highlightjs/carve.js
CHANGED
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
* @returns {object} the mode's `begin`/`end` pair, so the closer used by
|
|
153
153
|
* the guard and the closer used by the mode cannot drift apart.
|
|
154
154
|
*/
|
|
155
|
-
const paired = (opener, closer) => {
|
|
155
|
+
const paired = (opener, closer, { escapeAware = false, flanked = false } = {}) => {
|
|
156
156
|
// The guard used to be written `(?:[^\n]|\n(?!\s*\n))*?` - unbounded,
|
|
157
157
|
// lazy, and free to cross newlines. Proving there is NO closer therefore
|
|
158
158
|
// cost a whole paragraph from every position, and a document made of
|
|
@@ -183,11 +183,108 @@
|
|
|
183
183
|
const literal = lead.length === 2 ? lead[1] : lead;
|
|
184
184
|
const inClass = /[\\\]^-]/.test(literal) ? '\\' + literal : literal;
|
|
185
185
|
const run = `(?:[^${inClass}\\n]|\\n(?!\\s*\\n)){0,4096}`;
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
/*
|
|
187
|
+
* `escapeAware` opts a mode's GUARD into "an escaped delimiter is not a
|
|
188
|
+
* delimiter" (carve-grammars#385). Only the bare highlight asks for it,
|
|
189
|
+
* because it is the only closer here that a document can put a
|
|
190
|
+
* backslash in front of and mean it: `x =\= y` renders `x == y` with no
|
|
191
|
+
* mark, and this mode opened a run the engine does not.
|
|
192
|
+
*
|
|
193
|
+
* THE RUN CONSUMES AN ESCAPE AS A PAIR and refuses a bare backslash,
|
|
194
|
+
* rather than counting the backslash run in a lookbehind. Counting is
|
|
195
|
+
* what #385 shipped and what carve-grammars#390 replaces: a lookbehind
|
|
196
|
+
* that repeats a group has to be BOUNDED or it backtracks superlinearly
|
|
197
|
+
* (carve-grammars#380), and every bound is reachable - at `{0,32}` the
|
|
198
|
+
* guard stopped seeing the run at 66 backslashes and this mode refused a
|
|
199
|
+
* highlight the engine marks. A pair has no bound to reach.
|
|
200
|
+
*
|
|
201
|
+
* ONLY HALF THE FIX IS HERE. The guard proves a closer EXISTS; the span
|
|
202
|
+
* comes from `end`, which highlight.js applies on its own and which
|
|
203
|
+
* would still stop at an escaped `=`. What stops it is HIGHLIGHT listing
|
|
204
|
+
* ESCAPE in `contains` - that submode begins at the backslash, one
|
|
205
|
+
* column before the delimiter, so it wins by position and eats the pair.
|
|
206
|
+
* Neither half works alone: without this one the mode never opens,
|
|
207
|
+
* without the other it closes on the escaped delimiter.
|
|
208
|
+
*
|
|
209
|
+
* THE BOUND COUNTS ATOMS, and an escape pair is two characters, so the
|
|
210
|
+
* worst case is 8192 rather than 4096. Still a constant per position,
|
|
211
|
+
* which is all the bound is for; halving it would halve every ordinary
|
|
212
|
+
* body too.
|
|
213
|
+
*
|
|
214
|
+
* The default path is byte-identical to before, so the twelve other
|
|
215
|
+
* modes are untouched.
|
|
216
|
+
*/
|
|
217
|
+
const escapedRun = `(?:\\\\[^\\n]|[^${inClass}\\n\\\\]|\\n(?!\\s*\\n)){0,4096}`;
|
|
218
|
+
const body = escapeAware ? escapedRun : run;
|
|
219
|
+
/*
|
|
220
|
+
* `flanked` says a closer may not FOLLOW WHITESPACE, which is the
|
|
221
|
+
* mirror of the `(?=\S)` every bare opener already carries
|
|
222
|
+
* (carve-grammars#392). Without it `a =b = d` scoped a highlight the
|
|
223
|
+
* engine renders literally, and so did the four other bare delimiters -
|
|
224
|
+
* one gap with five spellings. The nine BRACED modes do not ask for it
|
|
225
|
+
* and must not: the engine builds `a {*b *} d`, so a guard there would
|
|
226
|
+
* turn an over-scope into an under-scope.
|
|
227
|
+
*
|
|
228
|
+
* `end` IS LEFT ALONE, and that is forced by the engine twice over.
|
|
229
|
+
*
|
|
230
|
+
* A lookbehind ANYWHERE in a mode's `end` stops highlight.js closing
|
|
231
|
+
* the mode at all - `(?<=\S)=(?!\w)` and `=(?!\w)(?<=\S=)` both leave
|
|
232
|
+
* `x =b= y` coloured to end of line, with no error. And a CONSUMED
|
|
233
|
+
* flank, `[^\s\n]=(?!\w)`, closes correctly but starts one column
|
|
234
|
+
* earlier, which is exactly where the escape submode starts: it wins
|
|
235
|
+
* the tie and closes on the `\=` that carve-grammars#390 exists to
|
|
236
|
+
* refuse. Both measured on a two-line grammar before this was written.
|
|
237
|
+
*
|
|
238
|
+
* SO THE FALSE CLOSER IS EATEN INSTEAD OF REFUSED, the same shape #390
|
|
239
|
+
* uses for the escape. `falseCloser` begins at the WHITESPACE, one
|
|
240
|
+
* column before `end` could fire, so it wins by position and takes the
|
|
241
|
+
* delimiter as content; `end` then only ever sees a closer that stands
|
|
242
|
+
* against a non-space. Nothing about `end` changes, so this composes
|
|
243
|
+
* with the escape submode rather than fighting it.
|
|
244
|
+
*
|
|
245
|
+
* THE GUARD IS THE OTHER HALF, and it may consume, because it is inside
|
|
246
|
+
* a lookahead where nothing is really taken. Without it a run with no
|
|
247
|
+
* real closer would open, eat its false closers and colour to the end
|
|
248
|
+
* of its parent - worse than the over-scope this fixes. `notCloser`
|
|
249
|
+
* widens for the same reason: `a *b * c* d` is a bold run in the
|
|
250
|
+
* engine, and the guard has to be able to step over the middle star to
|
|
251
|
+
* see the real one.
|
|
252
|
+
*/
|
|
253
|
+
/*
|
|
254
|
+
* THE FLANK CHARACTER SHARES THE BODY'S ALPHABET. A bare `[^\s\n]`
|
|
255
|
+
* would match a lone backslash, so on `x =\= y` the guard ends its body
|
|
256
|
+
* before the backslash, eats it as the flank, and proves a closer that
|
|
257
|
+
* `end` - correctly held off by the escape submode - then never fires
|
|
258
|
+
* on: the mode opens and colours to the end of its parent. An
|
|
259
|
+
* escape-aware flank is a PAIR or a non-backslash, exactly like the run.
|
|
260
|
+
*/
|
|
261
|
+
// AN ESCAPED SPACE IS STILL A SPACE to the flanking rule: the engine
|
|
262
|
+
// renders `x =\\ = y` with no mark, so the pair may not stand as the
|
|
263
|
+
// non-space character the closer needs behind it.
|
|
264
|
+
const flankUnit = escapeAware ? '(?:\\\\[^\\s\\n]|[^\\s\\n\\\\])' : '[^\\s\\n]';
|
|
265
|
+
const endSource = flanked ? `${flankUnit}${closer.source}` : closer.source;
|
|
266
|
+
const notCloser = flanked
|
|
267
|
+
? `(?:\\s${lead}|${lead}(?!${rest}))`
|
|
268
|
+
: `${lead}(?!${rest})`;
|
|
269
|
+
const guard = `${body}(?:${notCloser}${body}){0,32}${endSource}`;
|
|
270
|
+
const mode = {
|
|
188
271
|
begin: new RegExp(`${opener.source}(?=${guard})`),
|
|
189
272
|
end: closer,
|
|
190
273
|
};
|
|
274
|
+
if (flanked) mode.contains = [{ begin: new RegExp(`\\s${closer.source}`) }];
|
|
275
|
+
|
|
276
|
+
return mode;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Escaped characters: \* \[ etc
|
|
280
|
+
//
|
|
281
|
+
// DEFINED HERE, above the modes rather than beside HARD_BREAK where it used
|
|
282
|
+
// to sit, because HIGHLIGHT now `contains` it (carve-grammars#390) and a
|
|
283
|
+
// `const` cannot be read before its own declaration.
|
|
284
|
+
const ESCAPE = {
|
|
285
|
+
className: 'symbol',
|
|
286
|
+
begin: /\\[!"#$%&'()*+,.\/:;<=>?@\[\\\]^_`{|}~-]/,
|
|
287
|
+
relevance: 0,
|
|
191
288
|
};
|
|
192
289
|
|
|
193
290
|
// Forced intraword family (PART 9 S22). Content may contain the delimiter
|
|
@@ -277,14 +374,14 @@
|
|
|
277
374
|
// (a/b, ://); the end is a closing slash not followed by word char/slash.
|
|
278
375
|
const EMPHASIS = {
|
|
279
376
|
className: 'emphasis',
|
|
280
|
-
...paired(/(?<![\w:/])\/(?=\S)/, /\/(?![\w/])
|
|
377
|
+
...paired(/(?<![\w:/])\/(?=\S)/, /\/(?![\w/])/, { flanked: true }),
|
|
281
378
|
relevance: 0,
|
|
282
379
|
};
|
|
283
380
|
|
|
284
381
|
// Underline (Carve): _text_ - not in the middle of words
|
|
285
382
|
const UNDERLINE = {
|
|
286
383
|
className: 'emphasis',
|
|
287
|
-
...paired(/(?<!\w)_(?!\s)/, /_(?!\w)
|
|
384
|
+
...paired(/(?<!\w)_(?!\s)/, /_(?!\w)/, { flanked: true }),
|
|
288
385
|
relevance: 0,
|
|
289
386
|
};
|
|
290
387
|
|
|
@@ -309,17 +406,27 @@
|
|
|
309
406
|
*/
|
|
310
407
|
const BOLD_ITALIC = {
|
|
311
408
|
className: 'strong',
|
|
312
|
-
|
|
409
|
+
// The body is non-space at BOTH ends. It was guarded only at the
|
|
410
|
+
// opener, so `a /*b */ c` came back one combined run where the engine
|
|
411
|
+
// renders `<em>*b *</em>` - an italic run holding literal asterisks
|
|
412
|
+
// (carve-grammars#375). A fixed-width lookbehind, so the bounded
|
|
413
|
+
// repetition that keeps this rule linear is untouched.
|
|
414
|
+
begin: /\/\*(?=\S)(?:[^*\n]|\*(?!\/)|\n(?!\s*\n)){1,4096}(?<=\S)\*\//,
|
|
313
415
|
relevance: 5,
|
|
314
416
|
};
|
|
315
417
|
|
|
316
418
|
// Strong: *text* - not in the middle of words, can contain emphasis.
|
|
317
419
|
// Excludes *[ which is abbreviation-definition syntax.
|
|
420
|
+
const STRONG_PAIR = paired(/(?<!\w)\*(?![\s\[])/, /\*(?!\w)/, { flanked: true });
|
|
318
421
|
const STRONG = {
|
|
319
422
|
className: 'strong',
|
|
320
|
-
...
|
|
423
|
+
...STRONG_PAIR,
|
|
321
424
|
relevance: 0,
|
|
322
|
-
|
|
425
|
+
// SPREAD FIRST, then merge: `paired()` supplies a false-closer submode
|
|
426
|
+
// (carve-grammars#392) and a bare re-declaration would drop it, which
|
|
427
|
+
// is silent - the mode still opens, it just closes at the first
|
|
428
|
+
// delimiter standing after a space.
|
|
429
|
+
contains: [EMPHASIS, UNDERLINE, ...STRONG_PAIR.contains],
|
|
323
430
|
};
|
|
324
431
|
|
|
325
432
|
/*
|
|
@@ -351,9 +458,15 @@
|
|
|
351
458
|
* opened and closed on the two `=` of a doubled run and scoped an empty
|
|
352
459
|
* highlight over a line the engine renders literally.
|
|
353
460
|
*
|
|
354
|
-
* THE CLOSER IS DELIBERATELY NOT GUARDED
|
|
355
|
-
* engine's: once a highlight is open the closer wins over
|
|
356
|
-
* `x =y z<= w` marks `y z<` and `x =y z=> w` marks `y z`.
|
|
461
|
+
* THE CLOSER IS DELIBERATELY NOT GUARDED AGAINST SMART TYPOGRAPHY, and the
|
|
462
|
+
* asymmetry is the engine's: once a highlight is open the closer wins over
|
|
463
|
+
* the pattern, so `x =y z<= w` marks `y z<` and `x =y z=> w` marks `y z`.
|
|
464
|
+
*
|
|
465
|
+
* AN ESCAPE IS A DIFFERENT QUESTION (carve-grammars#385). An escaped `=` is
|
|
466
|
+
* not a delimiter at all - `x =\= y` renders `x == y` with no mark - and
|
|
467
|
+
* this mode closed on one. `paired`'s third argument is what says so; it is
|
|
468
|
+
* the only mode here that asks for it, because it is the only closer a
|
|
469
|
+
* document can put a backslash in front of and mean it.
|
|
357
470
|
*
|
|
358
471
|
* ONE SHAPE IT COSTS: `<https://e.example>=hi=`, where the `>` closes an
|
|
359
472
|
* autolink rather than opening a comparison. A fixed-width lookbehind
|
|
@@ -361,9 +474,16 @@
|
|
|
361
474
|
* that trade - the ticket's own reasoning, since a false highlight claims
|
|
362
475
|
* the document holds a construct it does not.
|
|
363
476
|
*/
|
|
477
|
+
const HIGHLIGHT_PAIR = paired(/(?<![=\w])=(?=\S)(?![>=])/, /=(?![=\w])/, { escapeAware: true, flanked: true });
|
|
364
478
|
const HIGHLIGHT = {
|
|
365
479
|
className: 'addition',
|
|
366
|
-
...
|
|
480
|
+
...HIGHLIGHT_PAIR,
|
|
481
|
+
// The other half of carve-grammars#390's escape fix - see `paired`.
|
|
482
|
+
// `end` would close on an escaped `=`; this submode begins one column
|
|
483
|
+
// earlier, at the backslash, and highlight.js resolves by position.
|
|
484
|
+
// The false-closer submode `paired()` supplies (carve-grammars#392) is
|
|
485
|
+
// kept alongside it: a bare `contains: [ESCAPE]` here would drop it.
|
|
486
|
+
contains: [ESCAPE, ...HIGHLIGHT_PAIR.contains],
|
|
367
487
|
relevance: 3,
|
|
368
488
|
};
|
|
369
489
|
|
|
@@ -375,16 +495,21 @@
|
|
|
375
495
|
};
|
|
376
496
|
|
|
377
497
|
// Delete: {-text-}
|
|
498
|
+
//
|
|
499
|
+
// THE BODY IS NOT EMPTY. `{--}` is a braced EN DASH, not an empty deletion -
|
|
500
|
+
// the engine renders `a {--} b` as `a \u2013 b` - and this mode read it as
|
|
501
|
+
// `{-` plus nothing plus `-}` (carve-grammars#378). One character is enough:
|
|
502
|
+
// `{- -}`, `{---}` and `{----}` are all deletions.
|
|
378
503
|
const DELETE = {
|
|
379
504
|
className: 'deletion',
|
|
380
|
-
...paired(/\{
|
|
505
|
+
...paired(/\{-(?!-\})/, /-\}/),
|
|
381
506
|
relevance: 5,
|
|
382
507
|
};
|
|
383
508
|
|
|
384
509
|
// Strikethrough (Carve): ~text~ (Djot uses ~ for subscript instead)
|
|
385
510
|
const STRIKETHROUGH = {
|
|
386
511
|
className: 'deletion',
|
|
387
|
-
...paired(/(?<!\w)~(?=\S)/, /~(?!\w)
|
|
512
|
+
...paired(/(?<!\w)~(?=\S)/, /~(?!\w)/, { flanked: true }),
|
|
388
513
|
relevance: 2,
|
|
389
514
|
};
|
|
390
515
|
|
|
@@ -642,7 +767,7 @@
|
|
|
642
767
|
// dash alternatives: preceded by a non-space, or not followed by a word.
|
|
643
768
|
const TYPOGRAPHY = {
|
|
644
769
|
className: 'literal',
|
|
645
|
-
begin:
|
|
770
|
+
begin: /\{--\}|\.\.\.|<-->|<--|-->|<=>|<==|==>|<->|<-|->|(?:(?<=\S)(?:---|--)|(?:---|--)(?!\w))|!=|<=|>=|\+-|\(c\)|\(r\)|\(tm\)/,
|
|
646
771
|
relevance: 0,
|
|
647
772
|
};
|
|
648
773
|
|
|
@@ -1467,13 +1592,6 @@
|
|
|
1467
1592
|
relevance: 5,
|
|
1468
1593
|
};
|
|
1469
1594
|
|
|
1470
|
-
// Escaped characters: \* \[ etc
|
|
1471
|
-
const ESCAPE = {
|
|
1472
|
-
className: 'symbol',
|
|
1473
|
-
begin: /\\[!"#$%&'()*+,.\/:;<=>?@\[\\\]^_`{|}~-]/,
|
|
1474
|
-
relevance: 0,
|
|
1475
|
-
};
|
|
1476
|
-
|
|
1477
1595
|
// Hard line break: \ at end of line
|
|
1478
1596
|
const HARD_BREAK = {
|
|
1479
1597
|
className: 'meta',
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markup-carve/carve-grammars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Grammars for the Carve markup language: Tiptap editor kit + serializer, plus Prism, highlight.js and TextMate syntax-highlighting grammars",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "tiptap/index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "npm run test:types && node tests/carve-editor-test.js && node tests/block-battery-test.js && node tests/smart-typography-test.js && node tests/highlight-opener-test.js && node tests/fenced-quote-opener-test.js && node tests/coverage-test.js && node tests/construct-ledger-test.js && node tests/opaque-payload-test.js && node tests/latest-syntax-test.js && node tests/schema-map-test.js && node tests/scans-are-bounded-test.js && node tests/braced-scan-equivalence-test.js && node tests/line-ambiguity-test.js && node tests/snapshot-test.js && node tests/roundtrip-test.js && node tests/node-producers-test.js && node tests/wire-fixtures-test.js && node tests/loss-report-test.js && node tests/authored-spelling-test.js && node tests/optional-corpus-test.js && node tests/mounted-roundtrip-test.js && node tests/serializer-test.js && node tests/tabs-roundtrip-test.js && node tests/panel-bar-test.js && node tests/parse-test.js && node tests/language-attribute-test.js && node tests/blockquote-caption-test.js && node tests/composite-figure-test.js && node tests/composite-figure-tiptap-test.js && node tests/grammar-test.js && node tests/shiki-test.js && node tests/alias-parity-test.js && node tests/engine-sweep-test.js && node tests/abbreviation-term-test.js && node tests/unclosed-delimiter-test.js && node tests/textmate-sweep-test.js && node tests/kroki-test.js && node tests/client-diagram-test.js && node tests/packaging-test.js && node tests/no-git-dependencies-test.js && node tests/engine-drift-test.js && node tests/surface-drift-test.js",
|
|
8
|
+
"test": "npm run test:types && node tests/carve-editor-test.js && node tests/block-battery-test.js && node tests/smart-typography-test.js && node tests/highlight-opener-test.js && node tests/highlight-escape-test.js && node tests/right-flank-test.js && node tests/bold-unclosed-test.js && node tests/textmate-harness-test.js && node tests/fenced-quote-opener-test.js && node tests/coverage-test.js && node tests/construct-ledger-test.js && node tests/opaque-payload-test.js && node tests/latest-syntax-test.js && node tests/schema-map-test.js && node tests/scans-are-bounded-test.js && node tests/braced-scan-equivalence-test.js && node tests/line-ambiguity-test.js && node tests/snapshot-test.js && node tests/roundtrip-test.js && node tests/node-producers-test.js && node tests/wire-fixtures-test.js && node tests/loss-report-test.js && node tests/authored-spelling-test.js && node tests/optional-corpus-test.js && node tests/mounted-roundtrip-test.js && node tests/serializer-test.js && node tests/task-state-roundtrip-test.js && node tests/tabs-roundtrip-test.js && node tests/visual-tabs-fuzz-test.js && node tests/panel-bar-test.js && node tests/parse-test.js && node tests/language-attribute-test.js && node tests/blockquote-caption-test.js && node tests/composite-figure-test.js && node tests/composite-figure-tiptap-test.js && node tests/grammar-test.js && node tests/shiki-test.js && node tests/alias-parity-test.js && node tests/engine-sweep-test.js && node tests/abbreviation-term-test.js && node tests/unclosed-delimiter-test.js && node tests/textmate-sweep-test.js && node tests/kroki-test.js && node tests/client-diagram-test.js && node tests/packaging-test.js && node tests/no-git-dependencies-test.js && node tests/engine-drift-test.js && node tests/surface-drift-test.js",
|
|
9
9
|
"test:types": "tsc -p tsconfig.types.json && node tests/source-merge-test.js",
|
|
10
10
|
"test:citations": "node tests/spec-citations-test.js",
|
|
11
11
|
"test:coverage": "node tests/coverage-test.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"./package.json": "./package.json"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@markup-carve/carve": "^0.1.
|
|
86
|
+
"@markup-carve/carve": "^0.1.6",
|
|
87
87
|
"node-diff3": "^3.2.1"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
package/prism/carve.js
CHANGED
|
@@ -198,8 +198,56 @@
|
|
|
198
198
|
// Shared inline emphasis/markup, referenced from block tokens that contain
|
|
199
199
|
// running text (headings, list items, table cells, quotes).
|
|
200
200
|
var inline = {
|
|
201
|
+
// BOTH NESTING ORDERS. The engine renders `/*both*/` and `*/both/*`
|
|
202
|
+
// identically - each is <strong><em>both</em></strong> - and only the
|
|
203
|
+
// canonical order had a branch, so the mirrored one fell through to
|
|
204
|
+
// 'bold' and came back a bold run holding two literal slashes
|
|
205
|
+
// (carve-grammars#375).
|
|
206
|
+
//
|
|
207
|
+
// The mirrored branch carries guards the canonical one must not have,
|
|
208
|
+
// and each is the engine's own asymmetry:
|
|
209
|
+
//
|
|
210
|
+
// - `x/*b*/y` IS bold-italic and `x*/b/*y` is `x*<em>b</em>*y`,
|
|
211
|
+
// because the mirrored opener leads with `*` and a `*` glued to a
|
|
212
|
+
// word opens nothing.
|
|
213
|
+
// - a `/` before the opener makes the pair ambiguous with a canonical
|
|
214
|
+
// opener: `a /*/a/* b` is `a <em>*/a</em>* b`.
|
|
215
|
+
// - the closer may not follow a `*`: `a */b*/* c` is
|
|
216
|
+
// `a <strong>/b</strong>/* c`, while `a */*b/* c` IS combined - so
|
|
217
|
+
// it is the character before the closer that decides.
|
|
218
|
+
//
|
|
219
|
+
// BOTH BODIES ARE TEMPERED, BY DIFFERENT AMOUNTS (carve-grammars#382),
|
|
220
|
+
// and the asymmetry was measured rather than chosen. The canonical body
|
|
221
|
+
// admits any `*` that does not start the closer. The MIRRORED body may
|
|
222
|
+
// not do the same: a `/` that does not start the closer is still
|
|
223
|
+
// ambiguous with a canonical opener, so its `/` is admitted only
|
|
224
|
+
// BETWEEN TWO WORD CHARACTERS, which is what lets `a */b/c/* d` read as
|
|
225
|
+
// the combined run the engine renders while `x *///* y` stays bold.
|
|
226
|
+
//
|
|
227
|
+
// The opener's `(?!\*[\s*])` is what pays for it. Over all 21,844
|
|
228
|
+
// documents `x */ body /* y` for every body of up to seven characters
|
|
229
|
+
// drawn from `* / space a`, judged by asking whether the engine renders
|
|
230
|
+
// the WHOLE body as one combined run:
|
|
231
|
+
//
|
|
232
|
+
// strict (before) over 440 missed 603 wrong 1043
|
|
233
|
+
// any non-closing slash (rejected) over 5091 missed 161 wrong 5252
|
|
234
|
+
// word-flanked slash alone over 510 missed 461 wrong 971
|
|
235
|
+
// word-flanked + opener guard (this) over 226 missed 461 wrong 687
|
|
236
|
+
//
|
|
237
|
+
// The temper alone RAISES over-colouring, and every shape it adds is
|
|
238
|
+
// one the opener guard refuses - `a */**a/a/* d` and `a */* a/a/* d`
|
|
239
|
+
// are bold runs in the engine. The guard costs nothing: `missed` is
|
|
240
|
+
// unchanged with and without it. Repeated over `* / space a .` to
|
|
241
|
+
// separate a word-character flank from a non-space one, the word-
|
|
242
|
+
// character spelling wins there too (311 wrong against 454).
|
|
243
|
+
//
|
|
244
|
+
// Both bodies are non-space at each end, and both may cross a line
|
|
245
|
+
// without crossing a blank one - corpus 208 is a combined run spanning
|
|
246
|
+
// two lines, and the highlight.js rule spells the same body. `a /*b */ c`
|
|
247
|
+
// is `<em>*b *</em>` and not a combined run; the old `[^*]+` body,
|
|
248
|
+
// guarded only at the opener, read it as one.
|
|
201
249
|
'bold-italic': {
|
|
202
|
-
pattern: /\/\*(?=\S)[
|
|
250
|
+
pattern: /\/\*(?=\S)(?:[^*\n]|\*(?!\/)|\n(?!\s*\n)){1,4096}(?<=\S)\*\/|(?<![\w*/])\*\/(?=\S)(?!\*[\s*])(?:[^/\n]|(?<=\w)\/(?=\w)|\n(?!\s*\n)){1,4096}(?<![\s*])\/\*(?!\w)/,
|
|
203
251
|
alias: 'important',
|
|
204
252
|
},
|
|
205
253
|
// The "no leading/trailing space" rule is expressed without JS
|
|
@@ -285,13 +333,70 @@
|
|
|
285
333
|
// the characters that changed a measurement when reverted, which
|
|
286
334
|
// is why the three spellings are not identical.
|
|
287
335
|
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// `
|
|
336
|
+
// AND AN ESCAPED FLANK IS NOT A FLANK (carve-grammars#380). A
|
|
337
|
+
// lookbehind sees a character, not whether it was escaped, so
|
|
338
|
+
// `a \!=b c= d` - where the `!` is literal and the `=` after it is
|
|
339
|
+
// an ordinary opener the engine marks - was refused, and
|
|
340
|
+
// `a \=b c= d`, whose `=` cannot open at all, was taken.
|
|
341
|
+
//
|
|
342
|
+
// BACKSLASHES NEST, so both halves count them rather than looking
|
|
343
|
+
// at one character: the opener needs an EVEN run behind it (`\\=`
|
|
344
|
+
// is a literal backslash and an ordinary opener) and the admission
|
|
345
|
+
// needs an ODD one in front of the flank (`\\!=` is a literal
|
|
346
|
+
// backslash and a comparison, which the engine leaves alone). A
|
|
347
|
+
// first draft looked one character back and got both of those
|
|
348
|
+
// backwards.
|
|
349
|
+
//
|
|
350
|
+
// THE PAIR REPETITION IS BOUNDED, like every other scan in this
|
|
351
|
+
// file. Written as an unbounded repetition inside a lookbehind it
|
|
352
|
+
// backtracks superlinearly over a long backslash run - measured at
|
|
353
|
+
// 7 / 30 / 91 / 346 ms for 4K / 8K / 16K / 32K backslashes, four
|
|
354
|
+
// times per doubling, and 4 / 3 / 8 / 7 ms bounded. 64 backslashes
|
|
355
|
+
// in front of a highlight opener is not a document; past the bound
|
|
356
|
+
// the run does not open, which is the safe direction.
|
|
357
|
+
//
|
|
358
|
+
// A LOOKBEHIND IS WHAT THIS RULE HAS TO USE. Moving the `escape`
|
|
359
|
+
// token in front of this one does not help: the rule is `greedy`,
|
|
360
|
+
// so Prism runs it against the ORIGINAL text from the current
|
|
361
|
+
// offset and the lookbehind still sees the flank an earlier token
|
|
362
|
+
// consumed. Measured - with `escape` moved ahead of the inline
|
|
363
|
+
// family, `a \!=b c= d` still scoped nothing. The other two
|
|
364
|
+
// grammars need none of this, because their escape rule really
|
|
365
|
+
// does consume the flank before the highlight rule is asked.
|
|
366
|
+
// THE CLOSER IS UNGUARDED AGAINST SMART TYPOGRAPHY, deliberately -
|
|
367
|
+
// once a highlight is open the closer wins over the pattern in the
|
|
368
|
+
// engine too, so `x =y z<= w` marks `y z<`. AN ESCAPE IS A
|
|
369
|
+
// DIFFERENT QUESTION (carve-grammars#385): an escaped `=` is not a
|
|
370
|
+
// delimiter at all, so `x =\= y` renders `x == y` with no mark and
|
|
371
|
+
// this rule closed on it. THE BODY CONSUMES AN ESCAPE AS A PAIR and
|
|
372
|
+
// refuses a bare backslash, which settles both directions at once -
|
|
373
|
+
// the escaped `=` is eaten before the closer can see it, and a body
|
|
374
|
+
// that could not hold one would stop there and never reach the real
|
|
375
|
+
// closer past it, so `a =b c\= d= e`, which the engine marks, would
|
|
376
|
+
// have gone from wrongly-coloured to uncoloured. The closer needs no
|
|
377
|
+
// guard of its own as a CONSEQUENCE, not a simplification: the body
|
|
378
|
+
// can only get past a backslash by taking the pair, so the position
|
|
379
|
+
// immediately before an escaped `=` is never a body end.
|
|
380
|
+
// #385 COUNTED THE RUN IN A LOOKBEHIND on the closer and its odd
|
|
381
|
+
// twin on the body, and carve-grammars#390 replaced both. Every
|
|
382
|
+
// bound is reachable: at `{0,32}` the guard stopped seeing the run
|
|
383
|
+
// at 66 backslashes and this rule refused a highlight the engine
|
|
384
|
+
// marks. A pair has no bound - and the same spelling is what the
|
|
385
|
+
// TextMate family had to use, since one TextMate engine refuses a
|
|
386
|
+
// variable-length lookbehind outright. The OPENER's two lookbehinds
|
|
387
|
+
// stay: they are the escaped-FLANK guard from carve-grammars#380,
|
|
388
|
+
// which is a different question and which this rule cannot avoid
|
|
389
|
+
// for the `greedy` reason above.
|
|
291
390
|
// ONE SHAPE IT COSTS: `<https://e.example>=hi=`, where the `>`
|
|
292
391
|
// closes an autolink rather than opening a comparison; a
|
|
293
392
|
// fixed-width lookbehind cannot tell the two apart, and this takes
|
|
294
393
|
// the under-colouring side of that trade.
|
|
394
|
+
// THE BOUND COUNTS ATOMS, and an escape pair is two characters, so
|
|
395
|
+
// the worst case is 8192 characters rather than 4096. That is still
|
|
396
|
+
// a constant per position, which is all the bound is for; measured,
|
|
397
|
+
// an all-escape body costs what a plain body of the same LENGTH
|
|
398
|
+
// costs. Halving it to keep the character count would halve every
|
|
399
|
+
// ordinary body too.
|
|
295
400
|
// ONE TOKEN, TWO FORMS, so the bare `=x=` alternative shares a
|
|
296
401
|
// line with the braced one. Its `[^=\n]+?` was never the defect
|
|
297
402
|
// this rule was fixed for - the class already excludes its own
|
|
@@ -299,7 +404,7 @@
|
|
|
299
404
|
// the last unbounded quantifier on a line that spells a braced
|
|
300
405
|
// construct, and the derived family check below reads lines. Given
|
|
301
406
|
// a bound, at the same 4096 the rest of the file uses.
|
|
302
|
-
pattern: /\{=(?=\S)[^=\n]{0,4096}(?:=(?!\})[^=\n]{0,4096}){0,32}=\}|(?<![\w=<>!])=(?=\S)(?!>)[^=\n]{1,4096}?(?<=\S)=(?![\w=])/,
|
|
407
|
+
pattern: /\{=(?=\S)[^=\n]{0,4096}(?:=(?!\})[^=\n]{0,4096}){0,32}=\}|(?:(?<=(?:^|[^\\])(?:\\\\){0,32})(?<![\w=<>!])|(?<=(?:^|[^\\])(?:\\\\){0,32}\\[<>!]))=(?=\S)(?!>)(?:\\.|[^=\n\\]){1,4096}?(?<=\S)=(?![\w=])/,
|
|
303
408
|
alias: 'important',
|
|
304
409
|
},
|
|
305
410
|
// Braced-only: a bare `^` / `,` is literal text (no bare sup/sub).
|
|
@@ -1366,8 +1471,12 @@
|
|
|
1366
1471
|
pattern: /\{\+[^+}]{0,4096}(?:\+(?!\})[^+}]{0,4096}){0,32}\+\}/,
|
|
1367
1472
|
alias: 'inserted',
|
|
1368
1473
|
},
|
|
1474
|
+
// THE BODY IS NOT EMPTY. `{--}` is a braced EN DASH, not an empty
|
|
1475
|
+
// deletion - the engine renders `a {--} b` as `a \u2013 b` - and this rule
|
|
1476
|
+
// read it as `{-` plus nothing plus `-}` (carve-grammars#378). One
|
|
1477
|
+
// character is enough: `{- -}`, `{---}` and `{----}` are all deletions.
|
|
1369
1478
|
'deleted': {
|
|
1370
|
-
pattern: /\{-[^\-}]{0,4096}(?:-(?!\})[^\-}]{0,4096}){0,32}-\}/,
|
|
1479
|
+
pattern: /\{-(?!-\})[^\-}]{0,4096}(?:-(?!\})[^\-}]{0,4096}){0,32}-\}/,
|
|
1371
1480
|
alias: 'deleted',
|
|
1372
1481
|
},
|
|
1373
1482
|
// The one `{~ ... ~}` rule the sweep did NOT flag - a substitution's two
|
|
@@ -1452,7 +1561,7 @@
|
|
|
1452
1561
|
// around the dash alternatives: preceded by a non-space, or not
|
|
1453
1562
|
// followed by a word character.
|
|
1454
1563
|
'typography': {
|
|
1455
|
-
pattern:
|
|
1564
|
+
pattern: /\{--\}|\.\.\.|<-->|<--|-->|<=>|<==|==>|<->|<-|->|(?:(?<=\S)(?:---|--)|(?:---|--)(?!\w))|!=|<=|>=|\+-|\(c\)|\(r\)|\(tm\)/,
|
|
1456
1565
|
alias: 'constant',
|
|
1457
1566
|
},
|
|
1458
1567
|
};
|
|
@@ -1173,7 +1173,8 @@
|
|
|
1173
1173
|
"name": "markup.inserted.carve"
|
|
1174
1174
|
},
|
|
1175
1175
|
"critic_del": {
|
|
1176
|
-
"
|
|
1176
|
+
"comment": "THE BODY IS NOT EMPTY. `{--}` is a braced EN DASH, not an empty deletion - grammar.ebnf gives `braced_en_dash = \"{--}\"` and the clause is normative (markup-carve/carve#1447); carve 0.1.5 renders `a {--} b` as `a \u2013 b`. This rule read it as `{-` plus nothing plus `-}` and coloured it a deletion, which is a colour that says the document holds something it does not, and no sweep could see it because the run WAS coloured (carve-grammars#378). One character of body is enough: `{- -}` deletes a space, `{---}` deletes a hyphen and `{----}` deletes an en dash, all measured against the engine.",
|
|
1177
|
+
"match": "\\{-(?!-\\})[^}]*-\\}",
|
|
1177
1178
|
"name": "markup.deleted.carve"
|
|
1178
1179
|
},
|
|
1179
1180
|
"critic_sub": {
|
|
@@ -1300,22 +1301,41 @@
|
|
|
1300
1301
|
}
|
|
1301
1302
|
},
|
|
1302
1303
|
"bold_italic": {
|
|
1303
|
-
"
|
|
1304
|
-
"
|
|
1305
|
-
|
|
1306
|
-
"
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1304
|
+
"comment": "BOTH NESTING ORDERS, because the engine renders them identically: `some /*both*/ text` and `some */both/* text` are each `<strong><em>both</em></strong>` (carve 0.1.5). Only the canonical order had a rule, so the mirrored one fell through to #strong and came back a bold run containing two literal slashes - a reading that says the document holds something it does not, and one no sweep could see because the row sampled the order this rule got right (carve-grammars#375). THE MIRRORED OPENER NEEDS A FLANKING GUARD AND THE CANONICAL ONE DOES NOT, and that asymmetry is the engine's: `x/*b*/y` IS bold-italic and `x*/b/*y` is `x*<em>b</em>*y`, because the mirrored opener leads with `*` and a `*` glued to a word opens nothing. Same at the other end: `x/*b*/y` closes into a word and `a */b/*y c` does not. CONTENT IS NON-SPACE AT BOTH ENDS in both orders. `a /* b */ c` is `<em>* b *</em>` and `a /*b */ c` is `<em>*b *</em>` - italic runs holding literal asterisks, not combined runs - and this rule read both as one combined run until the two guards arrived with the mirrored order. WHAT NEITHER ORDER COVERS: a body holding the OTHER order's delimiter. The engine reads `a /*b*c*/ d` and `a */b/c/* d` as combined runs and both branches decline them, which is the under-colouring side of the trade on the mirrored side - carve-grammars#382. BOTH BODIES ARE TEMPERED, BY DIFFERENT AMOUNTS, and the asymmetry was measured rather than chosen (carve-grammars#382). The canonical body admits any `*` that does not start the closer, which is what lets `a /*b*c*/ d` read as the combined run the engine renders. The MIRRORED body may not do the same: a `/` that does not start the closer is still ambiguous with a canonical opener, so its `/` is admitted only BETWEEN TWO WORD CHARACTERS - `a */b/c/* d` is claimed and `x *///* y` stays bold. The opener's `(?!\\*[\\s*])` is what pays for it. Over all 21,844 documents `x */ body /* y` for every body of up to seven characters drawn from `* / space a`, judged by asking whether the engine renders the WHOLE body as one combined run: strict was 440 over-coloured and 603 missed; admitting any non-closing slash is 5,091 and 161; the word-flanked slash alone is 510 and 461; the word-flanked slash with the opener guard is 226 and 461. The temper alone RAISES over-colouring and every shape it adds is one the opener guard refuses - `a */**a/a/* d` and `a */* a/a/* d` are bold runs in the engine - while the guard itself costs nothing, since `missed` is unchanged with and without it. TWO MORE GUARDS THE MIRRORED BRANCH NEEDS AND THE CANONICAL ONE DOES NOT, both found by probing the shapes where one order's delimiters sit inside the other's. A `/` before the opener makes the pair ambiguous with a canonical opener - `a /*/a/* b` is `a <em>*/a</em>* b`, an italic run - so the left guard refuses it as well as a word character and a `*`. And the closer may not follow a `*`: `a */b*/* c` is `a <strong>/b</strong>/* c` and `a */*a*/* b` is `a <strong>/*a</strong>/* b`, both bold runs, while `a */*b/* c` IS combined - so it is the character before the closer that decides, not the one after the opener.",
|
|
1305
|
+
"patterns": [
|
|
1306
|
+
{
|
|
1307
|
+
"match": "(/\\*)(?=\\S)((?:[^*\\n]|\\*(?!/)){1,4096})(?<=\\S)(\\*/)",
|
|
1308
|
+
"captures": {
|
|
1309
|
+
"1": {
|
|
1310
|
+
"name": "punctuation.definition.bold-italic.carve"
|
|
1311
|
+
},
|
|
1312
|
+
"2": {
|
|
1313
|
+
"name": "markup.bold.italic.carve"
|
|
1314
|
+
},
|
|
1315
|
+
"3": {
|
|
1316
|
+
"name": "punctuation.definition.bold-italic.carve"
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1310
1319
|
},
|
|
1311
|
-
|
|
1312
|
-
"
|
|
1320
|
+
{
|
|
1321
|
+
"match": "(?<![\\w*/])(\\*/)(?=\\S)(?!\\*[\\s*])((?:[^/\\n]|(?<=\\w)/(?=\\w))+)(?<![\\s*])(/\\*)(?!\\w)",
|
|
1322
|
+
"captures": {
|
|
1323
|
+
"1": {
|
|
1324
|
+
"name": "punctuation.definition.bold-italic.carve"
|
|
1325
|
+
},
|
|
1326
|
+
"2": {
|
|
1327
|
+
"name": "markup.bold.italic.carve"
|
|
1328
|
+
},
|
|
1329
|
+
"3": {
|
|
1330
|
+
"name": "punctuation.definition.bold-italic.carve"
|
|
1331
|
+
}
|
|
1313
1332
|
}
|
|
1314
|
-
|
|
1333
|
+
}
|
|
1334
|
+
]
|
|
1315
1335
|
},
|
|
1316
1336
|
"strong": {
|
|
1317
1337
|
"begin": "(?<![\\w*])(\\*)(?=\\S)",
|
|
1318
|
-
"end": "(?<=\\S)(\\*)(?![\\w*])",
|
|
1338
|
+
"end": "(?<=\\S)(\\*)(?![\\w*])|^[ \\t]*$",
|
|
1319
1339
|
"beginCaptures": {
|
|
1320
1340
|
"1": {
|
|
1321
1341
|
"name": "punctuation.definition.bold.carve"
|
|
@@ -1326,7 +1346,7 @@
|
|
|
1326
1346
|
"name": "punctuation.definition.bold.carve"
|
|
1327
1347
|
}
|
|
1328
1348
|
},
|
|
1329
|
-
"contentName": "markup.bold.carve",
|
|
1349
|
+
"comment": "A BLANK LINE ENDS THE RUN, because an opener with no closer must not colour the rest of the file (carve-grammars#393). This is the ONLY bare inline rule spelled begin/end rather than as one `match`, and it has to be: a TextMate `match` cannot cross a line break at all - measured on a two-rule grammar - while the engine does read `a *b` over `c* d` as one bold run, which this rule gets right and its four `match` siblings get wrong. So the two properties trade off, and the trade is what the blank line bounds: without it `a *b c` scoped the next paragraph and the heading after it, and now it stops at the paragraph. WHAT IT DOES NOT FIX: a genuinely unclosed opener still colours to the end of its own paragraph, because `begin` cannot see whether a closer exists on a later line and TextMate offers no way to ask. Pinned in tests/bold-unclosed-test.js rather than left silent. MEASURE THIS RULE WITH THE RAW TOKENIZER: Shiki reports the fix as having no effect at all (carve-grammars#395), so a test written against it would call a correct grammar broken.", "contentName": "markup.bold.carve",
|
|
1330
1350
|
"patterns": [
|
|
1331
1351
|
{
|
|
1332
1352
|
"include": "#braced_comment"
|
|
@@ -1334,6 +1354,7 @@
|
|
|
1334
1354
|
]
|
|
1335
1355
|
},
|
|
1336
1356
|
"italic": {
|
|
1357
|
+
"comment": "ONE `match` AND NOT `begin`/`end`, DELIBERATELY, and the trade was measured rather than assumed (carve-grammars#397). A TextMate `match` cannot cross a line break, so this rule does not see `a /b` over `c/ d` as one run the way the engine does, and it cannot hold a delimiter standing after a space either. `begin`/`end` buys both - and opens eagerly, because `begin` cannot see whether a closer exists on a later line, so an opener with no closer colours to the end of its paragraph the way `strong` still does. Converted and swept over the 1,545-document corpus, all four bare rules at once: documents holding a run this grammar MISSES fell from 13 to 9, and documents where it claims a run the document does NOT hold rose from 3 to 35. The whole corpus contains two runs that span a line break. A false run is the worse direction - carve-grammars#324 and #325 both turned on that - so the `match` spelling stays and the missed multi-line run is the price. Narrowing the openers first (`(?<![\\\\w:/])` for the URL, `(?!_)` for the dunder, `(?!>)` for the arrow) took the false count from 40 to 35 and no further.",
|
|
1337
1358
|
"match": "(?<![\\w/])(/)(?=\\S)([^/\\n]+?)(?<=\\S)(/)(?![\\w/])",
|
|
1338
1359
|
"captures": {
|
|
1339
1360
|
"1": {
|
|
@@ -1348,6 +1369,7 @@
|
|
|
1348
1369
|
}
|
|
1349
1370
|
},
|
|
1350
1371
|
"underline": {
|
|
1372
|
+
"comment": "ONE `match` AND NOT `begin`/`end`, deliberately: converting all four bare rules took corpus documents carrying a FALSE run from 3 to 35 to recover 4 missed ones. The measurement and the reasoning are on `italic` (carve-grammars#397).",
|
|
1351
1373
|
"match": "(?<![\\w_])(_)(?=\\S)([^_\\n]+?)(?<=\\S)(_)(?![\\w_])",
|
|
1352
1374
|
"captures": {
|
|
1353
1375
|
"1": {
|
|
@@ -1362,6 +1384,7 @@
|
|
|
1362
1384
|
}
|
|
1363
1385
|
},
|
|
1364
1386
|
"strike": {
|
|
1387
|
+
"comment": "ONE `match` AND NOT `begin`/`end`, deliberately: converting all four bare rules took corpus documents carrying a FALSE run from 3 to 35 to recover 4 missed ones. The measurement and the reasoning are on `italic` (carve-grammars#397).",
|
|
1365
1388
|
"match": "(?<![\\w~])(~)(?=\\S)([^~\\n]+?)(?<=\\S)(~)(?![\\w~])",
|
|
1366
1389
|
"captures": {
|
|
1367
1390
|
"1": {
|
|
@@ -1376,7 +1399,7 @@
|
|
|
1376
1399
|
}
|
|
1377
1400
|
},
|
|
1378
1401
|
"highlight": {
|
|
1379
|
-
"comment": "A BARE `=` THAT BEGINS OR ENDS A SMART-TYPOGRAPHY PATTERN IS NOT A HIGHLIGHT OPENER (grammar.ebnf, Inline parsing precedence: \"a delimiter that begins a multi-char smart-typography pattern: `=>` is the arrow, never a highlight opener - the pattern is consumed first\"). Corpus 386's third paragraph is the shape it costs: `Not an arrow: key => value stays literal, and p <= q is a comparison.` renders with NO mark, and the opener `=` of `=>` reaching the closer `=` of `<=` scoped the whole 68-character sentence as a highlight (carve-grammars#325). That is worse than leaving a run uncoloured, because the output then claims the document holds a construct it does not - the same reason carve-grammars#324 gave for the arrows. THE GUARD IS ONLY WHAT THIS GRAMMAR MEASURABLY NEEDS. The opener may not be FOLLOWED by `>`, which is `=>` and the tail of
|
|
1402
|
+
"comment": "A BARE `=` THAT BEGINS OR ENDS A SMART-TYPOGRAPHY PATTERN IS NOT A HIGHLIGHT OPENER (grammar.ebnf, Inline parsing precedence: \"a delimiter that begins a multi-char smart-typography pattern: `=>` is the arrow, never a highlight opener - the pattern is consumed first\"). Corpus 386's third paragraph is the shape it costs: `Not an arrow: key => value stays literal, and p <= q is a comparison.` renders with NO mark, and the opener `=` of `=>` reaching the closer `=` of `<=` scoped the whole 68-character sentence as a highlight (carve-grammars#325). That is worse than leaving a run uncoloured, because the output then claims the document holds a construct it does not - the same reason carve-grammars#324 gave for the arrows. THE GUARD IS ONLY WHAT THIS GRAMMAR MEASURABLY NEEDS. The opener may not be FOLLOWED by `>`, which is `=>` and the tail of `==>`. It needs no guard against `<`, `>` or `!` behind it, and carried one for each until it was reverted and measured: vscode-textmate resolves by POSITION, so on `a <=b c= d` the typography rule's `<=` starts one column before the highlight rule's `=` and takes it. `!` was the last of the three to go, and it went in carve-grammars#374 rather than with the other two: while the typography alternation carried no `!=`, nothing reached `a !=b c= d` before this rule and removing the guard scoped `b c` as a highlight. The alternation carries `!=` now, so the comparison starts one column early like the other two and the guard stops changing any answer. What holds the shape is the pair that measures it - the `!=` row in tests/lib/constructs.js and `a !=b c= d` in tests/highlight-opener-test.js - rather than a lookbehind nobody can see working. Removing it also stopped an under-colouring a lookbehind cannot avoid: `a \\!=b c= d` escapes the `!`, so the `=` after it is an ordinary opener and the engine marks `b c`; this grammar refused it while the guard was here. The Prism grammar still does, and still needs its own guard because Prism applies tokens IN ORDER rather than by position - carve-grammars#380. A leading `=` needs no guard either - the content class already refuses one. THE CLOSER IS DELIBERATELY NOT GUARDED AGAINST SMART TYPOGRAPHY, and the asymmetry is the engine's: once a highlight is OPEN the closer wins over the pattern, so `x =y z<= w` marks `y z<` and `x =y z=> w` marks `y z`. Measured against the pinned engine, both. AN ESCAPE IS A DIFFERENT QUESTION (carve-grammars#385): an escaped `=` is not a delimiter at all, so `x =\\= y` renders `x == y` with no mark and this rule closed on it. THE BODY CONSUMES AN ESCAPE AS A PAIR (`\\\\.`) and refuses a bare backslash, which settles both directions at once - the escaped `=` is eaten before the closer can see it, and a body that could not hold one would stop there and never reach the real closer past it, so `a =b c\\= d= e`, which the engine marks, would have gone from wrongly-coloured to uncoloured. The closer needs no guard of its own, and that is a consequence rather than a simplification: the body can only get past a backslash by taking the pair, so the position immediately before an escaped `=` is never a body end. #385 COUNTED THE BACKSLASH RUN IN A LOOKBEHIND INSTEAD, and carve-grammars#390 replaced it for two measured reasons. The IDE's TextMate engine refuses a variable-length lookbehind by not matching ANYTHING - no error, the rule simply goes inert and `x =b= y` loses its mark too (markup-carve/intellij-carve#117), which made this file unusable in a TextMate engine Carve ships a plugin for. And every bound is reachable: at `{0,32}` the guard stopped seeing the run at 66 backslashes and refused a highlight the engine marks. A pair has no bound. THREE SHAPES STAY WRONG, pinned in tests/highlight-escape-test.js - `x =<== y`, `x =!== y` and `x =a== y`, a closer followed by another `=`, which the engine marks and `(?![=\\w])` refuses; widening that guard means letting the body hold its own delimiter. ONE SHAPE IT COSTS: an autolink or a cross-reference glued straight to a highlight (`<https://e.example>=hi=`) really does mark, because there the `>` closes the link rather than opening a comparison - a flat token map cannot tell the two apart within a fixed-width lookbehind, and this takes the under-colouring side of that trade on the ticket's own reasoning. ONE `match` AND NOT `begin`/`end`, deliberately: converting all four bare rules took corpus documents carrying a FALSE run from 3 to 35 to recover 4 missed ones. The measurement and the reasoning are on `italic` (carve-grammars#397).",
|
|
1380
1403
|
"patterns": [
|
|
1381
1404
|
{
|
|
1382
1405
|
"match": "(\\{=)(?=\\S)([^\\n]*?)(=\\})",
|
|
@@ -1393,7 +1416,7 @@
|
|
|
1393
1416
|
}
|
|
1394
1417
|
},
|
|
1395
1418
|
{
|
|
1396
|
-
"match": "(?<![
|
|
1419
|
+
"match": "(?<![=\\w])(=)(?=\\S)(?!>)((?:\\\\.|[^=\\n\\\\])+?)(?<=\\S)(=)(?![=\\w])",
|
|
1397
1420
|
"captures": {
|
|
1398
1421
|
"1": {
|
|
1399
1422
|
"name": "punctuation.definition.highlight.carve"
|
|
@@ -1510,7 +1533,7 @@
|
|
|
1510
1533
|
}
|
|
1511
1534
|
},
|
|
1512
1535
|
"smart_typography": {
|
|
1513
|
-
"match": "(
|
|
1536
|
+
"match": "(\\{--\\}|<-->|<--|-->|<=>|<==|==>|---|--|\\.\\.\\.|<->|<-|->|<=|>=|!=|\\+-|\\(c\\)|\\(r\\)|\\(tm\\))",
|
|
1514
1537
|
"name": "constant.other.smart_typography.carve"
|
|
1515
1538
|
},
|
|
1516
1539
|
"math_inline": {
|