@aaqu/fromcubes-react 0.2.1 → 0.2.2

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.
@@ -0,0 +1,1116 @@
1
+ /**
2
+ * className completion, diagnostics and colour squares — the logic of
3
+ * Tailwind CSS IntelliSense (the VS Code extension), pure: where class lists
4
+ * are, which variants are already typed, which items to offer and how to
5
+ * sort them, which classes conflict or have a canonical spelling, which
6
+ * carry a colour. No DOM, no Monaco, no network; the glue in
7
+ * portal-react.html feeds it the engine's data (GET
8
+ * /fromcubes-react/css-classes, /css-describe) and maps the results onto
9
+ * Monaco.
10
+ *
11
+ * Ported from tailwindcss-intellisense (packages/tailwindcss-language-service:
12
+ * completionProvider.ts, util/getVariantsFromClassName.ts, util/lexers.ts,
13
+ * util/find.ts, util/doc.ts, util/segment.ts, util/naturalExpand.ts,
14
+ * util/joinWithAnd.ts, util/removeRangesFromString.ts, documentColorProvider.ts,
15
+ * diagnostics/getCssConflictDiagnostics.ts, diagnostics/canonical-classes.ts,
16
+ * codeActions/, and the server's colour presentations — MIT,
17
+ * © Tailwind Labs, Inc.; see THIRD-PARTY-NOTICES.md). Names follow the
18
+ * originals; departures are commented.
19
+ *
20
+ * Loaded in the browser by the asset loader in portal-react.html
21
+ * (`<base>/editor/class-completion.js`, exposes `window.__fcClassCompletion`)
22
+ * and in tests via `require()`.
23
+ */
24
+ (function (root, factory) {
25
+ const api = factory();
26
+ if (typeof module === "object" && module.exports) {
27
+ module.exports = api;
28
+ } else {
29
+ root.__fcClassCompletion = api;
30
+ }
31
+ })(typeof self !== "undefined" ? self : this, function () {
32
+ "use strict";
33
+
34
+ /** How far before the cursor a class attribute is looked for. */
35
+ const SEARCH_RANGE = 15000;
36
+
37
+ /** IntelliSense's trigger characters, plus its separator `:`. */
38
+ const TRIGGER_CHARACTERS = ['"', "'", "`", " ", ".", "(", "[", "]", "!", "/", "-", ":"];
39
+
40
+ /** `tailwindCSS.classAttributes` default. */
41
+ const CLASS_ATTRIBUTES = ["class", "className", "ngClass", "class:list"];
42
+
43
+ /** Matches MAX_DESCRIBE in lib/providers.js — candidates per request. */
44
+ const MAX_DESCRIBE = 64;
45
+
46
+ const SEP = ":";
47
+
48
+ // ── text ─────────────────────────────────────────────────────────────
49
+
50
+ const JS_TOKEN_RE = /\/\/.*$|\/\*[\s\S]*?\*\/|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/gm;
51
+
52
+ /**
53
+ * Blank out `//` and block comments, keeping length and line breaks, so
54
+ * a commented-out `className="…"` is not taken for a live one. One regex
55
+ * pass is the same tokenisation as the original's lexer: at each spot the
56
+ * first alternative that matches wins. (Its regex-literal stripping is not
57
+ * ported.)
58
+ *
59
+ * @param {string} text
60
+ * @returns {string}
61
+ */
62
+ function jsWithoutComments(text) {
63
+ return text.replace(JS_TOKEN_RE, function (m) {
64
+ if (m[0] !== "/") return m;
65
+ return m.replace(/[^\n]/g, " ");
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Split `input` on `separator` at the top level only — not inside
71
+ * `(…)`, `[…]`, `{…}` or quotes.
72
+ *
73
+ * @param {string} input
74
+ * @param {string} separator One character.
75
+ * @returns {Array<string>}
76
+ */
77
+ function segment(input, separator) {
78
+ const closing = [];
79
+ const parts = [];
80
+ let lastPos = 0;
81
+ for (let idx = 0; idx < input.length; idx++) {
82
+ const char = input[idx];
83
+ if (closing.length === 0 && char === separator) {
84
+ parts.push(input.slice(lastPos, idx));
85
+ lastPos = idx + 1;
86
+ continue;
87
+ }
88
+ switch (char) {
89
+ case "\\":
90
+ idx += 1;
91
+ break;
92
+ case "'":
93
+ case '"':
94
+ while (++idx < input.length) {
95
+ if (input[idx] === "\\") {
96
+ idx += 1;
97
+ continue;
98
+ }
99
+ if (input[idx] === char) break;
100
+ }
101
+ break;
102
+ case "(":
103
+ closing.push(")");
104
+ break;
105
+ case "[":
106
+ closing.push("]");
107
+ break;
108
+ case "{":
109
+ closing.push("}");
110
+ break;
111
+ case ")":
112
+ case "]":
113
+ case "}":
114
+ if (closing.length > 0 && char === closing[closing.length - 1]) closing.pop();
115
+ break;
116
+ }
117
+ }
118
+ parts.push(input.slice(lastPos));
119
+ return parts;
120
+ }
121
+
122
+ /**
123
+ * @param {number} value
124
+ * @param {number} [total]
125
+ * @returns {string} `value` zero-padded to the width of `total` (else 8)
126
+ */
127
+ function naturalExpand(value, total) {
128
+ const length = typeof total === "number" ? total.toString().length : 8;
129
+ return ("0".repeat(length) + value).slice(-length);
130
+ }
131
+
132
+ // ── where the class list is (util/lexers.ts, util/find.ts) ──────────────
133
+
134
+ function rule(type, re, action) {
135
+ return { type: type, re: new RegExp(re, "y"), push: action && action.push, pop: !!(action && action.pop) };
136
+ }
137
+
138
+ const CLASS_ATTRIBUTE_STATES = {
139
+ doubleClassList: [
140
+ rule("arb", "(?<!\\\\)\\[", { push: "arbitrary" }),
141
+ rule("lbrace", "(?<!\\\\)\\{", { push: "interpBrace" }),
142
+ rule("rbrace", "(?<!\\\\)\\}", { pop: true }),
143
+ rule("end", '(?<!\\\\)"', { pop: true }),
144
+ rule("classlist", "[\\s\\S]"),
145
+ ],
146
+ singleClassList: [
147
+ rule("lbrace", "(?<!\\\\)\\{", { push: "interpBrace" }),
148
+ rule("rbrace", "(?<!\\\\)\\}", { pop: true }),
149
+ rule("end", "(?<!\\\\)'", { pop: true }),
150
+ rule("classlist", "[\\s\\S]"),
151
+ ],
152
+ tickClassList: [
153
+ rule("lbrace", "(?<=(?<!\\\\)\\$)\\{", { push: "interpBrace" }),
154
+ rule("rbrace", "(?<!\\\\)\\}", { pop: true }),
155
+ rule("end", "(?<!\\\\)`", { pop: true }),
156
+ rule("classlist", "[\\s\\S]"),
157
+ ],
158
+ interpBrace: [
159
+ rule("startSingle", "(?<!\\\\)'", { push: "singleClassList" }),
160
+ rule("startDouble", '(?<!\\\\)"', { push: "doubleClassList" }),
161
+ rule("startTick", "(?<!\\\\)`", { push: "tickClassList" }),
162
+ rule("lbrace", "(?<!\\\\)\\{", { push: "interpBrace" }),
163
+ rule("rbrace", "(?<!\\\\)\\}", { pop: true }),
164
+ rule("text", "[\\s\\S]"),
165
+ ],
166
+ interpParen: [
167
+ rule("startSingle", "(?<!\\\\)'", { push: "singleClassList" }),
168
+ rule("startDouble", '(?<!\\\\)"', { push: "doubleClassList" }),
169
+ rule("startTick", "(?<!\\\\)`", { push: "tickClassList" }),
170
+ rule("lbrace", "(?<!\\\\)\\(", { push: "interpParen" }),
171
+ rule("rbrace", "(?<!\\\\)\\)", { pop: true }),
172
+ rule("text", "[\\s\\S]"),
173
+ ],
174
+ interpSingle: [
175
+ rule("startDouble", '(?<!\\\\)"', { push: "doubleClassList" }),
176
+ rule("startTick", "(?<!\\\\)`", { push: "tickClassList" }),
177
+ rule("single", "(?<!\\\\)'", { pop: true }),
178
+ rule("text", "[\\s\\S]"),
179
+ ],
180
+ interpDouble: [
181
+ rule("startSingle", "(?<!\\\\)'", { push: "singleClassList" }),
182
+ rule("startTick", "(?<!\\\\)`", { push: "tickClassList" }),
183
+ rule("double", '(?<!\\\\)"', { pop: true }),
184
+ rule("text", "[\\s\\S]"),
185
+ ],
186
+ arbitrary: [
187
+ rule("arb", "(?<!\\\\)\\]", { pop: true }),
188
+ rule("space", "\\s", { pop: true }),
189
+ rule("arb2", "[\\s\\S]"),
190
+ ],
191
+ };
192
+
193
+ const CLASS_ATTRIBUTE_LEXER = Object.assign(
194
+ {
195
+ main: [
196
+ rule("start1", '"', { push: "doubleClassList" }),
197
+ rule("start2", "'", { push: "singleClassList" }),
198
+ rule("start3", "\\{", { push: "interpBrace" }),
199
+ rule("start4", "`", { push: "tickClassList" }),
200
+ rule("start5", "\\(", { push: "interpParen" }),
201
+ ],
202
+ },
203
+ CLASS_ATTRIBUTE_STATES,
204
+ );
205
+
206
+ const COMPUTED_CLASS_ATTRIBUTE_LEXER = Object.assign(
207
+ {
208
+ main: [
209
+ rule("lbrace", "\\{", { push: "interpBrace" }),
210
+ rule("single", "'", { push: "interpSingle" }),
211
+ rule("double", '"', { push: "interpDouble" }),
212
+ ],
213
+ },
214
+ CLASS_ATTRIBUTE_STATES,
215
+ );
216
+
217
+ /**
218
+ * A state-machine lexer with moo's semantics: in each state the first
219
+ * rule that matches at the position wins; no match is a syntax error,
220
+ * which ends the run (`failed`) with the tokens read so far.
221
+ *
222
+ * @param {Object<string, Array<Object>>} states
223
+ * @param {string} input
224
+ * @param {number} [start] Where in `input` to begin (offsets count from it).
225
+ * @returns {{tokens: Array<{type: string, value: string, offset: number}>, failed: boolean}}
226
+ */
227
+ function lex(states, input, start) {
228
+ const tokens = [];
229
+ const stack = [];
230
+ const from = start || 0;
231
+ let state = "main";
232
+ let pos = from;
233
+ while (pos < input.length) {
234
+ const rules = states[state];
235
+ let hit = null;
236
+ let value = "";
237
+ for (let i = 0; i < rules.length; i++) {
238
+ rules[i].re.lastIndex = pos;
239
+ const m = rules[i].re.exec(input);
240
+ if (m) {
241
+ hit = rules[i];
242
+ value = m[0];
243
+ break;
244
+ }
245
+ }
246
+ if (!hit) return { tokens: tokens, failed: true };
247
+ tokens.push({ type: hit.type, value: value, offset: pos - from });
248
+ pos += value.length;
249
+ if (hit.push) {
250
+ stack.push(state);
251
+ state = hit.push;
252
+ } else if (hit.pop && stack.length) {
253
+ state = stack.pop();
254
+ }
255
+ }
256
+ return { tokens: tokens, failed: false };
257
+ }
258
+
259
+ const CLASS_ATTRIBUTE_RE = new RegExp(
260
+ "(?:\\s|:|\\()(" +
261
+ CLASS_ATTRIBUTES.flatMap(function (a) {
262
+ return [a, "\\[" + a + "\\]"];
263
+ }).join("|") +
264
+ ")\\s*=\\s*['\"`{]",
265
+ "gi",
266
+ );
267
+
268
+ function lexerFor(match) {
269
+ const computed = match[0][0] === ":" || (match[1].startsWith("[") && match[1].endsWith("]"));
270
+ return computed ? COMPUTED_CLASS_ATTRIBUTE_LEXER : CLASS_ATTRIBUTE_LEXER;
271
+ }
272
+
273
+ /**
274
+ * The class list the cursor is in — the text from its start to the
275
+ * cursor — or `null` outside one. `text` ends at the cursor and is already
276
+ * without comments. As IntelliSense: the last class attribute before the
277
+ * cursor is lexed up to it; the cursor is in its class list when the last
278
+ * token is part of one.
279
+ *
280
+ * @param {string} text
281
+ * @returns {string|null}
282
+ */
283
+ function findClassList(text) {
284
+ CLASS_ATTRIBUTE_RE.lastIndex = 0;
285
+ let match = null;
286
+ let m;
287
+ while ((m = CLASS_ATTRIBUTE_RE.exec(text)) !== null) match = m;
288
+ if (!match) return null;
289
+
290
+ const lexed = lex(lexerFor(match), text, match.index + match[0].length - 1);
291
+ if (lexed.failed) return null;
292
+ const tokens = lexed.tokens;
293
+ const last = tokens[tokens.length - 1];
294
+ if (!last) return null;
295
+ if (!(last.type.startsWith("start") || last.type === "classlist" || last.type.startsWith("arb"))) return null;
296
+
297
+ let classList = "";
298
+ for (let i = tokens.length - 1; i >= 0; i--) {
299
+ if (tokens[i].type === "classlist" || tokens[i].type.startsWith("arb")) {
300
+ classList = tokens[i].value + classList;
301
+ } else {
302
+ break;
303
+ }
304
+ }
305
+ return classList;
306
+ }
307
+
308
+ /**
309
+ * Every class list of every class attribute in `text` (comments already
310
+ * blanked), the way IntelliSense finds them for diagnostics and colour
311
+ * squares: offsets into `text`, surrounding whitespace trimmed, a list
312
+ * lexed up to a syntax error still counted, each range once.
313
+ *
314
+ * @param {string} text
315
+ * @returns {Array<{classList: string, start: number, end: number}>}
316
+ */
317
+ function findClassListsInText(text) {
318
+ const results = [];
319
+ const seen = new Set();
320
+ const re = new RegExp(CLASS_ATTRIBUTE_RE.source, "gi");
321
+ let match;
322
+ while ((match = re.exec(text)) !== null) {
323
+ const base = match.index + match[0].length - 1;
324
+ const lists = [];
325
+ let current = null;
326
+ for (const token of lex(lexerFor(match), text, base).tokens) {
327
+ if (token.type === "classlist" || token.type.startsWith("arb")) {
328
+ if (current) current.value += token.value;
329
+ else current = { value: token.value, offset: token.offset };
330
+ } else {
331
+ if (current) lists.push(current);
332
+ current = null;
333
+ }
334
+ }
335
+ if (current) lists.push(current);
336
+
337
+ for (const list of lists) {
338
+ if (list.value.trim() === "") continue;
339
+ const before = /^\s*/.exec(list.value)[0].length;
340
+ const after = /\s*$/.exec(list.value)[0].length;
341
+ const start = base + list.offset + before;
342
+ const end = base + list.offset + list.value.length - after;
343
+ const key = start + ":" + end;
344
+ if (seen.has(key)) continue;
345
+ seen.add(key);
346
+ results.push({ classList: text.slice(start, end), start: start, end: end });
347
+ }
348
+ }
349
+ return results;
350
+ }
351
+
352
+ /**
353
+ * The classes of one list with their offsets — absolute (`start`, `end`)
354
+ * and within the list (`relStart`, `relEnd`).
355
+ *
356
+ * @param {{classList: string, start: number}} list
357
+ * @returns {Array<{className: string, start: number, end: number, relStart: number, relEnd: number, list: Object}>}
358
+ */
359
+ function getClassNamesInClassList(list) {
360
+ const parts = list.classList.split(/(\s+)/);
361
+ const names = [];
362
+ let index = 0;
363
+ for (let i = 0; i < parts.length; i++) {
364
+ if (i % 2 === 0 && parts[i]) {
365
+ names.push({
366
+ className: parts[i],
367
+ start: list.start + index,
368
+ end: list.start + index + parts[i].length,
369
+ relStart: index,
370
+ relEnd: index + parts[i].length,
371
+ list: list,
372
+ });
373
+ }
374
+ index += parts[i].length;
375
+ }
376
+ return names;
377
+ }
378
+
379
+ /**
380
+ * @param {Array<Object>} classLists
381
+ * @returns {Array<string>} each class written in them, once
382
+ */
383
+ function uniqueClassNames(classLists) {
384
+ const set = new Set();
385
+ for (const list of classLists) {
386
+ for (const c of getClassNamesInClassList(list)) set.add(c.className);
387
+ }
388
+ return Array.from(set);
389
+ }
390
+
391
+ // ── the engine's data ────────────────────────────────────────────────
392
+
393
+ /**
394
+ * Index the engine's completion data in place (idempotent): class name →
395
+ * position, the variant names IntelliSense matches literally, and a cache
396
+ * for answers from the server.
397
+ *
398
+ * @param {Object} data GET /fromcubes-react/css-classes
399
+ * @returns {Object} data
400
+ */
401
+ function prepare(data) {
402
+ if (data._index) return data;
403
+ const index = new Map();
404
+ for (let i = 0; i < data.classes.length; i++) index.set(data.classes[i], i);
405
+ const all = new Set();
406
+ for (const variant of data.variants || []) {
407
+ if (variant.values.length) {
408
+ for (const value of variant.values) {
409
+ all.add(value === "DEFAULT" ? variant.name : variant.name + (variant.hasDash ? "-" : "") + value);
410
+ }
411
+ } else {
412
+ all.add(variant.name);
413
+ }
414
+ }
415
+ Object.defineProperty(data, "_index", { value: index });
416
+ Object.defineProperty(data, "_allVariants", { value: all });
417
+ // Kept when the glue carries them over from the previous fetch.
418
+ if (!data._described) Object.defineProperty(data, "_described", { value: new Map() });
419
+ if (!data._canonical) Object.defineProperty(data, "_canonical", { value: new Map() });
420
+ return data;
421
+ }
422
+
423
+ /**
424
+ * @param {Object} data
425
+ * @param {string} className
426
+ * @returns {Array<string>}
427
+ */
428
+ function modifiersOf(data, className) {
429
+ const i = data._index.get(className);
430
+ if (i === undefined) return [];
431
+ const m = data.classModifiers[i];
432
+ return m >= 0 ? data.modifiers[m] : [];
433
+ }
434
+
435
+ /**
436
+ * Whether `part` is worth asking the engine about as a variant — only
437
+ * text shaped like one of its variants (`group-hover/x`, `data-[…]`,
438
+ * `@md/x`, `[&>*]`). IntelliSense compiles every candidate locally; here
439
+ * each check is a request, so `bg-red-` is not one.
440
+ *
441
+ * @param {Object} data
442
+ * @param {string} part
443
+ * @returns {boolean}
444
+ */
445
+ function mayBeVariant(data, part) {
446
+ if (!part) return false;
447
+ if (part[0] === "[") return true;
448
+ for (const variant of data.variants || []) {
449
+ const name = variant.name;
450
+ if (part === name) return true;
451
+ if (part.startsWith(name + "/") || part.startsWith(name + "[")) return true;
452
+ if (variant.hasDash ? part.startsWith(name + "-") : part.startsWith(name)) return true;
453
+ }
454
+ return false;
455
+ }
456
+
457
+ /**
458
+ * @param {Object} data
459
+ * @param {string} token One class as typed, variants and all.
460
+ * @returns {boolean} whether its utility is one the engine lists
461
+ */
462
+ function isKnownClass(data, token) {
463
+ const parts = segment(token, SEP);
464
+ let base = parts[parts.length - 1];
465
+ if (base.endsWith("!")) base = base.slice(0, -1);
466
+ else if (base.startsWith("!")) base = base.slice(1);
467
+ if (data._index.has(base)) return true;
468
+ const slash = base.lastIndexOf("/");
469
+ return slash > 0 && modifiersOf(data, base.slice(0, slash)).length > 0;
470
+ }
471
+
472
+ // ── class list → items (completionProvider.ts) ───────────────────────
473
+
474
+ /**
475
+ * The variants already typed in front of the class: `hover:md:bg-` →
476
+ * `["hover", "md"]`, and how far into the class name they reach.
477
+ * `isValidVariant` answers for parts that are not one of the listed
478
+ * variant names (`group-hover/item`, `data-[open]`); it may be async.
479
+ *
480
+ * @param {Object} data
481
+ * @param {string} className
482
+ * @param {function(string): (boolean|Promise<boolean>)} [isValidVariant]
483
+ * @returns {Promise<{variants: Array<string>, offset: number}>}
484
+ */
485
+ async function getVariantsFromClassName(data, className, isValidVariant) {
486
+ const parts = segment(className, SEP);
487
+ if (parts.length < 2) return { variants: [], offset: 0 };
488
+
489
+ let offset = 0;
490
+ const variants = new Set();
491
+
492
+ for (let index = 0; index < parts.length; index++) {
493
+ const part = parts[index];
494
+ // An empty part is `hover::` (not a class) or a trailing `:` (a
495
+ // utility comes next): either way the variants end here.
496
+ if (part === "") break;
497
+ const valid = data._allVariants.has(part) || (isValidVariant ? await isValidVariant(part) : false);
498
+ if (!valid) break;
499
+
500
+ variants.add(part);
501
+ offset += part.length;
502
+ // Every variant but the last is followed by the separator.
503
+ offset += index < parts.length - 1 ? SEP.length : 0;
504
+ }
505
+
506
+ return { variants: Array.from(variants), offset: offset };
507
+ }
508
+
509
+ /**
510
+ * The completion list for the class list ending at the cursor — what
511
+ * IntelliSense's completionsFromClassList returns for Tailwind v4.
512
+ *
513
+ * Items carry a `kind` (`variant`, `color`, `constant`), `label`,
514
+ * `sortText`, and as needed `detail`, `documentation` (a swatch colour),
515
+ * `insertText` (`snippet` when it is one), `filterText`, `retrigger` (open
516
+ * the list again once inserted, as after `hover:`). `range` is relative
517
+ * to the cursor: the replaced text starts `-range.start` characters
518
+ * before it. `variants`/`important` are what the class items stand under,
519
+ * for fetching their CSS; `modifiers` marks a list of `/…` modifiers,
520
+ * whose colours the caller fetches (IntelliSense compiles each one).
521
+ *
522
+ * @param {Object} data prepare()d engine data
523
+ * @param {string} classList Class list text up to the cursor.
524
+ * @param {{triggerKind: "invoke"|"character"|"incomplete", triggerCharacter?: string}} [context]
525
+ * @param {function(string): (boolean|Promise<boolean>)} [isValidVariant]
526
+ * @returns {Promise<Object>}
527
+ */
528
+ async function completionsFromClassList(data, classList, context, isValidVariant) {
529
+ const classNames = classList.split(/[\s+]/);
530
+ const partialClassName = classNames[classNames.length - 1];
531
+ let start = 0 - partialClassName.length; // not -0 for an empty one
532
+
533
+ const found = await getVariantsFromClassName(data, partialClassName, isValidVariant);
534
+ const existingVariants = found.variants;
535
+ const offset = found.offset;
536
+
537
+ if (
538
+ context &&
539
+ (context.triggerKind === "invoke" || (context.triggerKind === "character" && context.triggerCharacter === "/")) &&
540
+ partialClassName.includes("/")
541
+ ) {
542
+ const beforeSlash = partialClassName.split("/").slice(0, -1).join("/");
543
+ const baseClassName = beforeSlash.slice(offset);
544
+ const modifiers = modifiersOf(data, baseClassName);
545
+
546
+ if (modifiers.length > 0) {
547
+ return {
548
+ range: { start: start, end: 0 },
549
+ variants: [],
550
+ important: false,
551
+ modifiers: true,
552
+ items: modifiers.map(function (modifier, index) {
553
+ return { label: beforeSlash + "/" + modifier, kind: "constant", sortText: naturalExpand(index) };
554
+ }),
555
+ };
556
+ }
557
+ }
558
+
559
+ start += offset;
560
+
561
+ // IntelliSense ends the range before a trailing `!`; Monaco wants the
562
+ // cursor inside the range, so the `!` stays in it and goes back in
563
+ // through insertText (and filterText, to keep matching).
564
+ const important = partialClassName.substr(offset).endsWith("!");
565
+
566
+ const items = [];
567
+ const seenVariants = new Set();
568
+ let variantOrder = 0;
569
+
570
+ function variantItem(item) {
571
+ item.kind = "variant";
572
+ item.sortText = "-" + naturalExpand(variantOrder++);
573
+ if (!item.snippet) item.retrigger = true;
574
+ return item;
575
+ }
576
+
577
+ for (const variant of data.variants || []) {
578
+ if (existingVariants.includes(variant.name)) continue;
579
+ if (seenVariants.has(variant.name)) continue;
580
+ seenVariants.add(variant.name);
581
+
582
+ const dash = variant.hasDash ? "-" : "";
583
+
584
+ if (variant.isArbitrary) {
585
+ items.push(
586
+ variantItem({
587
+ label: variant.name + dash + "[]" + SEP,
588
+ insertText: variant.name + dash + "[${1}]" + SEP + "${0}",
589
+ snippet: true,
590
+ }),
591
+ );
592
+ }
593
+
594
+ // Its selectors could not be computed: nothing more is offered.
595
+ if (variant.failed) continue;
596
+
597
+ // A variant that outputs nothing on its own (`group`) is offered only
598
+ // with its values.
599
+ if (variant.detail !== null && variant.detail !== undefined) {
600
+ items.push(variantItem({ label: variant.name + SEP, detail: variant.detail, insertText: variant.name + SEP }));
601
+ }
602
+
603
+ for (let i = 0; i < variant.values.length; i++) {
604
+ const value = variant.values[i];
605
+ const key = variant.name + "-" + value;
606
+ if (existingVariants.includes(key)) continue;
607
+ if (seenVariants.has(key)) continue;
608
+ seenVariants.add(key);
609
+
610
+ const detail = variant.valueDetails[i];
611
+ if (detail === null || detail === undefined) continue;
612
+
613
+ items.push(
614
+ variantItem({
615
+ label: value === "DEFAULT" ? variant.name + SEP : variant.name + dash + value + SEP,
616
+ detail: detail,
617
+ }),
618
+ );
619
+ }
620
+ }
621
+
622
+ const total = data.classes.length;
623
+ for (let index = 0; index < total; index++) {
624
+ const className = data.classes[index];
625
+ const color = data.colors[index];
626
+ const item = { label: className, kind: color ? "color" : "constant", sortText: naturalExpand(index, total) };
627
+ // The keywords are colours without a swatch.
628
+ if (color && color !== "transparent" && color !== "currentColor") item.documentation = color;
629
+ if (important) {
630
+ item.insertText = className + "!";
631
+ item.filterText = className + "!";
632
+ }
633
+ items.push(item);
634
+ }
635
+
636
+ return { range: { start: start, end: 0 }, variants: existingVariants, important: important, modifiers: false, items: items };
637
+ }
638
+
639
+ /**
640
+ * Give modifier items the colours the engine reports for them: Color kind,
641
+ * and a swatch unless the colour is a keyword or fully transparent.
642
+ *
643
+ * @param {Array<Object>} items
644
+ * @param {Array<Object|null>} described describe() results, item by item
645
+ * @returns {Array<Object>} items
646
+ */
647
+ function applyModifierColors(items, described) {
648
+ items.forEach(function (item, i) {
649
+ const color = described[i] && described[i].color;
650
+ if (!color) return;
651
+ item.kind = "color";
652
+ if (color !== "transparent" && color !== "currentColor" && !/,\s*0\)$/.test(color)) {
653
+ item.documentation = color;
654
+ }
655
+ });
656
+ return items;
657
+ }
658
+
659
+ // ── diagnostics (diagnostics/, codeActions/) ─────────────────────────
660
+
661
+ // Order-insensitive, as IntelliSense's `equal`.
662
+ function equal(a, b) {
663
+ if (a === b) return true;
664
+ if (a.length !== b.length) return false;
665
+ const x = a.concat().sort();
666
+ const y = b.concat().sort();
667
+ for (let i = 0; i < x.length; ++i) if (x[i] !== y[i]) return false;
668
+ return true;
669
+ }
670
+
671
+ function joinWithAnd(strings) {
672
+ return strings.reduce(function (acc, cur, i) {
673
+ if (i === 0) return cur;
674
+ if (strings.length > 1 && i === strings.length - 1) return acc + " and " + cur;
675
+ return acc + ", " + cur;
676
+ }, "");
677
+ }
678
+
679
+ /**
680
+ * Cut ranges out of a class list, each with the whitespace before it.
681
+ *
682
+ * @param {string} str
683
+ * @param {Array<{start: number, end: number}>} ranges
684
+ * @returns {string}
685
+ */
686
+ function removeRangesFromString(str, ranges) {
687
+ const indexRanges = ranges.map(function (range) {
688
+ let start = range.start;
689
+ for (let i = start - 1; i >= 0; i--) {
690
+ if (/\s/.test(str.charAt(i))) start = i;
691
+ else break;
692
+ }
693
+ return { start: start, end: range.end };
694
+ });
695
+ indexRanges.sort(function (a, b) {
696
+ return a.start - b.start;
697
+ });
698
+ let result = "";
699
+ let i = 0;
700
+ indexRanges.forEach(function (r) {
701
+ result += str.substring(i, r.start);
702
+ i = r.end;
703
+ });
704
+ return (result + str.substring(i)).trim();
705
+ }
706
+
707
+ /**
708
+ * Pairs of a class and the others in its list that set the same CSS
709
+ * properties in the same place — same rules, same properties, same
710
+ * selectors and at-rules around them.
711
+ *
712
+ * @param {Array<Object>} classNames
713
+ * @param {Map<string, Array<{properties: Array<string>, context: Array<string>}>>} groups
714
+ * @returns {Array<[Object, Array<Object>]>}
715
+ */
716
+ function findConflicts(classNames, groups) {
717
+ const out = [];
718
+ for (const className of classNames) {
719
+ const entries = groups.get(className.className) || [];
720
+ const conflicting = [];
721
+
722
+ for (const other of classNames) {
723
+ if (className === other) continue;
724
+ const otherEntries = groups.get(other.className) || [];
725
+ if (entries.length !== otherEntries.length) continue;
726
+
727
+ let hasConflict = false;
728
+ for (let i = 0; i < entries.length; i++) {
729
+ const entry = entries[i];
730
+ const otherEntry = otherEntries[i];
731
+ if (entry.properties.length !== otherEntry.properties.length) {
732
+ hasConflict = false;
733
+ break;
734
+ }
735
+ if (entry.context.length !== otherEntry.context.length) {
736
+ hasConflict = false;
737
+ break;
738
+ }
739
+ if (!equal(entry.properties, otherEntry.properties)) {
740
+ hasConflict = false;
741
+ break;
742
+ }
743
+ if (!equal(entry.context, otherEntry.context)) {
744
+ hasConflict = false;
745
+ break;
746
+ }
747
+ if (entry.properties.length === 0 && otherEntry.properties.length === 0) continue;
748
+ hasConflict = true;
749
+ }
750
+
751
+ if (hasConflict) conflicting.push(other);
752
+ }
753
+
754
+ if (conflicting.length > 0) out.push([className, conflicting]);
755
+ }
756
+ return out;
757
+ }
758
+
759
+ /**
760
+ * IntelliSense's diagnostics for class attributes under Tailwind v4, at
761
+ * their default severity (warning): `cssConflict` — two classes of a list
762
+ * set the same properties — and `suggestCanonicalClasses` — a class has a
763
+ * canonical spelling (`w-[16px]` → `w-4`). Each carries its quick fixes as
764
+ * plain edits: delete the other classes, or replace with the suggestion.
765
+ * Classes the engine does not generate are never reported, as there.
766
+ *
767
+ * @param {Array<Object>} classLists findClassListsInText()
768
+ * @param {Map<string, Object|null>} described describe() results by class
769
+ * @param {Map<string, string|null>} [canonical] canonical spellings by class
770
+ * @returns {Array<Object>}
771
+ */
772
+ function diagnose(classLists, described, canonical) {
773
+ const diagnostics = [];
774
+ const quote = function (c) {
775
+ return "'" + c.className + "'";
776
+ };
777
+
778
+ for (const list of classLists) {
779
+ const classNames = getClassNamesInClassList(list);
780
+ const groups = new Map();
781
+ for (const c of classNames) {
782
+ const d = described.get(c.className);
783
+ if (d && d.rules) groups.set(c.className, d.rules);
784
+ }
785
+ for (const [className, others] of findConflicts(classNames, groups)) {
786
+ diagnostics.push({
787
+ code: "cssConflict",
788
+ severity: "warning",
789
+ start: className.start,
790
+ end: className.end,
791
+ message: quote(className) + " applies the same CSS properties as " + joinWithAnd(others.map(quote)) + ".",
792
+ related: others.map(function (o) {
793
+ return { start: o.start, end: o.end, message: o.className };
794
+ }),
795
+ fixes: [
796
+ {
797
+ title: "Delete " + joinWithAnd(others.map(quote)),
798
+ start: list.start,
799
+ end: list.end,
800
+ text: removeRangesFromString(
801
+ list.classList,
802
+ others.map(function (o) {
803
+ return { start: o.relStart, end: o.relEnd };
804
+ }),
805
+ ),
806
+ },
807
+ ],
808
+ });
809
+ }
810
+ }
811
+
812
+ for (const list of classLists) {
813
+ for (const c of getClassNamesInClassList(list)) {
814
+ const spelling = canonical && canonical.get(c.className);
815
+ if (!spelling || spelling === c.className) continue;
816
+ diagnostics.push({
817
+ code: "suggestCanonicalClasses",
818
+ severity: "warning",
819
+ start: c.start,
820
+ end: c.end,
821
+ message: "The class `" + c.className + "` can be written as `" + spelling + "`",
822
+ related: [],
823
+ fixes: [{ title: "Replace with '" + spelling + "'", start: c.start, end: c.end, text: spelling }],
824
+ });
825
+ }
826
+ }
827
+
828
+ return diagnostics;
829
+ }
830
+
831
+ // ── colour squares (documentColorProvider.ts) ────────────────────────
832
+
833
+ const HEX_RE = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
834
+ const RGB_RE = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+)\s*)?\)$/i;
835
+
836
+ /**
837
+ * A swatch colour as sent by the engine, as Monaco's IColor (0–1
838
+ * channels), or `null` for the keywords and anything else.
839
+ *
840
+ * @param {string|null} str
841
+ * @returns {{red: number, green: number, blue: number, alpha: number}|null}
842
+ */
843
+ function parseColor(str) {
844
+ if (!str) return null;
845
+ let m = HEX_RE.exec(str);
846
+ if (m) {
847
+ let hex = m[1];
848
+ if (hex.length <= 4) {
849
+ hex = hex
850
+ .split("")
851
+ .map(function (c) {
852
+ return c + c;
853
+ })
854
+ .join("");
855
+ }
856
+ const n = function (i) {
857
+ return parseInt(hex.slice(i, i + 2), 16) / 255;
858
+ };
859
+ return { red: n(0), green: n(2), blue: n(4), alpha: hex.length === 8 ? n(6) : 1 };
860
+ }
861
+ m = RGB_RE.exec(str);
862
+ if (m) {
863
+ return {
864
+ red: Math.min(255, +m[1]) / 255,
865
+ green: Math.min(255, +m[2]) / 255,
866
+ blue: Math.min(255, +m[3]) / 255,
867
+ alpha: m[4] === undefined ? 1 : Math.min(1, +m[4]),
868
+ };
869
+ }
870
+ return null;
871
+ }
872
+
873
+ /**
874
+ * @param {Object} data prepare()d engine data
875
+ * @param {Map<string, Object|null>} described
876
+ * @param {string} className
877
+ * @returns {string|null} the class's colour, from the list or the engine
878
+ */
879
+ function colorOfClass(data, described, className) {
880
+ const i = data._index.get(className);
881
+ if (i !== undefined) return data.colors[i] || null;
882
+ const d = described.get(className);
883
+ return (d && d.color) || null;
884
+ }
885
+
886
+ /**
887
+ * Classes whose colour the list does not carry — with variants, modifiers
888
+ * or arbitrary values — and the engine has to be asked about.
889
+ *
890
+ * @param {Object} data
891
+ * @param {Array<Object>} classLists
892
+ * @returns {Array<string>}
893
+ */
894
+ function classesNeedingColor(data, classLists) {
895
+ return uniqueClassNames(classLists).filter(function (c) {
896
+ return !data._index.has(c);
897
+ });
898
+ }
899
+
900
+ /**
901
+ * The colour squares IntelliSense draws: every class in a class attribute
902
+ * that carries a colour, except the keywords and fully transparent ones.
903
+ *
904
+ * @param {Object} data
905
+ * @param {Array<Object>} classLists
906
+ * @param {Map<string, Object|null>} described
907
+ * @returns {Array<{start: number, end: number, color: Object}>}
908
+ */
909
+ function documentColors(data, classLists, described) {
910
+ const colors = [];
911
+ for (const list of classLists) {
912
+ for (const c of getClassNamesInClassList(list)) {
913
+ const color = parseColor(colorOfClass(data, described, c.className));
914
+ if (!color || color.alpha === 0) continue;
915
+ colors.push({ start: c.start, end: c.end, color: color });
916
+ }
917
+ }
918
+ return colors;
919
+ }
920
+
921
+ // The CSS named colours, as the color-name package IntelliSense uses.
922
+ const COLOR_NAMES = (
923
+ "aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown " +
924
+ "burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan " +
925
+ "darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid " +
926
+ "darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet " +
927
+ "deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro " +
928
+ "ghostwhite gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki " +
929
+ "lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow " +
930
+ "lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray " +
931
+ "lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine " +
932
+ "mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise " +
933
+ "mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab " +
934
+ "orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru " +
935
+ "pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown " +
936
+ "seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan " +
937
+ "teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen"
938
+ ).split(" ");
939
+
940
+ const PRESENTATION_RE = new RegExp(
941
+ "-\\[(" + COLOR_NAMES.join("|") + "|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$",
942
+ "i",
943
+ );
944
+
945
+ // culori's serializers: channels clamped and rounded to 0–255, alpha and
946
+ // HSL to two decimals.
947
+ const clamp01 = function (v) {
948
+ return Math.max(0, Math.min(1, v || 0));
949
+ };
950
+ const to255 = function (v) {
951
+ return Math.round(clamp01(v) * 255);
952
+ };
953
+ const twoDecimals = function (v) {
954
+ return Math.round(v * 100) / 100;
955
+ };
956
+ const hex2 = function (n) {
957
+ return ((1 << 8) | n).toString(16).slice(1);
958
+ };
959
+
960
+ function formatHex8(c) {
961
+ return "#" + hex2(to255(c.r)) + hex2(to255(c.g)) + hex2(to255(c.b)) + hex2(to255(c.alpha === undefined ? 1 : c.alpha));
962
+ }
963
+
964
+ function formatRgb(c) {
965
+ const rgb = to255(c.r) + ", " + to255(c.g) + ", " + to255(c.b);
966
+ if (c.alpha === undefined || c.alpha === 1) return "rgb(" + rgb + ")";
967
+ return "rgba(" + rgb + ", " + twoDecimals(clamp01(c.alpha)) + ")";
968
+ }
969
+
970
+ function formatHsl(c) {
971
+ const r = c.r || 0;
972
+ const g = c.g || 0;
973
+ const b = c.b || 0;
974
+ const M = Math.max(r, g, b);
975
+ const m = Math.min(r, g, b);
976
+ const s = M === m ? 0 : (M - m) / (1 - Math.abs(M + m - 1));
977
+ const l = 0.5 * (M + m);
978
+ let h = 0;
979
+ if (M - m !== 0) {
980
+ h = (M === r ? (g - b) / (M - m) + (g < b) * 6 : M === g ? (b - r) / (M - m) + 2 : (r - g) / (M - m) + 4) * 60;
981
+ }
982
+ const hsl = twoDecimals(h) + ", " + twoDecimals(clamp01(s) * 100) + "%, " + twoDecimals(clamp01(l) * 100) + "%";
983
+ if (c.alpha === undefined || c.alpha === 1) return "hsl(" + hsl + ")";
984
+ return "hsla(" + hsl + ", " + twoDecimals(clamp01(c.alpha)) + ")";
985
+ }
986
+
987
+ /**
988
+ * What the colour picker may write back for a class: only an arbitrary
989
+ * colour (`bg-[#ff0000]`, `text-[rgb(…)]`, `border-[tomato]`) can take a
990
+ * new one — as hex, rgb() and hsl(). Other classes get none, as there.
991
+ *
992
+ * @param {string} className
993
+ * @param {{red: number, green: number, blue: number, alpha: number}} color
994
+ * @returns {Array<string>}
995
+ */
996
+ function colorPresentations(className, color) {
997
+ const match = className.match(PRESENTATION_RE);
998
+ if (match === null) return [];
999
+
1000
+ const currentColor = match[1];
1001
+ const isNamedColor = COLOR_NAMES.includes(currentColor);
1002
+ const rgb = { r: color.red, g: color.green, b: color.blue, alpha: color.alpha };
1003
+
1004
+ let hexValue = formatHex8(rgb);
1005
+ if (!isNamedColor && (currentColor.length === 4 || currentColor.length === 5)) {
1006
+ const m = hexValue.match(/^#([a-f\d])\1([a-f\d])\2([a-f\d])\3(?:([a-f\d])\4)?$/i);
1007
+ const chars = m ? m.slice(1) : [];
1008
+ if (chars.length) hexValue = "#" + chars.filter(Boolean).join("");
1009
+ }
1010
+ if (hexValue.length === 5) hexValue = hexValue.replace(/f$/, "");
1011
+ else if (hexValue.length === 9) hexValue = hexValue.replace(/ff$/, "");
1012
+
1013
+ const prefix = className.substr(0, match.index);
1014
+
1015
+ return [
1016
+ hexValue,
1017
+ formatRgb(rgb).replace(/ /g, ""),
1018
+ formatHsl(rgb)
1019
+ .replace(/ /g, "")
1020
+ .replace(/\d+\.\d+(%?)/g, function (value, suffix) {
1021
+ return Math.round(parseFloat(value)) + suffix;
1022
+ }),
1023
+ ].map(function (value) {
1024
+ return prefix + "-[" + value + "]";
1025
+ });
1026
+ }
1027
+
1028
+ // ── entry points for the glue ────────────────────────────────────────
1029
+
1030
+ const OPEN_STRING_RE = /["'`]([^"'`]*)$/;
1031
+
1032
+ /**
1033
+ * The class list at the cursor: inside a class attribute (IntelliSense's
1034
+ * rule), or — this editor's addition, which IntelliSense leaves to
1035
+ * `classRegex` settings — inside any string on the line that already holds
1036
+ * a class the engine knows, or whose first word starts like one, so
1037
+ * `const variants = { primary: "bg-blue-600 …" }` completes too.
1038
+ *
1039
+ * @param {Object} data prepare()d engine data
1040
+ * @param {string} textBefore Up to SEARCH_RANGE characters ending at the cursor.
1041
+ * @param {string} lineBefore The cursor's line up to the cursor.
1042
+ * @returns {{classList: string, attribute: boolean}|null}
1043
+ */
1044
+ function classListAt(data, textBefore, lineBefore) {
1045
+ const inAttribute = findClassList(jsWithoutComments(textBefore));
1046
+ if (inAttribute !== null) return { classList: inAttribute, attribute: true };
1047
+
1048
+ const open = OPEN_STRING_RE.exec(lineBefore);
1049
+ if (!open) return null;
1050
+ const inside = open[1].replace(/\$\{[^}]*\}/g, " ");
1051
+ const parts = inside.split(/\s+/);
1052
+ const word = parts[parts.length - 1] || "";
1053
+ for (const part of parts) {
1054
+ if (part && isKnownClass(data, part)) return { classList: inside, attribute: false };
1055
+ }
1056
+ if (parts.length === 1 && word.length >= 2) {
1057
+ const typed = segment(word, SEP).pop();
1058
+ for (const cls of data.classes) {
1059
+ if (cls.indexOf(typed) === 0) return { classList: inside, attribute: false };
1060
+ }
1061
+ }
1062
+ return null;
1063
+ }
1064
+
1065
+ /**
1066
+ * The class under the cursor, for a hover: its text and where it lies
1067
+ * relative to the cursor (`start` ≤ 0 ≤ `end`).
1068
+ *
1069
+ * @param {Object} data
1070
+ * @param {string} textBefore
1071
+ * @param {string} lineBefore
1072
+ * @param {string} lineAfter The cursor's line from the cursor on.
1073
+ * @returns {{className: string, start: number, end: number}|null}
1074
+ */
1075
+ function classNameAt(data, textBefore, lineBefore, lineAfter) {
1076
+ const at = classListAt(data, textBefore, lineBefore);
1077
+ if (!at) return null;
1078
+ const before = /[^\s+]*$/.exec(at.classList)[0];
1079
+ const after = /^[^\s+"'`{}]*/.exec(lineAfter)[0];
1080
+ const className = before + after;
1081
+ if (!className) return null;
1082
+ if (!at.attribute && !isKnownClass(data, className)) return null;
1083
+ return { className: className, start: -before.length, end: after.length };
1084
+ }
1085
+
1086
+ return {
1087
+ SEARCH_RANGE: SEARCH_RANGE,
1088
+ TRIGGER_CHARACTERS: TRIGGER_CHARACTERS,
1089
+ CLASS_ATTRIBUTES: CLASS_ATTRIBUTES,
1090
+ MAX_DESCRIBE: MAX_DESCRIBE,
1091
+ jsWithoutComments: jsWithoutComments,
1092
+ segment: segment,
1093
+ naturalExpand: naturalExpand,
1094
+ findClassList: findClassList,
1095
+ findClassListsInText: findClassListsInText,
1096
+ getClassNamesInClassList: getClassNamesInClassList,
1097
+ uniqueClassNames: uniqueClassNames,
1098
+ findConflicts: findConflicts,
1099
+ joinWithAnd: joinWithAnd,
1100
+ removeRangesFromString: removeRangesFromString,
1101
+ diagnose: diagnose,
1102
+ parseColor: parseColor,
1103
+ classesNeedingColor: classesNeedingColor,
1104
+ documentColors: documentColors,
1105
+ colorPresentations: colorPresentations,
1106
+ prepare: prepare,
1107
+ modifiersOf: modifiersOf,
1108
+ mayBeVariant: mayBeVariant,
1109
+ isKnownClass: isKnownClass,
1110
+ getVariantsFromClassName: getVariantsFromClassName,
1111
+ completionsFromClassList: completionsFromClassList,
1112
+ applyModifierColors: applyModifierColors,
1113
+ classListAt: classListAt,
1114
+ classNameAt: classNameAt,
1115
+ };
1116
+ });