@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.
@@ -26,15 +26,17 @@ const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
26
26
  * Every entry point `package.json` promises, and where its declarations landed.
27
27
  *
28
28
  * The declarations come from `tsc` rather than from the JavaScript bundler, so they
29
- * mirror the source tree: the root entry is one file, while `react` and `grammars`
30
- * are directories with an `index.d.ts` in them. That extra level is the price of not
31
- * depending on a declaration bundler that predates TypeScript 7, and it is encoded
32
- * here so a change to the layout cannot pass unnoticed.
29
+ * mirror the source tree: the root entry is one file, while `react` is a directory
30
+ * with an `index.d.ts` in it. That extra level is the price of not depending on a
31
+ * declaration bundler that predates TypeScript 7, and it is encoded here so a change
32
+ * to the layout cannot pass unnoticed.
33
+ *
34
+ * There is deliberately no `grammars` entry: the library ships no syntax, so it
35
+ * publishes no grammar for a host to import and none for this verifier to borrow.
33
36
  */
34
37
  const ENTRIES = [
35
38
  { name: 'index', declarations: 'types/index.d.ts' },
36
39
  { name: 'react', declarations: 'types/react/index.d.ts' },
37
- { name: 'grammars', declarations: 'types/grammars/index.d.ts' },
38
40
  { name: 'styles', declarations: 'types/styles.d.ts' },
39
41
  ]
40
42
 
@@ -74,17 +76,6 @@ const CORE_EXPORTS = [
74
76
  'canEditThroughPipeline',
75
77
  ]
76
78
 
77
- /** The names the grammars entry must export. */
78
- const GRAMMAR_EXPORTS = [
79
- 'dshFontQueryGrammar',
80
- 'dshSentryStyleGrammar',
81
- 'fontWeightWord',
82
- 'fontFaceWeights',
83
- 'quoteFontFamily',
84
- 'FONT_WEIGHT_WORDS',
85
- 'FONT_GENERIC_FAMILIES',
86
- ]
87
-
88
79
  /** The names the React entry must export. */
89
80
  const REACT_EXPORTS = ['LiteAreaEditor']
90
81
 
@@ -171,30 +162,63 @@ const core = await import(pathToFileURL(join(dist, 'index.js')).href)
171
162
  const missing = CORE_EXPORTS.filter((name) => core[name] === undefined)
172
163
  if (missing.length > 0) fail(`index.js does not export: ${missing.join(', ')}`)
173
164
 
174
- const grammars = await import(pathToFileURL(join(dist, 'grammars.js')).href)
175
- const missingGrammars = GRAMMAR_EXPORTS.filter((name) => grammars[name] === undefined)
176
- if (missingGrammars.length > 0) fail(`grammars.js does not export: ${missingGrammars.join(', ')}`)
177
-
178
165
  const react = await import(pathToFileURL(join(dist, 'react.js')).href)
179
166
  const missingReact = REACT_EXPORTS.filter((name) => react[name] === undefined)
180
167
  if (missingReact.length > 0) fail(`react.js does not export: ${missingReact.join(', ')}`)
181
168
 
182
169
  // ── the engine works from the built artifact, not just from source ─────────
170
+ //
171
+ // The grammar below is a FIXTURE and lives only in this file. The package ships
172
+ // no syntax, so there is nothing to import: a verifier that borrowed a language
173
+ // from somewhere else would be checking that language rather than the artifact
174
+ // it is here to check. Two rules and one vocabulary are enough to prove the
175
+ // built engine still tokenizes, diagnoses, completes, and segments.
176
+
177
+ /** A closed set of shapes, small enough to read at a glance. */
178
+ const SHAPES = core.defineVocabulary({
179
+ id: 'shape',
180
+ words: ['circle', 'square'],
181
+ unknownMessage: '"{word}" is not a shape — expected {allowed}.',
182
+ docs: { circle: { detail: 'a disc', body: 'Verifier fixture documentation.' } },
183
+ })
184
+
185
+ /** The whole language: one keyword, one vocabulary, one completion source. */
186
+ const fixture = core.defineGrammar({
187
+ id: 'verify-fixture',
188
+ rules: [
189
+ { kind: 'match', scope: 'keyword', pattern: /draw/ },
190
+ { kind: 'words', words: SHAPES, unknown: {} },
191
+ ],
192
+ compose: [
193
+ {
194
+ id: 'shape',
195
+ range: (context) => context.word,
196
+ items: () =>
197
+ ['circle', 'square'].map((shape) => ({
198
+ label: shape,
199
+ append: ' ',
200
+ kind: 'shape',
201
+ detail: SHAPES.entryFor(shape)?.detail,
202
+ })),
203
+ },
204
+ ],
205
+ })
183
206
 
184
- const grammar = grammars.dshSentryStyleGrammar()
185
- const inspection = core.inspect('running circle blue turn 3\nbogus circle', grammar)
207
+ const inspection = core.inspect('draw circle\nbogus square', fixture)
186
208
  assert.ok(inspection.tokens.length > 0, 'the built engine produced no tokens')
187
209
  assert.ok(
188
- inspection.diagnostics.some((diagnostic) => diagnostic.code === 'vocabulary:state'),
189
- 'the built engine did not report the unknown state',
210
+ inspection.diagnostics.some((diagnostic) => diagnostic.code === 'vocabulary:shape'),
211
+ 'the built engine did not report the unknown shape',
190
212
  )
191
- const completion = core.complete(inspection, grammar, {
192
- text: 'running ',
193
- caret: 'running '.length,
213
+ const completion = core.complete(core.inspect('draw ', fixture), fixture, {
214
+ text: 'draw ',
215
+ caret: 'draw '.length,
194
216
  trigger: 'explicit',
195
217
  })
196
218
  assert.ok(completion !== undefined, 'the built engine offered no completion')
197
219
  assert.ok(completion.rows.length > 0, 'the built engine offered an empty completion')
220
+ assert.equal(completion.sourceId, 'shape', 'the built engine opened the wrong completion source')
221
+ assert.equal(completion.rows[0]?.item.append, ' ', 'the built engine dropped a row field')
198
222
 
199
223
  const segments = core.buildSegments(inspection.text, inspection)
200
224
  assert.equal(
package/src/core/types.ts CHANGED
@@ -67,9 +67,9 @@ export interface Token extends Range {
67
67
  * tokens and they are deliberately not tokens here.
68
68
  *
69
69
  * The distinction earns its keep. A token is what the *characters* are; a
70
- * decoration is what they *mean*, and the two change on different schedules. The
71
- * font-query grammar paints `Geist Mono` as a family from the characters alone,
72
- * but which family is *in effect* depends on the installed catalogue — the same
70
+ * decoration is what they *mean*, and the two change on different schedules. A
71
+ * grammar can paint `Geist Mono` as a family from the characters alone, but which
72
+ * family is *in effect* depends on a catalogue the HOST supplied — the same
73
73
  * characters mean something else on another machine. Painting that as a token
74
74
  * would mean re-lexing the document whenever the catalogue changed; painting it
75
75
  * as a decoration means recomputing one range list, which is what it is.
@@ -493,8 +493,8 @@ export interface CompletionContext<State = unknown> {
493
493
  * `runn|` is no longer "at the start of the line" in the strict sense, but it is
494
494
  * unmistakably completing the first word. A source that asked `firstOnLine` would
495
495
  * switch itself off after the very first keystroke — which is exactly the bug this
496
- * field was added to fix, and exactly the kind of thing a reference grammar is
497
- * for finding.
496
+ * field was added to fix, and exactly the kind of thing a grammar written against
497
+ * a real document is for finding.
498
498
  */
499
499
  firstWord: boolean
500
500
  /** The first non-whitespace token on the caret's line, when there is one. */
package/src/dom/editor.ts CHANGED
@@ -94,9 +94,14 @@ export interface LiteAreaCompletion {
94
94
  /**
95
95
  * Characters that open the list in addition to word characters.
96
96
  *
97
- * Default `' '`, and that default is doing real work: both reference languages
98
- * put a value after a name, so a space is exactly where the next word becomes
99
- * guessable.
97
+ * Empty by default, because a separator is the wrong moment to interrupt: it is
98
+ * where the next token starts, which is the argument FOR opening there, and it
99
+ * loses to the habit every reader already has — the editor this is modelled on
100
+ * offers nothing on a space and waits for a letter or for `Ctrl+Space`. Nothing
101
+ * is stranded by that. The list stays open once it is open, so a value typed
102
+ * after a name is still filtered in place, and `Ctrl+Space` opens it on demand.
103
+ * Set it to `' '` or `','` for a language where a separator really is where the
104
+ * next token becomes guessable.
100
105
  */
101
106
  triggerCharacters?: string
102
107
  /** The most rows to offer. Default 100. */
@@ -289,11 +294,16 @@ export class LiteArea<State = unknown> {
289
294
  { autoGrow: true, minRows: 1 },
290
295
  options.sizing as Partial<ResolvedSizing> | undefined,
291
296
  )
297
+ // A word character opens the list; a separator does not, unless the grammar's
298
+ // host asks for one by name. The list does not need a separator to be helpful
299
+ // — it is already open while the next token is typed, and it re-filters in
300
+ // place — and opening on a space spends the one keystroke a reader is least
301
+ // willing to have interrupted.
292
302
  this.completion =
293
303
  options.completion === false
294
304
  ? undefined
295
305
  : withDefaults<ResolvedCompletion>(
296
- { auto: true, triggerCharacters: ' ', limit: 100, showDocumentation: true },
306
+ { auto: true, triggerCharacters: '', limit: 100, showDocumentation: true },
297
307
  options.completion,
298
308
  )
299
309
  this.hover =
package/src/index.ts CHANGED
@@ -6,12 +6,13 @@
6
6
  //
7
7
  // The package has three entry points and this is the largest:
8
8
  //
9
- // @citisen/litearea the engine and the DOM layer (this file)
10
- // @citisen/litearea/react a React binding over the same editor
11
- // @citisen/litearea/grammars two worked grammars for two real DSLs
9
+ // @citisen/litearea the engine and the DOM layer (this file)
10
+ // @citisen/litearea/react a React binding over the same editor
11
+ // @citisen/litearea/styles.css the stylesheet, for hosts that link CSS
12
12
  //
13
- // Nothing here knows any syntax. `src/core/` is pure and needs no DOM; `src/dom/`
14
- // needs a document and no framework.
13
+ // Nothing here knows any syntax, and the package ships none: a caller supplies
14
+ // the rules. `src/core/` is pure and needs no DOM; `src/dom/` needs a document
15
+ // and no framework.
15
16
 
16
17
  export type {
17
18
  CheckRule,