@citisen/litearea 0.1.0 → 0.2.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.
package/dist/grammars.cjs DELETED
@@ -1,1228 +0,0 @@
1
- 'use strict';
2
-
3
- // src/core/grammar.ts
4
- function defineGrammar(grammar) {
5
- return grammar;
6
- }
7
-
8
- // src/core/format.ts
9
- var DEFAULT_LIST_LIMIT = 12;
10
- function fillTemplate(template, values) {
11
- return String(template).replace(
12
- /\{(\w+)\}/g,
13
- (match, key) => Object.hasOwn(values, key) ? String(values[key]) : match
14
- );
15
- }
16
- function listPhrase(items, options) {
17
- const conjunction = options?.conjunction;
18
- const limit = options?.limit ?? DEFAULT_LIST_LIMIT;
19
- const shown = items.slice(0, limit);
20
- const rest = items.length - shown.length;
21
- if (shown.length === 0) return "";
22
- if (shown.length === 1) {
23
- return rest > 0 ? `${String(shown[0])} and ${String(rest)} more` : String(shown[0]);
24
- }
25
- if (shown.length === 2) {
26
- const pair = `${String(shown[0])} ${conjunction} ${String(shown[1])}`;
27
- return rest > 0 ? `${pair}, and ${String(rest)} more` : pair;
28
- }
29
- const head = shown.slice(0, -1).join(", ");
30
- const tail = shown[shown.length - 1];
31
- const phrase = `${head}, ${conjunction} ${String(tail)}`;
32
- return rest > 0 ? `${phrase}, and ${String(rest)} more` : phrase;
33
- }
34
-
35
- // src/core/vocabulary.ts
36
- function defineVocabulary(spec) {
37
- const caseSensitive = spec.caseSensitive === true;
38
- const unknownScope = spec.unknownScope ?? "invalid";
39
- const defaultScope = `vocabulary:${spec.id}`;
40
- const unknownCode = spec.unknownCode ?? `vocabulary:${spec.id}`;
41
- const unknownSeverity = spec.unknownSeverity ?? "error";
42
- const fold = (word) => caseSensitive ? word : word.toLowerCase();
43
- const memberOf = (word, allowed) => {
44
- const needle = fold(word);
45
- return allowed.find((candidate) => fold(candidate) === needle);
46
- };
47
- const docOf = (word) => {
48
- if (spec.docs === void 0) return void 0;
49
- for (const [key, value] of Object.entries(spec.docs)) {
50
- if (fold(key) !== fold(word)) continue;
51
- return typeof value === "string" ? { body: value } : value;
52
- }
53
- return void 0;
54
- };
55
- return {
56
- id: spec.id,
57
- caseSensitive,
58
- resolve: (context) => {
59
- const words = typeof spec.words === "function" ? spec.words(context) : spec.words;
60
- return Array.isArray(words) ? words : [];
61
- },
62
- has: (word, context) => {
63
- const words = typeof spec.words === "function" ? spec.words(context) : spec.words;
64
- return memberOf(word, Array.isArray(words) ? words : []) !== void 0;
65
- },
66
- scopeFor: (word) => {
67
- if (typeof spec.scope === "function") return spec.scope(memberOf(word, [word]) ?? word);
68
- return spec.scope ?? defaultScope;
69
- },
70
- unknownScope,
71
- reject: (word, context) => {
72
- if (spec.unknownMessage === void 0) return void 0;
73
- const allowed = typeof spec.words === "function" ? spec.words(context) : spec.words;
74
- const members = Array.isArray(allowed) ? allowed : [];
75
- const message = typeof spec.unknownMessage === "function" ? spec.unknownMessage(word, members) : fillTemplate(spec.unknownMessage, {
76
- word,
77
- allowed: listPhrase(members, { conjunction: "or" })
78
- });
79
- return { message, severity: unknownSeverity, code: unknownCode };
80
- },
81
- entryFor: (word) => docOf(word),
82
- format: (word) => spec.format === void 0 ? word : spec.format(word)
83
- };
84
- }
85
-
86
- // src/grammars/dshFont.ts
87
- var FONT_WEIGHT_WORDS = {
88
- thin: 100,
89
- hairline: 100,
90
- extralight: 200,
91
- ultralight: 200,
92
- light: 300,
93
- book: 400,
94
- normal: 400,
95
- regular: 400,
96
- roman: 400,
97
- medium: 500,
98
- demibold: 600,
99
- semibold: 600,
100
- bold: 700,
101
- extrabold: 800,
102
- ultrabold: 800,
103
- black: 900,
104
- heavy: 900,
105
- extrablack: 900,
106
- ultrablack: 900
107
- };
108
- var FONT_WEIGHT_SCALE = [100, 200, 300, 400, 500, 600, 700, 800, 900];
109
- var FONT_WEIGHT_LABELS = {
110
- 100: "thin",
111
- 200: "extralight",
112
- 300: "light",
113
- 400: "regular",
114
- 500: "medium",
115
- 600: "semibold",
116
- 700: "bold",
117
- 800: "extrabold",
118
- 900: "black"
119
- };
120
- var FONT_GENERIC_FAMILIES = [
121
- "system-ui",
122
- "sans-serif",
123
- "serif",
124
- "monospace",
125
- "cursive",
126
- "fantasy",
127
- "math",
128
- "emoji",
129
- "fangsong",
130
- "ui-sans-serif",
131
- "ui-serif",
132
- "ui-monospace",
133
- "ui-rounded"
134
- ];
135
- var FONT_COMMON_FAMILIES = [
136
- "Inter",
137
- "IBM Plex Sans",
138
- "IBM Plex Mono",
139
- "Noto Sans",
140
- "Noto Sans SC",
141
- "Noto Serif",
142
- "Source Han Sans SC",
143
- "Source Han Serif SC",
144
- "JetBrains Mono",
145
- "Fira Code",
146
- "Fira Sans",
147
- "Cascadia Code",
148
- "Cascadia Mono",
149
- "Maple Mono",
150
- "Roboto",
151
- "Roboto Mono",
152
- "Open Sans",
153
- "Lato",
154
- "Montserrat",
155
- "Poppins",
156
- "Ubuntu",
157
- "Ubuntu Mono",
158
- "DejaVu Sans",
159
- "DejaVu Sans Mono",
160
- "Hack",
161
- "Inconsolata",
162
- "Iosevka",
163
- "Comic Sans MS",
164
- "PingFang SC",
165
- "Hiragino Sans GB",
166
- "Microsoft YaHei",
167
- "Microsoft YaHei UI",
168
- "Microsoft JhengHei",
169
- "SimSun",
170
- "SimHei",
171
- "KaiTi",
172
- "Segoe UI",
173
- "Segoe UI Variable",
174
- "Helvetica Neue",
175
- "Arial",
176
- "Consolas",
177
- "Menlo",
178
- "Monaco",
179
- "SF Mono",
180
- "Courier New",
181
- "Times New Roman",
182
- "Georgia"
183
- ];
184
- var SCOPE = {
185
- family: "family",
186
- generic: "family.generic",
187
- unknown: "family.unknown",
188
- unclosed: "family.unclosed",
189
- weight: "weight",
190
- weightMissing: "weight.missing",
191
- separator: "separator"
192
- };
193
- function fontWeightWord(weight) {
194
- return FONT_WEIGHT_LABELS[weight] ?? String(weight);
195
- }
196
- function fontFaceWeights(styles) {
197
- if (styles === void 0) return [];
198
- const found = /* @__PURE__ */ new Set();
199
- for (const style of styles) {
200
- const text = String(style).toLowerCase().replace(/\b(semi|demi)\s+(?=[a-z])/g, "semi").replace(/\b(extra|ultra)\s+(?=[a-z])/g, "extra");
201
- for (const word of text.split(/[^a-z]+/)) {
202
- const weight = FONT_WEIGHT_WORDS[word];
203
- if (weight !== void 0) found.add(weight);
204
- }
205
- }
206
- return [...found].sort((left, right) => left - right);
207
- }
208
- function quoteFontFamily(family) {
209
- const name = family.trim();
210
- if (name === "") return "";
211
- if (/^-?[A-Za-z][\w-]*$/.test(name)) return name;
212
- return `"${name.replaceAll('"', "")}"`;
213
- }
214
- function isGenericName(name, generics) {
215
- return generics.includes(name);
216
- }
217
- function splitEntries(source) {
218
- const bounds = [];
219
- let start = 0;
220
- let quote = "";
221
- for (let index = 0; index < source.length; index += 1) {
222
- const char = source.charAt(index);
223
- if (quote !== "") {
224
- if (char === quote) quote = "";
225
- continue;
226
- }
227
- if (char === '"' || char === "'") {
228
- quote = char;
229
- continue;
230
- }
231
- if (char === ",") {
232
- bounds.push({ from: start, to: index });
233
- start = index + 1;
234
- }
235
- }
236
- bounds.push({ from: start, to: source.length });
237
- return bounds;
238
- }
239
- function dshFontQueryGrammar(options = {}) {
240
- const CATALOGUE = options.catalogue ?? [];
241
- const COMMON = options.commonFamilies ?? FONT_COMMON_FAMILIES;
242
- const GENERICS = options.genericFamilies ?? FONT_GENERIC_FAMILIES;
243
- const STYLES = options.styles ?? {};
244
- const ENUMERATED = options.enumerated === true;
245
- const SHIPPED_WEIGHT = options.shippedWeight;
246
- const PHRASE_WORDS = options.phraseWords ?? 4;
247
- const KNOWN = new Set(CATALOGUE.map((family) => family.toLowerCase()));
248
- const SUGGESTION_SPACE = [.../* @__PURE__ */ new Set([...CATALOGUE, ...COMMON, ...GENERICS])];
249
- const WEIGHT_ALTERNATION = Object.keys(FONT_WEIGHT_WORDS).join("|");
250
- const FAMILY_VOCAB = defineVocabulary({
251
- id: "family",
252
- words: SUGGESTION_SPACE,
253
- scope: SCOPE.family,
254
- format: quoteFontFamily
255
- });
256
- const GENERIC_VOCAB = defineVocabulary({
257
- id: "generic",
258
- words: GENERICS,
259
- scope: SCOPE.generic,
260
- docs: Object.fromEntries(
261
- GENERICS.map((name) => [
262
- name,
263
- {
264
- detail: "a generic CSS family",
265
- body: "Always valid, and only useful at the end of the list: it is what the browser falls back to when nothing before it resolves."
266
- }
267
- ])
268
- )
269
- });
270
- defineVocabulary({
271
- id: "weight",
272
- words: Object.keys(FONT_WEIGHT_WORDS),
273
- scope: SCOPE.weight,
274
- unknownScope: SCOPE.weight,
275
- docs: Object.fromEntries(
276
- Object.entries(FONT_WEIGHT_WORDS).map(([word, weight]) => [
277
- word,
278
- { title: word, detail: `font-weight ${String(weight)}` }
279
- ])
280
- )
281
- });
282
- const readEntry = (source, bounds) => {
283
- const raw = source.slice(bounds.from, bounds.to);
284
- const trimmed = raw.trim();
285
- const lead = trimmed === "" ? raw.length : raw.indexOf(trimmed);
286
- const coreFrom = bounds.from + lead;
287
- const core = trimmed;
288
- const coreTo = coreFrom + core.length;
289
- const problems = [];
290
- const entry = {
291
- from: bounds.from,
292
- to: bounds.to,
293
- coreFrom,
294
- coreTo,
295
- core,
296
- quoted: false,
297
- name: "",
298
- nameFrom: coreFrom,
299
- nameTo: coreTo,
300
- word: void 0,
301
- wordFrom: coreTo,
302
- wordTo: coreTo,
303
- kind: "empty"
304
- };
305
- if (core === "") return { entry, problems };
306
- const quoteChar = core.charAt(0);
307
- const quote = quoteChar === '"' || quoteChar === "'" ? quoteChar : "";
308
- const lower = core.toLowerCase();
309
- let lookUp = true;
310
- if (quote !== "") {
311
- entry.quoted = true;
312
- const close = core.indexOf(quote, 1);
313
- if (close < 0) {
314
- entry.name = core.slice(1).trim();
315
- entry.nameFrom = coreFrom + 1;
316
- entry.kind = "family";
317
- problems.push({
318
- from: coreFrom,
319
- to: coreTo,
320
- message: "This quote is never closed, so everything after it is read as part of one family name.",
321
- code: "unclosed-quote",
322
- severity: "error"
323
- });
324
- return { entry, problems };
325
- }
326
- entry.name = core.slice(1, close);
327
- entry.nameFrom = coreFrom + 1;
328
- entry.nameTo = coreFrom + close;
329
- const rest = core.slice(close + 1).trim();
330
- if (rest !== "") {
331
- const restFrom = coreTo - rest.length;
332
- if (Object.hasOwn(FONT_WEIGHT_WORDS, rest.toLowerCase())) {
333
- entry.word = rest.toLowerCase();
334
- entry.wordFrom = restFrom;
335
- entry.wordTo = coreTo;
336
- } else {
337
- problems.push({
338
- from: restFrom,
339
- to: coreTo,
340
- message: `"${rest}" follows a quoted family name but is neither a weight nor part of it, so it is ignored.`,
341
- code: "trailing-text",
342
- severity: "error"
343
- });
344
- lookUp = false;
345
- }
346
- }
347
- entry.kind = "family";
348
- } else if (isGenericName(lower, GENERICS) || KNOWN.has(lower)) {
349
- entry.name = core;
350
- entry.kind = isGenericName(lower, GENERICS) ? "generic" : "family";
351
- } else if (Object.hasOwn(FONT_WEIGHT_WORDS, lower)) {
352
- entry.kind = "weight";
353
- entry.word = lower;
354
- entry.wordFrom = coreFrom;
355
- entry.wordTo = coreTo;
356
- } else {
357
- const cut = lower.lastIndexOf(" ");
358
- const tail = cut > 0 ? lower.slice(cut + 1) : "";
359
- if (cut > 0 && Object.hasOwn(FONT_WEIGHT_WORDS, tail)) {
360
- entry.word = tail;
361
- entry.wordFrom = coreFrom + cut + 1;
362
- entry.wordTo = coreTo;
363
- entry.name = core.slice(0, cut).trim();
364
- entry.nameTo = entry.nameFrom + entry.name.length;
365
- } else {
366
- entry.name = core;
367
- }
368
- entry.kind = "family";
369
- }
370
- const nameLower = entry.name.trim().toLowerCase();
371
- const generic = nameLower !== "" && isGenericName(nameLower, GENERICS);
372
- const catalogued = nameLower !== "" && KNOWN.has(nameLower);
373
- if (lookUp && nameLower !== "" && !generic && !catalogued && ENUMERATED) {
374
- entry.kind = "unknown";
375
- problems.push({
376
- from: entry.nameFrom,
377
- to: entry.nameFrom + entry.name.length,
378
- message: `"${entry.name}" is not in this machine's font list. It is still written, and the browser will fall back to whatever comes after it.`,
379
- code: "unknown-family",
380
- severity: "warning"
381
- });
382
- } else if (generic) {
383
- entry.kind = "generic";
384
- }
385
- return { entry, problems };
386
- };
387
- return defineGrammar({
388
- id: "dsh-font-query",
389
- name: "dsh-font font query",
390
- // A hyphen belongs to a name (`-apple-system`, `Helvetica Neue` has spaces and
391
- // is reached by the phrase rule instead), and `.` deliberately does not: no
392
- // family name in this vocabulary contains one, and leaving it out keeps a
393
- // stray `Inter.` from being read as a single unknown name.
394
- wordChars: /[\p{L}\p{N}_-]/u,
395
- rules: [
396
- // ── quoted names, before anything can split them ───────────────────
397
- {
398
- kind: "match",
399
- pattern: /"[^"\n]*"|'[^'\n]*'/,
400
- // The scope is decided by what is INSIDE the quotes: a quoted generic is
401
- // still a generic, and a quoted name the machine does not have is still
402
- // worth painting as unknown.
403
- scope: (match) => quotedScope(match.text, KNOWN, GENERICS, ENUMERATED)
404
- },
405
- {
406
- kind: "match",
407
- pattern: /["'][^\n]*/,
408
- scope: SCOPE.unclosed
409
- },
410
- { kind: "match", scope: SCOPE.separator, pattern: /,/ },
411
- // ── a weight word, but only where a weight may stand ──────────────
412
- // A weight is the LAST word of an entry, so the rule looks ahead for a comma
413
- // or the end of a line. Without that lookahead `Book` in `Book Antiqua`
414
- // would be painted as a weight. `prevNot` stops it matching the tail of a
415
- // longer word.
416
- //
417
- // The scope is decided from the ANALYSIS rather than from the characters,
418
- // which is the one place this grammar needs that: `weight` and
419
- // `weightMissing` are the same word, and only the machine knows which one it
420
- // is. A rule may look at `match.state` precisely so that a semantic
421
- // judgement does not have to become a second highlighter.
422
- {
423
- kind: "match",
424
- pattern: new RegExp(`(?:${WEIGHT_ALTERNATION})(?=\\s*(?:,|$))`, "im"),
425
- when: { prevNot: "\\w" },
426
- scope: (match) => {
427
- const state = match.state;
428
- if (state.weight === void 0 || state.effective < 0) return SCOPE.weight;
429
- const family = state.families[state.effective] ?? "";
430
- const faces = fontFaceWeights(lookupStyles(STYLES, family));
431
- if (faces.length === 0 || faces.includes(state.weight)) return SCOPE.weight;
432
- return SCOPE.weightMissing;
433
- }
434
- },
435
- // ── the catalogue, longest phrase first ──────────────────────────
436
- // The generics come first because they are ALSO in the suggestion space,
437
- // and a generic painted as an installed family would say the wrong thing
438
- // about a word whose whole point is that it names no particular font.
439
- { kind: "words", words: GENERIC_VOCAB },
440
- {
441
- kind: "words",
442
- words: FAMILY_VOCAB,
443
- phrase: { max: PHRASE_WORDS }
444
- },
445
- // ── anything else is a name the reader has not seen ──────────────
446
- // `unknown` when the catalogue is authoritative, because then the browser
447
- // really will fall through; plain `family` when it is only a suggestion
448
- // list, because the parser has no business doubting the user.
449
- {
450
- kind: "match",
451
- pattern: /[^\s,]+/,
452
- scope: ENUMERATED ? SCOPE.unknown : SCOPE.family
453
- }
454
- ],
455
- fallbackScope: "text",
456
- // ── what the query means ──────────────────────────────────────────────
457
- analyze: (text) => {
458
- const entries = [];
459
- const problems = [];
460
- for (const bounds of splitEntries(text)) {
461
- const read = readEntry(text, bounds);
462
- entries.push(read.entry);
463
- problems.push(...read.problems);
464
- }
465
- const families = [];
466
- let weight;
467
- let weightWord;
468
- let sawWeight = false;
469
- for (const entry of entries) {
470
- if (entry.name.trim() !== "") families.push(entry.name.trim());
471
- if (entry.word !== void 0) {
472
- if (!sawWeight) {
473
- sawWeight = true;
474
- weight = FONT_WEIGHT_WORDS[entry.word];
475
- weightWord = entry.word;
476
- } else {
477
- problems.push({
478
- from: entry.wordFrom,
479
- to: entry.wordTo,
480
- message: `The weight is stated more than once. The first one is used and this "${entry.word}" is ignored.`,
481
- code: "duplicate-weight",
482
- severity: "warning"
483
- });
484
- }
485
- }
486
- }
487
- let effective = -1;
488
- if (families.length > 0) {
489
- if (!ENUMERATED) {
490
- effective = 0;
491
- } else {
492
- for (let index = 0; index < families.length; index += 1) {
493
- const lower = (families[index] ?? "").toLowerCase();
494
- if (isGenericName(lower, GENERICS) || KNOWN.has(lower)) {
495
- effective = index;
496
- break;
497
- }
498
- }
499
- }
500
- }
501
- if (weight !== void 0 && effective >= 0) {
502
- const family = families[effective] ?? "";
503
- const faces = fontFaceWeights(lookupStyles(STYLES, family));
504
- if (faces.length > 0 && !faces.includes(weight)) {
505
- const entry = entries.find((candidate) => candidate.word !== void 0);
506
- if (entry !== void 0) {
507
- problems.push({
508
- from: entry.wordFrom,
509
- to: entry.wordTo,
510
- message: `"${family}" has no ${String(weight)} face, so the browser will synthesise one. It does have ${faces.join(", ")}.`,
511
- code: "missing-weight",
512
- severity: "warning"
513
- });
514
- }
515
- }
516
- }
517
- if (families.length > 0 && !families.some((family) => isGenericName(family.toLowerCase(), GENERICS))) {
518
- problems.push({
519
- from: text.length,
520
- to: text.length,
521
- message: "No generic family at the end, so a name that fails to resolve has nothing to fall back to. Adding one, such as `sans-serif`, is free.",
522
- code: "no-generic-fallback",
523
- severity: "info"
524
- });
525
- }
526
- return { entries, families, effective, weight, weightWord, problems };
527
- },
528
- validate: (context) => {
529
- for (const problem of context.state.problems) {
530
- context.report({
531
- from: problem.from,
532
- to: problem.to,
533
- message: problem.message,
534
- code: problem.code,
535
- severity: problem.severity
536
- });
537
- }
538
- },
539
- // ── the family in effect, marked rather than recoloured ───────────────
540
- // It is a decoration and not a scope because it depends on the machine, not on
541
- // the characters: the same text means something else on a computer with
542
- // different fonts, and re-lexing the document whenever the catalogue changed
543
- // would be the wrong shape of work.
544
- decorate: (_text, state) => {
545
- const decorations = [];
546
- if (state.effective < 0) return decorations;
547
- const family = state.families[state.effective] ?? "";
548
- const target = state.entries.find((candidate) => candidate.name.trim() === family);
549
- if (target !== void 0) {
550
- decorations.push({
551
- from: target.nameFrom,
552
- to: target.nameFrom + target.name.length,
553
- kind: "effective",
554
- title: `in effect: ${family}`
555
- });
556
- }
557
- return decorations;
558
- },
559
- // ── what can come next ────────────────────────────────────────────────
560
- compose: [
561
- {
562
- id: "family",
563
- // The whole query is entries, so this source is always eligible; the
564
- // entry under the caret decides what it replaces.
565
- when: () => true,
566
- range: (context) => entryRange(context.state, context.caret),
567
- items: (context) => {
568
- const entry = entryAt(context.state.entries, context.caret);
569
- const core = entry?.core ?? "";
570
- entry?.quoted === true;
571
- const inner = unquote(core);
572
- const carried = entry?.word;
573
- const needle = carried === void 0 ? inner.trim() : inner.slice(0, -carried.length).trim();
574
- const exact = findExact(SUGGESTION_SPACE, needle);
575
- const headFrom = entry?.coreFrom ?? context.word.from;
576
- const head = unquote(context.text.slice(headFrom, Math.max(context.word.from, headFrom))).trim();
577
- const atBoundary = entry !== void 0 && context.caret <= entry.coreFrom && exact !== void 0;
578
- const familyInEntry = exact ?? findExact(SUGGESTION_SPACE, head);
579
- const atFamilyEnd = entry === void 0 || context.caret >= entry.wordFrom;
580
- const weightRows = [];
581
- if (familyInEntry !== void 0 && !atBoundary && atFamilyEnd) {
582
- const detected = fontFaceWeights(lookupStyles(STYLES, familyInEntry));
583
- const pool = detected.length > 0 ? detected : [...FONT_WEIGHT_SCALE];
584
- const typedWord = context.word.prefix.toLowerCase();
585
- for (const value of pool) {
586
- const word = fontWeightWord(value);
587
- if (typedWord !== "" && !word.startsWith(typedWord)) continue;
588
- weightRows.push({
589
- label: `${familyInEntry} ${word}`,
590
- insert: quoteFontFamily(familyInEntry) + (value === SHIPPED_WEIGHT ? "" : ` ${word}`),
591
- kind: "weight",
592
- detail: `font-weight ${String(value)}`,
593
- documentation: value === SHIPPED_WEIGHT ? "The weight this axis already uses, so picking it takes the word away rather than spelling out a value nobody chose." : void 0,
594
- // A weight's position is decided by the SCALE and not by the length of
595
- // its label, which is what the zero-padded key is for: without it the
596
- // shortest word would lead, so `bold` would sit above the shipped
597
- // `regular` and the list would look shuffled. The weight the entry
598
- // already states outranks all of them, so an Enter that accepts the top
599
- // row re-applies what is written.
600
- sortText: word === carried ? "0" : `1${String(value).padStart(3, "0")}`
601
- });
602
- }
603
- }
604
- const familyRows = [];
605
- for (const name of SUGGESTION_SPACE) {
606
- const generic = isGenericName(name.toLowerCase(), GENERICS);
607
- familyRows.push({
608
- label: name,
609
- // A completion never drops a weight the entry already states: the
610
- // word travels with the pick, so swapping the family does not quietly
611
- // reset the weight.
612
- insert: quoteFontFamily(name) + (carried === void 0 ? "" : ` ${carried}`),
613
- mode: atBoundary ? "before" : "replace",
614
- // A comma invites the next fallback. Only after a family, never after
615
- // a weight, which completes the entry instead of starting one.
616
- append: atBoundary || lastEntry(context.state, context.caret) ? ", " : "",
617
- kind: generic ? "generic" : "family",
618
- detail: generic ? "generic family" : KNOWN.has(name.toLowerCase()) ? "installed" : "suggested",
619
- // After the weights when weights lead, which is the whole reason a
620
- // grammar gets to name the group: `2` sorts above `1xxx` and below `0`.
621
- sortText: weightRows.length > 0 ? "2" : "0"
622
- });
623
- }
624
- const rows = weightRows.length > 0 ? [...weightRows, ...familyRows] : familyRows;
625
- if (needle !== "" && exact === void 0 && !isGenericName(needle.toLowerCase(), GENERICS)) {
626
- rows.push({
627
- label: inner,
628
- insert: inner,
629
- kind: "custom",
630
- detail: "as typed",
631
- documentation: "Written exactly as it stands. A name the browser does not have is still a valid declaration: it is what lets a stack work on a machine this one cannot see.",
632
- sortText: "3"
633
- });
634
- }
635
- return rows;
636
- }
637
- }
638
- ],
639
- // ── what a thing is ───────────────────────────────────────────────────
640
- describe: (context) => {
641
- const token = context.token;
642
- if (token === void 0) return void 0;
643
- const state = context.state;
644
- const entry = entryAt(state.entries, context.offset);
645
- if (token.scope === SCOPE.separator) return void 0;
646
- if (token.scope === SCOPE.weight || token.scope === SCOPE.weightMissing) {
647
- const word = token.text.toLowerCase();
648
- const value = FONT_WEIGHT_WORDS[word];
649
- const family = state.effective >= 0 ? state.families[state.effective] : void 0;
650
- if (value === void 0) return { title: token.text };
651
- const faces2 = family === void 0 ? [] : fontFaceWeights(lookupStyles(STYLES, family));
652
- return {
653
- title: `${word} \u2014 font-weight ${String(value)}`,
654
- detail: entry?.name === "" ? "applies to the whole axis" : `applies to ${family ?? "the first family"}`,
655
- body: faces2.length === 0 ? `${family ?? "This family"} was not read, so whether it has a ${String(value)} face is unknown.` : faces2.includes(value) ? `${family ?? "This family"} has this face.` : `${family ?? "This family"} has no ${String(value)} face, so the browser will synthesise one.`
656
- };
657
- }
658
- if (token.scope === SCOPE.generic) {
659
- return {
660
- title: token.text,
661
- detail: "a generic CSS family",
662
- body: "Always valid, and only useful at the end of the list: it is what the browser falls back to when nothing before it resolves."
663
- };
664
- }
665
- if (token.scope === SCOPE.unclosed) {
666
- return {
667
- title: token.text,
668
- detail: "unclosed quote",
669
- body: "The closing quote is missing, so this and everything after it are read as one family name. A font family containing a quote character has to be written with the other quote style, because backslash escapes are deliberately not interpreted."
670
- };
671
- }
672
- if (entry !== void 0 && entry.kind === "unknown") {
673
- return {
674
- title: entry.name,
675
- detail: "not installed here",
676
- body: "Still a valid declaration: it is written to the setting, and the browser falls through to the next family in the list when it cannot resolve. Reordering it to the end of the list is what the fallbacks are for."
677
- };
678
- }
679
- const entryDoc = FAMILY_VOCAB.entryFor(token.text);
680
- const installed = KNOWN.has(token.text.toLowerCase());
681
- const faces = fontFaceWeights(lookupStyles(STYLES, token.text));
682
- return {
683
- title: token.text,
684
- detail: installed ? "installed" : "not read on this machine",
685
- body: entryDoc?.body ?? (faces.length > 0 ? `Faces read from this machine: ${faces.join(", ")}.` : "This family has no faces recorded, so its weights are unknown rather than absent.")
686
- };
687
- }
688
- });
689
- }
690
- function quotedScope(text, known, generics, enumerated) {
691
- const quote = text.charAt(0);
692
- const inner = text.length >= 2 && text.endsWith(quote) ? text.slice(1, -1) : text.slice(1);
693
- const lower = inner.trim().toLowerCase();
694
- if (lower === "") return SCOPE.family;
695
- if (generics.includes(lower)) return SCOPE.generic;
696
- if (known.has(lower)) return SCOPE.family;
697
- return enumerated ? SCOPE.unknown : SCOPE.family;
698
- }
699
- function unquote(name) {
700
- const text = String(name);
701
- const quote = text.charAt(0);
702
- if ((quote === '"' || quote === "'") && text.length >= 2 && text.endsWith(quote)) {
703
- return text.slice(1, -1);
704
- }
705
- return text;
706
- }
707
- function entryAt(entries, offset) {
708
- for (const entry of entries) {
709
- if (offset >= entry.from && offset <= entry.to) return entry;
710
- }
711
- return entries[entries.length - 1];
712
- }
713
- function entryRange(state, caret) {
714
- const entry = entryAt(state.entries, caret);
715
- return entry === void 0 ? { from: caret, to: caret } : { from: entry.coreFrom, to: entry.coreTo };
716
- }
717
- function lastEntry(state, caret) {
718
- const entry = entryAt(state.entries, caret);
719
- return entry === void 0 || entry === state.entries[state.entries.length - 1];
720
- }
721
- function findExact(names, needle) {
722
- const lower = needle.toLowerCase();
723
- return names.find((name) => name.toLowerCase() === lower);
724
- }
725
- function lookupStyles(styles, family) {
726
- if (Object.hasOwn(styles, family)) return styles[family];
727
- const lower = family.toLowerCase();
728
- for (const [name, faces] of Object.entries(styles)) {
729
- if (name.toLowerCase() === lower) return faces;
730
- }
731
- return void 0;
732
- }
733
- var DSH_FONT_INFO_CODES = ["no-generic-fallback"];
734
-
735
- // src/core/text.ts
736
- function lineStarts(source) {
737
- const starts = [0];
738
- for (let index = 0; index < source.length; index += 1) {
739
- const code = source.charCodeAt(index);
740
- if (code === 10) {
741
- starts.push(index + 1);
742
- } else if (code === 13) {
743
- if (source.charCodeAt(index + 1) === 10) index += 1;
744
- starts.push(index + 1);
745
- }
746
- }
747
- return starts;
748
- }
749
-
750
- // src/grammars/dshSentry.ts
751
- var DEFAULT_STATES = ["running", "waiting", "approval", "done"];
752
- var DEFAULT_SHAPES = ["circle", "rounded", "square", "none"];
753
- var DEFAULT_MOTIONS = ["still", "turn", "blink", "flush"];
754
- var DEFAULT_PATTERNS = [];
755
- var DEFAULT_COLORS = {
756
- blue: "#4d6bfe",
757
- amber: "#f59e0b",
758
- green: "#22c55e",
759
- red: "#ef4444",
760
- purple: "#8b5cf6",
761
- gray: "#8b8f97",
762
- dark: "#23262c",
763
- light: "#eef0f3"
764
- };
765
- var DEFAULT_OPTIONS = ["shape", "color", "pattern", "motion", "speed", "bg"];
766
- var DEFAULT_SLOTS = ["shape", "color", "pattern", "motion", "speed"];
767
- var DEFAULT_LOOK = {
768
- running: { shape: "circle", color: "blue", pattern: "none", motion: "turn", speed: 3 },
769
- waiting: { shape: "rounded", color: "amber", pattern: "none", motion: "blink", speed: 1.1 },
770
- approval: { shape: "rounded", color: "amber", pattern: "none", motion: "blink", speed: 1.9 },
771
- done: { shape: "circle", color: "green", pattern: "none", motion: "flush", speed: 1.6 }
772
- };
773
- var SLOT_SCOPE = {
774
- shape: "value.shape",
775
- color: "value.color",
776
- pattern: "value.pattern",
777
- motion: "value.motion",
778
- speed: "value.number"
779
- };
780
- var KEY_SLOT = {
781
- shape: "shape",
782
- color: "color",
783
- bg: "color",
784
- pattern: "pattern",
785
- motion: "motion",
786
- speed: "speed"
787
- };
788
- var COLOR_NAMES_OF = (colors) => Object.keys(colors);
789
- function closed(spec) {
790
- return defineVocabulary({ ...spec, caseSensitive: true });
791
- }
792
- function dshSentryStyleGrammar(options = {}) {
793
- const STATES = options.states ?? DEFAULT_STATES;
794
- const SHAPES = options.shapes ?? DEFAULT_SHAPES;
795
- const MOTIONS = options.motions ?? DEFAULT_MOTIONS;
796
- const PATTERNS = options.patterns ?? DEFAULT_PATTERNS;
797
- const COLORS = options.colors ?? DEFAULT_COLORS;
798
- const COLOR_NAMES = COLOR_NAMES_OF(COLORS);
799
- const OPTIONS = options.options ?? DEFAULT_OPTIONS;
800
- const LOOK = options.defaults ?? DEFAULT_LOOK;
801
- const SLOTS = DEFAULT_SLOTS;
802
- const PATTERN_WORDS = PATTERNS.length > 0 ? PATTERNS : ["none"];
803
- const STATE_VOCAB = closed({
804
- id: "state",
805
- words: STATES,
806
- scope: "state",
807
- unknownMessage: 'Unknown state "{word}" \u2014 this document understands {allowed}.',
808
- docs: {
809
- running: { detail: "a turn is in progress", body: "The agent is working and does not need anyone." },
810
- waiting: { detail: "a question is waiting", body: "The agent asked something, and the turn is blocked until it is answered." },
811
- approval: { detail: "a permission is waiting", body: "The agent requested an escalation or a plan review, and the turn is blocked on it." },
812
- done: { detail: "the turn finished", body: "The agent stopped and left the tab alone." }
813
- }
814
- });
815
- const SHAPE_VOCAB = closed({
816
- id: "shape",
817
- words: SHAPES,
818
- scope: SLOT_SCOPE.shape,
819
- docs: {
820
- circle: { detail: "a full disc" },
821
- rounded: { detail: "a rounded square" },
822
- square: { detail: "a square with sharp corners" },
823
- none: {
824
- detail: "no background",
825
- body: "The fish alone, on whatever the tab gives it. A bare `none` is a SHAPE and never a pattern: one word cannot mean two things."
826
- }
827
- }
828
- });
829
- const COLOR_VOCAB = closed({
830
- id: "color",
831
- words: COLOR_NAMES,
832
- scope: SLOT_SCOPE.color,
833
- docs: Object.fromEntries(Object.entries(COLORS).map(([name, hex]) => [name, { detail: hex }]))
834
- });
835
- const MOTION_VOCAB = closed({
836
- id: "motion",
837
- words: MOTIONS,
838
- scope: SLOT_SCOPE.motion,
839
- docs: {
840
- still: { detail: "nothing moves" },
841
- turn: { detail: "rotates", body: "speed is seconds per revolution." },
842
- blink: { detail: "alternates", body: "speed is seconds per cycle." },
843
- flush: { detail: "pulses", body: "speed is seconds per cycle." }
844
- }
845
- });
846
- const PATTERN_VOCAB = closed({
847
- id: "pattern",
848
- words: PATTERN_WORDS,
849
- scope: SLOT_SCOPE.pattern,
850
- docs: {
851
- none: {
852
- detail: "carve nothing",
853
- body: "Every dial-like pattern was tried on a real 16px favicon and read as noise, so `none` is the only pattern left. It is written as `pattern=none` and never as a bare word, because `none` is also a shape."
854
- }
855
- }
856
- });
857
- const KEY_DOCS = {
858
- shape: { detail: "background shape", body: `One of ${SHAPES.join(", ")}.` },
859
- color: { detail: "disc colour", body: `${COLOR_NAMES.join(", ")} \u2014 all presets, chosen so the fish stays legible.` },
860
- pattern: { detail: "carved pattern", body: "Only `none` remains; write it as `pattern=none`." },
861
- motion: { detail: "what moves", body: `One of ${MOTIONS.join(", ")}.` },
862
- speed: { detail: "seconds per cycle", body: "A number: seconds per revolution for `turn`, seconds per cycle otherwise." },
863
- bg: { detail: "an alias for color", body: "Kept so a document written against an earlier release still says what it means." }
864
- };
865
- const BY_SCOPE = /* @__PURE__ */ new Map([
866
- ["state", STATE_VOCAB],
867
- [SLOT_SCOPE.shape, SHAPE_VOCAB],
868
- [SLOT_SCOPE.color, COLOR_VOCAB],
869
- [SLOT_SCOPE.pattern, PATTERN_VOCAB],
870
- [SLOT_SCOPE.motion, MOTION_VOCAB]
871
- ]);
872
- const VOCAB_FOR_KEY = {
873
- shape: SHAPE_VOCAB,
874
- color: COLOR_VOCAB,
875
- bg: COLOR_VOCAB,
876
- pattern: PATTERN_VOCAB,
877
- motion: MOTION_VOCAB
878
- };
879
- const wordsForSlot = (slot) => {
880
- if (slot === "shape") return SHAPES;
881
- if (slot === "color" || slot === "bg") return COLOR_NAMES;
882
- if (slot === "motion") return MOTIONS;
883
- if (slot === "pattern") return PATTERN_WORDS;
884
- return [];
885
- };
886
- const expectedList = (slot) => {
887
- if (slot === "speed") return "a number of seconds, such as 3 or 1.1";
888
- const words = wordsForSlot(slot);
889
- if (words.length === 0) return `pattern=<name>`;
890
- return words.join(", ");
891
- };
892
- const valueProblem = (key, value) => {
893
- if (key === "speed") {
894
- return Number.isFinite(Number.parseFloat(value)) ? void 0 : `"${value}" is not a speed \u2014 write a number of seconds, such as 3 or 1.1. The shipped rate is used instead.`;
895
- }
896
- if (VOCAB_FOR_KEY[key] === void 0) return void 0;
897
- const words = wordsForSlot(key);
898
- if (words.includes(value)) return void 0;
899
- const noun = key === "bg" ? "preset colour" : key;
900
- return `"${value}" is not a ${noun} \u2014 expected ${words.join(", ")}. The shipped default is used instead.`;
901
- };
902
- return defineGrammar({
903
- id: "dsh-sentry-style",
904
- name: "dsh-sentry style document",
905
- // A decimal speed is one token because the number RULE says so, not because `.`
906
- // is a word character. Leaving `.` out of the predicate stops a stray
907
- // `circle.` from being read as one word, matching no shape, and earning a
908
- // diagnostic about a word the user never typed.
909
- wordChars: /[\p{L}\p{N}_]/u,
910
- rules: [
911
- { kind: "match", scope: "comment", pattern: /#[^\n]*/ },
912
- // The first word on a line is a state or it is a mistake. `unknown: {}` asks
913
- // for the vocabulary's own rejection, and this is the one place in the
914
- // language where the lexical layer can be that sure: nothing else may stand
915
- // at the head of a line, so there is no later rule to wait for.
916
- {
917
- kind: "words",
918
- words: STATE_VOCAB,
919
- when: { firstOnLine: true },
920
- unknown: {}
921
- },
922
- // `shape=` — the key, seen from the `=` so it cannot also swallow a value.
923
- { kind: "match", scope: "property", pattern: /[A-Za-z][\w-]*(?==)/ },
924
- { kind: "match", scope: "operator", pattern: /=/ },
925
- { kind: "match", scope: "separator", pattern: /,/ },
926
- // Values. The sets are disjoint, so membership alone places a bare word, and
927
- // none of these rules rejects: a rule that did would claim a word belonging
928
- // to the next vocabulary down the list.
929
- { kind: "words", words: SHAPE_VOCAB },
930
- { kind: "words", words: COLOR_VOCAB },
931
- { kind: "words", words: MOTION_VOCAB },
932
- { kind: "match", scope: SLOT_SCOPE.speed, pattern: /\d+(?:\.\d+)?/ },
933
- // Anything left is a word this language does not know. Painting it as invalid
934
- // rather than as plain text makes a typo visible before the structural pass
935
- // has even run, and that pass supplies the precise message.
936
- { kind: "match", scope: "invalid", pattern: /\S+/ }
937
- ],
938
- fallbackScope: "text",
939
- // ── what the document means ───────────────────────────────────────────
940
- //
941
- // This walk decides the slot filling AND records what went wrong, rather than
942
- // leaving the second job to a second walk. They are the same decision: the only
943
- // reason to know which slot is free is to say what a word that fits nothing
944
- // should have been, and splitting them would mean two implementations of one
945
- // rule — which is exactly how the editors this library replaces came to
946
- // disagree with themselves.
947
- analyze: (text) => {
948
- const starts = lineStarts(text);
949
- const lines = [];
950
- const problems = [];
951
- for (let number = 0; number < starts.length; number += 1) {
952
- const from = starts[number] ?? 0;
953
- const rawTo = starts[number + 1] ?? text.length;
954
- let to = rawTo;
955
- while (to > from && (text.charAt(to - 1) === "\n" || text.charAt(to - 1) === "\r")) to -= 1;
956
- const raw = text.slice(from, to);
957
- const comment = raw.indexOf("#");
958
- const body = comment === -1 ? raw : raw.slice(0, comment);
959
- const words = [];
960
- const wordPattern = /[^\s,]+/g;
961
- let match;
962
- while ((match = wordPattern.exec(body)) !== null) {
963
- words.push({
964
- text: match[0],
965
- from: from + match.index,
966
- to: from + match.index + match[0].length
967
- });
968
- }
969
- const line = {
970
- number,
971
- state: void 0,
972
- known: true,
973
- words,
974
- slots: {},
975
- keys: [],
976
- pendingKey: void 0
977
- };
978
- lines.push(line);
979
- const first = words[0];
980
- if (first === void 0) continue;
981
- line.state = first.text;
982
- line.known = STATES.includes(first.text);
983
- if (!line.known) continue;
984
- const claim = (slot, value) => {
985
- line.slots[slot] = value;
986
- };
987
- for (let index = 1; index < words.length; index += 1) {
988
- const word = words[index];
989
- if (word === void 0) continue;
990
- const equals = word.text.indexOf("=");
991
- const key = equals === -1 ? void 0 : word.text.slice(0, equals);
992
- const value = equals === -1 ? void 0 : word.text.slice(equals + 1);
993
- if (key !== void 0) {
994
- line.keys.push(key);
995
- const written = value ?? "";
996
- if (written === "") {
997
- line.pendingKey = key;
998
- continue;
999
- }
1000
- const complaint = valueProblem(key, written);
1001
- if (complaint !== void 0) {
1002
- problems.push({
1003
- from: word.from,
1004
- to: word.to,
1005
- message: complaint,
1006
- code: "bad-option-value",
1007
- severity: "warning"
1008
- });
1009
- continue;
1010
- }
1011
- const slot = KEY_SLOT[key];
1012
- if (slot !== void 0) claim(slot, written);
1013
- continue;
1014
- }
1015
- if (MOTIONS.includes(word.text)) {
1016
- claim("motion", word.text);
1017
- continue;
1018
- }
1019
- if (SHAPES.includes(word.text)) {
1020
- claim("shape", word.text);
1021
- continue;
1022
- }
1023
- if (COLOR_NAMES.includes(word.text)) {
1024
- claim("color", word.text);
1025
- continue;
1026
- }
1027
- if (PATTERNS.includes(word.text)) {
1028
- claim("pattern", word.text);
1029
- continue;
1030
- }
1031
- if (Number.isFinite(Number.parseFloat(word.text))) {
1032
- claim("speed", word.text);
1033
- continue;
1034
- }
1035
- const free = SLOTS.find((slot) => line.slots[slot] === void 0);
1036
- if (free === void 0) {
1037
- problems.push({
1038
- from: word.from,
1039
- to: word.to,
1040
- message: `"${word.text}" has nowhere to go \u2014 this line already names a shape, colour, pattern, motion, and speed.`,
1041
- code: "unexpected-value",
1042
- severity: "error"
1043
- });
1044
- } else {
1045
- problems.push({
1046
- from: word.from,
1047
- to: word.to,
1048
- message: `"${word.text}" is not a valid ${free} \u2014 expected ${expectedList(free)}.`,
1049
- code: "bad-value",
1050
- severity: "error"
1051
- });
1052
- }
1053
- }
1054
- }
1055
- return { lines, problems };
1056
- },
1057
- // ── what the tokens must be ───────────────────────────────────────────
1058
- // One declarative check, because "a key must be one it knows" is exactly the
1059
- // shape a check is for: a scope, an allowed set, and a message.
1060
- checks: [
1061
- {
1062
- code: "unknown-option",
1063
- scopes: ["property"],
1064
- allow: closed({ id: "option", words: [...OPTIONS, ...PATTERNS] }),
1065
- severity: "error",
1066
- message: 'Unknown option "{word}" \u2014 this document understands {allowed}.'
1067
- }
1068
- ],
1069
- validate: (context) => {
1070
- for (const problem of context.state.problems) {
1071
- context.report({
1072
- from: problem.from,
1073
- to: problem.to,
1074
- message: problem.message,
1075
- code: problem.code,
1076
- severity: problem.severity
1077
- });
1078
- }
1079
- },
1080
- // ── what can come next ────────────────────────────────────────────────
1081
- compose: [
1082
- {
1083
- id: "state",
1084
- // A state opens a line, so its list belongs at the head of one — and it has to
1085
- // stay offered while the state is being spelled, which is why this asks
1086
- // `firstWord` rather than `firstOnLine`. Asking the stricter question makes the
1087
- // list vanish after the first letter, which is precisely the "the completion
1088
- // feels unnatural" complaint this grammar exists to answer.
1089
- when: (context) => context.firstWord,
1090
- range: (context) => context.word,
1091
- items: () => STATES.map((state) => ({
1092
- label: state,
1093
- insert: state,
1094
- // A space is what the next word on the line needs. The engine will not
1095
- // add a second one if the document already has whitespace there.
1096
- append: " ",
1097
- kind: "state",
1098
- detail: STATE_VOCAB.entryFor(state)?.detail,
1099
- documentation: STATE_VOCAB.entryFor(state)?.body,
1100
- sortText: "0"
1101
- }))
1102
- },
1103
- {
1104
- id: "value",
1105
- when: (context) => {
1106
- const line = context.state.lines[context.line.number];
1107
- if (line === void 0 || line.state === void 0 || !line.known) return false;
1108
- const stateWord = line.words[0];
1109
- return stateWord !== void 0 && context.caret > stateWord.to;
1110
- },
1111
- range: (context) => context.word,
1112
- items: (context) => {
1113
- const line = context.state.lines[context.line.number];
1114
- const key = optionAtCaret(context.line.before);
1115
- if (key !== void 0 && key !== "") {
1116
- return valueItems(key, line);
1117
- }
1118
- const free = SLOTS.find((slot) => line?.slots[slot] === void 0);
1119
- const items = free === void 0 ? [] : valueItems(free, line);
1120
- for (const key2 of OPTIONS) {
1121
- if (line?.keys.includes(key2) === true) continue;
1122
- const doc = KEY_DOCS[key2];
1123
- items.push({
1124
- label: `${key2}=`,
1125
- insert: `${key2}=`,
1126
- kind: "property",
1127
- detail: doc?.detail,
1128
- documentation: doc?.body,
1129
- sortText: "1"
1130
- });
1131
- }
1132
- return items;
1133
- }
1134
- }
1135
- ],
1136
- // ── what a thing is ───────────────────────────────────────────────────
1137
- describe: (context) => {
1138
- const token = context.token;
1139
- if (token === void 0) return void 0;
1140
- if (token.scope === "comment") {
1141
- return { title: "comment", body: "Ignored by the parser. A `#` anywhere on a line starts one." };
1142
- }
1143
- if (token.scope === "property") {
1144
- const doc = KEY_DOCS[token.text];
1145
- return doc === void 0 ? void 0 : { title: token.text, detail: doc.detail, body: doc.body };
1146
- }
1147
- if (token.scope === "operator" || token.scope === "separator") return void 0;
1148
- if (token.scope === "invalid") {
1149
- return {
1150
- title: token.text,
1151
- detail: "not part of this language",
1152
- body: "Nothing here accepts this word: it is not a state, not one of the option keys, and not a value any slot recognises."
1153
- };
1154
- }
1155
- if (token.scope === SLOT_SCOPE.speed) {
1156
- return {
1157
- title: token.text,
1158
- detail: "seconds per cycle",
1159
- body: "Seconds per revolution for `turn`, seconds per cycle otherwise."
1160
- };
1161
- }
1162
- const entry = BY_SCOPE.get(token.scope)?.entryFor(token.text);
1163
- return entry === void 0 ? void 0 : { title: token.text, detail: entry.detail, body: entry.body };
1164
- }
1165
- });
1166
- function valueItems(slot, line) {
1167
- if (slot === "speed") {
1168
- const current = line?.state === void 0 ? void 0 : LOOK[line.state]?.speed;
1169
- const rates = [.../* @__PURE__ */ new Set([...Object.values(LOOK).map((look) => look.speed), 1, 2, 3])].sort(
1170
- (left, right) => left - right
1171
- );
1172
- return rates.map((rate) => ({
1173
- label: String(rate),
1174
- insert: String(rate),
1175
- kind: "number",
1176
- detail: current === rate ? "the shipped rate for this state" : "seconds per cycle",
1177
- sortText: current === rate ? "0" : "1"
1178
- }));
1179
- }
1180
- const words = wordsForSlot(slot);
1181
- if (words.length === 0) return [];
1182
- const scope = slot === "bg" ? SLOT_SCOPE.color : SLOT_SCOPE[slot];
1183
- const vocabulary = BY_SCOPE.get(scope);
1184
- const filled = line?.slots[KEY_SLOT[slot] ?? "shape"];
1185
- const noun = slot === "bg" ? "color" : slot;
1186
- return words.map((word) => {
1187
- const entry = vocabulary?.entryFor(word);
1188
- return {
1189
- label: word,
1190
- insert: word,
1191
- kind: "value",
1192
- detail: word === filled ? `the current ${noun}` : entry?.detail,
1193
- documentation: entry?.body,
1194
- sortText: word === filled ? "0" : "1"
1195
- };
1196
- });
1197
- }
1198
- }
1199
- function optionAtCaret(before) {
1200
- const match = /([A-Za-z][\w-]*)\s*=([^\s=]*)$/.exec(before);
1201
- return match?.[1];
1202
- }
1203
- var DSH_SENTRY_STATES = DEFAULT_STATES;
1204
- var DSH_SENTRY_COLORS = DEFAULT_COLORS;
1205
- var DSH_SENTRY_VOCABULARY = {
1206
- states: DEFAULT_STATES,
1207
- shapes: DEFAULT_SHAPES,
1208
- motions: DEFAULT_MOTIONS,
1209
- colors: DEFAULT_COLORS,
1210
- options: DEFAULT_OPTIONS
1211
- };
1212
-
1213
- exports.DSH_FONT_INFO_CODES = DSH_FONT_INFO_CODES;
1214
- exports.DSH_SENTRY_COLORS = DSH_SENTRY_COLORS;
1215
- exports.DSH_SENTRY_STATES = DSH_SENTRY_STATES;
1216
- exports.DSH_SENTRY_VOCABULARY = DSH_SENTRY_VOCABULARY;
1217
- exports.FONT_COMMON_FAMILIES = FONT_COMMON_FAMILIES;
1218
- exports.FONT_GENERIC_FAMILIES = FONT_GENERIC_FAMILIES;
1219
- exports.FONT_WEIGHT_LABELS = FONT_WEIGHT_LABELS;
1220
- exports.FONT_WEIGHT_SCALE = FONT_WEIGHT_SCALE;
1221
- exports.FONT_WEIGHT_WORDS = FONT_WEIGHT_WORDS;
1222
- exports.dshFontQueryGrammar = dshFontQueryGrammar;
1223
- exports.dshSentryStyleGrammar = dshSentryStyleGrammar;
1224
- exports.fontFaceWeights = fontFaceWeights;
1225
- exports.fontWeightWord = fontWeightWord;
1226
- exports.quoteFontFamily = quoteFontFamily;
1227
- //# sourceMappingURL=grammars.cjs.map
1228
- //# sourceMappingURL=grammars.cjs.map