@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.
- package/README.md +11 -1
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/nodes/editor/class-completion.js +1116 -0
- package/nodes/lib/admin-api.js +31 -1
- package/nodes/lib/providers.js +272 -30
- package/nodes/portal-react.html +494 -254
- package/package.json +1 -2
package/nodes/lib/admin-api.js
CHANGED
|
@@ -204,7 +204,7 @@ function registerProviderRoutes(RED, deps) {
|
|
|
204
204
|
res.json(providers.describe());
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
// Completion
|
|
207
|
+
// Completion data for className="…", from whichever CSS engine is in force.
|
|
208
208
|
// `engine` lets the editor tell a stale list from a fresh one.
|
|
209
209
|
RED.httpAdmin.get("/fromcubes-react/css-classes", permRead, async (_req, res) => {
|
|
210
210
|
try {
|
|
@@ -215,6 +215,36 @@ function registerProviderRoutes(RED, deps) {
|
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
// What the editor shows for classes it has in hand: a completion's CSS,
|
|
219
|
+
// a hover, a modifier's swatch, whether a variant exists. `c` carries the
|
|
220
|
+
// candidates newline-separated — a class never holds whitespace, and one
|
|
221
|
+
// string sidesteps query-string array parsing.
|
|
222
|
+
// Canonical spellings (`w-[16px]` → `w-4`) for the diagnostic — apart
|
|
223
|
+
// from css-describe because an engine may be slow here, and a hover must
|
|
224
|
+
// not wait for it. Same `c` format.
|
|
225
|
+
for (const [route, method, what] of [
|
|
226
|
+
["css-describe", "describeClasses", "describe"],
|
|
227
|
+
["css-canonical", "canonicalClasses", "canonicalise"],
|
|
228
|
+
]) {
|
|
229
|
+
RED.httpAdmin.get("/fromcubes-react/" + route, permRead, async (req, res) => {
|
|
230
|
+
const c = req.query.c;
|
|
231
|
+
if (typeof c !== "string") {
|
|
232
|
+
res.status(400).json({ error: "expected class candidates in c" });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
res.json(await providers[method](c.split("\n")));
|
|
237
|
+
} catch (e) {
|
|
238
|
+
if (e instanceof TypeError) {
|
|
239
|
+
res.status(400).json({ error: e.message });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
RED.log.error(`[portal-react] ${route}: ${e.message}`);
|
|
243
|
+
res.status(500).json({ error: `could not ${what} classes` });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
218
248
|
RED.httpAdmin.put(
|
|
219
249
|
"/fromcubes-react/providers",
|
|
220
250
|
permWrite,
|
package/nodes/lib/providers.js
CHANGED
|
@@ -15,9 +15,19 @@
|
|
|
15
15
|
* type: "fromcubes-css",
|
|
16
16
|
* apiVersion: 1,
|
|
17
17
|
* generate(source) { return { css } },
|
|
18
|
-
* editor: {
|
|
18
|
+
* editor: { // optional, all three
|
|
19
|
+
* classes() { return ["p-4", …] }, // names only
|
|
20
|
+
* completions() { return { classes: [{ name, modifiers, color }], variants } },
|
|
21
|
+
* describe(candidates) { return [{ hover, documentation, detail, color, rules } | null, …] },
|
|
22
|
+
* canonical(candidates) { return ["w-4" | null, …] },
|
|
23
|
+
* },
|
|
19
24
|
* })
|
|
20
25
|
*
|
|
26
|
+
* `completions`, `describe` and `canonical` are what Tailwind CSS
|
|
27
|
+
* IntelliSense works from (modifiers, swatches, variants, CSS previews, colour squares in the
|
|
28
|
+
* code, conflict and canonical-class diagnostics); an engine with only
|
|
29
|
+
* `classes` still gets plain completion.
|
|
30
|
+
*
|
|
21
31
|
* The core builds in only `none` for each kind. Everything that does real
|
|
22
32
|
* work — `headers` (@aaqu/fromcubes-auth-headers) and
|
|
23
33
|
* `tailwind` (@aaqu/fromcubes-tailwind) — is a Node-RED plugin like any third-party one,
|
|
@@ -69,6 +79,21 @@ const IDENTITY_TIMEOUT_MS = 5000;
|
|
|
69
79
|
// every editor that opens a dialog.
|
|
70
80
|
const MAX_EDITOR_CLASSES = 50000;
|
|
71
81
|
const MAX_EDITOR_CLASS_LEN = 200;
|
|
82
|
+
const MAX_EDITOR_MODIFIERS = 1000;
|
|
83
|
+
const MAX_EDITOR_VARIANTS = 1000;
|
|
84
|
+
const MAX_EDITOR_DETAIL_LEN = 2000;
|
|
85
|
+
// One describe request: what a completion's detail pane, a hover or a
|
|
86
|
+
// modifier list needs at once, for candidates that may carry an arbitrary
|
|
87
|
+
// value — `w-[calc(100%-2rem)]` is longer than any listed class.
|
|
88
|
+
const MAX_DESCRIBE = 64;
|
|
89
|
+
const MAX_DESCRIBE_LEN = 1000;
|
|
90
|
+
const MAX_DESCRIBE_TEXT = 100000;
|
|
91
|
+
// Conflict-check entries per class: Tailwind's largest (shadows, with their
|
|
92
|
+
// @property registrations) hold about twenty.
|
|
93
|
+
const MAX_DESCRIBE_RULES = 200;
|
|
94
|
+
// Swatch colours: the forms Monaco draws (hex, rgb()/rgba() with commas)
|
|
95
|
+
// and the two keywords IntelliSense marks as colours without one.
|
|
96
|
+
const EDITOR_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|rgba?\([\d\s.,%]{1,60}\)|transparent|currentColor)$/;
|
|
72
97
|
|
|
73
98
|
const USER_STRING_FIELDS = ["login", "displayName", "email", "role"];
|
|
74
99
|
|
|
@@ -138,6 +163,137 @@ function sanitizeUser(raw) {
|
|
|
138
163
|
return user;
|
|
139
164
|
}
|
|
140
165
|
|
|
166
|
+
/**
|
|
167
|
+
* @param {*} c
|
|
168
|
+
* @param {number} max
|
|
169
|
+
* @returns {boolean} a non-empty string without whitespace, at most `max` long
|
|
170
|
+
*/
|
|
171
|
+
function isToken(c, max) {
|
|
172
|
+
return typeof c === "string" && c.length > 0 && c.length <= max && !/\s/.test(c);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {*} v
|
|
177
|
+
* @param {number} max
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
function boundedOrNull(v, max) {
|
|
181
|
+
return typeof v === "string" && v.length <= max ? v : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {*} c
|
|
186
|
+
* @returns {string|null}
|
|
187
|
+
*/
|
|
188
|
+
function editorColor(c) {
|
|
189
|
+
return typeof c === "string" && EDITOR_COLOR_RE.test(c) ? c : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Validate a CSS engine's `editor.completions()` (or plain `classes()`)
|
|
194
|
+
* answer into what the editor is sent. Modifier lists are shared — Tailwind
|
|
195
|
+
* gives ~15 000 classes the same 21 opacity steps — so each distinct list
|
|
196
|
+
* travels once and classes point at it by index, as colours do by position.
|
|
197
|
+
*
|
|
198
|
+
* @param {*} rawClasses Array of names, or of `{name, modifiers?, color?}`.
|
|
199
|
+
* @param {*} [rawVariants]
|
|
200
|
+
* @returns {{classes: Array<string>, colors: Array<string>, modifiers: Array<Array<string>>, classModifiers: Array<number>, variants: Array<Object>}}
|
|
201
|
+
*/
|
|
202
|
+
function normalizeCompletions(rawClasses, rawVariants) {
|
|
203
|
+
const classes = [];
|
|
204
|
+
const colors = [];
|
|
205
|
+
const modifiers = [];
|
|
206
|
+
const classModifiers = [];
|
|
207
|
+
const modifierIndex = new Map();
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
for (const entry of Array.isArray(rawClasses) ? rawClasses : []) {
|
|
210
|
+
if (classes.length >= MAX_EDITOR_CLASSES) break;
|
|
211
|
+
const isObject = entry !== null && typeof entry === "object";
|
|
212
|
+
const name = isObject ? entry.name : entry;
|
|
213
|
+
if (!isToken(name, MAX_EDITOR_CLASS_LEN) || seen.has(name)) continue;
|
|
214
|
+
seen.add(name);
|
|
215
|
+
classes.push(name);
|
|
216
|
+
colors.push((isObject && editorColor(entry.color)) || "");
|
|
217
|
+
let mods = isObject && Array.isArray(entry.modifiers) ? entry.modifiers : [];
|
|
218
|
+
mods = mods.length <= MAX_EDITOR_MODIFIERS ? mods.filter((m) => isToken(m, MAX_EDITOR_CLASS_LEN)) : [];
|
|
219
|
+
if (mods.length === 0) {
|
|
220
|
+
classModifiers.push(-1);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const key = mods.join("\n");
|
|
224
|
+
let idx = modifierIndex.get(key);
|
|
225
|
+
if (idx === undefined) {
|
|
226
|
+
idx = modifiers.length;
|
|
227
|
+
modifiers.push(mods);
|
|
228
|
+
modifierIndex.set(key, idx);
|
|
229
|
+
}
|
|
230
|
+
classModifiers.push(idx);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const variants = [];
|
|
234
|
+
for (const v of Array.isArray(rawVariants) ? rawVariants : []) {
|
|
235
|
+
if (variants.length >= MAX_EDITOR_VARIANTS) break;
|
|
236
|
+
if (!v || typeof v !== "object" || !isToken(v.name, MAX_EDITOR_CLASS_LEN)) continue;
|
|
237
|
+
const values = Array.isArray(v.values) && v.values.length <= MAX_EDITOR_MODIFIERS ? v.values : [];
|
|
238
|
+
const valueDetails = Array.isArray(v.valueDetails) ? v.valueDetails : [];
|
|
239
|
+
const kept = [];
|
|
240
|
+
const details = [];
|
|
241
|
+
values.forEach((value, i) => {
|
|
242
|
+
if (!isToken(value, MAX_EDITOR_CLASS_LEN)) return;
|
|
243
|
+
kept.push(value);
|
|
244
|
+
details.push(boundedOrNull(valueDetails[i], MAX_EDITOR_DETAIL_LEN));
|
|
245
|
+
});
|
|
246
|
+
variants.push({
|
|
247
|
+
name: v.name,
|
|
248
|
+
values: kept,
|
|
249
|
+
hasDash: v.hasDash !== false,
|
|
250
|
+
isArbitrary: v.isArbitrary === true,
|
|
251
|
+
detail: boundedOrNull(v.detail, MAX_EDITOR_DETAIL_LEN),
|
|
252
|
+
valueDetails: details,
|
|
253
|
+
failed: v.failed === true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { classes, colors, modifiers, classModifiers, variants };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @param {*} list
|
|
262
|
+
* @param {number} maxItems
|
|
263
|
+
* @param {number} maxLen
|
|
264
|
+
* @returns {Array<string>|null} the list when it is short strings only
|
|
265
|
+
*/
|
|
266
|
+
function boundedStrings(list, maxItems, maxLen) {
|
|
267
|
+
if (!Array.isArray(list) || list.length > maxItems) return null;
|
|
268
|
+
return list.every((v) => typeof v === "string" && v.length <= maxLen) ? list.slice() : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate one entry of a CSS engine's `editor.describe()` answer. `rules`
|
|
273
|
+
* (what the CSS-conflict diagnostic compares) is optional for an engine.
|
|
274
|
+
*
|
|
275
|
+
* @param {*} d
|
|
276
|
+
* @returns {{hover: string|null, documentation: string|null, detail: string|null, color: string|null, rules: Array<{properties: Array<string>, context: Array<string>}>}|null}
|
|
277
|
+
*/
|
|
278
|
+
function sanitizeDescription(d) {
|
|
279
|
+
if (!d || typeof d !== "object") return null;
|
|
280
|
+
const rules = [];
|
|
281
|
+
if (Array.isArray(d.rules) && d.rules.length <= MAX_DESCRIBE_RULES) {
|
|
282
|
+
for (const r of d.rules) {
|
|
283
|
+
const properties = r && boundedStrings(r.properties, MAX_DESCRIBE_RULES, MAX_EDITOR_CLASS_LEN);
|
|
284
|
+
const context = r && boundedStrings(r.context, MAX_DESCRIBE_RULES, MAX_EDITOR_DETAIL_LEN);
|
|
285
|
+
if (properties && context) rules.push({ properties, context });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
hover: boundedOrNull(d.hover, MAX_DESCRIBE_TEXT),
|
|
290
|
+
documentation: boundedOrNull(d.documentation, MAX_DESCRIBE_TEXT),
|
|
291
|
+
detail: boundedOrNull(d.detail, MAX_DESCRIBE_TEXT),
|
|
292
|
+
color: editorColor(d.color),
|
|
293
|
+
rules,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
141
297
|
/**
|
|
142
298
|
* The only providers built into the core: one per kind that does nothing, so
|
|
143
299
|
* "no identity" and "no stylesheet" are always choosable. Their ids are
|
|
@@ -182,7 +338,9 @@ function builtinProviders() {
|
|
|
182
338
|
* cssEngineId: function(): string,
|
|
183
339
|
* describe: function(): Object,
|
|
184
340
|
* select: function(string, string): boolean,
|
|
185
|
-
* editorClasses: function(): Promise<
|
|
341
|
+
* editorClasses: function(): Promise<Object>,
|
|
342
|
+
* describeClasses: function(Array<string>): Promise<{engine: string, results: Array<Object|null>}>,
|
|
343
|
+
* canonicalClasses: function(Array<string>): Promise<{engine: string, results: Array<string|null>}>,
|
|
186
344
|
* }}
|
|
187
345
|
*/
|
|
188
346
|
function createProviders(RED, opts = {}) {
|
|
@@ -195,8 +353,8 @@ function createProviders(RED, opts = {}) {
|
|
|
195
353
|
const logged = new Set();
|
|
196
354
|
/** @type {{identity?: string, css?: string}} */
|
|
197
355
|
const chosen = loadChoice();
|
|
198
|
-
// provider object → its validated
|
|
199
|
-
// id, so a plugin reinstalled at runtime (a new definition) is asked again.
|
|
356
|
+
// provider object → its validated completion data. Keyed by the object, not
|
|
357
|
+
// the id, so a plugin reinstalled at runtime (a new definition) is asked again.
|
|
200
358
|
const classCache = new WeakMap();
|
|
201
359
|
|
|
202
360
|
/**
|
|
@@ -525,44 +683,126 @@ function createProviders(RED, opts = {}) {
|
|
|
525
683
|
}
|
|
526
684
|
|
|
527
685
|
/**
|
|
528
|
-
*
|
|
529
|
-
* force
|
|
530
|
-
*
|
|
531
|
-
*
|
|
686
|
+
* What the editor completes inside `className="…"` for the engine in
|
|
687
|
+
* force: class names in the engine's order, with per class a swatch
|
|
688
|
+
* colour (`colors[i]`, "" for none) and modifier list (`modifiers[
|
|
689
|
+
* classModifiers[i]]`, -1 for none), plus the variants. Taken from the
|
|
690
|
+
* plugin's `editor.completions()`, or `editor.classes()` for names alone.
|
|
691
|
+
* Both are optional; an engine without them, an unusable engine, or one
|
|
692
|
+
* that fails yields an empty list — completion just stays quiet, nothing
|
|
693
|
+
* else depends on it. `describe` / `canonical` say whether
|
|
694
|
+
* `describeClasses` / `canonicalClasses` have anything to ask the engine.
|
|
532
695
|
*
|
|
533
|
-
* @returns {Promise<{engine: string, classes: Array<string
|
|
696
|
+
* @returns {Promise<{engine: string, classes: Array<string>, colors: Array<string>, modifiers: Array<Array<string>>, classModifiers: Array<number>, variants: Array<Object>, describe: boolean, canonical: boolean}>}
|
|
534
697
|
*/
|
|
535
698
|
async function editorClasses() {
|
|
536
699
|
const engine = cssEngineId();
|
|
537
700
|
const { provider } = lookup(CSS_TYPE, builtins.css, engine);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
701
|
+
const editor = (provider && provider.editor) || {};
|
|
702
|
+
const describe = typeof editor.describe === "function";
|
|
703
|
+
const canonical = typeof editor.canonical === "function";
|
|
704
|
+
const empty = () => ({ engine, ...normalizeCompletions([]), describe, canonical });
|
|
705
|
+
const fn = typeof editor.completions === "function" ? "completions" : "classes";
|
|
706
|
+
if (typeof editor[fn] !== "function") return empty();
|
|
707
|
+
if (classCache.has(provider)) return { engine, ...classCache.get(provider), describe, canonical };
|
|
541
708
|
let raw;
|
|
542
709
|
try {
|
|
543
|
-
raw = await fn
|
|
710
|
+
raw = await editor[fn]();
|
|
544
711
|
} catch (e) {
|
|
545
|
-
RED.log.warn(`[portal-react] css engine "${engine}" editor
|
|
546
|
-
return
|
|
712
|
+
RED.log.warn(`[portal-react] css engine "${engine}" editor.${fn}() failed: ${e.message}`);
|
|
713
|
+
return empty();
|
|
547
714
|
}
|
|
548
|
-
const
|
|
549
|
-
if (Array.isArray(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
715
|
+
const rawClasses = fn === "completions" ? raw && raw.classes : raw;
|
|
716
|
+
if (!Array.isArray(rawClasses)) {
|
|
717
|
+
RED.log.warn(`[portal-react] css engine "${engine}" editor.${fn}() did not return ${fn === "completions" ? "a classes array" : "an array"}`);
|
|
718
|
+
}
|
|
719
|
+
const data = normalizeCompletions(rawClasses, fn === "completions" && raw ? raw.variants : []);
|
|
720
|
+
classCache.set(provider, data);
|
|
721
|
+
return { engine, ...data, describe, canonical };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* What the editor shows for classes it has in hand — a completion's
|
|
726
|
+
* detail pane, a hover, a modifier's swatch, a colour square, a
|
|
727
|
+
* diagnostic — from the engine's `editor.describe()`: per candidate
|
|
728
|
+
* `{hover, documentation, detail, color, rules}`, or `null`
|
|
729
|
+
* when the engine generates nothing for it (which is
|
|
730
|
+
* also how the editor asks whether `group-hover/x` is a variant). Never
|
|
731
|
+
* rejects for an engine problem: no describe, or a failing one, answers
|
|
732
|
+
* `null` throughout.
|
|
733
|
+
*
|
|
734
|
+
* @param {*} candidates At most 64 class candidates.
|
|
735
|
+
* @returns {Promise<{engine: string, results: Array<Object|null>}>}
|
|
736
|
+
* @throws {TypeError} for a malformed list — a caller's error, not the engine's.
|
|
737
|
+
*/
|
|
738
|
+
async function describeClasses(candidates) {
|
|
739
|
+
checkCandidates(candidates);
|
|
740
|
+
const engine = cssEngineId();
|
|
741
|
+
const { provider } = lookup(CSS_TYPE, builtins.css, engine);
|
|
742
|
+
const fn = provider && provider.editor && provider.editor.describe;
|
|
743
|
+
const nothing = { engine, results: candidates.map(() => null) };
|
|
744
|
+
if (typeof fn !== "function") return nothing;
|
|
745
|
+
let raw;
|
|
746
|
+
try {
|
|
747
|
+
raw = await fn.call(provider.editor, candidates.slice());
|
|
748
|
+
} catch (e) {
|
|
749
|
+
RED.log.warn(`[portal-react] css engine "${engine}" editor.describe() failed: ${e.message}`);
|
|
750
|
+
return nothing;
|
|
751
|
+
}
|
|
752
|
+
if (!Array.isArray(raw)) return nothing;
|
|
753
|
+
return { engine, results: candidates.map((_, i) => sanitizeDescription(raw[i])) };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* The canonical spelling the engine suggests per class (`w-[16px]` →
|
|
758
|
+
* `w-4`), or `null` — for the canonical-class diagnostic only. Kept apart
|
|
759
|
+
* from `describeClasses` because an engine may take its time here (the
|
|
760
|
+
* Tailwind add-on asks a worker thread whose first answer waits for an
|
|
761
|
+
* index) and a hover must never wait for that. Never rejects for an
|
|
762
|
+
* engine problem.
|
|
763
|
+
*
|
|
764
|
+
* @param {*} candidates At most 64 class candidates.
|
|
765
|
+
* @returns {Promise<{engine: string, results: Array<string|null>}>}
|
|
766
|
+
* @throws {TypeError} for a malformed list.
|
|
767
|
+
*/
|
|
768
|
+
async function canonicalClasses(candidates) {
|
|
769
|
+
checkCandidates(candidates);
|
|
770
|
+
const engine = cssEngineId();
|
|
771
|
+
const { provider } = lookup(CSS_TYPE, builtins.css, engine);
|
|
772
|
+
const fn = provider && provider.editor && provider.editor.canonical;
|
|
773
|
+
const nothing = { engine, results: candidates.map(() => null) };
|
|
774
|
+
if (typeof fn !== "function") return nothing;
|
|
775
|
+
let raw;
|
|
776
|
+
try {
|
|
777
|
+
raw = await fn.call(provider.editor, candidates.slice());
|
|
778
|
+
} catch (e) {
|
|
779
|
+
RED.log.warn(`[portal-react] css engine "${engine}" editor.canonical() failed: ${e.message}`);
|
|
780
|
+
return nothing;
|
|
781
|
+
}
|
|
782
|
+
if (!Array.isArray(raw)) return nothing;
|
|
783
|
+
return {
|
|
784
|
+
engine,
|
|
785
|
+
results: candidates.map((c, i) => (isToken(raw[i], MAX_DESCRIBE_LEN) && raw[i] !== c ? raw[i] : null)),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* @param {*} candidates
|
|
791
|
+
* @returns {void}
|
|
792
|
+
* @throws {TypeError} unless 1–64 class candidates
|
|
793
|
+
*/
|
|
794
|
+
function checkCandidates(candidates) {
|
|
795
|
+
if (
|
|
796
|
+
!Array.isArray(candidates) ||
|
|
797
|
+
candidates.length === 0 ||
|
|
798
|
+
candidates.length > MAX_DESCRIBE ||
|
|
799
|
+
!candidates.every((c) => isToken(c, MAX_DESCRIBE_LEN))
|
|
800
|
+
) {
|
|
801
|
+
throw new TypeError(`expected 1-${MAX_DESCRIBE} class candidates`);
|
|
560
802
|
}
|
|
561
|
-
classCache.set(provider, classes);
|
|
562
|
-
return { engine, classes };
|
|
563
803
|
}
|
|
564
804
|
|
|
565
|
-
return { resolveUser, generateCss, identityId, cssEngineId, describe, select, editorClasses };
|
|
805
|
+
return { resolveUser, generateCss, identityId, cssEngineId, describe, select, editorClasses, describeClasses, canonicalClasses };
|
|
566
806
|
}
|
|
567
807
|
|
|
568
808
|
module.exports = {
|
|
@@ -578,4 +818,6 @@ module.exports = {
|
|
|
578
818
|
MAX_USER_STRING,
|
|
579
819
|
MAX_GROUPS,
|
|
580
820
|
MAX_EDITOR_CLASSES,
|
|
821
|
+
MAX_DESCRIBE,
|
|
822
|
+
normalizeCompletions,
|
|
581
823
|
};
|