@citisen/litearea 0.1.0 → 0.2.1
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 +39 -18
- package/README.zh.md +25 -11
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/types/core/types.d.ts +5 -5
- package/dist/types/dom/editor.d.ts +8 -3
- package/dist/types/dom/editor.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/docs/architecture.md +10 -6
- package/docs/completion.md +13 -5
- package/docs/grammar.md +16 -15
- package/package.json +1 -6
- package/scripts/browser-check.mjs +133 -54
- package/scripts/verify-package.mjs +51 -27
- package/src/core/types.ts +5 -5
- package/src/dom/editor.ts +14 -4
- package/src/index.ts +6 -5
- package/dist/grammars.cjs +0 -1228
- package/dist/grammars.cjs.map +0 -1
- package/dist/grammars.js +0 -1213
- package/dist/grammars.js.map +0 -1
- package/dist/types/grammars/dshFont.d.ts +0 -127
- package/dist/types/grammars/dshFont.d.ts.map +0 -1
- package/dist/types/grammars/dshSentry.d.ts +0 -84
- package/dist/types/grammars/dshSentry.d.ts.map +0 -1
- package/dist/types/grammars/index.d.ts +0 -3
- package/dist/types/grammars/index.d.ts.map +0 -1
- package/src/grammars/dshFont.ts +0 -1004
- package/src/grammars/dshSentry.ts +0 -742
- package/src/grammars/index.ts +0 -57
|
@@ -36,9 +36,9 @@ export interface Token extends Range {
|
|
|
36
36
|
* tokens and they are deliberately not tokens here.
|
|
37
37
|
*
|
|
38
38
|
* The distinction earns its keep. A token is what the *characters* are; a
|
|
39
|
-
* decoration is what they *mean*, and the two change on different schedules.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* decoration is what they *mean*, and the two change on different schedules. A
|
|
40
|
+
* grammar can paint `Geist Mono` as a family from the characters alone, but which
|
|
41
|
+
* family is *in effect* depends on a catalogue the HOST supplied — the same
|
|
42
42
|
* characters mean something else on another machine. Painting that as a token
|
|
43
43
|
* would mean re-lexing the document whenever the catalogue changed; painting it
|
|
44
44
|
* as a decoration means recomputing one range list, which is what it is.
|
|
@@ -431,8 +431,8 @@ export interface CompletionContext<State = unknown> {
|
|
|
431
431
|
* `runn|` is no longer "at the start of the line" in the strict sense, but it is
|
|
432
432
|
* unmistakably completing the first word. A source that asked `firstOnLine` would
|
|
433
433
|
* switch itself off after the very first keystroke — which is exactly the bug this
|
|
434
|
-
* field was added to fix, and exactly the kind of thing a
|
|
435
|
-
* for finding.
|
|
434
|
+
* field was added to fix, and exactly the kind of thing a grammar written against
|
|
435
|
+
* a real document is for finding.
|
|
436
436
|
*/
|
|
437
437
|
firstWord: boolean;
|
|
438
438
|
/** The first non-whitespace token on the caret's line, when there is one. */
|
|
@@ -31,9 +31,14 @@ export interface LiteAreaCompletion {
|
|
|
31
31
|
/**
|
|
32
32
|
* Characters that open the list in addition to word characters.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Empty by default, because a separator is the wrong moment to interrupt: it is
|
|
35
|
+
* where the next token starts, which is the argument FOR opening there, and it
|
|
36
|
+
* loses to the habit every reader already has — the editor this is modelled on
|
|
37
|
+
* offers nothing on a space and waits for a letter or for `Ctrl+Space`. Nothing
|
|
38
|
+
* is stranded by that. The list stays open once it is open, so a value typed
|
|
39
|
+
* after a name is still filtered in place, and `Ctrl+Space` opens it on demand.
|
|
40
|
+
* Set it to `' '` or `','` for a language where a separator really is where the
|
|
41
|
+
* next token becomes guessable.
|
|
37
42
|
*/
|
|
38
43
|
triggerCharacters?: string;
|
|
39
44
|
/** The most rows to offer. Default 100. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../src/dom/editor.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,UAAU,EACV,OAAO,EACP,SAAS,EACT,KAAK,EACL,QAAQ,EAET,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAMpD,OAAO,EAAqC,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGzF,OAAO,EAOL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,cAAc,CAAA;AAOrB,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../src/dom/editor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAgBA,YAAY,EACV,SAAS,EACT,UAAU,EACV,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,EACV,eAAe,EACf,UAAU,EACV,OAAO,EACP,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,SAAS,EACT,KAAK,EACL,UAAU,EACV,kBAAkB,EAClB,IAAI,EACJ,WAAW,EACX,SAAS,EACT,KAAK,EACL,SAAS,EACT,QAAQ,EACR,cAAc,EACd,KAAK,EACL,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,SAAS,EACT,WAAW,GACZ,MAAM,iBAAiB,CAAA;AAExB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEpE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAEpE,OAAO,EACL,KAAK,EACL,cAAc,EACd,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,MAAM,EACN,WAAW,EACX,UAAU,EACV,OAAO,EACP,UAAU,EACV,OAAO,EACP,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,IAAI,EACJ,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAElF,OAAO,EACL,aAAa,EACb,cAAc,GACf,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,WAAW,EACX,IAAI,EACJ,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,eAAe,EACf,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE/D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEnE,OAAO,EACL,eAAe,EACf,eAAe,EACf,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,aAAa,CAAA;AAEpB,cAAc,gBAAgB,CAAA"}
|
package/docs/architecture.md
CHANGED
|
@@ -265,11 +265,15 @@ of them are exactly why `scripts/browser-check.mjs` exists: a textarea's undo
|
|
|
265
265
|
stack has no API to read and no way to fake, so the only honest test is to make an
|
|
266
266
|
edit and undo it in a browser that has a real one.
|
|
267
267
|
|
|
268
|
-
The harness bundles the **shipped** `dist
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
The harness bundles the **shipped** `dist/index.js` — the package's one JavaScript
|
|
269
|
+
entry point — rather than `src/`, so what it drives is the artifact a host
|
|
270
|
+
installs, and it inlines it as an IIFE (Chromium refuses to fetch an ES module
|
|
271
|
+
from a `file://` URL). The language the checks run against is defined inside the
|
|
272
|
+
checklist itself, because the package ships none: proving that a particular
|
|
273
|
+
product DSL works in a browser belongs beside the plugin that owns it, while the
|
|
274
|
+
questions here are all about the engine. It drives the public API and the public
|
|
275
|
+
DOM only: it mounts editors, types through `execCommand('insertText')`, presses
|
|
276
|
+
keys, dispatches a `mousedown` on a row, and hovers a squiggle. The page
|
|
273
277
|
reports through `document.title`, with `error` and `unhandledrejection` listeners
|
|
274
278
|
that write to the same channel, because a page whose only output channel is its
|
|
275
279
|
title has to report its own failures through that channel — otherwise a syntax
|
|
@@ -311,6 +315,6 @@ again", which is all most hosts need.
|
|
|
311
315
|
| `contenteditable` | It makes the document a DOM tree that the browser edits in ways you do not control — nested elements, `<br>` for a blank line, pasted HTML — so the value would have to be re-serialised on every keystroke, which is the same re-render problem with more failure modes and no native undo semantics worth having |
|
|
312
316
|
| A second highlighting layer per range list (one for scopes, one for decorations, one for squiggles) | Each layer would have to position its own spans, so each would need its own idea of where a range starts, and a span positioned by measuring drifts as soon as the font, the wrapping, or the padding is even slightly different from what was measured. One merged stream cannot drift because nothing is positioned |
|
|
313
317
|
| Measuring the live field (`scrollHeight` after collapsing it) | It reflows the page and flickers on every keystroke — the measurement that is supposed to decide the height is what makes the height unstable. The offscreen mirror pays one extra layout and keeps the field's geometry out of the question |
|
|
314
|
-
| A bundled grammar (a default language, or a language registry) | The claim of the library is that the core knows no syntax. A default grammar would make that claim false, and it would be the thing every host inherited. The
|
|
318
|
+
| A bundled grammar (a default language, or a language registry) | The claim of the library is that the core knows no syntax. A default grammar would make that claim false, and it would be the thing every host inherited. The package therefore ships no grammar at all: a DSL lives beside the plugin that owns it and is handed to the editor as a value |
|
|
315
319
|
| Synthetic `beforeinput`/`InputEvent` edits | Untrusted, so the browser will not run them through the editing pipeline: the text changes and the history does not, which is worse than a documented `'direct'` fallback because it fails silently |
|
|
316
320
|
| Tracking the caret as a character index in one module and a screen position in another | Every position that crosses a module boundary is a character offset; pixels enter only in `src/dom/`, only to place a floating element, and never travel back inward. Two coordinate systems for one caret is what produced the range that "had already moved" |
|
package/docs/completion.md
CHANGED
|
@@ -34,7 +34,7 @@ two bugs that make a completer feel broken have somewhere to live:
|
|
|
34
34
|
| Event | What happens |
|
|
35
35
|
| --- | --- |
|
|
36
36
|
| A word character is typed | The list opens, in `'auto'` mode. The predicate is the grammar's `wordChars` |
|
|
37
|
-
| A character typed through `triggerCharacters` |
|
|
37
|
+
| A character typed through `triggerCharacters` | Also opens it, in `'auto'` mode. **Empty by default**, so a separator does not open the list — see below |
|
|
38
38
|
| More characters are typed while the list is open | Re-resolved: the same source answers again if it is still eligible, the range is recomputed from the caret, and the rows are re-filtered |
|
|
39
39
|
| A character is deleted while the list is open | Re-filtered. A backspace with no list open opens nothing, because a deletion is not a request for suggestions |
|
|
40
40
|
| `Ctrl+Space` / `Cmd+Space` | Opens with no needle (`'explicit'`), or closes the list when it is already open |
|
|
@@ -45,6 +45,14 @@ two bugs that make a completer feel broken have somewhere to live:
|
|
|
45
45
|
| The editor itself is writing | Nothing re-opens: the editor knows its own edit from the user's |
|
|
46
46
|
| Any `input` event while `readOnly` | Nothing |
|
|
47
47
|
|
|
48
|
+
`triggerCharacters` is empty by default, so a separator does not open the list. A
|
|
49
|
+
space is where the next token starts, which is the argument for opening there,
|
|
50
|
+
and it loses to the habit a reader already has: the editor this is modelled on
|
|
51
|
+
offers nothing on a space and waits for a letter or for `Ctrl+Space`. Nothing is
|
|
52
|
+
stranded by it — the list stays open while the next token is typed, so a value
|
|
53
|
+
after a name is still suggested in place — and a language whose separator really
|
|
54
|
+
is the moment the next token becomes guessable names it explicitly.
|
|
55
|
+
|
|
48
56
|
`auto: false` (or `completion: false` on the editor) switches off every row above
|
|
49
57
|
except `Ctrl+Space`, `Escape`, and the arrows; `showCompletions()` and
|
|
50
58
|
`hideCompletions()` drive the same open and close by hand, and `currentCompletion`
|
|
@@ -163,9 +171,9 @@ eligible while that head is being spelled out. `runn\|` is no longer "at the sta
|
|
|
163
171
|
of the line" in the strict sense — the line already has content — but it is
|
|
164
172
|
unmistakably completing the first word. A source that asked `firstOnLine` would
|
|
165
173
|
switch itself off after the very first keystroke, which is the bug the second field
|
|
166
|
-
exists to prevent.
|
|
167
|
-
reason, and it is the difference between a list that
|
|
168
|
-
disappearing.
|
|
174
|
+
exists to prevent. A line-oriented grammar's first-word completion asks
|
|
175
|
+
`firstWord` for exactly this reason, and it is the difference between a list that
|
|
176
|
+
helps and a list that keeps disappearing.
|
|
169
177
|
|
|
170
178
|
## Ranking
|
|
171
179
|
|
|
@@ -276,7 +284,7 @@ default would be guessing about every other language.
|
|
|
276
284
|
|
|
277
285
|
| VSCode | litearea |
|
|
278
286
|
| --- | --- |
|
|
279
|
-
| Typing triggers suggestions | A word character or one of `triggerCharacters` (
|
|
287
|
+
| Typing triggers suggestions | A word character or one of `triggerCharacters` (empty by default) opens the list; `auto: false` turns it off |
|
|
280
288
|
| `Ctrl+Space` | `Ctrl+Space`/`Cmd+Space` opens on demand, and closes when the list is already open |
|
|
281
289
|
| Typing filters the list | The needle is the text between the source's current range start and the caret, matched fuzzily |
|
|
282
290
|
| `Enter` accepts | `Enter` accepts the active row |
|
package/docs/grammar.md
CHANGED
|
@@ -76,9 +76,9 @@ A function is handed everything a lexical decision could need:
|
|
|
76
76
|
| `state` | Whatever `analyze` returned |
|
|
77
77
|
|
|
78
78
|
The `state` field is the one that earns the function form: it is how a scope can
|
|
79
|
-
depend on something the characters do not say.
|
|
80
|
-
paint a weight word as `weight` or `weight.missing`, which
|
|
81
|
-
|
|
79
|
+
depend on something the characters do not say. A grammar for a host's installed
|
|
80
|
+
fonts uses it to paint a weight word as `weight` or `weight.missing`, which
|
|
81
|
+
depends on the faces the host reported, not on the word.
|
|
82
82
|
|
|
83
83
|
### `match`: a regular expression
|
|
84
84
|
|
|
@@ -116,7 +116,7 @@ states the host actually has — costs one call, not one per token.
|
|
|
116
116
|
|
|
117
117
|
| Field | Default | Notes |
|
|
118
118
|
| --- | --- | --- |
|
|
119
|
-
| `max` | 4 | The most words one entry may span. A span longer than `max` is never attempted, and four covers
|
|
119
|
+
| `max` | 4 | The most words one entry may span. A span longer than `max` is never attempted, and four covers a name like `Source Han Serif SC`, which is four words |
|
|
120
120
|
|
|
121
121
|
The engine tries the longest span first and keeps the longest member it finds, so
|
|
122
122
|
a catalogue holding both `IBM Plex` and `IBM Plex Mono` resolves the longer name.
|
|
@@ -346,10 +346,10 @@ decides three things at once:
|
|
|
346
346
|
3. **Where a double click puts the selection**, because that is the platform's
|
|
347
347
|
own word test and the browser uses the same notion of a word the field does.
|
|
348
348
|
|
|
349
|
-
A language whose names contain a dot or a hyphen must say so.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
A language whose names contain a dot or a hyphen must say so. A font query uses
|
|
350
|
+
`/[\p{L}\p{N}_-]/u` so `-apple-system` is one word; a grammar whose names never
|
|
351
|
+
contain a dot leaves it out, so a stray `circle.` is not read as one unknown word
|
|
352
|
+
and reported as a name the user never typed.
|
|
353
353
|
|
|
354
354
|
The flags are normalised: `g` and `y` are stripped from the predicate, because
|
|
355
355
|
`RegExp.prototype.test` advances `lastIndex` on a global pattern and a predicate
|
|
@@ -442,7 +442,8 @@ diagnostic's `source` is always the grammar's `id`.
|
|
|
442
442
|
|
|
443
443
|
`report` is also the place a structural walk that ran in `analyze` publishes what
|
|
444
444
|
it found — record the ranges while the structure is known, and report them here.
|
|
445
|
-
|
|
445
|
+
That is the usual arrangement: `analyze` decides what is wrong and `validate`
|
|
446
|
+
hands the ranges over.
|
|
446
447
|
|
|
447
448
|
### Codes and severities
|
|
448
449
|
|
|
@@ -476,12 +477,12 @@ detail and the body. Decorations are clamped into the document, dropped when the
|
|
|
476
477
|
come out empty, and sorted — a grammar computing them from a stale parse cannot
|
|
477
478
|
paint a span nobody can see.
|
|
478
479
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
480
|
+
A host's "in effect" mark is the worked case. `Geist Mono` is painted as a family
|
|
481
|
+
from the characters alone — that is a token. Which family is *in effect* depends
|
|
482
|
+
on the host's installed catalogue, so the same characters mean something else on
|
|
483
|
+
another computer. Painting that as a token would mean re-lexing the document
|
|
484
|
+
whenever the catalogue changed; as a decoration it is one recomputed range list,
|
|
485
|
+
which is what it actually is.
|
|
485
486
|
|
|
486
487
|
Reach for `decorate` when the mark depends on something outside the document. If
|
|
487
488
|
nothing in your language does, do not add one — a decoration that could have been
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citisen/litearea",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A lightweight textarea-based code editor: native undo/redo, fully custom syntax highlighting, VSCode-style completion, diagnostics, and hover tooltips. No CodeMirror, no Monaco.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,11 +27,6 @@
|
|
|
27
27
|
"import": "./dist/react.js",
|
|
28
28
|
"require": "./dist/react.cjs"
|
|
29
29
|
},
|
|
30
|
-
"./grammars": {
|
|
31
|
-
"types": "./dist/types/grammars/index.d.ts",
|
|
32
|
-
"import": "./dist/grammars.js",
|
|
33
|
-
"require": "./dist/grammars.cjs"
|
|
34
|
-
},
|
|
35
30
|
"./styles.css": "./dist/styles.css",
|
|
36
31
|
"./package.json": "./package.json"
|
|
37
32
|
},
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* 4. **Auto-sizing.** Whether a box has a scrollbar is `scrollHeight` against
|
|
21
21
|
* `clientHeight`, measured after a real layout.
|
|
22
22
|
*
|
|
23
|
+
* The checks are about the ENGINE, so the language they run against is a FIXTURE
|
|
24
|
+
* defined inside the checklist itself. The package ships no syntax and no grammar
|
|
25
|
+
* to import: a harness that borrowed a real DSL would be checking that DSL rather
|
|
26
|
+
* than the artifact this exists to check, and proving a particular product DSL
|
|
27
|
+
* belongs beside the plugin that owns it.
|
|
28
|
+
*
|
|
23
29
|
* Usage:
|
|
24
30
|
* node scripts/browser-check.mjs [path/to/chrome]
|
|
25
31
|
*
|
|
@@ -88,20 +94,20 @@ if (browser === undefined) {
|
|
|
88
94
|
* The page is opened over `file://`, and Chromium refuses to fetch an ES module from
|
|
89
95
|
* a file URL — so the library is bundled to an IIFE and inlined. The entry is built
|
|
90
96
|
* from the SHIPPED files rather than from `src/`, so what this check drives is the
|
|
91
|
-
* artifact a host would actually install
|
|
97
|
+
* artifact a host would actually install — and it is the root entry ALONE, because
|
|
98
|
+
* that is all the package publishes.
|
|
92
99
|
* @returns the script source.
|
|
93
100
|
*/
|
|
94
101
|
async function bundle() {
|
|
95
102
|
const index = join(root, 'dist', 'index.js')
|
|
96
|
-
|
|
97
|
-
if (!existsSync(index) || !existsSync(grammars)) {
|
|
103
|
+
if (!existsSync(index)) {
|
|
98
104
|
console.error('browser-check: dist/ is missing; run `npm run build` first')
|
|
99
105
|
process.exit(1)
|
|
100
106
|
}
|
|
101
107
|
const { build } = await import('esbuild')
|
|
102
108
|
const result = await build({
|
|
103
109
|
stdin: {
|
|
104
|
-
contents: `export * from ${JSON.stringify(index)}\
|
|
110
|
+
contents: `export * from ${JSON.stringify(index)}\n`,
|
|
105
111
|
resolveDir: root,
|
|
106
112
|
sourcefile: 'litearea-browser-entry.js',
|
|
107
113
|
loader: 'js',
|
|
@@ -156,12 +162,57 @@ const CHECKLIST = String.raw`
|
|
|
156
162
|
field.dispatchEvent(new KeyboardEvent('keydown', { key: key, bubbles: true, cancelable: true }))
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
// ── the fixture grammar ─────────────────────────────────────────────
|
|
166
|
+
// The package ships no syntax, so the checklist defines the language it
|
|
167
|
+
// drives. This is a FIXTURE and not a product DSL: one closed vocabulary,
|
|
168
|
+
// three rules, one completion source, and it is published nowhere. The two
|
|
169
|
+
// checks that need a differently-shaped grammar — the naive one in section
|
|
170
|
+
// 10 and the bulky one in section 8 — define their own, for the same reason.
|
|
171
|
+
const SHAPES = api.defineVocabulary({
|
|
172
|
+
id: 'shape',
|
|
173
|
+
words: ['circle', 'square', 'rounded'],
|
|
174
|
+
scope: 'shape',
|
|
175
|
+
unknownMessage: '"{word}" is not a shape — expected {allowed}.',
|
|
176
|
+
docs: {
|
|
177
|
+
circle: { detail: 'a disc', body: 'Fixture documentation for circle.' },
|
|
178
|
+
square: { detail: 'four corners', body: 'Fixture documentation for square.' },
|
|
179
|
+
rounded: { detail: 'a rounded box', body: 'Fixture documentation for rounded.' },
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const fixture = api.defineGrammar({
|
|
184
|
+
id: 'browser-fixture',
|
|
185
|
+
name: 'browser fixture',
|
|
186
|
+
rules: [
|
|
187
|
+
// The number rule sits ABOVE the vocabulary: digits are word characters,
|
|
188
|
+
// so a words rule placed first would swallow and reject them.
|
|
189
|
+
{ kind: 'match', scope: 'keyword', pattern: /draw|fill/ },
|
|
190
|
+
{ kind: 'match', scope: 'number', pattern: /\d+(?:\.\d+)?/ },
|
|
191
|
+
{ kind: 'words', words: SHAPES, unknown: {} },
|
|
192
|
+
],
|
|
193
|
+
compose: [
|
|
194
|
+
{
|
|
195
|
+
id: 'shape',
|
|
196
|
+
range: (context) => context.word,
|
|
197
|
+
items: (context) => {
|
|
198
|
+
const written = context.text.slice(0, context.caret)
|
|
199
|
+
return ['circle', 'square', 'rounded'].map((shape) => {
|
|
200
|
+
// A shape the document already uses leads the list, which is what
|
|
201
|
+
// sortText is for: the ranking must put that group first without
|
|
202
|
+
// pretending its label starts with a zero.
|
|
203
|
+
const used = written.indexOf(shape) >= 0
|
|
204
|
+
return {
|
|
205
|
+
label: shape,
|
|
206
|
+
append: ' ',
|
|
207
|
+
kind: 'shape',
|
|
208
|
+
detail: SHAPES.entryFor(shape)?.detail,
|
|
209
|
+
documentation: SHAPES.entryFor(shape)?.body,
|
|
210
|
+
sortText: used ? '0' : '1',
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
165
216
|
})
|
|
166
217
|
|
|
167
218
|
;(async () => {
|
|
@@ -169,7 +220,7 @@ const CHECKLIST = String.raw`
|
|
|
169
220
|
check(typeof api.createEditor === 'function', 'the bundle did not expose createEditor')
|
|
170
221
|
|
|
171
222
|
// ── 1. the layer reproduces the document, and lines up with it ──────
|
|
172
|
-
const one = mount(
|
|
223
|
+
const one = mount(fixture, 'draw circle fill rounded 10')
|
|
173
224
|
const paint = paintOf(one)
|
|
174
225
|
check(
|
|
175
226
|
paint.textContent === one.input.value,
|
|
@@ -190,7 +241,7 @@ const CHECKLIST = String.raw`
|
|
|
190
241
|
// screen: a stylesheet rule of higher specificity once set text-decoration on every
|
|
191
242
|
// painted span, which quietly switched off every squiggle in the library. Nothing in
|
|
192
243
|
// the DOM reveals that, so the only honest check is the browser's own computed style.
|
|
193
|
-
const marked = mount(
|
|
244
|
+
const marked = mount(fixture, 'draw bogus 10')
|
|
194
245
|
const markedSpans = [...marked.element.querySelectorAll('.litearea-paint > span')]
|
|
195
246
|
const flaggedSpan = markedSpans.find((span) => span.className.indexOf('litearea-diag-error') >= 0)
|
|
196
247
|
check(flaggedSpan !== undefined, 'a rejected word must carry a severity class')
|
|
@@ -220,14 +271,14 @@ const CHECKLIST = String.raw`
|
|
|
220
271
|
// is the first thing a host asks about, so it is measured rather than asserted in prose.
|
|
221
272
|
// Georgia is chosen precisely because it is unlike the default in every metric.
|
|
222
273
|
const themed = mount(
|
|
223
|
-
|
|
224
|
-
'
|
|
274
|
+
fixture,
|
|
275
|
+
'draw circle 10\nfill square 20',
|
|
225
276
|
{
|
|
226
277
|
variables: {
|
|
227
278
|
font: 'Georgia, Times New Roman, serif',
|
|
228
279
|
'font-size': '15px',
|
|
229
280
|
'line-height': '26px',
|
|
230
|
-
'scope-
|
|
281
|
+
'scope-keyword': '#b91c1c',
|
|
231
282
|
accent: '#c2410c',
|
|
232
283
|
},
|
|
233
284
|
},
|
|
@@ -253,8 +304,8 @@ const CHECKLIST = String.raw`
|
|
|
253
304
|
'a themed layer must still reproduce the document',
|
|
254
305
|
)
|
|
255
306
|
const themedState = [...themed.element.querySelectorAll('.litearea-paint > span')]
|
|
256
|
-
.find((span) => span.className.indexOf('litearea-scope-
|
|
257
|
-
check(themedState !== undefined, 'the themed document must have a
|
|
307
|
+
.find((span) => span.className.indexOf('litearea-scope-keyword') >= 0)
|
|
308
|
+
check(themedState !== undefined, 'the themed document must have a keyword token')
|
|
258
309
|
if (themedState !== undefined) {
|
|
259
310
|
check(
|
|
260
311
|
getComputedStyle(themedState).color === 'rgb(185, 28, 28)',
|
|
@@ -280,10 +331,10 @@ const CHECKLIST = String.raw`
|
|
|
280
331
|
' > ' + one.input.clientHeight + ')',
|
|
281
332
|
)
|
|
282
333
|
const oneLine = one.input.offsetHeight
|
|
283
|
-
one.setValue('
|
|
334
|
+
one.setValue('draw circle 10\nfill square 20\ndraw rounded 30', true)
|
|
284
335
|
const threeLines = one.input.offsetHeight
|
|
285
336
|
check(threeLines > oneLine, 'more lines must grow the box (was ' + oneLine + ', now ' + threeLines + ')')
|
|
286
|
-
one.setValue('
|
|
337
|
+
one.setValue('draw circle 10', true)
|
|
287
338
|
const backToOne = one.input.offsetHeight
|
|
288
339
|
check(
|
|
289
340
|
Math.abs(backToOne - oneLine) <= 1,
|
|
@@ -292,8 +343,8 @@ const CHECKLIST = String.raw`
|
|
|
292
343
|
|
|
293
344
|
// ── 3. a maximum height clamps the box and introduces a scrollbar ───
|
|
294
345
|
const clamped = mount(
|
|
295
|
-
|
|
296
|
-
'
|
|
346
|
+
fixture,
|
|
347
|
+
'draw circle 1\nfill square 2\ndraw rounded 3\nfill circle 4',
|
|
297
348
|
{ sizing: { maxRows: 2 } },
|
|
298
349
|
)
|
|
299
350
|
check(
|
|
@@ -333,22 +384,22 @@ const CHECKLIST = String.raw`
|
|
|
333
384
|
notes.push('control: a direct value assignment is not undoable, as expected')
|
|
334
385
|
|
|
335
386
|
// ── 5. undo and redo survive a completion ──────────────────────────
|
|
336
|
-
const undoable = mount(
|
|
387
|
+
const undoable = mount(fixture, '')
|
|
337
388
|
undoable.focus()
|
|
338
|
-
document.execCommand('insertText', false, '
|
|
389
|
+
document.execCommand('insertText', false, 'rou')
|
|
339
390
|
check(
|
|
340
|
-
undoable.value === '
|
|
391
|
+
undoable.value === 'rou',
|
|
341
392
|
'typing through the editing pipeline must land (value is ' + JSON.stringify(undoable.value) + ')',
|
|
342
393
|
)
|
|
343
394
|
check(
|
|
344
395
|
undoable.currentCompletion !== undefined,
|
|
345
|
-
'typing a
|
|
396
|
+
'typing a shape prefix must open the completion list',
|
|
346
397
|
)
|
|
347
398
|
check(rowsOf(undoable).length > 0, 'the completion list must render rows')
|
|
348
399
|
const beforeCompletion = undoable.value
|
|
349
400
|
press(undoable.input, 'Enter')
|
|
350
401
|
check(
|
|
351
|
-
undoable.value.indexOf('
|
|
402
|
+
undoable.value.indexOf('rounded') === 0,
|
|
352
403
|
'Enter must accept the highlighted completion (value is ' + JSON.stringify(undoable.value) + ')',
|
|
353
404
|
)
|
|
354
405
|
check(
|
|
@@ -381,7 +432,7 @@ const CHECKLIST = String.raw`
|
|
|
381
432
|
// letter — so accepting the suggestion produced the completion followed by the
|
|
382
433
|
// leftover character. Every character must be typed SEPARATELY here, so that the list
|
|
383
434
|
// is filtered once per keystroke exactly as a person would drive it.
|
|
384
|
-
const grown = mount(
|
|
435
|
+
const grown = mount(fixture, '')
|
|
385
436
|
grown.focus()
|
|
386
437
|
document.execCommand('insertText', false, 'r')
|
|
387
438
|
check(grown.currentCompletion !== undefined, 'the first letter must open the list')
|
|
@@ -394,26 +445,28 @@ const CHECKLIST = String.raw`
|
|
|
394
445
|
)
|
|
395
446
|
press(grown.input, 'Enter')
|
|
396
447
|
check(
|
|
397
|
-
grown.value === '
|
|
448
|
+
grown.value === 'rounded ',
|
|
398
449
|
'accepting after two letters must replace BOTH of them (got ' + JSON.stringify(grown.value) + ')',
|
|
399
450
|
)
|
|
400
451
|
|
|
401
452
|
// The same thing with a longer word and a middle-of-document caret, because the
|
|
402
|
-
// off-by-one is easiest to miss when the range is not anchored at zero.
|
|
403
|
-
|
|
453
|
+
// off-by-one is easiest to miss when the range is not anchored at zero. The
|
|
454
|
+
// inserted letters are a subsequence of the word rather than its next letters, so
|
|
455
|
+
// the row is still offered and the replacement has to cover everything typed.
|
|
456
|
+
const midword = mount(fixture, 'draw circle 1\nrounded square 2')
|
|
404
457
|
midword.focus()
|
|
405
|
-
const lineStart = midword.input.value.indexOf('
|
|
458
|
+
const lineStart = midword.input.value.indexOf('rounded')
|
|
406
459
|
midword.setSelection(lineStart + 1)
|
|
407
|
-
document.execCommand('insertText', false, '
|
|
460
|
+
document.execCommand('insertText', false, 'ou')
|
|
408
461
|
press(midword.input, 'Enter')
|
|
409
462
|
check(
|
|
410
|
-
midword.input.value.split('\n')[1] === '
|
|
463
|
+
midword.input.value.split('\n')[1] === 'rounded square 2',
|
|
411
464
|
'completing a word in the middle of a document must leave no debris (line is ' +
|
|
412
465
|
JSON.stringify(midword.input.value.split('\n')[1]) + ')',
|
|
413
466
|
)
|
|
414
467
|
|
|
415
468
|
// ── 7. a repaint does not move the caret ───────────────────────────
|
|
416
|
-
const careful = mount(
|
|
469
|
+
const careful = mount(fixture, 'draw circle fill rounded 10\nfill square 20')
|
|
417
470
|
careful.focus()
|
|
418
471
|
const at = careful.input.value.indexOf('rounded')
|
|
419
472
|
careful.setSelection(at)
|
|
@@ -435,7 +488,7 @@ const CHECKLIST = String.raw`
|
|
|
435
488
|
)
|
|
436
489
|
|
|
437
490
|
// ── 7. the list opens, navigates, and dismisses ─────────────────────
|
|
438
|
-
const listed = mount(
|
|
491
|
+
const listed = mount(fixture, 'draw ')
|
|
439
492
|
listed.focus()
|
|
440
493
|
listed.setSelection(listed.input.value.length)
|
|
441
494
|
listed.showCompletions()
|
|
@@ -470,7 +523,7 @@ const CHECKLIST = String.raw`
|
|
|
470
523
|
// row rather than the scrollbar, so a keyboard user never saw it, and a mouse user had
|
|
471
524
|
// to scroll down to read it and back up to reach the next row. The fix is positional, so
|
|
472
525
|
// this asks the layout rather than the DOM.
|
|
473
|
-
const bulky =
|
|
526
|
+
const bulky = api.defineGrammar({
|
|
474
527
|
id: 'bulky',
|
|
475
528
|
rules: [{ kind: 'match', scope: 'word', pattern: /[a-z]+/ }],
|
|
476
529
|
wordChars: /[\p{L}]/u,
|
|
@@ -540,10 +593,10 @@ const CHECKLIST = String.raw`
|
|
|
540
593
|
}
|
|
541
594
|
|
|
542
595
|
// ── 9. the tooltip explains a problem ───────────────────────────────
|
|
543
|
-
const hovered = mount(
|
|
596
|
+
const hovered = mount(fixture, 'draw bogus 10', { hover: { enabled: true, delay: 0 } })
|
|
544
597
|
const flagged = [...hovered.element.querySelectorAll('.litearea-paint > span')]
|
|
545
598
|
.find((span) => span.className.indexOf('litearea-diag-') >= 0)
|
|
546
|
-
check(flagged !== undefined, 'an unknown
|
|
599
|
+
check(flagged !== undefined, 'an unknown shape must be underlined')
|
|
547
600
|
if (flagged !== undefined) {
|
|
548
601
|
const box = flagged.getBoundingClientRect()
|
|
549
602
|
hovered.input.dispatchEvent(new MouseEvent('mousemove', {
|
|
@@ -567,19 +620,42 @@ const CHECKLIST = String.raw`
|
|
|
567
620
|
check(tip.dataset.open === 'false', 'leaving the field must close the tooltip')
|
|
568
621
|
}
|
|
569
622
|
|
|
570
|
-
// ──
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
)
|
|
623
|
+
// ── 9b. the fixture's own completion offers its own vocabulary ─────
|
|
624
|
+
// This replaced a check that drove the dsh-font DSL in a browser. Proving
|
|
625
|
+
// that a particular product DSL works is the plugin's job and not the
|
|
626
|
+
// library's: a DSL belongs beside the plugin that owns it. What is left is
|
|
627
|
+
// the engine claim the old check was reaching for — rows carry an append
|
|
628
|
+
// and a sortText, and the list honours both.
|
|
629
|
+
const offered = mount(fixture, 'draw circle 1\nfill ')
|
|
630
|
+
offered.focus()
|
|
631
|
+
offered.setSelection(offered.input.value.length)
|
|
632
|
+
offered.showCompletions()
|
|
633
|
+
check(
|
|
634
|
+
rowsOf(offered).length === 3,
|
|
635
|
+
'the fixture completion must offer its whole vocabulary (got ' + rowsOf(offered).length + ')',
|
|
636
|
+
)
|
|
637
|
+
const offeredState = offered.currentCompletion
|
|
638
|
+
check(offeredState !== undefined, 'showCompletions must open the list over an empty word')
|
|
639
|
+
if (offeredState !== undefined) {
|
|
640
|
+
const firstRow = offeredState.rows[0]
|
|
641
|
+
check(
|
|
642
|
+
firstRow !== undefined && firstRow.item.sortText === '0',
|
|
643
|
+
'A ROW THAT LEADS BY sortText MUST LEAD THE LIST (got ' +
|
|
644
|
+
JSON.stringify(firstRow === undefined ? null : firstRow.item.label) + ')',
|
|
645
|
+
)
|
|
646
|
+
check(
|
|
647
|
+
firstRow !== undefined && firstRow.item.label === 'circle',
|
|
648
|
+
'the shape the document already uses must be offered first (got ' +
|
|
649
|
+
JSON.stringify(firstRow === undefined ? null : firstRow.item.label) + ')',
|
|
650
|
+
)
|
|
651
|
+
check(
|
|
652
|
+
offeredState.rows.some((row) => row.item.append === ' '),
|
|
653
|
+
'a row must carry the append a host would type next',
|
|
654
|
+
)
|
|
655
|
+
}
|
|
580
656
|
check(
|
|
581
|
-
paintOf(
|
|
582
|
-
'the
|
|
657
|
+
paintOf(offered).textContent === offered.input.value,
|
|
658
|
+
'the fixture layer must reproduce the document it is showing a list over',
|
|
583
659
|
)
|
|
584
660
|
|
|
585
661
|
// ── 10. resting on blank space must explain nothing ────────────────
|
|
@@ -589,10 +665,10 @@ const CHECKLIST = String.raw`
|
|
|
589
665
|
// an implementation detail and not an explanation of anything.
|
|
590
666
|
//
|
|
591
667
|
// The grammar here is naive ON PURPOSE: it describes every token it is handed, including
|
|
592
|
-
// whitespace. Testing this against
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
const naive =
|
|
668
|
+
// whitespace. Testing this against a grammar that happens to stay quiet about a scope it
|
|
669
|
+
// has no documentation for would prove nothing — the check has to exercise the ENGINE's
|
|
670
|
+
// refusal to ask, not a grammar's manners.
|
|
671
|
+
const naive = api.defineGrammar({
|
|
596
672
|
id: 'naive',
|
|
597
673
|
rules: [{ kind: 'match', scope: 'word', pattern: /[a-z]+/ }],
|
|
598
674
|
describe: (context) =>
|
|
@@ -665,6 +741,9 @@ const CHECKLIST = String.raw`
|
|
|
665
741
|
document.querySelectorAll('style[data-litearea-styles]').length + ')',
|
|
666
742
|
)
|
|
667
743
|
|
|
744
|
+
// A host's own write at the very end: it must be accepted without throwing,
|
|
745
|
+
// and it is deliberately not a check, because a direct assignment is exactly
|
|
746
|
+
// what the control above proved the pipeline does not record.
|
|
668
747
|
const edits = one.input.value
|
|
669
748
|
one.input.value = edits
|
|
670
749
|
|