@citisen/litearea 0.1.0

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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +514 -0
  3. package/README.zh.md +362 -0
  4. package/dist/grammars.cjs +1228 -0
  5. package/dist/grammars.cjs.map +1 -0
  6. package/dist/grammars.js +1213 -0
  7. package/dist/grammars.js.map +1 -0
  8. package/dist/index.cjs +3103 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.js +3040 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/react.cjs +3032 -0
  13. package/dist/react.cjs.map +1 -0
  14. package/dist/react.js +3010 -0
  15. package/dist/react.js.map +1 -0
  16. package/dist/styles.cjs +453 -0
  17. package/dist/styles.cjs.map +1 -0
  18. package/dist/styles.css +432 -0
  19. package/dist/styles.js +447 -0
  20. package/dist/styles.js.map +1 -0
  21. package/dist/types/core/complete.d.ts +70 -0
  22. package/dist/types/core/complete.d.ts.map +1 -0
  23. package/dist/types/core/format.d.ts +34 -0
  24. package/dist/types/core/format.d.ts.map +1 -0
  25. package/dist/types/core/grammar.d.ts +14 -0
  26. package/dist/types/core/grammar.d.ts.map +1 -0
  27. package/dist/types/core/hover.d.ts +23 -0
  28. package/dist/types/core/hover.d.ts.map +1 -0
  29. package/dist/types/core/index.d.ts +12 -0
  30. package/dist/types/core/index.d.ts.map +1 -0
  31. package/dist/types/core/inspect.d.ts +34 -0
  32. package/dist/types/core/inspect.d.ts.map +1 -0
  33. package/dist/types/core/rank.d.ts +82 -0
  34. package/dist/types/core/rank.d.ts.map +1 -0
  35. package/dist/types/core/scan.d.ts +51 -0
  36. package/dist/types/core/scan.d.ts.map +1 -0
  37. package/dist/types/core/segments.d.ts +44 -0
  38. package/dist/types/core/segments.d.ts.map +1 -0
  39. package/dist/types/core/text.d.ts +113 -0
  40. package/dist/types/core/text.d.ts.map +1 -0
  41. package/dist/types/core/types.d.ts +604 -0
  42. package/dist/types/core/types.d.ts.map +1 -0
  43. package/dist/types/core/vocabulary.d.ts +82 -0
  44. package/dist/types/core/vocabulary.d.ts.map +1 -0
  45. package/dist/types/dom/create.d.ts +17 -0
  46. package/dist/types/dom/create.d.ts.map +1 -0
  47. package/dist/types/dom/editing.d.ts +89 -0
  48. package/dist/types/dom/editing.d.ts.map +1 -0
  49. package/dist/types/dom/editor.d.ts +366 -0
  50. package/dist/types/dom/editor.d.ts.map +1 -0
  51. package/dist/types/dom/index.d.ts +9 -0
  52. package/dist/types/dom/index.d.ts.map +1 -0
  53. package/dist/types/dom/mirror.d.ts +107 -0
  54. package/dist/types/dom/mirror.d.ts.map +1 -0
  55. package/dist/types/dom/overlay.d.ts +52 -0
  56. package/dist/types/dom/overlay.d.ts.map +1 -0
  57. package/dist/types/dom/popup.d.ts +95 -0
  58. package/dist/types/dom/popup.d.ts.map +1 -0
  59. package/dist/types/dom/support.d.ts +41 -0
  60. package/dist/types/dom/support.d.ts.map +1 -0
  61. package/dist/types/dom/tooltip.d.ts +39 -0
  62. package/dist/types/dom/tooltip.d.ts.map +1 -0
  63. package/dist/types/grammars/dshFont.d.ts +127 -0
  64. package/dist/types/grammars/dshFont.d.ts.map +1 -0
  65. package/dist/types/grammars/dshSentry.d.ts +84 -0
  66. package/dist/types/grammars/dshSentry.d.ts.map +1 -0
  67. package/dist/types/grammars/index.d.ts +3 -0
  68. package/dist/types/grammars/index.d.ts.map +1 -0
  69. package/dist/types/index.d.ts +15 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/react/index.d.ts +91 -0
  72. package/dist/types/react/index.d.ts.map +1 -0
  73. package/dist/types/styles.d.ts +29 -0
  74. package/dist/types/styles.d.ts.map +1 -0
  75. package/docs/architecture.md +316 -0
  76. package/docs/completion.md +320 -0
  77. package/docs/grammar.md +823 -0
  78. package/package.json +105 -0
  79. package/scripts/browser-check.mjs +838 -0
  80. package/scripts/build-css.mjs +35 -0
  81. package/scripts/release.mjs +91 -0
  82. package/scripts/verify-package.mjs +253 -0
  83. package/src/core/complete.ts +286 -0
  84. package/src/core/format.ts +71 -0
  85. package/src/core/grammar.ts +40 -0
  86. package/src/core/hover.ts +129 -0
  87. package/src/core/index.ts +98 -0
  88. package/src/core/inspect.ts +198 -0
  89. package/src/core/rank.ts +317 -0
  90. package/src/core/scan.ts +720 -0
  91. package/src/core/segments.ts +185 -0
  92. package/src/core/text.ts +238 -0
  93. package/src/core/types.ts +681 -0
  94. package/src/core/vocabulary.ts +196 -0
  95. package/src/dom/create.ts +31 -0
  96. package/src/dom/editing.ts +213 -0
  97. package/src/dom/editor.ts +1143 -0
  98. package/src/dom/index.ts +46 -0
  99. package/src/dom/mirror.ts +305 -0
  100. package/src/dom/overlay.ts +106 -0
  101. package/src/dom/popup.ts +323 -0
  102. package/src/dom/support.ts +88 -0
  103. package/src/dom/tooltip.ts +112 -0
  104. package/src/grammars/dshFont.ts +1004 -0
  105. package/src/grammars/dshSentry.ts +742 -0
  106. package/src/grammars/index.ts +57 -0
  107. package/src/index.ts +122 -0
  108. package/src/react/index.tsx +248 -0
  109. package/src/styles.ts +529 -0
package/dist/react.js ADDED
@@ -0,0 +1,3010 @@
1
+ import * as React from 'react';
2
+
3
+ // src/react/index.tsx
4
+
5
+ // src/core/rank.ts
6
+ var TIER_STRIDE = 1e6;
7
+ var TIER = {
8
+ /** The needle is the label, in the same case. */
9
+ exact: 5,
10
+ /** The needle is the label, ignoring case. */
11
+ exactFold: 4,
12
+ /** The label starts with the needle. */
13
+ prefix: 3,
14
+ /**
15
+ * Every needle character either starts a word or continues one the match has
16
+ * already started. This is what makes initials work: `fm` finds `Fira Mono` and
17
+ * `IPM` finds `IBM Plex Mono`, without `IPM` being a substring of anything.
18
+ */
19
+ boundary: 2,
20
+ /** The needle appears as one unbroken run inside the label. */
21
+ substring: 1,
22
+ /** The needle's characters appear in order, with gaps. */
23
+ subsequence: 0
24
+ };
25
+ var SEPARATORS = /[\s\-_./:@()+[\]]/;
26
+ function isWordStart(label, index) {
27
+ if (index <= 0) return index === 0;
28
+ const previous = label.charAt(index - 1);
29
+ if (SEPARATORS.test(previous)) return true;
30
+ const current = label.charAt(index);
31
+ return previous === previous.toLowerCase() && current !== current.toLowerCase();
32
+ }
33
+ function wantsExactCase(needle) {
34
+ return needle !== needle.toLowerCase();
35
+ }
36
+ function localScore(label, needle, indices, exactCase) {
37
+ let score = 0;
38
+ for (let position = 0; position < indices.length; position += 1) {
39
+ const index = indices[position];
40
+ if (index === void 0) continue;
41
+ if (index === 0) score += 24;
42
+ else if (isWordStart(label, index)) score += 14;
43
+ if (position > 0 && index === (indices[position - 1] ?? -2) + 1) score += 10;
44
+ if (exactCase && label.charAt(index) === needle.charAt(position)) score += 4;
45
+ }
46
+ const first = indices[0] ?? 0;
47
+ const last = indices[indices.length - 1] ?? 0;
48
+ score -= (last - first + 1 - needle.length) * 1.5;
49
+ score -= first * 2;
50
+ score -= label.length * 0.05;
51
+ return score;
52
+ }
53
+ function fuzzyMatch(needle, label) {
54
+ if (needle === "") return { score: 0, indices: [], tier: 0 };
55
+ const exactCase = wantsExactCase(needle);
56
+ const haystack = exactCase ? label : label.toLowerCase();
57
+ const query = exactCase ? needle : needle.toLowerCase();
58
+ if (label === needle) {
59
+ return { score: TIER.exact * TIER_STRIDE + localScore(label, needle, range(needle.length), true), indices: range(needle.length), tier: TIER.exact };
60
+ }
61
+ if (label.toLowerCase() === query) {
62
+ const indices = range(needle.length);
63
+ return {
64
+ score: TIER.exactFold * TIER_STRIDE + localScore(label, needle, indices, exactCase),
65
+ indices,
66
+ tier: TIER.exactFold
67
+ };
68
+ }
69
+ if (haystack.startsWith(query)) {
70
+ const indices = range(needle.length);
71
+ return {
72
+ score: TIER.prefix * TIER_STRIDE + localScore(label, needle, indices, exactCase),
73
+ indices,
74
+ tier: TIER.prefix
75
+ };
76
+ }
77
+ let best;
78
+ const consider = (candidate) => {
79
+ if (best === void 0 || candidate.score > best.score) best = candidate;
80
+ };
81
+ for (let at = haystack.indexOf(query); at >= 0; at = haystack.indexOf(query, at + 1)) {
82
+ const indices = range(needle.length, at);
83
+ const tier = isWordStart(label, at) ? TIER.boundary : TIER.substring;
84
+ consider({ score: tier * TIER_STRIDE + localScore(label, needle, indices, exactCase), indices, tier });
85
+ }
86
+ for (let start = 0; start < haystack.length; start += 1) {
87
+ if (haystack.charAt(start) !== query.charAt(0)) continue;
88
+ const indices = [start];
89
+ let cursor = start + 1;
90
+ let complete2 = true;
91
+ for (let position = 1; position < query.length; position += 1) {
92
+ const found = haystack.indexOf(query.charAt(position), cursor);
93
+ if (found < 0) {
94
+ complete2 = false;
95
+ break;
96
+ }
97
+ indices.push(found);
98
+ cursor = found + 1;
99
+ }
100
+ if (!complete2) continue;
101
+ const tier = indicesAreBoundaryish(label, indices) ? TIER.boundary : TIER.subsequence;
102
+ consider({ score: tier * TIER_STRIDE + localScore(label, needle, indices, exactCase), indices, tier });
103
+ }
104
+ return best;
105
+ }
106
+ function indicesAreBoundaryish(label, indices) {
107
+ for (let position = 0; position < indices.length; position += 1) {
108
+ const index = indices[position];
109
+ if (index === void 0) continue;
110
+ if (isWordStart(label, index)) continue;
111
+ if (position > 0 && index === (indices[position - 1] ?? -2) + 1) continue;
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+ function range(count, from = 0) {
117
+ const out = [];
118
+ for (let index = 0; index < count; index += 1) out.push(from + index);
119
+ return out;
120
+ }
121
+ function rank(items, needle, options) {
122
+ const out = [];
123
+ for (const item of items) {
124
+ if (needle === "") {
125
+ out.push({ item, score: 0, indices: [] });
126
+ continue;
127
+ }
128
+ const label = options.label(item);
129
+ const subject = options.filterText?.(item) ?? label;
130
+ const match = fuzzyMatch(needle, subject);
131
+ if (match === void 0) continue;
132
+ out.push({ item, score: match.score, indices: subject === label ? match.indices : [] });
133
+ }
134
+ if (options.sortText === void 0) {
135
+ out.sort((left, right) => right.score - left.score);
136
+ return out;
137
+ }
138
+ return out.sort((left, right) => {
139
+ const leftKey = options.sortText?.(left.item) ?? options.label(left.item);
140
+ const rightKey = options.sortText?.(right.item) ?? options.label(right.item);
141
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : right.score - left.score;
142
+ });
143
+ }
144
+ function highlightSegments(label, indices) {
145
+ if (indices.length === 0) return label === "" ? [] : [{ text: label, matched: false }];
146
+ const marked = new Set(indices);
147
+ const segments = [];
148
+ let current;
149
+ for (let index = 0; index < label.length; index += 1) {
150
+ const matched = marked.has(index);
151
+ if (current === void 0 || current.matched !== matched) {
152
+ if (current !== void 0) segments.push(current);
153
+ current = { text: "", matched };
154
+ }
155
+ current.text += label.charAt(index);
156
+ }
157
+ if (current !== void 0) segments.push(current);
158
+ return segments;
159
+ }
160
+
161
+ // src/core/text.ts
162
+ function clamp(value, low, high) {
163
+ return value < low ? low : value > high ? high : value;
164
+ }
165
+ function lineStarts(source) {
166
+ const starts = [0];
167
+ for (let index = 0; index < source.length; index += 1) {
168
+ const code = source.charCodeAt(index);
169
+ if (code === 10) {
170
+ starts.push(index + 1);
171
+ } else if (code === 13) {
172
+ if (source.charCodeAt(index + 1) === 10) index += 1;
173
+ starts.push(index + 1);
174
+ }
175
+ }
176
+ return starts;
177
+ }
178
+ function lineIndexAt(starts, offset) {
179
+ let low = 0;
180
+ let high = starts.length - 1;
181
+ while (low < high) {
182
+ const middle = Math.ceil((low + high) / 2);
183
+ if ((starts[middle] ?? 0) <= offset) low = middle;
184
+ else high = middle - 1;
185
+ }
186
+ return low;
187
+ }
188
+ function lineAt(source, offset, starts) {
189
+ const position = clamp(offset, 0, source.length);
190
+ const boundaries = starts ?? lineStarts(source);
191
+ const index = lineIndexAt(boundaries, position);
192
+ const from = boundaries[index] ?? 0;
193
+ const rawTo = boundaries[index + 1] ?? source.length;
194
+ let to = rawTo;
195
+ while (to > from) {
196
+ const code = source.charCodeAt(to - 1);
197
+ if (code === 10 || code === 13) to -= 1;
198
+ else break;
199
+ }
200
+ const text = source.slice(from, to);
201
+ const column = clamp(position - from, 0, text.length);
202
+ return {
203
+ from,
204
+ to,
205
+ text,
206
+ number: index,
207
+ column,
208
+ before: text.slice(0, column),
209
+ after: text.slice(column)
210
+ };
211
+ }
212
+ function isWordChar(char, wordChars) {
213
+ return char !== "" && wordChars.test(char);
214
+ }
215
+ function wordInfoAt(source, offset, wordChars) {
216
+ const position = clamp(offset, 0, source.length);
217
+ let from = position;
218
+ let to = position;
219
+ while (from > 0 && isWordChar(source.charAt(from - 1), wordChars)) from -= 1;
220
+ while (to < source.length && isWordChar(source.charAt(to), wordChars)) to += 1;
221
+ const text = source.slice(from, to);
222
+ const column = position - from;
223
+ return {
224
+ from,
225
+ to,
226
+ text,
227
+ prefix: text.slice(0, column),
228
+ suffix: text.slice(column)
229
+ };
230
+ }
231
+ function containsOffset(range2, offset) {
232
+ return offset >= range2.from && offset < range2.to;
233
+ }
234
+ function tokenAt(tokens, offset) {
235
+ for (const token of tokens) {
236
+ if (token.from <= offset && offset < token.to) return token;
237
+ if (token.from > offset) break;
238
+ }
239
+ return void 0;
240
+ }
241
+ function scopeAt(tokens, offset) {
242
+ return tokenAt(tokens, offset)?.scope;
243
+ }
244
+ function tokenBefore(tokens, offset) {
245
+ let found;
246
+ for (const token of tokens) {
247
+ if (token.to > offset) break;
248
+ if (token.text.trim() !== "") found = token;
249
+ }
250
+ return found;
251
+ }
252
+ function tokensOnLine(tokens, line) {
253
+ const number = typeof line === "number" ? line : line.number;
254
+ return tokens.filter((token) => token.line === number && token.text.trim() !== "");
255
+ }
256
+
257
+ // src/core/format.ts
258
+ var DEFAULT_LIST_LIMIT = 12;
259
+ function listPhrase(items, options) {
260
+ const conjunction = "or";
261
+ const limit = DEFAULT_LIST_LIMIT;
262
+ const shown = items.slice(0, limit);
263
+ const rest = items.length - shown.length;
264
+ if (shown.length === 0) return "";
265
+ if (shown.length === 1) {
266
+ return rest > 0 ? `${String(shown[0])} and ${String(rest)} more` : String(shown[0]);
267
+ }
268
+ if (shown.length === 2) {
269
+ const pair = `${String(shown[0])} ${conjunction} ${String(shown[1])}`;
270
+ return rest > 0 ? `${pair}, and ${String(rest)} more` : pair;
271
+ }
272
+ const head = shown.slice(0, -1).join(", ");
273
+ const tail = shown[shown.length - 1];
274
+ const phrase = `${head}, ${conjunction} ${String(tail)}`;
275
+ return rest > 0 ? `${phrase}, and ${String(rest)} more` : phrase;
276
+ }
277
+
278
+ // src/core/vocabulary.ts
279
+ function asResolvedVocabulary(source) {
280
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return void 0;
281
+ const candidate = source;
282
+ return typeof candidate.resolve === "function" ? source : void 0;
283
+ }
284
+ function resolveWordsSource(source, context) {
285
+ const vocabulary = asResolvedVocabulary(source);
286
+ const raw = vocabulary !== void 0 ? vocabulary.resolve(context) : typeof source === "function" ? source(context) : source;
287
+ return Array.isArray(raw) ? raw.filter((word) => word !== "") : [];
288
+ }
289
+
290
+ // src/core/scan.ts
291
+ function isResolvedGrammar(value) {
292
+ return value.__resolved === true;
293
+ }
294
+ var DEFAULT_WORD_CHARS = /[\p{L}\p{N}_$]/u;
295
+ var stickyCache = /* @__PURE__ */ new WeakMap();
296
+ function sticky(pattern) {
297
+ const cached = stickyCache.get(pattern);
298
+ if (cached !== void 0) return cached;
299
+ const flags = pattern.flags.replace(/[gy]/g, "");
300
+ const compiled = new RegExp(pattern.source, `${flags}y`);
301
+ stickyCache.set(pattern, compiled);
302
+ return compiled;
303
+ }
304
+ function execAt(pattern, source, index) {
305
+ pattern.lastIndex = index;
306
+ const match = pattern.exec(source);
307
+ return match !== null && match.index === index ? match : null;
308
+ }
309
+ function withoutStatefulFlags(pattern) {
310
+ const flags = pattern.flags.replace(/[gy]/g, "");
311
+ return flags === pattern.flags ? pattern : new RegExp(pattern.source, flags);
312
+ }
313
+ function resolveGrammar(grammar) {
314
+ return {
315
+ __resolved: true,
316
+ grammar,
317
+ rules: grammar.rules,
318
+ fallbackScope: grammar.fallbackScope ?? "text",
319
+ wordChars: withoutStatefulFlags(grammar.wordChars ?? DEFAULT_WORD_CHARS)
320
+ };
321
+ }
322
+ function scan(source, grammar) {
323
+ const resolved = isResolvedGrammar(grammar) ? grammar : resolveGrammar(grammar);
324
+ const { rules, fallbackScope, wordChars } = resolved;
325
+ const declared = resolved.grammar;
326
+ const sourceId = declared.id;
327
+ const state = declared.analyze === void 0 ? declared.initialState : declared.analyze(source);
328
+ const vocabularyContext = { text: source, state };
329
+ const tokens = [];
330
+ const diagnostics = [];
331
+ const starts = lineStarts(source);
332
+ const length = source.length;
333
+ const wordsCache = /* @__PURE__ */ new Map();
334
+ let cachedLineNumber = -1;
335
+ let cachedLineBounds;
336
+ const prevNotCache = /* @__PURE__ */ new WeakMap();
337
+ const lineInfoAt = (position) => {
338
+ const number = lineIndexAt(starts, position);
339
+ if (number !== cachedLineNumber || cachedLineBounds === void 0) {
340
+ const info = lineAt(source, position, starts);
341
+ cachedLineBounds = { from: info.from, to: info.to, text: info.text };
342
+ cachedLineNumber = number;
343
+ }
344
+ const column = clamp(position - cachedLineBounds.from, 0, cachedLineBounds.text.length);
345
+ return {
346
+ from: cachedLineBounds.from,
347
+ to: cachedLineBounds.to,
348
+ text: cachedLineBounds.text,
349
+ number,
350
+ column,
351
+ before: cachedLineBounds.text.slice(0, column),
352
+ after: cachedLineBounds.text.slice(column)
353
+ };
354
+ };
355
+ const resolveWords = (from) => {
356
+ const cached = wordsCache.get(from);
357
+ if (cached !== void 0) return cached;
358
+ const vocabulary = asResolvedVocabulary(from);
359
+ const caseSensitive = vocabulary?.caseSensitive === true;
360
+ const raw = vocabulary !== void 0 ? vocabulary.resolve(vocabularyContext) : typeof from === "function" ? from(vocabularyContext) : from;
361
+ const list = Array.isArray(raw) ? raw.filter((word) => word !== "") : [];
362
+ const fold = (word) => caseSensitive ? word : word.toLowerCase();
363
+ const entry = {
364
+ list,
365
+ set: new Set(list.map(fold)),
366
+ literal: list.filter((word) => [...word].some((char) => !isWordChar(char, wordChars))).slice().sort((left, right) => right.length - left.length),
367
+ caseSensitive
368
+ };
369
+ wordsCache.set(from, entry);
370
+ return entry;
371
+ };
372
+ const scopeOf = (spec, match, fallback) => spec === void 0 ? fallback : typeof spec === "function" ? spec(match) : spec;
373
+ const contextHolds = (when, index2, previousScope2) => {
374
+ if (when === void 0) return true;
375
+ const info = lineInfoAt(index2);
376
+ if (when.firstOnLine === true && info.text.slice(0, info.column).trim() !== "") return false;
377
+ if (when.after !== void 0) {
378
+ if (previousScope2 === void 0 || !when.after.includes(previousScope2)) return false;
379
+ }
380
+ if (when.notAfter !== void 0 && previousScope2 !== void 0) {
381
+ if (when.notAfter.includes(previousScope2)) return false;
382
+ }
383
+ if (when.line !== void 0 && !when.line.test(info.text)) return false;
384
+ if (when.minColumn !== void 0 && info.column < when.minColumn) return false;
385
+ if (when.maxColumn !== void 0 && info.column > when.maxColumn) return false;
386
+ if (when.prevNot !== void 0 && index2 > 0) {
387
+ let pattern = prevNotCache.get(when);
388
+ if (pattern === void 0) {
389
+ pattern = new RegExp(`[${when.prevNot}]`);
390
+ prevNotCache.set(when, pattern);
391
+ }
392
+ if (pattern.test(source.charAt(index2 - 1))) return false;
393
+ }
394
+ return true;
395
+ };
396
+ let regionScope;
397
+ let previousScope;
398
+ const push = (scope, from, to, region) => {
399
+ if (to <= from) return;
400
+ const text = source.slice(from, to);
401
+ const last = tokens[tokens.length - 1];
402
+ if (last !== void 0 && last.scope === scope && last.to === from && last.region === region && !/[\r\n]/.test(text) && !/[\r\n]/.test(last.text)) {
403
+ last.to = to;
404
+ last.text = source.slice(last.from, to);
405
+ } else {
406
+ const info = lineInfoAt(from);
407
+ tokens.push({ from, to, scope, text, line: info.number, column: from - info.from, region });
408
+ }
409
+ if (text.trim() !== "") previousScope = scope;
410
+ };
411
+ const report = (problem) => {
412
+ diagnostics.push({
413
+ from: problem.from,
414
+ to: problem.to,
415
+ message: problem.message,
416
+ severity: problem.severity ?? "error",
417
+ code: problem.code ?? "lexical",
418
+ source: sourceId
419
+ });
420
+ };
421
+ const ruleMatch = (text, from, groups) => ({ text, source, from, groups, state });
422
+ const readWordRun = (index2) => {
423
+ let to = index2;
424
+ while (to < length && isWordChar(source.charAt(to), wordChars)) to += 1;
425
+ return to;
426
+ };
427
+ const readSegments = (index2, limit) => {
428
+ const segments = [];
429
+ let cursor = index2;
430
+ while (segments.length < limit && cursor < length) {
431
+ const to = readWordRun(cursor);
432
+ if (to === cursor) break;
433
+ segments.push({ from: cursor, to });
434
+ cursor = to;
435
+ const gap = /^[^\S\r\n]+/.exec(source.slice(cursor))?.[0];
436
+ if (gap === void 0) break;
437
+ cursor += gap.length;
438
+ }
439
+ return segments;
440
+ };
441
+ const matchWords = (rule, words, index2) => {
442
+ const fold = (word) => words.caseSensitive ? word : word.toLowerCase();
443
+ const firstTo = readWordRun(index2);
444
+ if (firstTo > index2) {
445
+ if (rule.phrase !== void 0) {
446
+ const segments = readSegments(index2, Math.max(rule.phrase.max ?? 4, 1));
447
+ for (let count = segments.length; count >= 1; count -= 1) {
448
+ const texts = segments.slice(0, count).map((segment) => source.slice(segment.from, segment.to));
449
+ const candidate = texts.join(" ");
450
+ if (words.set.has(fold(candidate))) {
451
+ const last = segments[count - 1];
452
+ if (last !== void 0) return { to: last.to, member: candidate };
453
+ }
454
+ }
455
+ } else {
456
+ const candidate = source.slice(index2, firstTo);
457
+ if (words.set.has(fold(candidate))) return { to: firstTo, member: candidate };
458
+ }
459
+ }
460
+ for (const member of words.literal) {
461
+ if (rule.phrase !== void 0 && /\s/.test(member)) continue;
462
+ if (source.startsWith(member, index2)) return { to: index2 + member.length, member };
463
+ }
464
+ return void 0;
465
+ };
466
+ const anyRuleClaims = (index2, previous) => {
467
+ const openRegion = openRegions[openRegions.length - 1];
468
+ if (openRegion !== void 0) {
469
+ const endMatch = execAt(sticky(openRegion.rule.end), source, index2);
470
+ if (endMatch !== null && endMatch[0].length > 0) return true;
471
+ }
472
+ for (const rule of rules) {
473
+ if (!contextHolds(rule.when, index2, previous)) continue;
474
+ if (rule.kind === "region") {
475
+ const match2 = execAt(sticky(rule.begin), source, index2);
476
+ if (match2 !== null && match2[0].length > 0) return true;
477
+ continue;
478
+ }
479
+ if (rule.kind === "words") {
480
+ if (matchWords(rule, resolveWords(rule.words), index2) !== void 0) return true;
481
+ if (rule.unknown !== void 0 && readWordRun(index2) > index2) return true;
482
+ continue;
483
+ }
484
+ const match = execAt(sticky(rule.pattern), source, index2);
485
+ if (match !== null && match[0].length > 0) return true;
486
+ }
487
+ return false;
488
+ };
489
+ const openRegions = [];
490
+ let index = 0;
491
+ while (index < length) {
492
+ const open = openRegions[openRegions.length - 1];
493
+ if (open !== void 0) {
494
+ const endMatch = execAt(sticky(open.rule.end), source, index);
495
+ if (endMatch !== null && endMatch[0].length > 0) {
496
+ const closeScope = scopeOf(
497
+ open.rule.closeScope ?? open.rule.scope,
498
+ ruleMatch(endMatch[0], index, [...endMatch]),
499
+ open.scope
500
+ );
501
+ push(closeScope, index, index + endMatch[0].length, open.scope);
502
+ index += endMatch[0].length;
503
+ openRegions.pop();
504
+ regionScope = openRegions[openRegions.length - 1]?.scope;
505
+ continue;
506
+ }
507
+ if (open.rule.transparent !== true) {
508
+ if (open.rule.nested === true) {
509
+ const inner = execAt(sticky(open.rule.begin), source, index);
510
+ if (inner !== null && inner[0].length > 0) {
511
+ const innerMatch = ruleMatch(inner[0], index, [...inner]);
512
+ push(
513
+ scopeOf(open.rule.openScope ?? open.rule.scope, innerMatch, open.scope),
514
+ index,
515
+ index + inner[0].length,
516
+ open.scope
517
+ );
518
+ openRegions.push({
519
+ rule: open.rule,
520
+ scope: open.scope,
521
+ beginFrom: index,
522
+ beginTo: index + inner[0].length
523
+ });
524
+ index += inner[0].length;
525
+ continue;
526
+ }
527
+ }
528
+ push(open.scope, index, index + 1, open.scope);
529
+ index += 1;
530
+ continue;
531
+ }
532
+ }
533
+ let matched = false;
534
+ for (const rule of rules) {
535
+ if (!contextHolds(rule.when, index, previousScope)) continue;
536
+ if (rule.kind === "region") {
537
+ const begin = execAt(sticky(rule.begin), source, index);
538
+ if (begin === null || begin[0].length === 0) continue;
539
+ const beginMatch = ruleMatch(begin[0], index, [...begin]);
540
+ const scope = scopeOf(rule.scope, beginMatch, "text");
541
+ const openScope = scopeOf(rule.openScope ?? rule.scope, beginMatch, scope);
542
+ const contentScope = scopeOf(rule.contentScope ?? rule.scope, beginMatch, scope);
543
+ const closeScope = scopeOf(rule.closeScope ?? rule.scope, beginMatch, scope);
544
+ const beginEnd = index + begin[0].length;
545
+ push(openScope, index, beginEnd, regionScope);
546
+ if (rule.nested === true || rule.transparent === true) {
547
+ openRegions.push({ rule, scope: contentScope, beginFrom: index, beginTo: beginEnd });
548
+ regionScope = contentScope;
549
+ index = beginEnd;
550
+ matched = true;
551
+ break;
552
+ }
553
+ const close = findRegionEnd(rule.end, source, beginEnd);
554
+ if (close === void 0) {
555
+ push(
556
+ scopeOf(rule.unclosed?.scope === void 0 ? void 0 : rule.unclosed.scope, beginMatch, contentScope),
557
+ beginEnd,
558
+ length,
559
+ scope
560
+ );
561
+ report({
562
+ from: index,
563
+ to: beginEnd,
564
+ message: rule.unclosed?.message === void 0 ? `Unterminated ${scope}.` : typeof rule.unclosed.message === "function" ? rule.unclosed.message(beginMatch) : rule.unclosed.message,
565
+ severity: rule.unclosed?.severity ?? "error",
566
+ code: rule.unclosed?.code ?? "unclosed-region"
567
+ });
568
+ index = length;
569
+ matched = true;
570
+ break;
571
+ }
572
+ push(contentScope, beginEnd, close.from, scope);
573
+ push(closeScope, close.from, close.to, scope);
574
+ index = close.to;
575
+ matched = true;
576
+ break;
577
+ }
578
+ if (rule.kind === "words") {
579
+ const words = resolveWords(rule.words);
580
+ const vocabulary = asResolvedVocabulary(rule.words);
581
+ const hit = matchWords(rule, words, index);
582
+ if (hit !== void 0) {
583
+ const text = source.slice(index, hit.to);
584
+ const match2 = ruleMatch(text, index, [text]);
585
+ const scope = rule.scope !== void 0 ? scopeOf(rule.scope, match2, "word") : vocabulary?.scopeFor !== void 0 ? vocabulary.scopeFor(hit.member) : "word";
586
+ push(scope, index, hit.to, regionScope);
587
+ index = hit.to;
588
+ matched = true;
589
+ break;
590
+ }
591
+ if (rule.unknown === void 0) continue;
592
+ const candidateTo = readWordRun(index);
593
+ if (candidateTo <= index) continue;
594
+ const word = source.slice(index, candidateTo);
595
+ const rejected = vocabulary?.reject?.(word, vocabularyContext);
596
+ const message = rule.unknown.message === void 0 ? rejected?.message : typeof rule.unknown.message === "function" ? rule.unknown.message(word, ruleMatch(word, index, [word])) : rule.unknown.message.replace(/\{word\}/g, word).replace(/\{allowed\}/g, listPhrase(words.list));
597
+ push(rule.unknown.scope ?? vocabulary?.unknownScope ?? "invalid", index, candidateTo, regionScope);
598
+ if (message !== void 0) {
599
+ report({
600
+ from: index,
601
+ to: candidateTo,
602
+ message,
603
+ severity: rule.unknown.severity ?? rejected?.severity ?? "error",
604
+ code: rule.unknown.code ?? rejected?.code ?? "unknown-word"
605
+ });
606
+ }
607
+ index = candidateTo;
608
+ matched = true;
609
+ break;
610
+ }
611
+ const match = execAt(sticky(rule.pattern), source, index);
612
+ if (match === null || match[0].length === 0) continue;
613
+ const matchInfo = ruleMatch(match[0], index, [...match]);
614
+ push(scopeOf(rule.scope, matchInfo, fallbackScope), index, index + match[0].length, regionScope);
615
+ index += match[0].length;
616
+ matched = true;
617
+ break;
618
+ }
619
+ if (matched) continue;
620
+ const unclaimed = regionScope ?? fallbackScope;
621
+ let to = index;
622
+ let runPrevious = previousScope;
623
+ while (to < length) {
624
+ const char = source.charAt(to);
625
+ if (char === "\n" || char === "\r") break;
626
+ if (anyRuleClaims(to, runPrevious)) break;
627
+ if (/\S/.test(char)) runPrevious = unclaimed;
628
+ to += 1;
629
+ }
630
+ push(unclaimed, index, Math.max(to, index + 1), regionScope);
631
+ index = Math.max(to, index + 1);
632
+ }
633
+ for (let frame = openRegions.length - 1; frame >= 0; frame -= 1) {
634
+ const open = openRegions[frame];
635
+ if (open === void 0) continue;
636
+ const message = open.rule.unclosed?.message === void 0 ? `Unterminated ${open.scope}.` : typeof open.rule.unclosed.message === "function" ? open.rule.unclosed.message(ruleMatch("", open.beginFrom, [""])) : open.rule.unclosed.message;
637
+ report({
638
+ from: open.beginFrom,
639
+ to: open.beginTo,
640
+ message,
641
+ severity: open.rule.unclosed?.severity ?? "error",
642
+ code: open.rule.unclosed?.code ?? "unclosed-region"
643
+ });
644
+ }
645
+ return { tokens, diagnostics, state };
646
+ }
647
+ function findRegionEnd(pattern, source, from) {
648
+ const probe = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
649
+ probe.lastIndex = from;
650
+ const match = probe.exec(source);
651
+ if (match === null || match[0].length === 0) return void 0;
652
+ return { from: match.index, to: match.index + match[0].length };
653
+ }
654
+
655
+ // src/core/complete.ts
656
+ var DEFAULT_LIMIT = 100;
657
+ function complete(inspection, grammar, request) {
658
+ const resolved = isResolvedGrammar(grammar) ? grammar : resolveGrammar(grammar);
659
+ const sources = resolved.grammar.compose;
660
+ if (sources === void 0 || sources.length === 0) return void 0;
661
+ const text = request.text;
662
+ const caret = clamp(request.caret, 0, text.length);
663
+ const context = completionContext(inspection, resolved, caret, request.trigger);
664
+ const eligible = sources.filter((source) => source.when === void 0 || source.when(context));
665
+ if (eligible.length === 0) return void 0;
666
+ const ordered = [...eligible].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
667
+ const reopening = request.previousSourceId === void 0 ? void 0 : ordered.find((source) => source.id === request.previousSourceId);
668
+ const winner = reopening ?? ordered[0];
669
+ if (winner === void 0) return void 0;
670
+ const range2 = resolveRange(winner, context);
671
+ const merged = [...winner.items(context)];
672
+ for (const source of ordered) {
673
+ if (source === winner || source.merge !== true) continue;
674
+ merged.push(...source.items(context));
675
+ }
676
+ if (merged.length === 0) return void 0;
677
+ const needleFrom = clamp(range2.from, 0, caret);
678
+ const needleTo = clamp(caret, needleFrom, Math.max(range2.to, needleFrom));
679
+ const needle = text.slice(needleFrom, needleTo);
680
+ const ranked = rank(merged, needle, {
681
+ label: (item) => item.label,
682
+ ...merged.some((item) => item.filterText !== void 0) ? { filterText: (item) => item.filterText ?? item.label } : {},
683
+ ...merged.some((item) => item.sortText !== void 0) ? { sortText: (item) => item.sortText } : {}
684
+ });
685
+ const limit = request.limit ?? DEFAULT_LIMIT;
686
+ return {
687
+ range: range2,
688
+ rows: ranked.slice(0, limit).map((entry) => ({
689
+ item: entry.item,
690
+ score: entry.score,
691
+ indices: entry.indices
692
+ })),
693
+ needle,
694
+ sourceId: winner.id
695
+ };
696
+ }
697
+ function resolveRange(source, context) {
698
+ if (typeof source.range !== "function") return source.range;
699
+ const range2 = source.range(context);
700
+ return {
701
+ from: Math.min(range2.from, range2.to),
702
+ to: Math.max(range2.from, range2.to)
703
+ };
704
+ }
705
+ function completionContext(inspection, resolved, caret, trigger) {
706
+ const { text, tokens, diagnostics, state } = inspection;
707
+ const line = lineAt(text, caret, lineStarts(text));
708
+ const lineTokens = tokensOnLine(tokens, line.number);
709
+ const word = wordInfoAt(text, caret, resolved.wordChars);
710
+ return {
711
+ text,
712
+ caret,
713
+ word,
714
+ line,
715
+ tokens,
716
+ diagnostics,
717
+ state,
718
+ scope: scopeAt(tokens, caret),
719
+ scopeBefore: tokenBefore(tokens, caret)?.scope,
720
+ // "First on line" means only whitespace precedes the caret, which is a question
721
+ // about the caret and not about the token under it: a caret in the indentation of
722
+ // a line that already has content is not first on that line. `lineTokens` is what
723
+ // tells the difference, so the tokens are read even though the answer looks like a
724
+ // string test.
725
+ firstOnLine: line.before.trim() === "" && !lineTokens.some((token) => token.to <= caret),
726
+ // "First word" additionally allows the rest of the word the caret is in, which is
727
+ // what keeps a line-head completion alive while the head is being typed.
728
+ firstWord: line.text.slice(0, Math.max(0, word.from - line.from)).trim() === "",
729
+ firstToken: lineTokens[0],
730
+ trigger
731
+ };
732
+ }
733
+ function applyCompletion(text, range2, item) {
734
+ const from = clamp(Math.min(range2.from, range2.to), 0, text.length);
735
+ const to = clamp(Math.max(range2.from, range2.to), from, text.length);
736
+ const insert = item.insert ?? item.label;
737
+ const append = item.append ?? "";
738
+ const offset = item.caretOffset ?? 0;
739
+ if (item.mode === "before") {
740
+ const head2 = text.slice(0, from);
741
+ const gap = head2 !== "" && !/\s$/.test(head2) ? " " : "";
742
+ const rest2 = text.slice(from).replace(/^\s+/, "");
743
+ const written2 = `${gap}${insert}${redundant(append, rest2) ? "" : append}`;
744
+ return {
745
+ text: `${head2}${written2}${rest2}`,
746
+ caret: head2.length + written2.length + offset,
747
+ range: { from: head2.length, to: head2.length + written2.length },
748
+ from,
749
+ to: from,
750
+ insert: written2
751
+ };
752
+ }
753
+ const head = text.slice(0, from);
754
+ const rest = text.slice(to);
755
+ const written = `${insert}${redundant(append, rest) ? "" : append}`;
756
+ return {
757
+ text: `${head}${written}${rest}`,
758
+ caret: head.length + written.length + offset,
759
+ range: { from: head.length, to: head.length + written.length },
760
+ from,
761
+ to,
762
+ insert: written
763
+ };
764
+ }
765
+ function redundant(append, rest) {
766
+ if (append === "") return true;
767
+ return append.trim() === "" ? /^\s/.test(rest) : rest.startsWith(append);
768
+ }
769
+
770
+ // src/core/hover.ts
771
+ var SEVERITY_TITLE = {
772
+ error: "Error",
773
+ warning: "Warning",
774
+ info: "Info",
775
+ hint: "Hint"
776
+ };
777
+ function diagnosticHover(diagnostic) {
778
+ return {
779
+ kind: "diagnostic",
780
+ title: SEVERITY_TITLE[diagnostic.severity],
781
+ detail: diagnostic.source,
782
+ body: diagnostic.detail === void 0 ? diagnostic.message : `${diagnostic.message}
783
+
784
+ ${diagnostic.detail}`,
785
+ range: { from: diagnostic.from, to: diagnostic.to }
786
+ };
787
+ }
788
+ function resolveHover(inspection, grammar, offset) {
789
+ const resolved = isResolvedGrammar(grammar) ? grammar : resolveGrammar(grammar);
790
+ const { text, tokens, diagnostics, decorations, state } = inspection;
791
+ let covering;
792
+ for (const diagnostic of diagnostics) {
793
+ if (!containsOffset(diagnostic, offset)) continue;
794
+ if (covering === void 0 || diagnostic.to - diagnostic.from < covering.to - covering.from) {
795
+ covering = diagnostic;
796
+ }
797
+ }
798
+ if (covering !== void 0) return diagnosticHover(covering);
799
+ const decoration = decorations.find((entry) => containsOffset(entry, offset));
800
+ const described = describeAt(inspection, resolved, offset);
801
+ if (decoration?.title !== void 0) {
802
+ return {
803
+ kind: "decoration",
804
+ title: decoration.title,
805
+ detail: described?.detail ?? described?.title,
806
+ body: described?.body,
807
+ range: { from: decoration.from, to: decoration.to }
808
+ };
809
+ }
810
+ return described;
811
+ }
812
+ function describeAt(inspection, resolved, offset) {
813
+ if (resolved.grammar.describe === void 0) return void 0;
814
+ const { text, tokens, diagnostics, state } = inspection;
815
+ const found = tokenAt(tokens, offset);
816
+ const token = found === void 0 || found.text.trim() === "" ? void 0 : found;
817
+ const context = {
818
+ text,
819
+ offset,
820
+ token,
821
+ word: wordInfoAt(text, offset, resolved.wordChars),
822
+ line: lineAt(text, offset, lineStarts(text)),
823
+ tokens,
824
+ diagnostics,
825
+ state
826
+ };
827
+ return resolved.grammar.describe(context) ?? void 0;
828
+ }
829
+
830
+ // src/core/inspect.ts
831
+ function runChecks(source, grammar, tokens, state) {
832
+ const checks = grammar.grammar.checks;
833
+ if (checks === void 0 || checks.length === 0) return [];
834
+ const diagnostics = [];
835
+ const context = { text: source, state };
836
+ for (const check of checks) {
837
+ const members = check.allow === void 0 ? void 0 : resolveWordsSource(check.allow, context);
838
+ const vocabulary = check.allow === void 0 ? void 0 : check.allow;
839
+ const caseSensitive = typeof vocabulary === "object" && vocabulary !== null && !Array.isArray(vocabulary) ? vocabulary.caseSensitive === true : false;
840
+ const fold = (word) => caseSensitive ? word : word.toLowerCase();
841
+ const set = members === void 0 ? void 0 : new Set(members.map((member) => fold(member)));
842
+ const reportedLines = /* @__PURE__ */ new Set();
843
+ for (const token of tokens) {
844
+ if (token.text.trim() === "") continue;
845
+ if (!check.scopes.includes("*") && !check.scopes.includes(token.scope)) continue;
846
+ if (check.except !== void 0 && check.except.test(token.text)) continue;
847
+ if (set !== void 0 && set.has(fold(token.text))) continue;
848
+ if (check.perLine === true) {
849
+ if (reportedLines.has(token.line)) continue;
850
+ reportedLines.add(token.line);
851
+ }
852
+ diagnostics.push({
853
+ from: token.from,
854
+ to: token.to,
855
+ severity: check.severity ?? "error",
856
+ message: check.message.replace(/\{word\}/g, token.text).replace(/\{allowed\}/g, listPhrase(members ?? [])),
857
+ code: check.code,
858
+ detail: check.detail,
859
+ source: grammar.grammar.id
860
+ });
861
+ }
862
+ }
863
+ return diagnostics;
864
+ }
865
+ function inspect(source, grammar) {
866
+ const resolved = isResolvedGrammar(grammar) ? grammar : resolveGrammar(grammar);
867
+ const scanned = scan(source, resolved);
868
+ const declared = resolved.grammar;
869
+ const diagnostics = [...scanned.diagnostics];
870
+ diagnostics.push(...runChecks(source, resolved, scanned.tokens, scanned.state));
871
+ if (declared.validate !== void 0) {
872
+ const context = {
873
+ text: source,
874
+ tokens: scanned.tokens,
875
+ state: scanned.state,
876
+ report: (problem) => {
877
+ diagnostics.push({
878
+ from: problem.from,
879
+ to: problem.to,
880
+ severity: problem.severity ?? "error",
881
+ message: problem.message,
882
+ code: problem.code ?? "validate",
883
+ detail: problem.detail,
884
+ source: declared.id
885
+ });
886
+ }
887
+ };
888
+ declared.validate(context);
889
+ }
890
+ const decorations = declared.decorate === void 0 ? [] : [...declared.decorate(source, scanned.state)];
891
+ return {
892
+ text: source,
893
+ tokens: scanned.tokens,
894
+ diagnostics: normalizeDiagnostics(diagnostics),
895
+ decorations: normalizeDecorations(decorations, source.length),
896
+ state: scanned.state
897
+ };
898
+ }
899
+ function normalizeDiagnostics(diagnostics) {
900
+ const seen = /* @__PURE__ */ new Set();
901
+ const out = [];
902
+ for (const diagnostic of diagnostics) {
903
+ const key = `${diagnostic.from}:${diagnostic.to}:${diagnostic.code ?? ""}:${diagnostic.message}`;
904
+ if (seen.has(key)) continue;
905
+ seen.add(key);
906
+ out.push(diagnostic);
907
+ }
908
+ return out.sort((left, right) => left.from - right.from || left.to - right.to);
909
+ }
910
+ function normalizeDecorations(decorations, length) {
911
+ const out = [];
912
+ for (const decoration of decorations) {
913
+ const from = Math.max(0, Math.min(decoration.from, length));
914
+ const to = Math.max(from, Math.min(decoration.to, length));
915
+ if (to <= from) continue;
916
+ out.push({ ...decoration, from, to });
917
+ }
918
+ return out.sort((left, right) => left.from - right.from || left.to - right.to);
919
+ }
920
+
921
+ // src/styles.ts
922
+ var SCOPE_PALETTE = [
923
+ { scope: "text", light: "var(--litearea-fg)" },
924
+ { scope: "word", light: "var(--litearea-fg)" },
925
+ { scope: "family", light: "var(--litearea-fg)" },
926
+ { scope: "family-generic", light: "#7c3aed", dark: "#c4a2ff" },
927
+ { scope: "family-unknown", light: "var(--litearea-warning)" },
928
+ { scope: "family-unclosed", light: "var(--litearea-error)" },
929
+ { scope: "weight", light: "var(--litearea-accent)" },
930
+ { scope: "weight-missing", light: "var(--litearea-warning)" },
931
+ { scope: "state", light: "var(--litearea-accent)" },
932
+ { scope: "property", light: "#7c3aed", dark: "#c4a2ff" },
933
+ { scope: "operator", light: "var(--litearea-fg-dim)" },
934
+ { scope: "separator", light: "var(--litearea-fg-dim)" },
935
+ { scope: "value-shape", light: "#0f766e", dark: "#5eead4" },
936
+ { scope: "value-color", light: "#0f766e", dark: "#5eead4" },
937
+ { scope: "value-pattern", light: "#0f766e", dark: "#5eead4" },
938
+ { scope: "value-motion", light: "#0f766e", dark: "#5eead4" },
939
+ { scope: "value-number", light: "#b45309", dark: "#fbbf24" },
940
+ { scope: "comment", light: "var(--litearea-fg-dim)" },
941
+ { scope: "invalid", light: "var(--litearea-error)" },
942
+ { scope: "keyword", light: "var(--litearea-accent)" },
943
+ { scope: "string", light: "#0f766e", dark: "#5eead4" },
944
+ { scope: "number", light: "#b45309", dark: "#fbbf24" }
945
+ ];
946
+ function scopeVariables(scheme) {
947
+ return SCOPE_PALETTE.filter((entry) => scheme === "light" || entry.dark !== void 0).map((entry) => {
948
+ const value = scheme === "light" ? entry.light : entry.dark;
949
+ return ` --litearea-scope-${entry.scope}: ${String(value)};`;
950
+ }).join("\n");
951
+ }
952
+ function scopeRules() {
953
+ return SCOPE_PALETTE.map(
954
+ (entry) => `.litearea-scope-${entry.scope} { color: var(--litearea-scope-${entry.scope}); }`
955
+ ).join("\n");
956
+ }
957
+ var LITEAREA_STYLES = `
958
+ .litearea {
959
+ --litearea-font: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
960
+ --litearea-font-size: 13px;
961
+ --litearea-line-height: 20px;
962
+ --litearea-padding-block: 6px;
963
+ --litearea-padding-inline: 10px;
964
+ --litearea-radius: 8px;
965
+ --litearea-fg: #1f2328;
966
+ --litearea-fg-dim: #6b7280;
967
+ --litearea-fg-strong: #111827;
968
+ --litearea-bg: #ffffff;
969
+ --litearea-bg-raised: #ffffff;
970
+ --litearea-border: #d8dbe0;
971
+ --litearea-border-focus: #4d6bfe;
972
+ --litearea-accent: #4d6bfe;
973
+ --litearea-accent-soft: rgba(77, 107, 254, 0.12);
974
+ --litearea-selection: rgba(77, 107, 254, 0.22);
975
+ --litearea-error: #e5484d;
976
+ --litearea-warning: #d97706;
977
+ --litearea-info: #4d6bfe;
978
+ --litearea-hint: #8b8f97;
979
+ --litearea-shadow: 0 6px 24px rgba(15, 23, 42, 0.14);
980
+
981
+ ${scopeVariables("light")}
982
+
983
+ position: relative;
984
+ display: block;
985
+ color: var(--litearea-fg);
986
+ }
987
+
988
+ @media (prefers-color-scheme: dark) {
989
+ .litearea {
990
+ --litearea-fg: #e6e8eb;
991
+ --litearea-fg-dim: #8b919b;
992
+ --litearea-fg-strong: #ffffff;
993
+ --litearea-bg: #1b1e24;
994
+ --litearea-bg-raised: #23262c;
995
+ --litearea-border: #363b44;
996
+ ${scopeVariables("dark")}
997
+ --litearea-error: #ff6b6b;
998
+ --litearea-warning: #f59e0b;
999
+ --litearea-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
1000
+ }
1001
+ }
1002
+
1003
+ /* \u2500\u2500 the box \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1004
+
1005
+ .litearea-box {
1006
+ position: relative;
1007
+ display: block;
1008
+ border: 1px solid var(--litearea-border);
1009
+ border-radius: var(--litearea-radius);
1010
+ background: var(--litearea-bg);
1011
+ transition: border-color 120ms ease;
1012
+ }
1013
+
1014
+ .litearea-box:focus-within {
1015
+ border-color: var(--litearea-border-focus);
1016
+ }
1017
+
1018
+ .litearea-invalid .litearea-box {
1019
+ border-color: var(--litearea-error);
1020
+ }
1021
+
1022
+ /*
1023
+ * The layer and the field share this rule and nothing may be added to one alone.
1024
+ * Every property here is a property the mirror copies too \u2014 three elements, one
1025
+ * typography, or the paint drifts.
1026
+ */
1027
+ .litearea-layer,
1028
+ .litearea-input {
1029
+ box-sizing: border-box;
1030
+ width: 100%;
1031
+ margin: 0;
1032
+ padding: var(--litearea-padding-block) var(--litearea-padding-inline);
1033
+ border: none;
1034
+ font-family: var(--litearea-font);
1035
+ font-size: var(--litearea-font-size);
1036
+ font-weight: 400;
1037
+ font-style: normal;
1038
+ font-stretch: normal;
1039
+ font-variant-ligatures: none;
1040
+ font-kerning: none;
1041
+ font-feature-settings: "liga" 0, "calt" 0, "dlig" 0;
1042
+ line-height: var(--litearea-line-height);
1043
+ letter-spacing: normal;
1044
+ word-spacing: normal;
1045
+ text-transform: none;
1046
+ text-indent: 0;
1047
+ text-align: left;
1048
+ direction: ltr;
1049
+ tab-size: 2;
1050
+ white-space: pre-wrap;
1051
+ overflow-wrap: break-word;
1052
+ word-break: break-word;
1053
+ }
1054
+
1055
+ .litearea-layer {
1056
+ position: absolute;
1057
+ inset: 0;
1058
+ overflow: hidden;
1059
+ pointer-events: none;
1060
+ user-select: none;
1061
+ }
1062
+
1063
+ /*
1064
+ * When the box is clamped to its maximum height the field grows a scrollbar, and a
1065
+ * scrollbar narrows the text. If the layer kept the full width it would wrap
1066
+ * differently from the field and every colour would slide off its character, so the
1067
+ * field's measured scrollbar width is published as --litearea-scrollbar and added
1068
+ * here. The value is 0 whenever no scrollbar is shown.
1069
+ */
1070
+ .litearea-layer {
1071
+ padding-right: calc(var(--litearea-padding-inline) + var(--litearea-scrollbar, 0px));
1072
+ }
1073
+
1074
+ .litearea-paint {
1075
+ position: relative;
1076
+ min-height: 100%;
1077
+ /* A zero-width space keeps the layer's last line box as tall as the field's:
1078
+ a trailing newline would otherwise collapse in the paint alone, and the box
1079
+ would jump the moment one was typed. */
1080
+ will-change: transform;
1081
+ }
1082
+
1083
+ .litearea-input {
1084
+ position: relative;
1085
+ display: block;
1086
+ resize: none;
1087
+ background: transparent;
1088
+ color: transparent;
1089
+ caret-color: var(--litearea-fg);
1090
+ outline: none;
1091
+ overflow-y: hidden;
1092
+ }
1093
+
1094
+ .litearea-growable .litearea-input {
1095
+ resize: none;
1096
+ }
1097
+
1098
+ .litearea-resizable .litearea-input {
1099
+ resize: vertical;
1100
+ }
1101
+
1102
+ .litearea-input::placeholder {
1103
+ color: var(--litearea-fg-dim);
1104
+ }
1105
+
1106
+ .litearea-input::selection {
1107
+ background: var(--litearea-selection);
1108
+ }
1109
+
1110
+ .litearea-readonly .litearea-input {
1111
+ caret-color: transparent;
1112
+ }
1113
+
1114
+ /*
1115
+ * The rules that spend the variables, generated from the same list that declares them. There is
1116
+ * deliberately NO catch-all rule setting a property on every painted span: there was one, and a
1117
+ * class plus an element outranks a bare class on specificity, so it silently switched off every
1118
+ * squiggle in the library.
1119
+ *
1120
+ * A scope that is not in the list gets no colour from here, and a host writing a grammar of its
1121
+ * own styles it with a plain rule \u2014 .litearea-scope-my-thing { color: \u2026 } \u2014 since nothing in
1122
+ * this file competes for that selector.
1123
+ */
1124
+ ${scopeRules()}
1125
+
1126
+ .litearea-dec-effective {
1127
+ border-radius: 3px;
1128
+ background: var(--litearea-accent-soft);
1129
+ box-shadow: 0 0 0 1px var(--litearea-accent-soft);
1130
+ }
1131
+
1132
+ /* The four shapes a diagnostic can take. Wavy for the two that mean "fix this", dotted for
1133
+ the two that mean "worth knowing" \u2014 the same distinction VSCode draws, and the reason
1134
+ severity is a class rather than an inline colour. */
1135
+ .litearea-diag-error {
1136
+ text-decoration-line: underline;
1137
+ text-decoration-style: wavy;
1138
+ text-decoration-color: var(--litearea-error);
1139
+ text-decoration-skip-ink: none;
1140
+ text-underline-offset: 3px;
1141
+ }
1142
+
1143
+ .litearea-diag-warning {
1144
+ text-decoration-line: underline;
1145
+ text-decoration-style: wavy;
1146
+ text-decoration-color: var(--litearea-warning);
1147
+ text-decoration-skip-ink: none;
1148
+ text-underline-offset: 3px;
1149
+ }
1150
+
1151
+ .litearea-diag-info {
1152
+ text-decoration-line: underline;
1153
+ text-decoration-style: dotted;
1154
+ text-decoration-color: var(--litearea-info);
1155
+ text-decoration-skip-ink: none;
1156
+ text-underline-offset: 3px;
1157
+ }
1158
+
1159
+ .litearea-diag-hint {
1160
+ text-decoration-line: underline;
1161
+ text-decoration-style: dotted;
1162
+ text-decoration-color: var(--litearea-hint);
1163
+ text-decoration-skip-ink: none;
1164
+ text-underline-offset: 3px;
1165
+ }
1166
+
1167
+ /* \u2500\u2500 the completion list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1168
+
1169
+ /*
1170
+ * The container holds two things and scrolls NEITHER of them: the list scrolls itself, and the
1171
+ * documentation is pinned below it. Putting the documentation inside the scroll range \u2014 which is
1172
+ * what this used to do \u2014 makes it unreachable with a long list and a long explanation: the
1173
+ * arrows move the active row rather than the scrollbar, so a keyboard user never sees it, and a
1174
+ * mouse user has to scroll down to read it and back up to reach the next row.
1175
+ */
1176
+ .litearea-popup {
1177
+ position: absolute;
1178
+ z-index: 30;
1179
+ display: none;
1180
+ flex-direction: column;
1181
+ max-width: 460px;
1182
+ padding: 4px;
1183
+ border: 1px solid var(--litearea-border);
1184
+ border-radius: 10px;
1185
+ background: var(--litearea-bg-raised);
1186
+ box-shadow: var(--litearea-shadow);
1187
+ font-family: var(--litearea-font);
1188
+ font-size: 12px;
1189
+ line-height: 18px;
1190
+ color: var(--litearea-fg);
1191
+ overflow: hidden;
1192
+ }
1193
+
1194
+ .litearea-popup[data-open="true"] {
1195
+ display: flex;
1196
+ }
1197
+
1198
+ /* The rows, and the only part that scrolls. Its height is bounded so the documentation below
1199
+ always has somewhere to live. */
1200
+ .litearea-list {
1201
+ min-height: 0;
1202
+ max-height: 208px;
1203
+ overflow-y: auto;
1204
+ }
1205
+
1206
+ /*
1207
+ * The explanation, pinned. Its own height is bounded and it scrolls ITSELF, so a long
1208
+ * explanation stays readable while the rows stay put \u2014 and arrowing to the next row swaps the
1209
+ * text in place instead of requiring a scroll back up.
1210
+ */
1211
+ .litearea-docs {
1212
+ flex: none;
1213
+ max-height: 132px;
1214
+ overflow-y: auto;
1215
+ margin: 4px -4px -4px;
1216
+ padding: 6px 10px;
1217
+ border-top: 1px solid var(--litearea-border);
1218
+ background: var(--litearea-bg);
1219
+ border-radius: 0 0 10px 10px;
1220
+ color: var(--litearea-fg);
1221
+ white-space: pre-wrap;
1222
+ }
1223
+
1224
+ .litearea-popup[data-docs="false"] .litearea-docs {
1225
+ display: none;
1226
+ }
1227
+
1228
+ .litearea-row {
1229
+ display: flex;
1230
+ align-items: baseline;
1231
+ gap: 8px;
1232
+ padding: 3px 6px;
1233
+ border-radius: 6px;
1234
+ cursor: pointer;
1235
+ white-space: nowrap;
1236
+ overflow: hidden;
1237
+ }
1238
+
1239
+ .litearea-row[aria-selected="true"] {
1240
+ background: var(--litearea-accent-soft);
1241
+ }
1242
+
1243
+ .litearea-rowKind {
1244
+ flex: none;
1245
+ width: 7px;
1246
+ height: 7px;
1247
+ border-radius: 2px;
1248
+ background: var(--litearea-fg-dim);
1249
+ transform: translateY(-1px);
1250
+ }
1251
+
1252
+ .litearea-kind-family { background: var(--litearea-scope-family-generic); }
1253
+ .litearea-kind-generic { background: var(--litearea-scope-family-generic); }
1254
+ .litearea-kind-weight { background: var(--litearea-scope-weight); }
1255
+ .litearea-kind-state { background: var(--litearea-scope-state); }
1256
+ .litearea-kind-property { background: var(--litearea-scope-property); }
1257
+ .litearea-kind-value { background: var(--litearea-scope-value-shape); }
1258
+ .litearea-kind-number { background: var(--litearea-scope-value-number); }
1259
+ .litearea-kind-custom { background: var(--litearea-fg-dim); }
1260
+
1261
+ .litearea-rowLabel {
1262
+ flex: 1 1 auto;
1263
+ min-width: 0;
1264
+ overflow: hidden;
1265
+ text-overflow: ellipsis;
1266
+ }
1267
+
1268
+ .litearea-rowMatch {
1269
+ color: var(--litearea-accent);
1270
+ font-weight: 600;
1271
+ }
1272
+
1273
+ .litearea-rowDetail {
1274
+ flex: none;
1275
+ color: var(--litearea-fg-dim);
1276
+ font-size: 11px;
1277
+ }
1278
+
1279
+ .litearea-docsTitle {
1280
+ font-weight: 600;
1281
+ }
1282
+
1283
+ .litearea-docsDetail {
1284
+ color: var(--litearea-fg-dim);
1285
+ }
1286
+
1287
+ .litearea-docsBody {
1288
+ margin-top: 2px;
1289
+ color: var(--litearea-fg-dim);
1290
+ }
1291
+
1292
+ /* \u2500\u2500 the hover tooltip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1293
+
1294
+ .litearea-tooltip {
1295
+ position: absolute;
1296
+ z-index: 40;
1297
+ display: none;
1298
+ max-width: 340px;
1299
+ padding: 6px 10px;
1300
+ border: 1px solid var(--litearea-border);
1301
+ border-radius: 8px;
1302
+ background: var(--litearea-bg-raised);
1303
+ box-shadow: var(--litearea-shadow);
1304
+ font-family: var(--litearea-font);
1305
+ font-size: 11px;
1306
+ line-height: 16px;
1307
+ color: var(--litearea-fg);
1308
+ pointer-events: none;
1309
+ white-space: pre-wrap;
1310
+ }
1311
+
1312
+ .litearea-tooltip[data-open="true"] {
1313
+ display: block;
1314
+ }
1315
+
1316
+ .litearea-tooltipTitle {
1317
+ font-weight: 600;
1318
+ }
1319
+
1320
+ .litearea-tooltipDetail {
1321
+ color: var(--litearea-fg-dim);
1322
+ }
1323
+
1324
+ .litearea-tooltipBody {
1325
+ margin-top: 3px;
1326
+ color: var(--litearea-fg-dim);
1327
+ }
1328
+
1329
+ .litearea-srOnly {
1330
+ position: absolute;
1331
+ width: 1px;
1332
+ height: 1px;
1333
+ padding: 0;
1334
+ margin: -1px;
1335
+ overflow: hidden;
1336
+ clip: rect(0, 0, 0, 0);
1337
+ white-space: nowrap;
1338
+ border: 0;
1339
+ }
1340
+ `.trim();
1341
+ function injectStyles(ownerDocument, nonce) {
1342
+ if (ownerDocument === null || ownerDocument === void 0) return void 0;
1343
+ const existing = ownerDocument.querySelector("style[data-litearea-styles]");
1344
+ if (existing !== null) return existing;
1345
+ const style = ownerDocument.createElement("style");
1346
+ style.dataset.liteareaStyles = "";
1347
+ if (nonce !== void 0) style.nonce = nonce;
1348
+ style.textContent = LITEAREA_STYLES;
1349
+ const head = ownerDocument.head ?? ownerDocument.documentElement;
1350
+ if (head === null || head === void 0) return void 0;
1351
+ head.appendChild(style);
1352
+ return style;
1353
+ }
1354
+ function scopeClass(scope) {
1355
+ const folded = scope.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
1356
+ return folded === "" ? "litearea-scope-text" : `litearea-scope-${folded}`;
1357
+ }
1358
+ function decorationClass(kind) {
1359
+ return `litearea-dec-${kind.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
1360
+ }
1361
+ function severityClass(severity) {
1362
+ return `litearea-diag-${severity.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
1363
+ }
1364
+
1365
+ // src/dom/support.ts
1366
+ function hasDocument() {
1367
+ return typeof document !== "undefined" && document !== null;
1368
+ }
1369
+ function canEditThroughPipeline() {
1370
+ return hasDocument() && typeof document.execCommand === "function";
1371
+ }
1372
+ function withDefaults(defaults, given) {
1373
+ if (given === void 0) return { ...defaults };
1374
+ const merged = { ...defaults };
1375
+ for (const key of Object.keys(given)) {
1376
+ const value = given[key];
1377
+ if (value !== void 0) merged[key] = value;
1378
+ }
1379
+ return merged;
1380
+ }
1381
+
1382
+ // src/dom/editing.ts
1383
+ function readSelection(field) {
1384
+ const start = field.selectionStart ?? 0;
1385
+ const end = field.selectionEnd ?? start;
1386
+ return { start, end };
1387
+ }
1388
+ function writeSelection(field, start, end = start) {
1389
+ const length = field.value.length;
1390
+ const from = clamp(Math.min(start, end), 0, length);
1391
+ const to = clamp(Math.max(start, end), from, length);
1392
+ field.setSelectionRange(from, to);
1393
+ }
1394
+ function replaceThroughPipeline(field, from, to, text) {
1395
+ const start = clamp(Math.min(from, to), 0, field.value.length);
1396
+ const end = clamp(Math.max(from, to), start, field.value.length);
1397
+ if (start === end && text === "") return "unchanged";
1398
+ if (field.ownerDocument.activeElement !== field) field.focus({ preventScroll: true });
1399
+ writeSelection(field, start, end);
1400
+ const before = field.value;
1401
+ if (canEditThroughPipeline()) {
1402
+ try {
1403
+ field.ownerDocument.execCommand("insertText", false, text);
1404
+ } catch {
1405
+ }
1406
+ if (field.value !== before) return "pipeline";
1407
+ }
1408
+ field.setRangeText(text, start, end, "end");
1409
+ if (field.value === before) return "unchanged";
1410
+ dispatchInput(field, text);
1411
+ return "direct";
1412
+ }
1413
+ function dispatchInput(field, text) {
1414
+ const view = field.ownerDocument.defaultView;
1415
+ const InputEventCtor = view === null ? void 0 : view.InputEvent;
1416
+ if (typeof InputEventCtor === "function") {
1417
+ field.dispatchEvent(
1418
+ new InputEventCtor("input", { bubbles: true, inputType: "insertText", data: text })
1419
+ );
1420
+ return;
1421
+ }
1422
+ field.dispatchEvent(new Event("input", { bubbles: true }));
1423
+ }
1424
+ function undoField(field) {
1425
+ if (!canEditThroughPipeline()) return false;
1426
+ if (field.ownerDocument.activeElement !== field) field.focus({ preventScroll: true });
1427
+ try {
1428
+ return field.ownerDocument.execCommand("undo");
1429
+ } catch {
1430
+ return false;
1431
+ }
1432
+ }
1433
+ function redoField(field) {
1434
+ if (!canEditThroughPipeline()) return false;
1435
+ if (field.ownerDocument.activeElement !== field) field.focus({ preventScroll: true });
1436
+ try {
1437
+ return field.ownerDocument.execCommand("redo");
1438
+ } catch {
1439
+ return false;
1440
+ }
1441
+ }
1442
+ function writeDocument(field, next, preserveHistory = false) {
1443
+ if (field.value === next) return "unchanged";
1444
+ if (preserveHistory) return replaceThroughPipeline(field, 0, field.value.length, next);
1445
+ field.value = next;
1446
+ writeSelection(field, next.length, next.length);
1447
+ return "direct";
1448
+ }
1449
+
1450
+ // src/dom/mirror.ts
1451
+ var COPIED_PROPERTIES = [
1452
+ "fontFamily",
1453
+ "fontSize",
1454
+ "fontWeight",
1455
+ "fontStyle",
1456
+ "fontStretch",
1457
+ "fontVariantLigatures",
1458
+ "fontKerning",
1459
+ "fontFeatureSettings",
1460
+ "lineHeight",
1461
+ "letterSpacing",
1462
+ "wordSpacing",
1463
+ "textTransform",
1464
+ "textIndent",
1465
+ "textAlign",
1466
+ "direction",
1467
+ "tabSize",
1468
+ "whiteSpace",
1469
+ "overflowWrap",
1470
+ "wordBreak",
1471
+ "hyphens",
1472
+ "paddingTop",
1473
+ "paddingRight",
1474
+ "paddingBottom",
1475
+ "paddingLeft",
1476
+ "borderTopWidth",
1477
+ "borderRightWidth",
1478
+ "borderBottomWidth",
1479
+ "borderLeftWidth",
1480
+ "borderTopStyle",
1481
+ "borderRightStyle",
1482
+ "borderBottomStyle",
1483
+ "borderLeftStyle",
1484
+ "boxSizing"
1485
+ ];
1486
+ var SENTINEL = "\u200B";
1487
+ var TextMirror = class {
1488
+ /** The measuring element. Kept out of the document's flow by `position: fixed`. */
1489
+ element;
1490
+ document;
1491
+ view;
1492
+ /** The field this mirror is currently shaped like. */
1493
+ adopted;
1494
+ /** The line height measured with the field, or 0 before the first measurement. */
1495
+ measuredLineHeight = 0;
1496
+ /**
1497
+ * @param ownerDocument - the document to create the element in.
1498
+ */
1499
+ constructor(ownerDocument) {
1500
+ this.document = ownerDocument;
1501
+ this.view = ownerDocument.defaultView ?? void 0;
1502
+ this.element = ownerDocument.createElement("div");
1503
+ this.element.setAttribute("aria-hidden", "true");
1504
+ this.element.dataset.liteareaPart = "mirror";
1505
+ this.element.style.cssText = [
1506
+ "position:fixed",
1507
+ "top:0",
1508
+ "left:0",
1509
+ "visibility:hidden",
1510
+ "pointer-events:none",
1511
+ "z-index:-1",
1512
+ "margin:0",
1513
+ "overflow:hidden",
1514
+ "white-space:pre-wrap",
1515
+ "overflow-wrap:break-word",
1516
+ "word-break:break-word",
1517
+ "box-sizing:border-box"
1518
+ ].join(";");
1519
+ }
1520
+ /** Whether the mirror is in a document. */
1521
+ get mounted() {
1522
+ return this.element.isConnected;
1523
+ }
1524
+ /**
1525
+ * Mount the mirror, once, so measurements have a layout to read.
1526
+ * @param parent - where to mount it. The body is right unless the document has none.
1527
+ */
1528
+ mount(parent) {
1529
+ if (this.mounted) return;
1530
+ const host = parent ?? this.document.body ?? this.document.documentElement;
1531
+ if (host === null || host === void 0) return;
1532
+ host.appendChild(this.element);
1533
+ }
1534
+ /**
1535
+ * Shape the mirror like a field.
1536
+ *
1537
+ * The width is the interesting part. It has to be the field's CONTENT width, or
1538
+ * text wraps in one and not the other — and the field's `clientWidth` is its
1539
+ * content plus padding but NOT its border, while the mirror is `border-box`. So
1540
+ * the borders are added back, and what is deliberately left out is the
1541
+ * scrollbar: a field clamped to its maximum height has one, and the text wraps
1542
+ * inside the narrower area above it.
1543
+ * @param field - the textarea to imitate.
1544
+ */
1545
+ adopt(field) {
1546
+ this.mount();
1547
+ this.adopted = field;
1548
+ const view = this.view;
1549
+ if (view === null || view === void 0) return;
1550
+ const styles = view.getComputedStyle(field);
1551
+ for (const property of COPIED_PROPERTIES) {
1552
+ this.element.style.setProperty(
1553
+ property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
1554
+ styles.getPropertyValue(property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`))
1555
+ );
1556
+ }
1557
+ const borderLeft = Number.parseFloat(styles.borderLeftWidth) || 0;
1558
+ const borderRight = Number.parseFloat(styles.borderRightWidth) || 0;
1559
+ this.element.style.width = `${String(field.clientWidth + borderLeft + borderRight)}px`;
1560
+ this.measuredLineHeight = 0;
1561
+ }
1562
+ /**
1563
+ * The line height in force, measured rather than assumed.
1564
+ *
1565
+ * `line-height: normal` is a real value and a common one, and it cannot be read
1566
+ * as a number — `parseFloat('normal')` is `NaN`. So it is measured by laying one
1567
+ * line of text out and taking the height, which is also the only way to be right
1568
+ * about a font whose normal leading is not 1.2.
1569
+ * @param field - the field to measure against.
1570
+ * @returns the line height in pixels.
1571
+ */
1572
+ lineHeight(field) {
1573
+ if (this.measuredLineHeight > 0) return this.measuredLineHeight;
1574
+ const view = this.view;
1575
+ if (view === null || view === void 0) return 0;
1576
+ const styles = view.getComputedStyle(field);
1577
+ const declared = Number.parseFloat(styles.lineHeight);
1578
+ if (Number.isFinite(declared) && declared > 0) {
1579
+ this.measuredLineHeight = declared;
1580
+ return declared;
1581
+ }
1582
+ this.adopt(field);
1583
+ this.setText("M");
1584
+ const padding = this.verticalPadding();
1585
+ const height = this.element.getBoundingClientRect().height - padding;
1586
+ const fontSize = Number.parseFloat(styles.fontSize);
1587
+ const fallback = (Number.isFinite(fontSize) ? fontSize : 16) * 1.2;
1588
+ this.measuredLineHeight = height > 0 ? height : fallback;
1589
+ return this.measuredLineHeight;
1590
+ }
1591
+ /** The mirror's vertical padding plus border, which every height includes. */
1592
+ verticalPadding() {
1593
+ const view = this.view;
1594
+ if (view === null || view === void 0) return 0;
1595
+ const styles = view.getComputedStyle(this.element);
1596
+ const sum = (Number.parseFloat(styles.paddingTop) || 0) + (Number.parseFloat(styles.paddingBottom) || 0) + (Number.parseFloat(styles.borderTopWidth) || 0) + (Number.parseFloat(styles.borderBottomWidth) || 0);
1597
+ return sum;
1598
+ }
1599
+ /**
1600
+ * Put text in the mirror with nothing else in it.
1601
+ * @param text - the content.
1602
+ */
1603
+ setText(text) {
1604
+ this.element.textContent = text;
1605
+ }
1606
+ /**
1607
+ * The height a field needs to show a document without scrolling.
1608
+ *
1609
+ * The trailing-newline problem is handled here. A div with `white-space:
1610
+ * pre-wrap` and content ending in `\n` does not lay out a final empty line — the
1611
+ * newline breaks the line but nothing follows it — so the measured height comes
1612
+ * back one line short and the field grows a scrollbar exactly when the user
1613
+ * presses Enter at the end. A zero-width space after the newline gives that last
1614
+ * line something to be.
1615
+ * @param field - the field being sized.
1616
+ * @param value - the text it holds.
1617
+ * @returns the border-box height the content requires.
1618
+ */
1619
+ contentHeight(field, value) {
1620
+ this.adopt(field);
1621
+ const needsSentinel = value === "" || value.endsWith("\n") || value.endsWith("\r");
1622
+ this.setText(needsSentinel ? `${value}${SENTINEL}` : value);
1623
+ return this.element.getBoundingClientRect().height;
1624
+ }
1625
+ /**
1626
+ * Where the caret sits, in pixels relative to the field's border box.
1627
+ *
1628
+ * The trick is a marker element holding the character AFTER the caret, measured
1629
+ * against the mirror. Everything before the caret lays out normally, so the
1630
+ * marker lands exactly where the next glyph will be — which is where the caret
1631
+ * is. At the end of the document there is no next character, so a zero-width
1632
+ * space stands in for it.
1633
+ *
1634
+ * The field's own scroll offset is subtracted, because the caret's position on
1635
+ * screen is what the popup has to be placed against, and a scrolled field moves
1636
+ * its text without moving its border box.
1637
+ *
1638
+ * @param field - the field the caret is in.
1639
+ * @param offset - the caret's character offset.
1640
+ * @returns the caret box, or undefined when there is no layout to measure.
1641
+ */
1642
+ caretBox(field, offset) {
1643
+ const view = this.view;
1644
+ if (view === null || view === void 0) return void 0;
1645
+ this.adopt(field);
1646
+ const value = field.value;
1647
+ const position = clamp(offset, 0, value.length);
1648
+ const before = value.slice(0, position);
1649
+ const after = value.slice(position);
1650
+ const next = after === "" ? SENTINEL : after.charAt(0);
1651
+ this.element.textContent = "";
1652
+ this.element.appendChild(this.document.createTextNode(before));
1653
+ const marker = this.document.createElement("span");
1654
+ marker.textContent = next;
1655
+ marker.style.whiteSpace = "pre";
1656
+ this.element.appendChild(marker);
1657
+ this.element.appendChild(this.document.createTextNode(after.slice(next.length)));
1658
+ const mirrorRect = this.element.getBoundingClientRect();
1659
+ const markerRect = marker.getBoundingClientRect();
1660
+ const lineHeight = this.lineHeight(field);
1661
+ return {
1662
+ x: markerRect.left - mirrorRect.left - field.scrollLeft,
1663
+ y: markerRect.top - mirrorRect.top - field.scrollTop,
1664
+ height: markerRect.height > 0 ? markerRect.height : lineHeight,
1665
+ lineHeight
1666
+ };
1667
+ }
1668
+ /** Take the mirror out of the document. */
1669
+ destroy() {
1670
+ this.element.remove();
1671
+ this.adopted = void 0;
1672
+ }
1673
+ /**
1674
+ * The field this mirror was last shaped like.
1675
+ * @returns the field, or undefined before {@link adopt}.
1676
+ */
1677
+ get field() {
1678
+ return this.adopted;
1679
+ }
1680
+ };
1681
+
1682
+ // src/core/segments.ts
1683
+ var SEVERITY_RANK = { error: 4, warning: 3, info: 2, hint: 1 };
1684
+ function buildSegments(text, input, fallbackScope = "text") {
1685
+ const length = text.length;
1686
+ if (length === 0) return [];
1687
+ const cuts = /* @__PURE__ */ new Set([0, length]);
1688
+ for (const token of input.tokens) {
1689
+ cuts.add(Math.max(0, Math.min(token.from, length)));
1690
+ cuts.add(Math.max(0, Math.min(token.to, length)));
1691
+ }
1692
+ for (const decoration of input.decorations) {
1693
+ cuts.add(Math.max(0, Math.min(decoration.from, length)));
1694
+ cuts.add(Math.max(0, Math.min(decoration.to, length)));
1695
+ }
1696
+ for (const diagnostic of input.diagnostics) {
1697
+ cuts.add(Math.max(0, Math.min(diagnostic.from, length)));
1698
+ cuts.add(Math.max(0, Math.min(diagnostic.to, length)));
1699
+ }
1700
+ const bounds = [...cuts].sort((left, right) => left - right);
1701
+ const segments = [];
1702
+ for (let index = 0; index < bounds.length - 1; index += 1) {
1703
+ const from = bounds[index] ?? 0;
1704
+ const to = bounds[index + 1] ?? 0;
1705
+ if (to <= from) continue;
1706
+ const scope = coverScope(input.tokens, from, fallbackScope);
1707
+ const decorations = coverDecorations(input.decorations, from, to);
1708
+ const severity = coverSeverity(input.diagnostics, from, to);
1709
+ const title = input.decorations.find(
1710
+ (decoration) => decoration.title !== void 0 && from >= decoration.from && to <= decoration.to
1711
+ )?.title;
1712
+ const last = segments[segments.length - 1];
1713
+ if (last !== void 0 && last.scope === scope && last.severity === severity && last.title === title && sameList(last.decorations, decorations)) {
1714
+ last.to = to;
1715
+ last.text = text.slice(last.from, to);
1716
+ continue;
1717
+ }
1718
+ segments.push({ from, to, text: text.slice(from, to), scope, decorations, severity, title });
1719
+ }
1720
+ return segments;
1721
+ }
1722
+ function coverScope(tokens, offset, fallback) {
1723
+ for (const token of tokens) {
1724
+ if (token.from > offset) break;
1725
+ if (offset >= token.from && offset < token.to) return token.scope;
1726
+ }
1727
+ return fallback;
1728
+ }
1729
+ function coverDecorations(decorations, from, to) {
1730
+ const kinds = [];
1731
+ for (const decoration of decorations) {
1732
+ if (decoration.from <= from && to <= decoration.to) kinds.push(decoration.kind);
1733
+ }
1734
+ return kinds;
1735
+ }
1736
+ function coverSeverity(diagnostics, from, to) {
1737
+ let loudest;
1738
+ for (const diagnostic of diagnostics) {
1739
+ if (diagnostic.from > from || diagnostic.to < to) continue;
1740
+ if (loudest === void 0 || SEVERITY_RANK[diagnostic.severity] > SEVERITY_RANK[loudest]) {
1741
+ loudest = diagnostic.severity;
1742
+ }
1743
+ }
1744
+ return loudest;
1745
+ }
1746
+ function sameList(left, right) {
1747
+ if (left.length !== right.length) return false;
1748
+ for (let index = 0; index < left.length; index += 1) {
1749
+ if (left[index] !== right[index]) return false;
1750
+ }
1751
+ return true;
1752
+ }
1753
+ function segmentClasses(segment, scopeClass2, decorationClass2, severityClass2) {
1754
+ const classes = [scopeClass2(segment.scope)];
1755
+ for (const kind of segment.decorations) classes.push(decorationClass2(kind));
1756
+ if (segment.severity !== void 0) classes.push(severityClass2(segment.severity));
1757
+ return classes;
1758
+ }
1759
+
1760
+ // src/dom/overlay.ts
1761
+ var Overlay = class {
1762
+ /** The scroll container. Same box as the field. */
1763
+ element;
1764
+ /** The element the spans are written into. */
1765
+ paint;
1766
+ document;
1767
+ classNames;
1768
+ /** What was painted last, so an unchanged document is not repainted. */
1769
+ paintedText;
1770
+ paintedKey = "";
1771
+ /**
1772
+ * @param ownerDocument - the document to build in.
1773
+ * @param classNames - the three class-name mappings.
1774
+ */
1775
+ constructor(ownerDocument, classNames) {
1776
+ this.document = ownerDocument;
1777
+ this.classNames = classNames;
1778
+ this.element = ownerDocument.createElement("div");
1779
+ this.element.className = "litearea-layer";
1780
+ this.element.setAttribute("aria-hidden", "true");
1781
+ this.element.dataset.liteareaPart = "layer";
1782
+ this.paint = ownerDocument.createElement("div");
1783
+ this.paint.className = "litearea-paint";
1784
+ this.element.appendChild(this.paint);
1785
+ }
1786
+ /**
1787
+ * Paint a document.
1788
+ *
1789
+ * `key` is whatever the caller knows changed. When it and the text both match
1790
+ * the last call the work is skipped, which is what keeps a caret move or a
1791
+ * mouse hover from rebuilding every span on the page.
1792
+ * @param text - the document.
1793
+ * @param input - the tokens, decorations, and diagnostics.
1794
+ * @param key - a cheap signature of everything that affects the paint.
1795
+ * @param fallbackScope - the scope for characters no token covers.
1796
+ */
1797
+ render(text, input, key, fallbackScope = "text") {
1798
+ if (this.paintedText === text && this.paintedKey === key) return;
1799
+ const segments = buildSegments(text, input, fallbackScope);
1800
+ const fragment = this.document.createDocumentFragment();
1801
+ for (const segment of segments) {
1802
+ const span = this.document.createElement("span");
1803
+ span.className = segmentClasses(
1804
+ segment,
1805
+ this.classNames.scope,
1806
+ this.classNames.decoration,
1807
+ this.classNames.severity
1808
+ ).join(" ");
1809
+ if (segment.title !== void 0) span.title = segment.title;
1810
+ span.textContent = segment.text;
1811
+ fragment.appendChild(span);
1812
+ }
1813
+ this.paint.replaceChildren(fragment);
1814
+ this.paintedText = text;
1815
+ this.paintedKey = key;
1816
+ }
1817
+ /**
1818
+ * Follow the field's scroll position.
1819
+ *
1820
+ * The layer is `overflow: hidden` and its content is taller than its box exactly
1821
+ * when the field is: an auto-grown field has nothing to scroll and a field
1822
+ * clamped to its maximum height has everything to scroll. Copying the offset is
1823
+ * therefore enough, and it is more reliable than a transform, which can leave
1824
+ * the text on a half pixel.
1825
+ * @param field - the textarea.
1826
+ */
1827
+ syncScroll(field) {
1828
+ if (this.element.scrollTop !== field.scrollTop) this.element.scrollTop = field.scrollTop;
1829
+ if (this.element.scrollLeft !== field.scrollLeft) this.element.scrollLeft = field.scrollLeft;
1830
+ }
1831
+ /** Forget what was painted, so the next render rebuilds. */
1832
+ invalidate() {
1833
+ this.paintedText = void 0;
1834
+ this.paintedKey = "";
1835
+ }
1836
+ /** Take the layer out of the document. */
1837
+ destroy() {
1838
+ this.element.remove();
1839
+ this.paint.replaceChildren();
1840
+ }
1841
+ };
1842
+
1843
+ // src/dom/popup.ts
1844
+ var Popup = class {
1845
+ /** The positioned container. */
1846
+ element;
1847
+ /** The scrolling element, which holds the rows and nothing else. */
1848
+ list;
1849
+ document;
1850
+ handlers;
1851
+ rows = [];
1852
+ active = -1;
1853
+ open = false;
1854
+ showDocs = true;
1855
+ docs;
1856
+ prefix;
1857
+ /**
1858
+ * @param ownerDocument - the document to build in.
1859
+ * @param handlers - how to report a pick and a hover.
1860
+ * @param idPrefix - a stable prefix for row ids, so two editors do not collide.
1861
+ */
1862
+ constructor(ownerDocument, handlers, idPrefix) {
1863
+ this.document = ownerDocument;
1864
+ this.handlers = handlers;
1865
+ this.prefix = idPrefix;
1866
+ this.element = ownerDocument.createElement("div");
1867
+ this.element.className = "litearea-popup";
1868
+ this.element.dataset.liteareaPart = "popup";
1869
+ this.element.dataset.open = "false";
1870
+ this.list = ownerDocument.createElement("div");
1871
+ this.list.className = "litearea-list";
1872
+ this.list.setAttribute("role", "listbox");
1873
+ this.list.id = `${idPrefix}-listbox`;
1874
+ this.element.appendChild(this.list);
1875
+ this.docs = ownerDocument.createElement("div");
1876
+ this.docs.className = "litearea-docs";
1877
+ this.docs.hidden = true;
1878
+ this.element.appendChild(this.docs);
1879
+ this.list.addEventListener("mousedown", this.onMouseDown);
1880
+ this.list.addEventListener("mousemove", this.onMouseMove);
1881
+ }
1882
+ /** Whether the list is showing. */
1883
+ get isOpen() {
1884
+ return this.open;
1885
+ }
1886
+ /** The active row's index, or -1. */
1887
+ get activeIndex() {
1888
+ return this.active;
1889
+ }
1890
+ /** The rows currently shown. */
1891
+ get items() {
1892
+ return this.rows;
1893
+ }
1894
+ /** The active row, when there is one. */
1895
+ get activeRow() {
1896
+ return this.rows[this.active];
1897
+ }
1898
+ /** The id the field's `aria-controls` should name. It is the list, not the container. */
1899
+ get listId() {
1900
+ return this.list.id;
1901
+ }
1902
+ /**
1903
+ * The id of the active row, for the field's `aria-activedescendant`.
1904
+ *
1905
+ * The list does not write that attribute itself: the field belongs to the editor, and a
1906
+ * floating list reaching out of its own subtree to find it is the kind of coupling that
1907
+ * breaks the moment the two are mounted somewhere unexpected.
1908
+ * @returns the row id, or undefined when no row is active.
1909
+ */
1910
+ get activeRowId() {
1911
+ return this.active >= 0 ? this.rowId(this.active) : void 0;
1912
+ }
1913
+ /**
1914
+ * Show a list.
1915
+ * @param rows - the rows, already ranked.
1916
+ * @param active - the row to make active.
1917
+ * @param showDocs - whether to render the documentation panel.
1918
+ */
1919
+ show(rows, active, showDocs) {
1920
+ this.rows = rows;
1921
+ this.active = active;
1922
+ this.showDocs = showDocs;
1923
+ this.open = true;
1924
+ this.element.dataset.open = "true";
1925
+ this.element.dataset.docs = showDocs ? "true" : "false";
1926
+ this.list.scrollTop = 0;
1927
+ this.render();
1928
+ }
1929
+ /** Hide the list and forget its rows. */
1930
+ close() {
1931
+ if (!this.open) return;
1932
+ this.open = false;
1933
+ this.active = -1;
1934
+ this.rows = [];
1935
+ this.element.dataset.open = "false";
1936
+ this.list.replaceChildren();
1937
+ this.docs.hidden = true;
1938
+ this.docs.replaceChildren();
1939
+ }
1940
+ /**
1941
+ * Make a row active without rebuilding the list.
1942
+ * @param index - the row index.
1943
+ */
1944
+ setActive(index) {
1945
+ if (index === this.active) return;
1946
+ const previous = this.list.querySelector(`#${this.rowId(this.active)}`);
1947
+ if (previous !== null) previous.setAttribute("aria-selected", "false");
1948
+ this.active = index;
1949
+ const next = this.list.querySelector(`#${this.rowId(index)}`);
1950
+ if (next !== null) {
1951
+ next.setAttribute("aria-selected", "true");
1952
+ const top = next.offsetTop;
1953
+ const bottom = top + next.offsetHeight;
1954
+ if (top < this.list.scrollTop) this.list.scrollTop = top;
1955
+ else if (bottom > this.list.scrollTop + this.list.clientHeight) {
1956
+ this.list.scrollTop = bottom - this.list.clientHeight;
1957
+ }
1958
+ }
1959
+ this.renderDocs();
1960
+ }
1961
+ /**
1962
+ * Place the list under an anchor, flipping above it when there is no room below.
1963
+ *
1964
+ * The container's own box is what is measured, so the documentation panel counts towards the
1965
+ * height and a popup whose rows and explanation together would run off the bottom flips as a
1966
+ * whole rather than being cut in half.
1967
+ * @param anchor - the caret's box, in the container's coordinates.
1968
+ * @param container - the element the list is positioned against.
1969
+ * @param viewport - the visible area to stay inside.
1970
+ */
1971
+ place(anchor, container, viewport) {
1972
+ const box = this.element.getBoundingClientRect();
1973
+ const containerBox = container.getBoundingClientRect();
1974
+ const gap = 4;
1975
+ let top = anchor.y + anchor.height + gap;
1976
+ if (containerBox.top + top + box.height > viewport.height - gap) {
1977
+ const above = anchor.y - box.height - gap;
1978
+ if (containerBox.top + above >= gap) top = above;
1979
+ }
1980
+ let left = anchor.x;
1981
+ const maxLeft = containerBox.width - box.width;
1982
+ if (left > maxLeft) left = Math.max(0, maxLeft);
1983
+ this.element.style.top = `${String(Math.round(top))}px`;
1984
+ this.element.style.left = `${String(Math.round(left))}px`;
1985
+ }
1986
+ /** Take the list out of the document. */
1987
+ destroy() {
1988
+ this.list.removeEventListener("mousedown", this.onMouseDown);
1989
+ this.list.removeEventListener("mousemove", this.onMouseMove);
1990
+ this.element.remove();
1991
+ this.list.replaceChildren();
1992
+ this.docs.replaceChildren();
1993
+ }
1994
+ /** The DOM id of a row, which `aria-activedescendant` points at. */
1995
+ rowId(index) {
1996
+ return `${this.prefix}-row-${String(index)}`;
1997
+ }
1998
+ // ── internals ────────────────────────────────────────────────────────────
1999
+ render() {
2000
+ const fragment = this.document.createDocumentFragment();
2001
+ this.rows.forEach((row, index) => {
2002
+ const element = this.document.createElement("div");
2003
+ element.className = "litearea-row";
2004
+ element.id = this.rowId(index);
2005
+ element.setAttribute("role", "option");
2006
+ element.setAttribute("aria-selected", index === this.active ? "true" : "false");
2007
+ element.dataset.index = String(index);
2008
+ const kind = this.document.createElement("span");
2009
+ kind.className = `litearea-rowKind litearea-kind-${row.item.kind ?? "value"}`;
2010
+ element.appendChild(kind);
2011
+ const label = this.document.createElement("span");
2012
+ label.className = "litearea-rowLabel";
2013
+ for (const piece of highlightSegments(row.item.label, row.indices)) {
2014
+ if (piece.matched) {
2015
+ const mark = this.document.createElement("span");
2016
+ mark.className = "litearea-rowMatch";
2017
+ mark.textContent = piece.text;
2018
+ label.appendChild(mark);
2019
+ } else {
2020
+ label.appendChild(this.document.createTextNode(piece.text));
2021
+ }
2022
+ }
2023
+ element.appendChild(label);
2024
+ if (row.item.detail !== void 0) {
2025
+ const detail = this.document.createElement("span");
2026
+ detail.className = "litearea-rowDetail";
2027
+ detail.textContent = row.item.detail;
2028
+ element.appendChild(detail);
2029
+ }
2030
+ fragment.appendChild(element);
2031
+ });
2032
+ this.list.replaceChildren(fragment);
2033
+ this.renderDocs();
2034
+ }
2035
+ /** Refresh the documentation panel from the active row. */
2036
+ renderDocs() {
2037
+ const item = this.rows[this.active]?.item;
2038
+ const hasDocs = this.showDocs && this.open && item !== void 0 && (item.documentation !== void 0 || item.detail !== void 0);
2039
+ this.docs.hidden = !hasDocs;
2040
+ if (!hasDocs || item === void 0) {
2041
+ this.docs.replaceChildren();
2042
+ return;
2043
+ }
2044
+ const fragment = this.document.createDocumentFragment();
2045
+ const title = this.document.createElement("div");
2046
+ title.className = "litearea-docsTitle";
2047
+ title.textContent = item.label;
2048
+ fragment.appendChild(title);
2049
+ if (item.detail !== void 0) {
2050
+ const detail = this.document.createElement("div");
2051
+ detail.className = "litearea-docsDetail";
2052
+ detail.textContent = item.detail;
2053
+ fragment.appendChild(detail);
2054
+ }
2055
+ if (item.documentation !== void 0) {
2056
+ const body = this.document.createElement("div");
2057
+ body.className = "litearea-docsBody";
2058
+ body.textContent = item.documentation;
2059
+ fragment.appendChild(body);
2060
+ }
2061
+ this.docs.replaceChildren(fragment);
2062
+ this.docs.scrollTop = 0;
2063
+ }
2064
+ onMouseDown = (event) => {
2065
+ const row = event.target?.closest(".litearea-row");
2066
+ if (row === null || row === void 0) return;
2067
+ event.preventDefault();
2068
+ const index = Number.parseInt(row.dataset.index ?? "-1", 10);
2069
+ if (Number.isInteger(index) && index >= 0) this.handlers.accept(index);
2070
+ };
2071
+ onMouseMove = (event) => {
2072
+ const row = event.target?.closest(".litearea-row");
2073
+ if (row === null || row === void 0) return;
2074
+ const index = Number.parseInt(row.dataset.index ?? "-1", 10);
2075
+ if (!Number.isInteger(index) || index < 0 || index === this.active) return;
2076
+ this.handlers.hover(index);
2077
+ };
2078
+ };
2079
+
2080
+ // src/dom/tooltip.ts
2081
+ var Tooltip = class {
2082
+ /** The tooltip element. */
2083
+ element;
2084
+ document;
2085
+ open = false;
2086
+ /**
2087
+ * @param ownerDocument - the document to build in.
2088
+ */
2089
+ constructor(ownerDocument) {
2090
+ this.document = ownerDocument;
2091
+ this.element = ownerDocument.createElement("div");
2092
+ this.element.className = "litearea-tooltip";
2093
+ this.element.setAttribute("role", "tooltip");
2094
+ this.element.dataset.liteareaPart = "tooltip";
2095
+ this.element.dataset.open = "false";
2096
+ }
2097
+ /** Whether the tooltip is showing. */
2098
+ get isOpen() {
2099
+ return this.open;
2100
+ }
2101
+ /**
2102
+ * Show a hover.
2103
+ * @param info - what to say.
2104
+ * @param anchor - where the thing being described is.
2105
+ * @param container - the element the tooltip is positioned against.
2106
+ * @param viewport - the visible area to stay inside.
2107
+ */
2108
+ show(info, anchor, container, viewport) {
2109
+ const fragment = this.document.createDocumentFragment();
2110
+ if (info.title !== void 0) {
2111
+ const title = this.document.createElement("div");
2112
+ title.className = "litearea-tooltipTitle";
2113
+ title.textContent = info.title;
2114
+ fragment.appendChild(title);
2115
+ }
2116
+ if (info.detail !== void 0 && info.detail !== "") {
2117
+ const detail = this.document.createElement("div");
2118
+ detail.className = "litearea-tooltipDetail";
2119
+ detail.textContent = info.detail;
2120
+ fragment.appendChild(detail);
2121
+ }
2122
+ if (info.body !== void 0 && info.body !== "") {
2123
+ const body = this.document.createElement("div");
2124
+ body.className = "litearea-tooltipBody";
2125
+ body.textContent = info.body;
2126
+ fragment.appendChild(body);
2127
+ }
2128
+ this.element.replaceChildren(fragment);
2129
+ this.open = true;
2130
+ this.element.dataset.open = "true";
2131
+ const box = this.element.getBoundingClientRect();
2132
+ const containerBox = container.getBoundingClientRect();
2133
+ const gap = 6;
2134
+ let top = anchor.y + anchor.height + gap;
2135
+ if (containerBox.top + top + box.height > viewport.height - gap) {
2136
+ const above = anchor.y - box.height - gap;
2137
+ top = containerBox.top + above >= gap ? above : Math.max(0, viewport.height - gap - box.height - containerBox.top);
2138
+ }
2139
+ let left = anchor.x;
2140
+ const overflowRight = containerBox.left + left + box.width - (viewport.width - gap);
2141
+ if (overflowRight > 0) left = Math.max(0, left - overflowRight);
2142
+ this.element.style.top = `${String(Math.round(top))}px`;
2143
+ this.element.style.left = `${String(Math.round(left))}px`;
2144
+ }
2145
+ /** Hide the tooltip. */
2146
+ hide() {
2147
+ if (!this.open) return;
2148
+ this.open = false;
2149
+ this.element.dataset.open = "false";
2150
+ this.element.replaceChildren();
2151
+ }
2152
+ /** Take the tooltip out of the document. */
2153
+ destroy() {
2154
+ this.element.remove();
2155
+ this.element.replaceChildren();
2156
+ }
2157
+ };
2158
+
2159
+ // src/dom/editor.ts
2160
+ var PAGE_STEP = 8;
2161
+ var LiteArea = class {
2162
+ /** The positioning container. Put this in the page. */
2163
+ element;
2164
+ /** The real field. Exposed for a host that needs the element itself. */
2165
+ input;
2166
+ document;
2167
+ view;
2168
+ /**
2169
+ * The grammar as the host declared it, kept so {@link refresh} can re-resolve it.
2170
+ *
2171
+ * Mutable on purpose, and it is what lets a host pass a live object — a proxy
2172
+ * reading the newest props, say — without the editor having to be rebuilt when the
2173
+ * language changes. A rebuild would throw away the undo history, which is the one
2174
+ * thing this library must not do, so re-resolving is the only acceptable answer.
2175
+ */
2176
+ declaredGrammar;
2177
+ /** The grammar the engine is currently running, with its defaults filled in. */
2178
+ grammar;
2179
+ box;
2180
+ overlay;
2181
+ popup;
2182
+ tooltip;
2183
+ mirror;
2184
+ sizing;
2185
+ completion;
2186
+ hover;
2187
+ paintDecorations;
2188
+ handlers;
2189
+ injectedStyle;
2190
+ instanceId;
2191
+ fontsReady;
2192
+ /** The current inspection, and the text it was computed from. */
2193
+ current;
2194
+ currentText;
2195
+ /** Bumped whenever the inspection changes, so the painter can skip work. */
2196
+ revision = 0;
2197
+ /** The list on screen, with the range frozen when it opened. */
2198
+ completionState;
2199
+ /** True during an IME composition, when no completion may run. */
2200
+ composing = false;
2201
+ /** True while the editor itself is writing, so its own edit does not re-open the list. */
2202
+ applying = false;
2203
+ /** The height and overflow last written, so unchanged values are not rewritten. */
2204
+ appliedHeight = -1;
2205
+ appliedOverflow = "";
2206
+ appliedScrollbar = -1;
2207
+ /** The offset the tooltip last described, so a resting pointer does not re-query. */
2208
+ hoverOffset;
2209
+ /** The custom properties this instance set, so one that disappears can be removed. */
2210
+ appliedVariables = /* @__PURE__ */ new Set();
2211
+ hoverTimer;
2212
+ /** Watches for a width change, which invalidates wrapping and the box height. */
2213
+ resizeObserver;
2214
+ observedWidth = -1;
2215
+ /**
2216
+ * A signature of the last announced problem list, so the host is told once.
2217
+ *
2218
+ * Starts as `undefined` rather than as the empty string, because "no problems" is a
2219
+ * real signature and a host waiting to be told that the list is clear would otherwise
2220
+ * never hear it — it would keep whatever it was showing before the editor existed.
2221
+ */
2222
+ announced;
2223
+ destroyed = false;
2224
+ /**
2225
+ * @param options - the grammar, the initial text, and the behaviour to use.
2226
+ */
2227
+ constructor(options) {
2228
+ const probe = typeof document === "undefined" ? void 0 : document;
2229
+ if (probe === void 0) {
2230
+ throw new Error("litearea: an editor needs a document, and there is none in this environment");
2231
+ }
2232
+ this.document = probe;
2233
+ this.view = probe.defaultView ?? void 0;
2234
+ this.handlers = options;
2235
+ const declared = options.grammar;
2236
+ this.declaredGrammar = isResolvedGrammar(declared) ? declared.grammar : declared;
2237
+ this.grammar = resolveGrammar(this.declaredGrammar);
2238
+ this.instanceId = `litearea-${Math.random().toString(36).slice(2, 9)}`;
2239
+ this.sizing = withDefaults(
2240
+ { autoGrow: true, minRows: 1 },
2241
+ options.sizing
2242
+ );
2243
+ this.completion = options.completion === false ? void 0 : withDefaults(
2244
+ { auto: true, triggerCharacters: " ", limit: 100, showDocumentation: true },
2245
+ options.completion
2246
+ );
2247
+ this.hover = options.hover === false ? void 0 : withDefaults({ enabled: true, delay: 140 }, options.hover);
2248
+ this.paintDecorations = options.decorations !== false;
2249
+ if (options.injectStyles !== false) {
2250
+ this.injectedStyle = injectStyles(this.document, options.styleNonce);
2251
+ }
2252
+ this.element = this.document.createElement("div");
2253
+ this.element.className = options.className === void 0 ? "litearea" : `litearea ${options.className}`;
2254
+ this.element.classList.add(this.sizing.autoGrow ? "litearea-growable" : "litearea-resizable");
2255
+ if (options.readOnly === true) this.element.classList.add("litearea-readonly");
2256
+ if (options.variables !== void 0) this.applyVariables(options.variables);
2257
+ this.box = this.document.createElement("div");
2258
+ this.box.className = "litearea-box";
2259
+ this.element.appendChild(this.box);
2260
+ this.overlay = new Overlay(this.document, {
2261
+ scope: scopeClass,
2262
+ decoration: decorationClass,
2263
+ severity: severityClass
2264
+ });
2265
+ this.box.appendChild(this.overlay.element);
2266
+ this.input = this.document.createElement("textarea");
2267
+ this.input.className = "litearea-input";
2268
+ this.input.dataset.liteareaPart = "input";
2269
+ this.input.spellcheck = options.spellCheck === true;
2270
+ this.input.autocomplete = "off";
2271
+ this.input.setAttribute("autocorrect", "off");
2272
+ this.input.setAttribute("autocapitalize", "off");
2273
+ this.input.setAttribute("wrap", "soft");
2274
+ this.input.readOnly = options.readOnly === true;
2275
+ this.input.rows = this.sizing.minRows;
2276
+ if (options.placeholder !== void 0) this.input.placeholder = options.placeholder;
2277
+ if (options.ariaLabel !== void 0) this.input.setAttribute("aria-label", options.ariaLabel);
2278
+ this.input.setAttribute("aria-autocomplete", this.completion === void 0 ? "none" : "list");
2279
+ this.input.setAttribute("aria-expanded", "false");
2280
+ this.input.setAttribute("role", "combobox");
2281
+ this.input.value = options.value ?? "";
2282
+ this.box.appendChild(this.input);
2283
+ this.popup = new Popup(
2284
+ this.document,
2285
+ {
2286
+ accept: (index) => {
2287
+ this.acceptCompletion(index);
2288
+ },
2289
+ hover: (index) => {
2290
+ this.setActive(index);
2291
+ }
2292
+ },
2293
+ this.instanceId
2294
+ );
2295
+ this.element.appendChild(this.popup.element);
2296
+ this.input.setAttribute("aria-controls", this.popup.listId);
2297
+ this.tooltip = new Tooltip(this.document);
2298
+ this.element.appendChild(this.tooltip.element);
2299
+ this.mirror = new TextMirror(this.document);
2300
+ this.mirror.mount(this.document.body ?? this.document.documentElement);
2301
+ const Observer = this.view?.ResizeObserver;
2302
+ if (typeof Observer === "function") {
2303
+ this.resizeObserver = new Observer(() => {
2304
+ const width = this.element.clientWidth;
2305
+ if (width === this.observedWidth) return;
2306
+ this.observedWidth = width;
2307
+ this.mirror.adopt(this.input);
2308
+ this.resize(this.input.value);
2309
+ this.placePopup();
2310
+ });
2311
+ this.resizeObserver.observe(this.element);
2312
+ }
2313
+ const fonts = this.document.fonts;
2314
+ this.fontsReady = fonts?.ready;
2315
+ this.bind();
2316
+ this.sync();
2317
+ if (this.fontsReady !== void 0) {
2318
+ void this.fontsReady.then(() => {
2319
+ if (!this.destroyed) this.refresh();
2320
+ });
2321
+ }
2322
+ }
2323
+ // ── what a host reads ──────────────────────────────────────────────────────
2324
+ /** The current text. */
2325
+ get value() {
2326
+ return this.input.value;
2327
+ }
2328
+ /** The caret or selection. */
2329
+ get selection() {
2330
+ return readSelection(this.input);
2331
+ }
2332
+ /** The last inspection, or undefined before the first one. */
2333
+ get inspection() {
2334
+ return this.current;
2335
+ }
2336
+ /** The problems the grammar found. */
2337
+ get diagnostics() {
2338
+ return this.current?.diagnostics ?? [];
2339
+ }
2340
+ /** The list on screen, when one is. */
2341
+ get currentCompletion() {
2342
+ return this.completionState;
2343
+ }
2344
+ /** Whether the editor has focus. */
2345
+ get focused() {
2346
+ return this.document.activeElement === this.input;
2347
+ }
2348
+ // ── what a host calls ─────────────────────────────────────────────────────
2349
+ /**
2350
+ * Replace the text.
2351
+ *
2352
+ * `preserveHistory` writes through the editing pipeline, so the replacement is
2353
+ * one undoable edit and Ctrl+Z brings the old text back — what a Reset button
2354
+ * wants. Without it the value property is assigned, which is faster and clears
2355
+ * the history, which is what loading a different document wants.
2356
+ *
2357
+ * This is the only path that writes the text, and it is never used for an edit
2358
+ * the user could have made.
2359
+ * @param next - the new text.
2360
+ * @param preserveHistory - whether Ctrl+Z should be able to undo it.
2361
+ * @returns how the write landed.
2362
+ */
2363
+ setValue(next, preserveHistory = false) {
2364
+ if (this.input.value === next) return "unchanged";
2365
+ this.applying = true;
2366
+ let outcome;
2367
+ try {
2368
+ outcome = writeDocument(this.input, next, preserveHistory);
2369
+ } finally {
2370
+ this.applying = false;
2371
+ }
2372
+ this.sync();
2373
+ return outcome;
2374
+ }
2375
+ /**
2376
+ * Put the caret somewhere.
2377
+ * @param start - the anchor offset.
2378
+ * @param end - the moving offset; defaults to `start`.
2379
+ */
2380
+ setSelection(start, end = start) {
2381
+ writeSelection(this.input, start, end);
2382
+ this.updateCompletionForCaret();
2383
+ }
2384
+ /** Move the caret into the field. */
2385
+ focus() {
2386
+ this.input.focus();
2387
+ }
2388
+ /** Undo, through the browser's history. */
2389
+ undo() {
2390
+ const done = undoField(this.input);
2391
+ if (done) this.sync();
2392
+ return done;
2393
+ }
2394
+ /** Redo, through the browser's history. */
2395
+ redo() {
2396
+ const done = redoField(this.input);
2397
+ if (done) this.sync();
2398
+ return done;
2399
+ }
2400
+ /**
2401
+ * Set CSS custom properties on the wrapper, replacing whatever this method set last time.
2402
+ *
2403
+ * A property that has disappeared from the record is removed rather than left behind, so a
2404
+ * host can un-theme by passing a smaller object. Properties set by other means — a stylesheet,
2405
+ * or the wrapper's inline style directly — are not touched, and a removed one falls back to
2406
+ * whatever CSS says.
2407
+ *
2408
+ * Safe to call at any time after construction. The constructor uses the private write-only
2409
+ * half, because re-measuring needs the field, the mirror, and the overlay, none of which exist
2410
+ * until it has finished.
2411
+ * @param next - the properties, keyed as {@link LiteAreaOptions.variables} describes.
2412
+ */
2413
+ setVariables(next) {
2414
+ this.applyVariables(next);
2415
+ this.mirror.adopt(this.input);
2416
+ this.appliedHeight = -1;
2417
+ this.sync();
2418
+ }
2419
+ /** Write the properties, and forget the ones that are gone. No re-measure. */
2420
+ applyVariables(next) {
2421
+ const keep = /* @__PURE__ */ new Set();
2422
+ for (const [key, value] of Object.entries(next)) {
2423
+ const name = variableName(key);
2424
+ keep.add(name);
2425
+ this.element.style.setProperty(name, value);
2426
+ }
2427
+ for (const name of this.appliedVariables) {
2428
+ if (!keep.has(name)) this.element.style.removeProperty(name);
2429
+ }
2430
+ this.appliedVariables = keep;
2431
+ }
2432
+ /**
2433
+ * Re-read the document with the same text.
2434
+ *
2435
+ * For a host whose grammar depends on something outside it — an installed font
2436
+ * list that has just been re-read, a palette that changed — and for the moment a
2437
+ * web font finishes loading.
2438
+ *
2439
+ * The grammar is re-resolved here, which is what makes a live grammar object
2440
+ * work: a host that rebuilds its rules on every render can call `refresh()` and
2441
+ * the editor picks the new ones up without being rebuilt, so the undo history
2442
+ * survives a language change.
2443
+ */
2444
+ refresh() {
2445
+ this.grammar = resolveGrammar(this.declaredGrammar);
2446
+ this.current = void 0;
2447
+ this.currentText = void 0;
2448
+ this.mirror.adopt(this.input);
2449
+ this.overlay.invalidate();
2450
+ this.appliedHeight = -1;
2451
+ this.appliedOverflow = "";
2452
+ this.appliedScrollbar = -1;
2453
+ this.sync();
2454
+ }
2455
+ /** Open the completion list on demand, as Ctrl+Space does. */
2456
+ showCompletions() {
2457
+ if (this.completion === void 0) return;
2458
+ this.openCompletion("explicit");
2459
+ }
2460
+ /** Close the completion list. */
2461
+ hideCompletions() {
2462
+ this.closeCompletion();
2463
+ }
2464
+ /** Remove the editor and every listener it owns. */
2465
+ destroy() {
2466
+ if (this.destroyed) return;
2467
+ this.destroyed = true;
2468
+ this.unbind();
2469
+ this.resizeObserver?.disconnect();
2470
+ this.resizeObserver = void 0;
2471
+ if (this.hoverTimer !== void 0) this.view?.clearTimeout(this.hoverTimer);
2472
+ this.popup.destroy();
2473
+ this.tooltip.destroy();
2474
+ this.overlay.destroy();
2475
+ this.mirror.destroy();
2476
+ void this.injectedStyle;
2477
+ this.element.remove();
2478
+ }
2479
+ // ── the pipeline ──────────────────────────────────────────────────────────
2480
+ /**
2481
+ * Bring everything up to date with the field.
2482
+ *
2483
+ * The inspection is cached on the text, so moving the caret or scrolling costs
2484
+ * nothing beyond the paint — and the paint itself is skipped when neither the
2485
+ * text nor the analysis changed.
2486
+ */
2487
+ sync() {
2488
+ const text = this.input.value;
2489
+ if (this.current === void 0 || this.currentText !== text) {
2490
+ this.current = inspect(text, this.grammar);
2491
+ this.currentText = text;
2492
+ this.revision += 1;
2493
+ }
2494
+ const inspection = this.current;
2495
+ this.paint(inspection);
2496
+ this.resize(text);
2497
+ this.overlay.syncScroll(this.input);
2498
+ this.announceDiagnostics(inspection);
2499
+ }
2500
+ /** Paint the layer, and mark the box when a problem is an error. */
2501
+ paint(inspection) {
2502
+ const painting = {
2503
+ tokens: inspection.tokens,
2504
+ decorations: this.paintDecorations ? inspection.decorations : [],
2505
+ diagnostics: inspection.diagnostics
2506
+ };
2507
+ const key = [
2508
+ String(this.revision),
2509
+ this.paintDecorations ? "d" : "-",
2510
+ String(painting.decorations.length),
2511
+ String(painting.diagnostics.length)
2512
+ ].join(":");
2513
+ this.overlay.render(inspection.text, painting, key, this.grammar.fallbackScope);
2514
+ const hasError = inspection.diagnostics.some((diagnostic) => diagnostic.severity === "error");
2515
+ this.element.classList.toggle("litearea-invalid", hasError);
2516
+ this.input.setAttribute("aria-invalid", hasError ? "true" : "false");
2517
+ }
2518
+ /** Tell the host, once, when the problem list actually changed. */
2519
+ announceDiagnostics(inspection) {
2520
+ if (this.handlers.onDiagnostics === void 0) return;
2521
+ const key = inspection.diagnostics.map(
2522
+ (diagnostic) => `${String(diagnostic.from)}:${diagnostic.code ?? ""}:${diagnostic.message}`
2523
+ ).join("|");
2524
+ if (key === this.announced) return;
2525
+ this.announced = key;
2526
+ this.handlers.onDiagnostics(inspection.diagnostics);
2527
+ }
2528
+ /**
2529
+ * Size the box to its content.
2530
+ *
2531
+ * The mirror has no scrollbar, so its measurement is the height the content wants
2532
+ * with the full width available — which is exactly the number that decides whether
2533
+ * a scrollbar is needed at all. If it fits under the maximum, the height becomes
2534
+ * the content height and the overflow stays hidden, which is the promise of "grow
2535
+ * and shrink so no scrollbar is ever shown". If it does not fit, the height is
2536
+ * clamped and the overflow becomes `auto`.
2537
+ *
2538
+ * Clamping introduces a second problem that is easy to miss: a scrollbar narrows
2539
+ * the text, so the field rewraps, so the PAINT no longer wraps the same way and
2540
+ * every coloured span slides off its character. The measured scrollbar width is
2541
+ * therefore published as a custom property, and the stylesheet adds it to the
2542
+ * layer's own right padding so the two keep wrapping identically.
2543
+ *
2544
+ * An UNMOUNTED field is skipped rather than measured. Before the element is in the
2545
+ * document it has no layout, so its `clientWidth` is zero, the mirror wraps at every
2546
+ * character, and the measurement comes back several times too tall — a wrong height
2547
+ * that then has to be corrected on the next keystroke, which is what a box that
2548
+ * jumps on first focus actually is. The `ResizeObserver` installed at construction
2549
+ * does the first real measurement as soon as there is a width to measure against.
2550
+ * @param text - the current text.
2551
+ */
2552
+ resize(text) {
2553
+ if (!this.sizing.autoGrow) {
2554
+ this.mirror.adopt(this.input);
2555
+ return;
2556
+ }
2557
+ if (!this.input.isConnected || this.input.clientWidth === 0) return;
2558
+ const chrome = this.mirror.verticalPadding();
2559
+ const lineHeight = this.mirror.lineHeight(this.input);
2560
+ const minPx = Math.max(
2561
+ this.sizing.minHeight ?? 0,
2562
+ lineHeight * (this.sizing.minRows ?? 1) + chrome
2563
+ );
2564
+ const maxCandidate = Math.min(
2565
+ this.sizing.maxHeight ?? Number.POSITIVE_INFINITY,
2566
+ this.sizing.maxRows === void 0 ? Number.POSITIVE_INFINITY : lineHeight * this.sizing.maxRows + chrome
2567
+ );
2568
+ const maxPx = Math.max(maxCandidate, minPx);
2569
+ const content = this.mirror.contentHeight(this.input, text);
2570
+ const wanted = clamp(Math.max(content, minPx), minPx, maxPx);
2571
+ const overflow = content > wanted + 0.5 ? "auto" : "hidden";
2572
+ if (Math.abs(wanted - this.appliedHeight) > 0.5) {
2573
+ this.input.style.height = `${String(Math.round(wanted))}px`;
2574
+ this.appliedHeight = wanted;
2575
+ }
2576
+ if (overflow !== this.appliedOverflow) {
2577
+ this.input.style.overflowY = overflow;
2578
+ this.appliedOverflow = overflow;
2579
+ }
2580
+ const scrollbar = overflow === "auto" ? this.input.offsetWidth - this.input.clientWidth : 0;
2581
+ const width = Math.max(0, scrollbar);
2582
+ if (width !== this.appliedScrollbar) {
2583
+ this.element.style.setProperty("--litearea-scrollbar", `${String(width)}px`);
2584
+ this.appliedScrollbar = width;
2585
+ }
2586
+ }
2587
+ // ── completion ────────────────────────────────────────────────────────────
2588
+ /** The caret's offset, clamped into the text. */
2589
+ caret() {
2590
+ return clamp(readSelection(this.input).start, 0, this.input.value.length);
2591
+ }
2592
+ /** Resolve and show a list for the caret. */
2593
+ openCompletion(trigger) {
2594
+ if (this.completion === void 0 || this.current === void 0) return;
2595
+ const result = complete(this.current, this.grammar, {
2596
+ text: this.input.value,
2597
+ caret: this.caret(),
2598
+ trigger,
2599
+ previousSourceId: this.completionState?.sourceId,
2600
+ limit: this.completion.limit
2601
+ });
2602
+ if (result === void 0 || result.rows.length === 0) {
2603
+ this.closeCompletion();
2604
+ return;
2605
+ }
2606
+ this.completionState = result;
2607
+ this.popup.show(result.rows, 0, this.completion.showDocumentation);
2608
+ this.input.setAttribute("aria-expanded", "true");
2609
+ this.syncActiveDescendant();
2610
+ this.placePopup();
2611
+ this.handlers.onCompletion?.(result);
2612
+ }
2613
+ /** Close the list and tell the host. */
2614
+ closeCompletion() {
2615
+ if (this.completionState === void 0 && !this.popup.isOpen) return;
2616
+ this.completionState = void 0;
2617
+ this.popup.close();
2618
+ this.input.setAttribute("aria-expanded", "false");
2619
+ this.input.removeAttribute("aria-activedescendant");
2620
+ this.handlers.onCompletion?.(void 0);
2621
+ }
2622
+ /** Keep the field's `aria-activedescendant` pointing at the active row. */
2623
+ syncActiveDescendant() {
2624
+ const id = this.popup.activeRowId;
2625
+ if (id === void 0) this.input.removeAttribute("aria-activedescendant");
2626
+ else this.input.setAttribute("aria-activedescendant", id);
2627
+ }
2628
+ /** Put the list under the caret. */
2629
+ placePopup() {
2630
+ if (this.completionState === void 0) return;
2631
+ const box = this.mirror.caretBox(this.input, this.caret());
2632
+ if (box === void 0) return;
2633
+ const inputRect = this.input.getBoundingClientRect();
2634
+ const elementRect = this.element.getBoundingClientRect();
2635
+ this.popup.place(
2636
+ {
2637
+ x: inputRect.left - elementRect.left + box.x,
2638
+ y: inputRect.top - elementRect.top + box.y,
2639
+ height: box.height
2640
+ },
2641
+ this.element,
2642
+ { width: this.view?.innerWidth ?? 0, height: this.view?.innerHeight ?? 0 }
2643
+ );
2644
+ }
2645
+ /**
2646
+ * Take the active row.
2647
+ * @param index - the row to take.
2648
+ * @param commitCharacter - a character typed to trigger the pick, written after
2649
+ * the completion so the keystroke is not swallowed.
2650
+ */
2651
+ acceptCompletion(index, commitCharacter) {
2652
+ const state = this.completionState;
2653
+ const row = this.popup.items[index]?.item;
2654
+ if (state === void 0 || row === void 0) return;
2655
+ const applied = applyCompletion(this.input.value, state.range, row);
2656
+ this.applying = true;
2657
+ try {
2658
+ replaceThroughPipeline(this.input, applied.from, applied.to, applied.insert);
2659
+ writeSelection(this.input, applied.caret);
2660
+ if (commitCharacter !== void 0) {
2661
+ replaceThroughPipeline(this.input, applied.caret, applied.caret, commitCharacter);
2662
+ writeSelection(this.input, applied.caret + commitCharacter.length);
2663
+ }
2664
+ } finally {
2665
+ this.applying = false;
2666
+ }
2667
+ this.closeCompletion();
2668
+ this.sync();
2669
+ const inspection = this.current;
2670
+ if (inspection !== void 0) {
2671
+ this.grammar.grammar.onAccept?.({
2672
+ text: this.input.value,
2673
+ caret: this.caret(),
2674
+ item: row,
2675
+ state: inspection.state
2676
+ });
2677
+ }
2678
+ }
2679
+ /** Decide what an edit does to the list. */
2680
+ updateCompletionAfterInput(data, deletion) {
2681
+ if (this.completion === void 0 || this.applying || this.composing) return;
2682
+ if (this.input.readOnly) return;
2683
+ if (deletion) {
2684
+ if (this.completionState !== void 0) this.openCompletion("auto");
2685
+ return;
2686
+ }
2687
+ if (this.completionState !== void 0) {
2688
+ this.openCompletion("auto");
2689
+ return;
2690
+ }
2691
+ if (!this.completion.auto) return;
2692
+ if (!this.shouldAutoOpen(data)) return;
2693
+ this.openCompletion("auto");
2694
+ }
2695
+ /** Whether a keystroke is a reason to offer suggestions. */
2696
+ shouldAutoOpen(data) {
2697
+ if (data === "" || this.completion === void 0) return false;
2698
+ const last = data.slice(-1);
2699
+ if (this.completion.triggerCharacters.includes(last)) return true;
2700
+ return this.grammar.wordChars.test(last);
2701
+ }
2702
+ /** Close a list the caret has left. */
2703
+ updateCompletionForCaret() {
2704
+ const state = this.completionState;
2705
+ if (state === void 0) return;
2706
+ const caret = this.caret();
2707
+ if (caret < state.range.from || caret > state.range.to) this.closeCompletion();
2708
+ else this.placePopup();
2709
+ }
2710
+ // ── hover ─────────────────────────────────────────────────────────────────
2711
+ /** Start, or restart, the timer that shows a tooltip. */
2712
+ queueHover(offset, event) {
2713
+ if (this.hover === void 0 || !this.hover.enabled || this.current === void 0) return;
2714
+ if (offset === this.hoverOffset && this.tooltip.isOpen) return;
2715
+ this.hoverOffset = offset;
2716
+ this.hideTooltip();
2717
+ const clientX = event.clientX;
2718
+ const clientY = event.clientY;
2719
+ this.hoverTimer = this.view?.setTimeout(() => {
2720
+ this.hoverTimer = void 0;
2721
+ this.showHover(offset, clientX, clientY);
2722
+ }, this.hover.delay);
2723
+ }
2724
+ /** Resolve and show a tooltip. */
2725
+ showHover(offset, clientX, clientY) {
2726
+ if (this.current === void 0 || this.destroyed) return;
2727
+ const info = resolveHover(this.current, this.grammar, offset);
2728
+ if (info === void 0) {
2729
+ this.hideTooltip();
2730
+ return;
2731
+ }
2732
+ const rect = this.element.getBoundingClientRect();
2733
+ const lineHeight = this.mirror.lineHeight(this.input);
2734
+ this.tooltip.show(
2735
+ info,
2736
+ { x: clientX - rect.left, y: clientY - rect.top, height: lineHeight },
2737
+ this.element,
2738
+ { width: this.view?.innerWidth ?? 0, height: this.view?.innerHeight ?? 0 }
2739
+ );
2740
+ this.handlers.onHover?.(info);
2741
+ }
2742
+ /** Hide the tooltip and forget what it described. */
2743
+ hideTooltip() {
2744
+ if (this.hoverTimer !== void 0) {
2745
+ this.view?.clearTimeout(this.hoverTimer);
2746
+ this.hoverTimer = void 0;
2747
+ }
2748
+ if (!this.tooltip.isOpen) return;
2749
+ this.tooltip.hide();
2750
+ this.handlers.onHover?.(void 0);
2751
+ }
2752
+ // ── events ────────────────────────────────────────────────────────────────
2753
+ /** Attach every listener. */
2754
+ bind() {
2755
+ this.input.addEventListener("input", this.onInput);
2756
+ this.input.addEventListener("keydown", this.onKeyDown);
2757
+ this.input.addEventListener("scroll", this.onScroll);
2758
+ this.input.addEventListener("click", this.onCaretMoved);
2759
+ this.input.addEventListener("keyup", this.onKeyUp);
2760
+ this.input.addEventListener("select", this.onCaretMoved);
2761
+ this.input.addEventListener("blur", this.onBlur);
2762
+ this.input.addEventListener("mousemove", this.onMouseMove);
2763
+ this.input.addEventListener("mouseleave", this.onMouseLeave);
2764
+ this.input.addEventListener("compositionstart", this.onCompositionStart);
2765
+ this.input.addEventListener("compositionend", this.onCompositionEnd);
2766
+ this.document.addEventListener("selectionchange", this.onSelectionChange);
2767
+ }
2768
+ /** Detach every listener. */
2769
+ unbind() {
2770
+ this.input.removeEventListener("input", this.onInput);
2771
+ this.input.removeEventListener("keydown", this.onKeyDown);
2772
+ this.input.removeEventListener("scroll", this.onScroll);
2773
+ this.input.removeEventListener("click", this.onCaretMoved);
2774
+ this.input.removeEventListener("keyup", this.onKeyUp);
2775
+ this.input.removeEventListener("select", this.onCaretMoved);
2776
+ this.input.removeEventListener("blur", this.onBlur);
2777
+ this.input.removeEventListener("mousemove", this.onMouseMove);
2778
+ this.input.removeEventListener("mouseleave", this.onMouseLeave);
2779
+ this.input.removeEventListener("compositionstart", this.onCompositionStart);
2780
+ this.input.removeEventListener("compositionend", this.onCompositionEnd);
2781
+ this.document.removeEventListener("selectionchange", this.onSelectionChange);
2782
+ }
2783
+ onInput = (event) => {
2784
+ this.sync();
2785
+ const input = event;
2786
+ const type = input.inputType ?? "";
2787
+ const deletion = type.startsWith("delete");
2788
+ const data = typeof input.data === "string" ? input.data : "";
2789
+ this.hideTooltip();
2790
+ this.updateCompletionAfterInput(data, deletion);
2791
+ if (!this.applying) this.handlers.onChange?.(this.input.value);
2792
+ };
2793
+ onKeyDown = (event) => {
2794
+ if (this.commitCharacter(event)) return;
2795
+ if (event.key === "Escape") {
2796
+ if (this.popup.isOpen) {
2797
+ event.preventDefault();
2798
+ this.closeCompletion();
2799
+ return;
2800
+ }
2801
+ this.hideTooltip();
2802
+ return;
2803
+ }
2804
+ if (event.key === " " && (event.ctrlKey || event.metaKey)) {
2805
+ event.preventDefault();
2806
+ if (this.popup.isOpen) this.closeCompletion();
2807
+ else this.openCompletion("explicit");
2808
+ return;
2809
+ }
2810
+ if (!this.popup.isOpen) return;
2811
+ const last = this.popup.items.length - 1;
2812
+ switch (event.key) {
2813
+ case "ArrowDown":
2814
+ event.preventDefault();
2815
+ this.setActive(Math.min(this.popup.activeIndex + 1, last));
2816
+ return;
2817
+ case "ArrowUp":
2818
+ event.preventDefault();
2819
+ this.setActive(Math.max(this.popup.activeIndex - 1, 0));
2820
+ return;
2821
+ case "PageDown":
2822
+ event.preventDefault();
2823
+ this.setActive(Math.min(this.popup.activeIndex + PAGE_STEP, last));
2824
+ return;
2825
+ case "PageUp":
2826
+ event.preventDefault();
2827
+ this.setActive(Math.max(this.popup.activeIndex - PAGE_STEP, 0));
2828
+ return;
2829
+ case "Enter":
2830
+ event.preventDefault();
2831
+ this.acceptCompletion(this.popup.activeIndex);
2832
+ return;
2833
+ case "Tab":
2834
+ event.preventDefault();
2835
+ this.acceptCompletion(this.popup.activeIndex);
2836
+ return;
2837
+ default:
2838
+ return;
2839
+ }
2840
+ };
2841
+ /**
2842
+ * Whether a keystroke is a commit character for the active row.
2843
+ *
2844
+ * Typing `=` at the end of `shape` takes the `shape=` row and keeps the
2845
+ * character, rather than either swallowing the keystroke or leaving the row to be
2846
+ * clicked. A row opts in; nothing has a commit character by default.
2847
+ * @param event - the key event.
2848
+ * @returns whether the keystroke was consumed.
2849
+ */
2850
+ commitCharacter(event) {
2851
+ if (!this.popup.isOpen) return false;
2852
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return false;
2853
+ const characters = this.popup.activeRow?.item.commitCharacters;
2854
+ if (characters === void 0 || !characters.includes(event.key)) return false;
2855
+ event.preventDefault();
2856
+ this.acceptCompletion(this.popup.activeIndex, event.key);
2857
+ return true;
2858
+ }
2859
+ /** Make a row active and keep the ARIA pointer in step. */
2860
+ setActive(index) {
2861
+ this.popup.setActive(index);
2862
+ this.syncActiveDescendant();
2863
+ }
2864
+ onScroll = () => {
2865
+ this.overlay.syncScroll(this.input);
2866
+ this.placePopup();
2867
+ this.hideTooltip();
2868
+ };
2869
+ onCaretMoved = () => {
2870
+ this.updateCompletionForCaret();
2871
+ this.handlers.onSelectionChange?.(readSelection(this.input));
2872
+ };
2873
+ onKeyUp = (event) => {
2874
+ if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End") {
2875
+ this.updateCompletionForCaret();
2876
+ }
2877
+ };
2878
+ onSelectionChange = () => {
2879
+ if (this.document.activeElement !== this.input) return;
2880
+ this.handlers.onSelectionChange?.(readSelection(this.input));
2881
+ };
2882
+ onBlur = () => {
2883
+ this.closeCompletion();
2884
+ this.hideTooltip();
2885
+ };
2886
+ onMouseMove = (event) => {
2887
+ if (this.hover === void 0 || !this.hover.enabled) return;
2888
+ if (this.popup.isOpen) {
2889
+ this.hideTooltip();
2890
+ return;
2891
+ }
2892
+ const offset = caretOffsetFromPoint(this.document, event.clientX, event.clientY);
2893
+ if (offset === void 0) return;
2894
+ this.queueHover(offset, event);
2895
+ };
2896
+ onMouseLeave = () => {
2897
+ this.hoverOffset = void 0;
2898
+ this.hideTooltip();
2899
+ };
2900
+ onCompositionStart = () => {
2901
+ this.composing = true;
2902
+ this.closeCompletion();
2903
+ };
2904
+ onCompositionEnd = () => {
2905
+ this.composing = false;
2906
+ this.sync();
2907
+ };
2908
+ };
2909
+ function variableName(key) {
2910
+ if (key.startsWith("--")) return key;
2911
+ return key.startsWith("litearea-") ? `--${key}` : `--litearea-${key}`;
2912
+ }
2913
+ function caretOffsetFromPoint(ownerDocument, x, y) {
2914
+ const probe = ownerDocument;
2915
+ if (typeof probe.caretPositionFromPoint === "function") {
2916
+ return probe.caretPositionFromPoint(x, y)?.offset;
2917
+ }
2918
+ if (typeof probe.caretRangeFromPoint === "function") {
2919
+ return probe.caretRangeFromPoint(x, y)?.startOffset;
2920
+ }
2921
+ return void 0;
2922
+ }
2923
+
2924
+ // src/react/index.tsx
2925
+ function liveGrammar(holder) {
2926
+ return new Proxy({}, {
2927
+ get: (_target, key) => Reflect.get(holder.current, key),
2928
+ has: (_target, key) => Reflect.has(holder.current, key),
2929
+ ownKeys: () => Reflect.ownKeys(holder.current),
2930
+ getOwnPropertyDescriptor: (_target, key) => Reflect.getOwnPropertyDescriptor(holder.current, key)
2931
+ });
2932
+ }
2933
+ function sameVariables(left, right) {
2934
+ if (left === right) return true;
2935
+ const a = Object.entries(left ?? {});
2936
+ const b = Object.entries(right ?? {});
2937
+ if (a.length !== b.length) return false;
2938
+ return a.every(([key, value]) => (right ?? {})[key] === value);
2939
+ }
2940
+ function LiteAreaEditor(props) {
2941
+ const hostRef = React.useRef(null);
2942
+ const editorRef = React.useRef(void 0);
2943
+ const latest = React.useRef(props);
2944
+ latest.current = props;
2945
+ const grammarBox = React.useRef(props.grammar);
2946
+ grammarBox.current = props.grammar;
2947
+ const live = React.useMemo(() => liveGrammar(grammarBox), []);
2948
+ const variablesRef = React.useRef(void 0);
2949
+ React.useLayoutEffect(() => {
2950
+ const host = hostRef.current;
2951
+ if (host === null) return;
2952
+ const core = latest.current;
2953
+ const editor = new LiteArea({
2954
+ grammar: live,
2955
+ value: latest.current.defaultValue ?? latest.current.value ?? "",
2956
+ readOnly: latest.current.readOnly === true,
2957
+ ...core,
2958
+ // Every callback reads through `latest`, so a host that passes a new closure
2959
+ // on each render does not need the editor to know about it.
2960
+ onChange: (value) => {
2961
+ latest.current.onChange?.(value);
2962
+ },
2963
+ onSelectionChange: (selection) => {
2964
+ latest.current.onSelectionChange?.(selection);
2965
+ },
2966
+ onDiagnostics: (diagnostics) => {
2967
+ latest.current.onDiagnostics?.(diagnostics);
2968
+ },
2969
+ onCompletion: (completion) => {
2970
+ latest.current.onCompletion?.(completion);
2971
+ },
2972
+ onHover: (info) => {
2973
+ latest.current.onHover?.(info);
2974
+ }
2975
+ });
2976
+ host.appendChild(editor.element);
2977
+ editorRef.current = editor;
2978
+ latest.current.editorRef?.(editor);
2979
+ return () => {
2980
+ editor.destroy();
2981
+ editorRef.current = void 0;
2982
+ latest.current.editorRef?.(void 0);
2983
+ };
2984
+ }, [live]);
2985
+ React.useLayoutEffect(() => {
2986
+ const editor = editorRef.current;
2987
+ if (editor === void 0) return;
2988
+ if (!sameVariables(props.variables, variablesRef.current)) {
2989
+ variablesRef.current = props.variables;
2990
+ editor.setVariables(props.variables ?? {});
2991
+ }
2992
+ editor.refresh();
2993
+ if (props.value !== void 0 && editor.value !== props.value) {
2994
+ editor.setValue(props.value, props.preserveHistory === true);
2995
+ }
2996
+ if (editor.input.readOnly !== (props.readOnly === true)) {
2997
+ editor.input.readOnly = props.readOnly === true;
2998
+ editor.element.classList.toggle("litearea-readonly", props.readOnly === true);
2999
+ }
3000
+ });
3001
+ return React.createElement("div", {
3002
+ ref: hostRef,
3003
+ className: props.className,
3004
+ style: props.style
3005
+ });
3006
+ }
3007
+
3008
+ export { LiteAreaEditor };
3009
+ //# sourceMappingURL=react.js.map
3010
+ //# sourceMappingURL=react.js.map