@daydreamui/tokens 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -2
- package/dist/bridge.js +973 -0
- package/dist/index.js +1658 -0
- package/icon.svg +5 -0
- package/manifest.json +27 -0
- package/package.json +15 -4
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
var ENTITIES = {
|
|
2
|
+
""": "\"",
|
|
3
|
+
"'": "'",
|
|
4
|
+
"<": "<",
|
|
5
|
+
">": ">",
|
|
6
|
+
"&": "&"
|
|
7
|
+
};
|
|
8
|
+
var unescapeXml = (text) => text.replace(/&(?:quot|apos|lt|gt|amp);|&#(\d+);|&#x([0-9a-fA-F]+);/g, (match, dec, hex) => dec !== void 0 ? String.fromCodePoint(Number(dec)) : hex !== void 0 ? String.fromCodePoint(parseInt(hex, 16)) : ENTITIES[match] ?? match);
|
|
9
|
+
var sizeOf = (value) => {
|
|
10
|
+
if (value === void 0) return null;
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
13
|
+
};
|
|
14
|
+
function frameOf(xml) {
|
|
15
|
+
const root = /^\s*<([A-Za-z][\w-]*)\b([^>]*?)\/?>/.exec(xml);
|
|
16
|
+
if (root === null) return listedFrame(xml);
|
|
17
|
+
const attributes = new Map();
|
|
18
|
+
for (const match of root[2].matchAll(/(?:^|\s)([\w:-]+)\s*=\s*"([^"]*)"/g)) attributes.set(match[1], unescapeXml(match[2]));
|
|
19
|
+
const id = attributes.get("id");
|
|
20
|
+
const name = (attributes.get("name") ?? "").replace(/\p{Cc}+/gu, " ").trim().slice(0, 80).trimEnd();
|
|
21
|
+
return {
|
|
22
|
+
id: id === void 0 || id === "" ? null : id,
|
|
23
|
+
name: name === "" ? null : name,
|
|
24
|
+
width: sizeOf(attributes.get("width")),
|
|
25
|
+
height: sizeOf(attributes.get("height"))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
var listedFrame = (text) => selectionOf(text)[0] ?? null;
|
|
29
|
+
function selectionOf(text) {
|
|
30
|
+
if (!/^\s*Currently selected nodes\b/.test(text)) return [];
|
|
31
|
+
const frames = [];
|
|
32
|
+
for (const line of text.matchAll(/^\s*-\s+([\w:;-]+):[ \t]*(.*)$/gm)) {
|
|
33
|
+
const name = line[2].replace(/\p{Cc}+/gu, " ").trim().slice(0, 80).trimEnd();
|
|
34
|
+
frames.push({
|
|
35
|
+
id: line[1],
|
|
36
|
+
name: name === "" ? null : name,
|
|
37
|
+
width: null,
|
|
38
|
+
height: null
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return frames;
|
|
42
|
+
}
|
|
43
|
+
var NODE_ID = /^\d+[:-]\d+$/;
|
|
44
|
+
function nodeIdFromUrl(urlOrId) {
|
|
45
|
+
const given = urlOrId.trim();
|
|
46
|
+
if (NODE_ID.test(given)) return given.replace("-", ":");
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(given);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const id = url.searchParams.get("node-id");
|
|
54
|
+
return id !== null && NODE_ID.test(id) ? id.replace("-", ":") : null;
|
|
55
|
+
}
|
|
56
|
+
var NOT_REACHABLE = "Figma is not reachable: open Figma desktop and enable the Dev Mode MCP server in its preferences";
|
|
57
|
+
var TimeoutError = class extends Error {
|
|
58
|
+
constructor(timeoutMs) {
|
|
59
|
+
super(`Figma did not answer within ${Math.round(timeoutMs / 1e3)} s`);
|
|
60
|
+
this.name = "TimeoutError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var UnreachableError = class extends Error {
|
|
64
|
+
constructor(message, cause) {
|
|
65
|
+
super(message, { cause });
|
|
66
|
+
this.name = "UnreachableError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var MAX_STREAM_BYTES = 33554432;
|
|
70
|
+
var OversizeError = class extends Error {
|
|
71
|
+
constructor() {
|
|
72
|
+
super(`Figma streamed more than ${MAX_STREAM_BYTES / 1048576} MB without answering; the answer was dropped`);
|
|
73
|
+
this.name = "OversizeError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
async function post(options, message, wantId, unreachable) {
|
|
77
|
+
const headers = {
|
|
78
|
+
"content-type": "application/json",
|
|
79
|
+
accept: "application/json, text/event-stream"
|
|
80
|
+
};
|
|
81
|
+
if (options.session !== null) headers["mcp-session-id"] = options.session;
|
|
82
|
+
if (options.protocolVersion !== null) headers["MCP-Protocol-Version"] = options.protocolVersion;
|
|
83
|
+
const signal = AbortSignal.timeout(options.timeoutMs);
|
|
84
|
+
let response;
|
|
85
|
+
try {
|
|
86
|
+
response = await options.fetch(options.url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers,
|
|
89
|
+
body: JSON.stringify(message),
|
|
90
|
+
signal
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw signal.aborted ? new TimeoutError(options.timeoutMs) : new UnreachableError(unreachable, error);
|
|
94
|
+
}
|
|
95
|
+
const session = response.headers.get("mcp-session-id");
|
|
96
|
+
const type = response.headers.get("content-type") ?? "";
|
|
97
|
+
let messages = [];
|
|
98
|
+
try {
|
|
99
|
+
if (type.startsWith("text/event-stream") && response.body !== null) messages = await readEvents(response.body, wantId);
|
|
100
|
+
else if (response.status !== 202 && response.status !== 204) messages = parseMessages(await response.text());
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (signal.aborted) throw new TimeoutError(options.timeoutMs);
|
|
103
|
+
if (error instanceof OversizeError) throw error;
|
|
104
|
+
throw new UnreachableError(unreachable, error);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
status: response.status,
|
|
108
|
+
session,
|
|
109
|
+
messages
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function parseMessages(text) {
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(text);
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
return (Array.isArray(parsed) ? parsed : [parsed]).filter(isResponse);
|
|
120
|
+
}
|
|
121
|
+
var isResponse = (value) => typeof value === "object" && value !== null && value.jsonrpc === "2.0" && "id" in value && !("method" in value) && ("result" in value || "error" in value);
|
|
122
|
+
function dataOf(event) {
|
|
123
|
+
const lines = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, ""));
|
|
124
|
+
return lines.length === 0 ? null : lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
async function readEvents(body, wantId) {
|
|
127
|
+
const reader = body.getReader();
|
|
128
|
+
const decoder = new TextDecoder();
|
|
129
|
+
const messages = [];
|
|
130
|
+
let buffered = "";
|
|
131
|
+
let searched = 0;
|
|
132
|
+
let received = 0;
|
|
133
|
+
let heldCr = false;
|
|
134
|
+
const take = (event) => {
|
|
135
|
+
const data = dataOf(event);
|
|
136
|
+
if (data === null) return false;
|
|
137
|
+
const read = parseMessages(data);
|
|
138
|
+
messages.push(...read);
|
|
139
|
+
return wantId !== null && read.some((m) => m.id === wantId);
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
for (;;) {
|
|
143
|
+
const { value, done } = await reader.read();
|
|
144
|
+
received += value?.byteLength ?? 0;
|
|
145
|
+
if (received > 33554432) {
|
|
146
|
+
await reader.cancel().catch(() => {});
|
|
147
|
+
throw new OversizeError();
|
|
148
|
+
}
|
|
149
|
+
let text = (heldCr ? "\r" : "") + decoder.decode(value, { stream: !done });
|
|
150
|
+
heldCr = !done && text.endsWith("\r");
|
|
151
|
+
if (heldCr) text = text.slice(0, -1);
|
|
152
|
+
buffered += text.replace(/\r\n?/g, "\n");
|
|
153
|
+
let end;
|
|
154
|
+
while ((end = buffered.indexOf("\n\n", searched)) !== -1) {
|
|
155
|
+
const event = buffered.slice(0, end);
|
|
156
|
+
buffered = buffered.slice(end + 2);
|
|
157
|
+
searched = 0;
|
|
158
|
+
if (take(event)) {
|
|
159
|
+
await reader.cancel().catch(() => {});
|
|
160
|
+
return messages;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
searched = Math.max(0, buffered.length - 1);
|
|
164
|
+
if (done) {
|
|
165
|
+
if (buffered.trim() !== "") take(buffered);
|
|
166
|
+
return messages;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
reader.releaseLock();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
174
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
175
|
+
var DEFAULT_BUDGET_MS = 45e3;
|
|
176
|
+
var isContentItem = (value) => typeof value === "object" && value !== null && typeof value.type === "string";
|
|
177
|
+
var textsOf = (content) => content.flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : []);
|
|
178
|
+
var imageOf = (content) => {
|
|
179
|
+
for (const item of content) {
|
|
180
|
+
if (item.type !== "image") continue;
|
|
181
|
+
const { data, mimeType } = item;
|
|
182
|
+
if (typeof data === "string") return {
|
|
183
|
+
mimeType: typeof mimeType === "string" ? mimeType : "image/png",
|
|
184
|
+
base64: data
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
var textResult = (content) => {
|
|
190
|
+
const [text = "", ...notes] = textsOf(content);
|
|
191
|
+
return {
|
|
192
|
+
text,
|
|
193
|
+
notes
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
var xmlResult = (content) => {
|
|
197
|
+
const texts = textsOf(content);
|
|
198
|
+
const at = texts.findIndex((text) => /^\s*</.test(text));
|
|
199
|
+
if (at === -1) return textResult(content);
|
|
200
|
+
return {
|
|
201
|
+
text: texts[at],
|
|
202
|
+
notes: texts.filter((_, i) => i !== at)
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
var acknowledged = (status) => status >= 200 && status < 300;
|
|
206
|
+
var failedRead = (error) => error instanceof TimeoutError ? { kind: "late" } : {
|
|
207
|
+
kind: "unreachable",
|
|
208
|
+
said: error instanceof Error ? error.message : String(error)
|
|
209
|
+
};
|
|
210
|
+
function connectFigma(options = {}) {
|
|
211
|
+
const settings = {
|
|
212
|
+
fetch: options.fetch ?? fetch,
|
|
213
|
+
url: options.url ?? "http://127.0.0.1:3845/mcp",
|
|
214
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
215
|
+
budgetMs: options.budgetMs ?? DEFAULT_BUDGET_MS
|
|
216
|
+
};
|
|
217
|
+
const client = {
|
|
218
|
+
clientLanguages: options.clientLanguages ?? "html,css",
|
|
219
|
+
clientFrameworks: options.clientFrameworks ?? "unknown"
|
|
220
|
+
};
|
|
221
|
+
let nextId = 0;
|
|
222
|
+
let session = null;
|
|
223
|
+
const request = (method, params) => ({
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
id: ++nextId,
|
|
226
|
+
method,
|
|
227
|
+
params
|
|
228
|
+
});
|
|
229
|
+
const statusError = (answer, method) => new Error(answer.messages.find((m) => m.error !== void 0)?.error?.message ?? `Figma's Dev Mode server answered ${method} with HTTP ${answer.status}`);
|
|
230
|
+
function resultOf(answer, id, method) {
|
|
231
|
+
const response = answer.messages.find((m) => m.id === id);
|
|
232
|
+
if (response?.error !== void 0) throw new Error(response.error.message);
|
|
233
|
+
if (response !== void 0 && answer.status === 200) return response.result;
|
|
234
|
+
throw statusError(answer, method);
|
|
235
|
+
}
|
|
236
|
+
const budget = (ms = settings.budgetMs) => ({
|
|
237
|
+
deadline: Date.now() + ms,
|
|
238
|
+
ms
|
|
239
|
+
});
|
|
240
|
+
function bound(budget) {
|
|
241
|
+
const left = budget.deadline - Date.now();
|
|
242
|
+
if (left <= 0) throw new TimeoutError(budget.ms);
|
|
243
|
+
return Math.min(settings.timeoutMs, left);
|
|
244
|
+
}
|
|
245
|
+
function within(pending, budget) {
|
|
246
|
+
const left = bound(budget);
|
|
247
|
+
let timer;
|
|
248
|
+
const late = new Promise((_resolve, reject) => {
|
|
249
|
+
timer = setTimeout(() => reject(new TimeoutError(budget.ms)), left);
|
|
250
|
+
});
|
|
251
|
+
return Promise.race([pending, late]).finally(() => clearTimeout(timer));
|
|
252
|
+
}
|
|
253
|
+
async function initialize(budget) {
|
|
254
|
+
const hello = request("initialize", {
|
|
255
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
256
|
+
capabilities: {},
|
|
257
|
+
clientInfo: {
|
|
258
|
+
name: "daydream-figma",
|
|
259
|
+
version: "0.1.0"
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const answer = await post({
|
|
263
|
+
...settings,
|
|
264
|
+
session: null,
|
|
265
|
+
protocolVersion: null,
|
|
266
|
+
timeoutMs: bound(budget)
|
|
267
|
+
}, hello, hello.id, NOT_REACHABLE);
|
|
268
|
+
const agreed = resultOf(answer, hello.id, "initialize")?.protocolVersion;
|
|
269
|
+
const opened = {
|
|
270
|
+
id: answer.session,
|
|
271
|
+
protocol: typeof agreed === "string" && agreed !== "" ? agreed : PROTOCOL_VERSION
|
|
272
|
+
};
|
|
273
|
+
const ready = await post({
|
|
274
|
+
...settings,
|
|
275
|
+
session: opened.id,
|
|
276
|
+
protocolVersion: opened.protocol,
|
|
277
|
+
timeoutMs: bound(budget)
|
|
278
|
+
}, {
|
|
279
|
+
jsonrpc: "2.0",
|
|
280
|
+
method: "notifications/initialized"
|
|
281
|
+
}, null, NOT_REACHABLE);
|
|
282
|
+
if (!acknowledged(ready.status)) throw statusError(ready, "notifications/initialized");
|
|
283
|
+
return opened;
|
|
284
|
+
}
|
|
285
|
+
function ensureSession(budget) {
|
|
286
|
+
if (session === null) {
|
|
287
|
+
const opening = initialize(budget);
|
|
288
|
+
session = opening;
|
|
289
|
+
opening.catch(() => {
|
|
290
|
+
if (session === opening) session = null;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return within(session, budget);
|
|
294
|
+
}
|
|
295
|
+
const currentSession = () => session;
|
|
296
|
+
async function call(method, params, budget, retried = false) {
|
|
297
|
+
const sessionPromise = ensureSession(budget);
|
|
298
|
+
const rode = currentSession();
|
|
299
|
+
const { id, protocol } = await sessionPromise;
|
|
300
|
+
const message = request(method, params);
|
|
301
|
+
const answer = await post({
|
|
302
|
+
...settings,
|
|
303
|
+
session: id,
|
|
304
|
+
protocolVersion: protocol,
|
|
305
|
+
timeoutMs: bound(budget)
|
|
306
|
+
}, message, message.id, NOT_REACHABLE);
|
|
307
|
+
if (answer.status === 404 && id !== null && !retried) {
|
|
308
|
+
if (session === rode) session = null;
|
|
309
|
+
return call(method, params, budget, true);
|
|
310
|
+
}
|
|
311
|
+
return resultOf(answer, message.id, method);
|
|
312
|
+
}
|
|
313
|
+
async function tool(name, args, under = budget()) {
|
|
314
|
+
const result = await call("tools/call", {
|
|
315
|
+
name,
|
|
316
|
+
arguments: args
|
|
317
|
+
}, under);
|
|
318
|
+
const content = Array.isArray(result?.content) ? result.content.filter(isContentItem) : [];
|
|
319
|
+
if (result?.isError === true) {
|
|
320
|
+
const said = textsOf(content).join(" ").trim();
|
|
321
|
+
throw new Error(said === "" ? `Figma could not answer ${name}` : said);
|
|
322
|
+
}
|
|
323
|
+
return content;
|
|
324
|
+
}
|
|
325
|
+
function nodeArgs(node) {
|
|
326
|
+
if (node === void 0 || node.trim() === "") return {};
|
|
327
|
+
const nodeId = nodeIdFromUrl(node);
|
|
328
|
+
if (nodeId === null) throw new Error(`not a Figma node id or link: ${node}`);
|
|
329
|
+
return { nodeId };
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
async reachable() {
|
|
333
|
+
try {
|
|
334
|
+
const under = budget();
|
|
335
|
+
if (session === null) await ensureSession(under);
|
|
336
|
+
else await call("ping", {}, under);
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
async variableDefs(node) {
|
|
343
|
+
return textResult(await tool("get_variable_defs", {
|
|
344
|
+
...nodeArgs(node),
|
|
345
|
+
...client
|
|
346
|
+
}));
|
|
347
|
+
},
|
|
348
|
+
async designContext(node, options = {}) {
|
|
349
|
+
const content = await tool("get_design_context", {
|
|
350
|
+
...nodeArgs(node),
|
|
351
|
+
...client,
|
|
352
|
+
...options.excludeScreenshot === void 0 ? {} : { excludeScreenshot: options.excludeScreenshot },
|
|
353
|
+
...options.forceCode === void 0 ? {} : { forceCode: options.forceCode }
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
...textResult(content),
|
|
357
|
+
screenshot: imageOf(content)
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
async metadata(node) {
|
|
361
|
+
return xmlResult(await tool("get_metadata", {
|
|
362
|
+
...nodeArgs(node),
|
|
363
|
+
...client
|
|
364
|
+
}));
|
|
365
|
+
},
|
|
366
|
+
async frame(node, options = {}) {
|
|
367
|
+
try {
|
|
368
|
+
const frame = frameOf(xmlResult(await tool("get_metadata", {
|
|
369
|
+
...nodeArgs(node),
|
|
370
|
+
...client
|
|
371
|
+
}, budget(options.timeoutMs ?? 3e3))).text);
|
|
372
|
+
return frame === null ? { kind: "none" } : {
|
|
373
|
+
kind: "frame",
|
|
374
|
+
frame
|
|
375
|
+
};
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return failedRead(error);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
async selection(options = {}) {
|
|
381
|
+
try {
|
|
382
|
+
const listing = textsOf(await tool("get_metadata", { ...client }, budget(options.timeoutMs ?? 5e3))).find((text) => /^\s*Currently selected nodes\b/.test(text));
|
|
383
|
+
const frames = listing === void 0 ? [] : selectionOf(listing);
|
|
384
|
+
return frames.length === 0 ? { kind: "none" } : {
|
|
385
|
+
kind: "frames",
|
|
386
|
+
frames
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return failedRead(error);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
async screenshot(node, options = {}) {
|
|
393
|
+
const content = await tool("get_screenshot", {
|
|
394
|
+
...nodeArgs(node),
|
|
395
|
+
...options.contentsOnly === void 0 ? {} : { contentsOnly: options.contentsOnly }
|
|
396
|
+
});
|
|
397
|
+
const image = imageOf(content);
|
|
398
|
+
if (image === null) {
|
|
399
|
+
const said = textsOf(content).join(" ").trim();
|
|
400
|
+
throw new Error(said === "" ? "Figma answered no image" : said);
|
|
401
|
+
}
|
|
402
|
+
return image;
|
|
403
|
+
},
|
|
404
|
+
async close() {
|
|
405
|
+
const opened = session;
|
|
406
|
+
session = null;
|
|
407
|
+
if (opened === null) return;
|
|
408
|
+
let id;
|
|
409
|
+
try {
|
|
410
|
+
id = (await opened).id;
|
|
411
|
+
} catch {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (id === null) return;
|
|
415
|
+
try {
|
|
416
|
+
await settings.fetch(settings.url, {
|
|
417
|
+
method: "DELETE",
|
|
418
|
+
headers: { "mcp-session-id": id },
|
|
419
|
+
signal: AbortSignal.timeout(settings.timeoutMs)
|
|
420
|
+
});
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
var NODE_SHAPE = "Give a Figma link or a node id, or nothing for Figma's selection.";
|
|
426
|
+
function nodeInput(input) {
|
|
427
|
+
if (input === void 0 || input === null) return void 0;
|
|
428
|
+
if (typeof input !== "object") throw new Error(NODE_SHAPE);
|
|
429
|
+
const node = input.node;
|
|
430
|
+
if (node === void 0 || node === null) return void 0;
|
|
431
|
+
if (typeof node !== "string" || node.length > 2048) throw new Error(NODE_SHAPE);
|
|
432
|
+
return node.trim() === "" ? void 0 : node;
|
|
433
|
+
}
|
|
434
|
+
function parseVariableDefs(text) {
|
|
435
|
+
const given = text.trim();
|
|
436
|
+
if (given === "") return [];
|
|
437
|
+
return fromJson(given) ?? fromPythonDict(given) ?? [];
|
|
438
|
+
}
|
|
439
|
+
function fromJson(text) {
|
|
440
|
+
let parsed;
|
|
441
|
+
try {
|
|
442
|
+
parsed = JSON.parse(text);
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
447
|
+
return Object.entries(parsed).map(([name, value]) => ({
|
|
448
|
+
name,
|
|
449
|
+
value: typeof value === "string" ? value : JSON.stringify(value)
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
function fromPythonDict(text) {
|
|
453
|
+
if (!text.startsWith("{") || !text.endsWith("}")) return null;
|
|
454
|
+
const entry = /(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")\s*:\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|((?:[^,}]|,(?!\s*['"]))+))/g;
|
|
455
|
+
const defs = [];
|
|
456
|
+
for (const match of text.slice(1, -1).matchAll(entry)) {
|
|
457
|
+
const name = unescape(match[1] ?? match[2] ?? "");
|
|
458
|
+
const value = match[3] !== void 0 || match[4] !== void 0 ? unescape(match[3] ?? match[4] ?? "") : (match[5] ?? "").trim();
|
|
459
|
+
defs.push({
|
|
460
|
+
name,
|
|
461
|
+
value
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return defs.length === 0 ? null : defs;
|
|
465
|
+
}
|
|
466
|
+
var unescape = (quoted) => quoted.replace(/\\(.)/g, "$1");
|
|
467
|
+
var COLOR_KINDS = ["color"];
|
|
468
|
+
var SIZE_KINDS = ["number", "dimension"];
|
|
469
|
+
var FONT_KINDS = ["font"];
|
|
470
|
+
var ROLES = [
|
|
471
|
+
[
|
|
472
|
+
"background",
|
|
473
|
+
[
|
|
474
|
+
"background",
|
|
475
|
+
"bg",
|
|
476
|
+
"canvas",
|
|
477
|
+
"page",
|
|
478
|
+
"surface-0"
|
|
479
|
+
],
|
|
480
|
+
COLOR_KINDS
|
|
481
|
+
],
|
|
482
|
+
[
|
|
483
|
+
"foreground",
|
|
484
|
+
[
|
|
485
|
+
"foreground",
|
|
486
|
+
"fg",
|
|
487
|
+
"text",
|
|
488
|
+
"text-primary",
|
|
489
|
+
"text-default",
|
|
490
|
+
"on-background",
|
|
491
|
+
"ink"
|
|
492
|
+
],
|
|
493
|
+
COLOR_KINDS
|
|
494
|
+
],
|
|
495
|
+
[
|
|
496
|
+
"card",
|
|
497
|
+
[
|
|
498
|
+
"card",
|
|
499
|
+
"surface",
|
|
500
|
+
"elevated",
|
|
501
|
+
"panel"
|
|
502
|
+
],
|
|
503
|
+
COLOR_KINDS
|
|
504
|
+
],
|
|
505
|
+
[
|
|
506
|
+
"muted",
|
|
507
|
+
[
|
|
508
|
+
"muted",
|
|
509
|
+
"subtle",
|
|
510
|
+
"secondary-surface"
|
|
511
|
+
],
|
|
512
|
+
COLOR_KINDS
|
|
513
|
+
],
|
|
514
|
+
[
|
|
515
|
+
"muted-foreground",
|
|
516
|
+
[
|
|
517
|
+
"muted-foreground",
|
|
518
|
+
"text-secondary",
|
|
519
|
+
"secondary",
|
|
520
|
+
"hint",
|
|
521
|
+
"placeholder"
|
|
522
|
+
],
|
|
523
|
+
COLOR_KINDS
|
|
524
|
+
],
|
|
525
|
+
[
|
|
526
|
+
"primary",
|
|
527
|
+
[
|
|
528
|
+
"primary",
|
|
529
|
+
"brand",
|
|
530
|
+
"accent",
|
|
531
|
+
"action",
|
|
532
|
+
"cta"
|
|
533
|
+
],
|
|
534
|
+
COLOR_KINDS
|
|
535
|
+
],
|
|
536
|
+
[
|
|
537
|
+
"primary-foreground",
|
|
538
|
+
[
|
|
539
|
+
"primary-foreground",
|
|
540
|
+
"on-primary",
|
|
541
|
+
"on-brand",
|
|
542
|
+
"on-accent"
|
|
543
|
+
],
|
|
544
|
+
COLOR_KINDS
|
|
545
|
+
],
|
|
546
|
+
[
|
|
547
|
+
"border",
|
|
548
|
+
[
|
|
549
|
+
"border",
|
|
550
|
+
"stroke",
|
|
551
|
+
"outline",
|
|
552
|
+
"divider"
|
|
553
|
+
],
|
|
554
|
+
COLOR_KINDS
|
|
555
|
+
],
|
|
556
|
+
[
|
|
557
|
+
"radius",
|
|
558
|
+
[
|
|
559
|
+
"radius",
|
|
560
|
+
"corner",
|
|
561
|
+
"rounded"
|
|
562
|
+
],
|
|
563
|
+
SIZE_KINDS
|
|
564
|
+
],
|
|
565
|
+
[
|
|
566
|
+
"spacing",
|
|
567
|
+
[
|
|
568
|
+
"spacing",
|
|
569
|
+
"space",
|
|
570
|
+
"gap",
|
|
571
|
+
"unit"
|
|
572
|
+
],
|
|
573
|
+
SIZE_KINDS
|
|
574
|
+
],
|
|
575
|
+
[
|
|
576
|
+
"font-sans",
|
|
577
|
+
[
|
|
578
|
+
"font-sans",
|
|
579
|
+
"font-family",
|
|
580
|
+
"sans",
|
|
581
|
+
"body-font",
|
|
582
|
+
"font-body"
|
|
583
|
+
],
|
|
584
|
+
FONT_KINDS
|
|
585
|
+
],
|
|
586
|
+
[
|
|
587
|
+
"font-heading",
|
|
588
|
+
[
|
|
589
|
+
"font-heading",
|
|
590
|
+
"heading-font",
|
|
591
|
+
"display"
|
|
592
|
+
],
|
|
593
|
+
FONT_KINDS
|
|
594
|
+
],
|
|
595
|
+
[
|
|
596
|
+
"heading-weight",
|
|
597
|
+
["heading-weight", "weight-bold"],
|
|
598
|
+
["number", "keyword"]
|
|
599
|
+
],
|
|
600
|
+
[
|
|
601
|
+
"heading-tracking",
|
|
602
|
+
[
|
|
603
|
+
"heading-tracking",
|
|
604
|
+
"tracking",
|
|
605
|
+
"letter-spacing"
|
|
606
|
+
],
|
|
607
|
+
SIZE_KINDS
|
|
608
|
+
]
|
|
609
|
+
];
|
|
610
|
+
var KINDS_OF_ROLE = new Map(ROLES.map(([role, , kinds]) => [role, kinds]));
|
|
611
|
+
var fits = (role, kind) => kind !== null && (KINDS_OF_ROLE.get(role)?.includes(kind) ?? false);
|
|
612
|
+
var SCALE_STEP = /^(\d+|xs|sm|md|lg|xl|\dxl|default|base|main|regular|normal)$/;
|
|
613
|
+
var PREFERRED_STEP = /^(500|default|base|main|regular|normal|md)$/;
|
|
614
|
+
function tokenName(figmaName) {
|
|
615
|
+
const slug = figmaName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
616
|
+
return `--${slug === "" ? "token" : slug}`;
|
|
617
|
+
}
|
|
618
|
+
var segmentsOf = (name) => tokenName(name).slice(2).split("-");
|
|
619
|
+
var COLOR = /^(#[0-9a-f]{3,8}|transparent|(rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*\))$/i;
|
|
620
|
+
var NUMBER = /^[-+]?(\d+\.?\d*|\.\d+)$/;
|
|
621
|
+
var DIMENSION = /^[-+]?(\d+\.?\d*|\.\d+)(px|rem|em|%|vw|vh|pt|ch|ex|ms|s|deg)$/;
|
|
622
|
+
var FONT_STACK = /^[A-Za-z0-9 ,'"._-]+$/;
|
|
623
|
+
var UNITLESS = /(^|-)(opacity|weight|line-height|lineheight|ratio|z)(-|$)/;
|
|
624
|
+
var FONT_NAME = /font|family|type/;
|
|
625
|
+
var WEIGHT_KEYWORD = /^(normal|bold|bolder|lighter)$/i;
|
|
626
|
+
function aliasOf(value) {
|
|
627
|
+
const braced = /^\{([^{}"']+)\}$/.exec(value);
|
|
628
|
+
return braced === null ? null : braced[1].trim();
|
|
629
|
+
}
|
|
630
|
+
function valueKind(name, raw) {
|
|
631
|
+
const value = raw.trim();
|
|
632
|
+
if (value === "" || value === "true" || value === "false") return null;
|
|
633
|
+
if (value.startsWith("{") || value.startsWith("[")) return null;
|
|
634
|
+
if (COLOR.test(value)) return {
|
|
635
|
+
kind: "color",
|
|
636
|
+
value
|
|
637
|
+
};
|
|
638
|
+
if (DIMENSION.test(value)) return {
|
|
639
|
+
kind: "dimension",
|
|
640
|
+
value
|
|
641
|
+
};
|
|
642
|
+
const slug = tokenName(name).slice(2);
|
|
643
|
+
if (NUMBER.test(value)) {
|
|
644
|
+
const number = value.replace(/^\+/, "");
|
|
645
|
+
return UNITLESS.test(slug) ? {
|
|
646
|
+
kind: "number",
|
|
647
|
+
value: number
|
|
648
|
+
} : {
|
|
649
|
+
kind: "dimension",
|
|
650
|
+
value: `${number}px`
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (WEIGHT_KEYWORD.test(value)) return {
|
|
654
|
+
kind: "keyword",
|
|
655
|
+
value
|
|
656
|
+
};
|
|
657
|
+
if (FONT_NAME.test(slug) && FONT_STACK.test(value)) return {
|
|
658
|
+
kind: "font",
|
|
659
|
+
value
|
|
660
|
+
};
|
|
661
|
+
return {
|
|
662
|
+
kind: "text",
|
|
663
|
+
value: quoted(value)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function readValue(name, raw, names) {
|
|
667
|
+
const value = raw.trim();
|
|
668
|
+
if (value === "" || value === "true" || value === "false") return { kind: "dropped" };
|
|
669
|
+
const alias = aliasOf(value);
|
|
670
|
+
if (alias !== null) return names.has(alias) ? {
|
|
671
|
+
kind: "alias",
|
|
672
|
+
to: alias
|
|
673
|
+
} : { kind: "dropped" };
|
|
674
|
+
const found = valueKind(name, value);
|
|
675
|
+
return found === null ? { kind: "dropped" } : {
|
|
676
|
+
kind: "value",
|
|
677
|
+
value: found.value,
|
|
678
|
+
of: found.kind
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function quoted(text) {
|
|
682
|
+
return `"${text.replace(/[\r\n]+/g, " ").replace(/[\\"]/g, "\\$&")}"`;
|
|
683
|
+
}
|
|
684
|
+
function roleOf(name) {
|
|
685
|
+
const segments = segmentsOf(name);
|
|
686
|
+
let best = roleIn(segments.slice(-2), false);
|
|
687
|
+
let preferred = false;
|
|
688
|
+
while (segments.length > 1 && SCALE_STEP.test(segments.at(-1))) preferred = PREFERRED_STEP.test(segments.pop());
|
|
689
|
+
const stripped = roleIn(segments.slice(-2), preferred);
|
|
690
|
+
if (stripped !== null && (best === null || stripped.score > best.score)) best = stripped;
|
|
691
|
+
return best;
|
|
692
|
+
}
|
|
693
|
+
function roleIn(window, preferred) {
|
|
694
|
+
let best = null;
|
|
695
|
+
for (const [role, keywords] of ROLES) for (const keyword of keywords) {
|
|
696
|
+
const words = keyword.split("-");
|
|
697
|
+
let score = 0;
|
|
698
|
+
if (words.length === 1) {
|
|
699
|
+
if (window.at(-1) === words[0]) score = 3;
|
|
700
|
+
else if (window.length === 2 && window[0] === words[0]) score = 2;
|
|
701
|
+
} else if (window.length === 2 && window[0] === words[0] && window[1] === words[1]) score = 5;
|
|
702
|
+
if (score > 0 && (best === null || score > best.score)) best = {
|
|
703
|
+
role,
|
|
704
|
+
score,
|
|
705
|
+
preferred
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return best;
|
|
709
|
+
}
|
|
710
|
+
function readVariables(variables) {
|
|
711
|
+
const names = new Set(variables.map((v) => v.name));
|
|
712
|
+
const read = new Map();
|
|
713
|
+
let dropped = 0;
|
|
714
|
+
for (const variable of variables) {
|
|
715
|
+
if (read.has(variable.name)) {
|
|
716
|
+
dropped += 1;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const reading = readValue(variable.name, variable.value, names);
|
|
720
|
+
if (reading.kind === "dropped") dropped += 1;
|
|
721
|
+
else read.set(variable.name, reading);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
read,
|
|
725
|
+
dropped
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function resolveChains(read) {
|
|
729
|
+
const chains = new Map();
|
|
730
|
+
let dropped = 0;
|
|
731
|
+
for (const name of read.keys()) {
|
|
732
|
+
const chain = chainFrom(name, read);
|
|
733
|
+
if (chain === null) dropped += 1;
|
|
734
|
+
else chains.set(name, chain);
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
chains,
|
|
738
|
+
dropped
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function chainFrom(name, read) {
|
|
742
|
+
const names = [];
|
|
743
|
+
const seen = new Set();
|
|
744
|
+
for (let at = name;;) {
|
|
745
|
+
if (seen.has(at)) return null;
|
|
746
|
+
const reading = read.get(at);
|
|
747
|
+
if (reading === void 0) return null;
|
|
748
|
+
seen.add(at);
|
|
749
|
+
names.push(at);
|
|
750
|
+
if (reading.kind === "value") return {
|
|
751
|
+
names,
|
|
752
|
+
kind: reading.of
|
|
753
|
+
};
|
|
754
|
+
at = reading.to;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
var ROLE_NAMES = new Set(ROLES.map(([role]) => `--${role}`));
|
|
758
|
+
function nameTokens(chains) {
|
|
759
|
+
const property = new Map();
|
|
760
|
+
const taken = new Set();
|
|
761
|
+
for (const [name, chain] of chains) {
|
|
762
|
+
const base = tokenName(name);
|
|
763
|
+
let n = ROLE_NAMES.has(base) && !fits(base.slice(2), chain.kind) ? 2 : 1;
|
|
764
|
+
let candidate = n === 1 ? base : `${base}-${n}`;
|
|
765
|
+
while (taken.has(candidate)) candidate = `${base}-${++n}`;
|
|
766
|
+
taken.add(candidate);
|
|
767
|
+
property.set(name, candidate);
|
|
768
|
+
}
|
|
769
|
+
return property;
|
|
770
|
+
}
|
|
771
|
+
function rawTokens(read, property) {
|
|
772
|
+
const tokens = new Map();
|
|
773
|
+
for (const [name, token] of property) {
|
|
774
|
+
const reading = read.get(name);
|
|
775
|
+
tokens.set(name, {
|
|
776
|
+
name: token,
|
|
777
|
+
value: reading.kind === "alias" ? `var(${property.get(reading.to)})` : reading.value
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return tokens;
|
|
781
|
+
}
|
|
782
|
+
function suggestRoles(chains, property, tokens) {
|
|
783
|
+
const judged = [];
|
|
784
|
+
for (const [name, chain] of chains) {
|
|
785
|
+
const token = tokens.get(name);
|
|
786
|
+
const best = ROLE_NAMES.has(token.name) ? {
|
|
787
|
+
role: token.name.slice(2),
|
|
788
|
+
score: 10,
|
|
789
|
+
preferred: true
|
|
790
|
+
} : roleOf(name);
|
|
791
|
+
if (best !== null && fits(best.role, chain.kind)) judged.push({
|
|
792
|
+
name,
|
|
793
|
+
token,
|
|
794
|
+
best
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const claimed = new Set();
|
|
798
|
+
const roles = [];
|
|
799
|
+
for (const [role] of ROLES) {
|
|
800
|
+
const candidates = judged.filter((j) => j.best.role === role && !claimed.has(j.name));
|
|
801
|
+
const pick = candidates.find((j) => j.token.name === `--${role}`) ?? candidates.find((j) => j.best.preferred) ?? candidates[0];
|
|
802
|
+
if (pick === void 0) continue;
|
|
803
|
+
claimed.add(pick.name);
|
|
804
|
+
roles.push({
|
|
805
|
+
token: pick.token.name === `--${role}` ? pick.token : {
|
|
806
|
+
name: `--${role}`,
|
|
807
|
+
value: `var(${pick.token.name})`
|
|
808
|
+
},
|
|
809
|
+
via: pick.name
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return roles;
|
|
813
|
+
}
|
|
814
|
+
function underCap(roles, chains, tokens) {
|
|
815
|
+
const chosenRoles = [];
|
|
816
|
+
const chosen = [];
|
|
817
|
+
const placed = new Set();
|
|
818
|
+
let cut = 0;
|
|
819
|
+
const linksOf = (via) => chains.get(via).names.map((name) => tokens.get(name));
|
|
820
|
+
const take = (links, role) => {
|
|
821
|
+
const chain = links.filter((link) => !placed.has(link.name));
|
|
822
|
+
if (placed.size + chain.length > 100) {
|
|
823
|
+
cut += 1;
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
for (const link of chain) {
|
|
827
|
+
placed.add(link.name);
|
|
828
|
+
(link === role ? chosenRoles : chosen).push(link);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
for (const { token, via } of roles) {
|
|
832
|
+
const links = linksOf(via);
|
|
833
|
+
take(links[0] === token ? links : [token, ...links], token);
|
|
834
|
+
}
|
|
835
|
+
for (const [name, token] of tokens) if (!placed.has(token.name)) take(linksOf(name), null);
|
|
836
|
+
return {
|
|
837
|
+
tokens: [...chosenRoles, ...chosen],
|
|
838
|
+
cut
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function convertFigmaVariables(variables) {
|
|
842
|
+
const { read, dropped: unread } = readVariables(variables);
|
|
843
|
+
const { chains, dropped: dangling } = resolveChains(read);
|
|
844
|
+
const property = nameTokens(chains);
|
|
845
|
+
const tokens = rawTokens(read, property);
|
|
846
|
+
const kept = underCap(suggestRoles(chains, property, tokens), chains, tokens);
|
|
847
|
+
return {
|
|
848
|
+
tokens: kept.tokens,
|
|
849
|
+
omitted: unread + dangling + kept.cut
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
var NO_VARIABLES = "The Figma selection uses no variables.";
|
|
853
|
+
var DEFAULT_ORIGIN = "From Figma";
|
|
854
|
+
var NO_FRAME = "Select a frame in Figma first.";
|
|
855
|
+
var TOO_SLOW = "Figma took too long to answer; try again.";
|
|
856
|
+
var MAX_SELECTION = 12;
|
|
857
|
+
var tooManySelected = (count) => `${count} frames are selected in Figma; From Figma takes at most 12 at once. Select fewer.`;
|
|
858
|
+
var EXTRACT_BUDGET_MS = 4e4;
|
|
859
|
+
var CLIENT = Symbol.for("daydream.figma-devmode.client");
|
|
860
|
+
function processClient(holder = globalThis, connect = () => connectFigma()) {
|
|
861
|
+
return holder[CLIENT] ??= connect();
|
|
862
|
+
}
|
|
863
|
+
function tokensOf(variables, origin, omitted = 0) {
|
|
864
|
+
if (variables.length === 0) throw new Error(NO_VARIABLES);
|
|
865
|
+
const converted = convertFigmaVariables(variables);
|
|
866
|
+
if (converted.tokens.length === 0) throw new Error(NO_VARIABLES);
|
|
867
|
+
return {
|
|
868
|
+
...converted,
|
|
869
|
+
omitted: converted.omitted + omitted,
|
|
870
|
+
origin
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
var originOf = (name) => name === null ? DEFAULT_ORIGIN : `${name} from Figma`;
|
|
874
|
+
function askable(frames) {
|
|
875
|
+
const kept = frames.filter((frame) => frame.id !== null && nodeIdFromUrl(frame.id) !== null);
|
|
876
|
+
return {
|
|
877
|
+
frames: kept,
|
|
878
|
+
leftOut: frames.length - kept.length
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function by(pending, deadline) {
|
|
882
|
+
let timer;
|
|
883
|
+
const late = new Promise((resolve) => {
|
|
884
|
+
timer = setTimeout(() => resolve({ kind: "late" }), Math.max(0, deadline - Date.now()));
|
|
885
|
+
});
|
|
886
|
+
return Promise.race([pending.then((value) => ({
|
|
887
|
+
kind: "answered",
|
|
888
|
+
value
|
|
889
|
+
})), late]).finally(() => clearTimeout(timer));
|
|
890
|
+
}
|
|
891
|
+
var OLDER_CONNECTION = "Daydream's host loaded an older Figma connection than this plugin; quit and reopen Daydream, then try again.";
|
|
892
|
+
var NEEDED = [
|
|
893
|
+
"reachable",
|
|
894
|
+
"frame",
|
|
895
|
+
"selection",
|
|
896
|
+
"variableDefs"
|
|
897
|
+
];
|
|
898
|
+
function olderConnection(client) {
|
|
899
|
+
return NEEDED.some((name) => typeof client?.[name] !== "function");
|
|
900
|
+
}
|
|
901
|
+
function createTokensPart(given = {}) {
|
|
902
|
+
const client = given.client ?? processClient(given.holder, given.connect);
|
|
903
|
+
const budgetMs = given.extractBudgetMs ?? 4e4;
|
|
904
|
+
if (olderConnection(client)) return {
|
|
905
|
+
reachable: () => typeof client.reachable === "function" ? client.reachable() : Promise.resolve(false),
|
|
906
|
+
async extract() {
|
|
907
|
+
throw new Error(OLDER_CONNECTION);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
async function extractSeveral(frames, leftOut) {
|
|
911
|
+
const deadline = Date.now() + budgetMs;
|
|
912
|
+
const reads = await Promise.allSettled(frames.map((frame) => by(client.variableDefs(frame.id), deadline)));
|
|
913
|
+
const merged = new Map();
|
|
914
|
+
const answered = [];
|
|
915
|
+
let refused = null;
|
|
916
|
+
let omitted = leftOut;
|
|
917
|
+
for (const [i, read] of reads.entries()) {
|
|
918
|
+
if (read.status === "rejected") {
|
|
919
|
+
refused ??= read.reason;
|
|
920
|
+
omitted += 1;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (read.value.kind === "late") {
|
|
924
|
+
omitted += 1;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
for (const variable of parseVariableDefs(read.value.value.text)) if (!merged.has(variable.name)) merged.set(variable.name, variable);
|
|
928
|
+
answered.push(frames[i]);
|
|
929
|
+
}
|
|
930
|
+
if (answered.length === 0) throw refused ?? new Error("Figma took too long to answer; try again.");
|
|
931
|
+
const origin = answered.length === 1 ? originOf(answered[0].name) : `${answered.length} frames from Figma`;
|
|
932
|
+
return tokensOf([...merged.values()], origin, omitted);
|
|
933
|
+
}
|
|
934
|
+
async function extractSelection() {
|
|
935
|
+
const read = await client.selection();
|
|
936
|
+
switch (read.kind) {
|
|
937
|
+
case "none": throw new Error(NO_FRAME);
|
|
938
|
+
case "unreachable": throw new Error(read.said);
|
|
939
|
+
case "late": throw new Error(TOO_SLOW);
|
|
940
|
+
case "frames": {
|
|
941
|
+
const { frames, leftOut } = askable(read.frames);
|
|
942
|
+
if (frames.length > 12) throw new Error(tooManySelected(frames.length));
|
|
943
|
+
if (frames.length === 0) throw new Error(`${NO_FRAME} ${leftOut === 1 ? "1 layer inside an instance was left out; select its frame." : `${leftOut} layers inside instances were left out; select their frames.`}`);
|
|
944
|
+
if (frames.length === 1) {
|
|
945
|
+
const read = await by(client.variableDefs(frames[0].id), Date.now() + budgetMs);
|
|
946
|
+
if (read.kind === "late") throw new Error(TOO_SLOW);
|
|
947
|
+
return tokensOf(parseVariableDefs(read.value.text), originOf(frames[0].name), leftOut);
|
|
948
|
+
}
|
|
949
|
+
return extractSeveral(frames, leftOut);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
async reachable() {
|
|
955
|
+
try {
|
|
956
|
+
return await client.reachable();
|
|
957
|
+
} catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
async extract(node) {
|
|
962
|
+
if (node === void 0) return extractSelection();
|
|
963
|
+
const [answer, frame] = await Promise.all([client.variableDefs(node), client.frame(node)]);
|
|
964
|
+
return tokensOf(parseVariableDefs(answer.text), originOf(frame.kind === "frame" ? frame.frame.name : null));
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function activate(host) {
|
|
969
|
+
const part = createTokensPart();
|
|
970
|
+
host.browserPart.handle("figma-reachable", () => part.reachable());
|
|
971
|
+
host.browserPart.handle("figma-extract", (input) => part.extract(nodeInput(input)));
|
|
972
|
+
}
|
|
973
|
+
export { DEFAULT_ORIGIN, EXTRACT_BUDGET_MS, MAX_SELECTION, NO_FRAME, NO_VARIABLES, OLDER_CONNECTION, TOO_SLOW, createTokensPart, activate as default, olderConnection, processClient, tooManySelected };
|