@aaqu/fromcubes-react 0.2.0 → 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.
- package/README.md +11 -1
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/nodes/editor/class-completion.js +1116 -0
- package/nodes/editor/file-tree.js +1 -1
- package/nodes/editor/format.js +1 -1
- package/nodes/editor/provider-settings.js +8 -8
- package/nodes/editor/workspace.js +5 -3
- package/nodes/lib/admin-api.js +64 -23
- package/nodes/lib/providers.js +272 -30
- package/nodes/portal-react.html +517 -261
- package/nodes/portal-react.js +6 -1
- package/package.json +1 -2
|
@@ -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
|
+
});
|