├── Variables Import & Export Icon.png
├── Variables Import & Export Banner.png
├── manifest.json
├── export.html
├── import.html
└── code.js
/Variables Import & Export Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jake-figma/variables-import-export/HEAD/Variables Import & Export Icon.png
--------------------------------------------------------------------------------
/Variables Import & Export Banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jake-figma/variables-import-export/HEAD/Variables Import & Export Banner.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Variables Import Export",
3 | "id": "1225498390710809905",
4 | "api": "1.0.0",
5 | "editorType": ["figma"],
6 | "permissions": [],
7 | "main": "code.js",
8 | "menu": [
9 | { "command": "import", "name": "Import Variables" },
10 | { "command": "export", "name": "Export Variables" }
11 | ],
12 | "ui": { "import": "import.html", "export": "export.html" }
13 | }
14 |
--------------------------------------------------------------------------------
/export.html:
--------------------------------------------------------------------------------
1 |
71 |
72 |
73 |
77 |
78 |
93 |
--------------------------------------------------------------------------------
/import.html:
--------------------------------------------------------------------------------
1 |
87 |
134 |
135 |
264 |
--------------------------------------------------------------------------------
/code.js:
--------------------------------------------------------------------------------
1 | console.clear();
2 |
3 | function createCollection(selectedCollection, selectedMode) {
4 | // collection exists
5 | if (selectedCollection.id) {
6 | const collection = figma.variables.getVariableCollectionById(
7 | selectedCollection.id
8 | );
9 | let modeId = selectedMode.id;
10 | // mode exists
11 | if (modeId) {
12 | return { collection, modeId };
13 | }
14 |
15 | // otherwise create new mode
16 | const newMode = collection.addMode(selectedMode.name);
17 | return { collection, modeId: newMode };
18 | }
19 |
20 | // collection doesn't exist, so mode doesn't exist
21 | const collection = figma.variables.createVariableCollection(
22 | selectedCollection.name
23 | );
24 | const modeId = collection.modes[0].modeId;
25 | collection.renameMode(modeId, selectedMode.name);
26 | return { collection, modeId };
27 | }
28 |
29 | function createToken(variableMap, collection, modeId, type, name, value) {
30 | const existingCollection = variableMap[collection.id];
31 | let token = existingCollection ? existingCollection[name] : null;
32 |
33 | if (!token) {
34 | token = figma.variables.createVariable(name, collection.id, type);
35 | }
36 | token.setValueForMode(modeId, value);
37 | return token;
38 | }
39 |
40 | function createVariable(
41 | variableMap,
42 | collection,
43 | modeId,
44 | key,
45 | valueKey,
46 | tokens
47 | ) {
48 | const token = tokens[valueKey];
49 | return createToken(variableMap, collection, modeId, token.resolvedType, key, {
50 | type: "VARIABLE_ALIAS",
51 | id: `${token.id}`,
52 | });
53 | }
54 |
55 | function getExistingCollectionsAndModes() {
56 | const collections = figma.variables
57 | .getLocalVariableCollections()
58 | .reduce((into, collection) => {
59 | into[collection.name] = {
60 | name: collection.name,
61 | id: collection.id,
62 | defaultModeId: collection.defaultModeId,
63 | modes: collection.modes,
64 | };
65 | return into;
66 | }, {});
67 |
68 | figma.ui.postMessage({
69 | type: "LOAD_COLLECTIONS",
70 | collections,
71 | });
72 | }
73 |
74 | function importJSONFile({ selectedCollection, selectedMode, body }) {
75 | const json = JSON.parse(body);
76 | console.log("IMPORT");
77 | const { collection, modeId } = createCollection(
78 | selectedCollection,
79 | selectedMode
80 | );
81 | const variableMap = loadExistingVariableMap();
82 |
83 | const aliases = {};
84 | const tokens = {};
85 | Object.entries(json).forEach(([key, object]) => {
86 | traverseToken({
87 | variableMap,
88 | collection,
89 | modeId,
90 | type: json.$type,
91 | key,
92 | object,
93 | tokens,
94 | aliases,
95 | });
96 | });
97 | processAliases({ variableMap, collection, modeId, aliases, tokens });
98 | }
99 |
100 | function loadExistingVariableMap() {
101 | const variables = figma.variables.getLocalVariables();
102 | const map = {};
103 | variables.forEach((variable) => {
104 | map[variable.variableCollectionId] =
105 | map[variable.variableCollectionId] || {};
106 | map[variable.variableCollectionId][variable.name] = variable;
107 | });
108 | return map;
109 | }
110 |
111 | function processAliases({ variableMap, collection, modeId, aliases, tokens }) {
112 | aliases = Object.values(aliases);
113 | let generations = aliases.length;
114 | while (aliases.length && generations > 0) {
115 | for (let i = 0; i < aliases.length; i++) {
116 | const { key, type, valueKey } = aliases[i];
117 | const token = tokens[valueKey];
118 | if (token) {
119 | aliases.splice(i, 1);
120 | tokens[key] = createVariable(
121 | variableMap,
122 | collection,
123 | modeId,
124 | key,
125 | valueKey,
126 | tokens
127 | );
128 | }
129 | }
130 | generations--;
131 | }
132 | }
133 |
134 | function isAlias(value) {
135 | return value.toString().trim().charAt(0) === "{";
136 | }
137 |
138 | function traverseToken({
139 | variableMap,
140 | collection,
141 | modeId,
142 | type,
143 | key,
144 | object,
145 | tokens,
146 | aliases,
147 | }) {
148 | type = type || object.$type;
149 | // if key is a meta field, move on
150 | if (key.charAt(0) === "$") {
151 | return;
152 | }
153 | if (object.$value !== undefined) {
154 | if (isAlias(object.$value)) {
155 | const valueKey = object.$value
156 | .trim()
157 | .replace(/\./g, "/")
158 | .replace(/[\{\}]/g, "");
159 | if (tokens[valueKey]) {
160 | tokens[key] = createVariable(
161 | variableMap,
162 | collection,
163 | modeId,
164 | key,
165 | valueKey,
166 | tokens
167 | );
168 | } else {
169 | aliases[key] = {
170 | key,
171 | type,
172 | valueKey,
173 | };
174 | }
175 | } else if (type === "color") {
176 | tokens[key] = createToken(
177 | variableMap,
178 | collection,
179 | modeId,
180 | "COLOR",
181 | key,
182 | parseColor(object.$value)
183 | );
184 | } else if (type === "number") {
185 | tokens[key] = createToken(
186 | variableMap,
187 | collection,
188 | modeId,
189 | "FLOAT",
190 | key,
191 | object.$value
192 | );
193 | } else {
194 | console.log("unsupported type", type, object);
195 | }
196 | } else {
197 | Object.entries(object).forEach(([key2, object2]) => {
198 | if (key2.charAt(0) !== "$") {
199 | traverseToken({
200 | variableMap,
201 | collection,
202 | modeId,
203 | type,
204 | key: `${key}/${key2}`,
205 | object: object2,
206 | tokens,
207 | aliases,
208 | });
209 | }
210 | });
211 | }
212 | }
213 |
214 | function exportToJSON() {
215 | const collections = figma.variables.getLocalVariableCollections();
216 | const files = [];
217 | collections.forEach((collection) =>
218 | files.push(...processCollection(collection))
219 | );
220 | figma.ui.postMessage({ type: "EXPORT_RESULT", files });
221 | }
222 |
223 | function processCollection({ name, modes, variableIds }) {
224 | const files = [];
225 | modes.forEach((mode) => {
226 | const file = { fileName: `${name}.${mode.name}.tokens.json`, body: {} };
227 | variableIds.forEach((variableId) => {
228 | const { name, resolvedType, valuesByMode } =
229 | figma.variables.getVariableById(variableId);
230 | const value = valuesByMode[mode.modeId];
231 | if (value !== undefined && ["COLOR", "FLOAT"].includes(resolvedType)) {
232 | let obj = file.body;
233 | name.split("/").forEach((groupName) => {
234 | obj[groupName] = obj[groupName] || {};
235 | obj = obj[groupName];
236 | });
237 | obj.$type = resolvedType === "COLOR" ? "color" : "number";
238 | if (value.type === "VARIABLE_ALIAS") {
239 | obj.$value = `{${figma.variables
240 | .getVariableById(value.id)
241 | .name.replace(/\//g, ".")}}`;
242 | } else {
243 | obj.$value = resolvedType === "COLOR" ? rgbToHex(value) : value;
244 | }
245 | }
246 | });
247 | files.push(file);
248 | });
249 | return files;
250 | }
251 |
252 | figma.ui.onmessage = (e) => {
253 | console.log("code received message", e);
254 | if (e.type === "IMPORT") {
255 | const { selectedCollection, selectedMode, body } = e;
256 | importJSONFile({ selectedCollection, selectedMode, body });
257 | getExistingCollectionsAndModes();
258 | } else if (e.type === "EXPORT") {
259 | exportToJSON();
260 | }
261 | };
262 | if (figma.command === "import") {
263 | figma.showUI(__uiFiles__["import"], {
264 | width: 500,
265 | height: 500,
266 | themeColors: true,
267 | });
268 | getExistingCollectionsAndModes();
269 | } else if (figma.command === "export") {
270 | figma.showUI(__uiFiles__["export"], {
271 | width: 500,
272 | height: 500,
273 | themeColors: true,
274 | });
275 | }
276 |
277 | function rgbToHex({ r, g, b, a }) {
278 | if (a !== 1) {
279 | return `rgba(${[r, g, b]
280 | .map((n) => Math.round(n * 255))
281 | .join(", ")}, ${a.toFixed(4)})`;
282 | }
283 | const toHex = (value) => {
284 | const hex = Math.round(value * 255).toString(16);
285 | return hex.length === 1 ? "0" + hex : hex;
286 | };
287 |
288 | const hex = [toHex(r), toHex(g), toHex(b)].join("");
289 | return `#${hex}`;
290 | }
291 |
292 | function parseColor(color) {
293 | color = color.trim();
294 | const rgbRegex = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
295 | const rgbaRegex =
296 | /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d.]+)\s*\)$/;
297 | const hslRegex = /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/;
298 | const hslaRegex =
299 | /^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*([\d.]+)\s*\)$/;
300 | const hexRegex = /^#([A-Fa-f0-9]{3}){1,2}$/;
301 | const floatRgbRegex =
302 | /^\{\s*r:\s*[\d\.]+,\s*g:\s*[\d\.]+,\s*b:\s*[\d\.]+(,\s*opacity:\s*[\d\.]+)?\s*\}$/;
303 |
304 | if (rgbRegex.test(color)) {
305 | const [, r, g, b] = color.match(rgbRegex);
306 | return { r: parseInt(r) / 255, g: parseInt(g) / 255, b: parseInt(b) / 255 };
307 | } else if (rgbaRegex.test(color)) {
308 | const [, r, g, b, a] = color.match(rgbaRegex);
309 | return {
310 | r: parseInt(r) / 255,
311 | g: parseInt(g) / 255,
312 | b: parseInt(b) / 255,
313 | a: parseFloat(a),
314 | };
315 | } else if (hslRegex.test(color)) {
316 | const [, h, s, l] = color.match(hslRegex);
317 | return hslToRgbFloat(parseInt(h), parseInt(s) / 100, parseInt(l) / 100);
318 | } else if (hslaRegex.test(color)) {
319 | const [, h, s, l, a] = color.match(hslaRegex);
320 | return Object.assign(
321 | hslToRgbFloat(parseInt(h), parseInt(s) / 100, parseInt(l) / 100),
322 | { a: parseFloat(a) }
323 | );
324 | } else if (hexRegex.test(color)) {
325 | const hexValue = color.substring(1);
326 | const expandedHex =
327 | hexValue.length === 3
328 | ? hexValue
329 | .split("")
330 | .map((char) => char + char)
331 | .join("")
332 | : hexValue;
333 | return {
334 | r: parseInt(expandedHex.slice(0, 2), 16) / 255,
335 | g: parseInt(expandedHex.slice(2, 4), 16) / 255,
336 | b: parseInt(expandedHex.slice(4, 6), 16) / 255,
337 | };
338 | } else if (floatRgbRegex.test(color)) {
339 | return JSON.parse(color);
340 | } else {
341 | throw new Error("Invalid color format");
342 | }
343 | }
344 |
345 | function hslToRgbFloat(h, s, l) {
346 | const hue2rgb = (p, q, t) => {
347 | if (t < 0) t += 1;
348 | if (t > 1) t -= 1;
349 | if (t < 1 / 6) return p + (q - p) * 6 * t;
350 | if (t < 1 / 2) return q;
351 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
352 | return p;
353 | };
354 |
355 | if (s === 0) {
356 | return { r: l, g: l, b: l };
357 | }
358 |
359 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
360 | const p = 2 * l - q;
361 | const r = hue2rgb(p, q, (h + 1 / 3) % 1);
362 | const g = hue2rgb(p, q, h % 1);
363 | const b = hue2rgb(p, q, (h - 1 / 3) % 1);
364 |
365 | return { r, g, b };
366 | }
367 |
--------------------------------------------------------------------------------