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