figma-json-plugin
Advanced tools
Comparing version 0.0.5-alpha.15 to 0.0.5-alpha.16
@@ -556,3 +556,2 @@ export declare type Base64String = string; | ||
guides: ReadonlyArray<Guide>; | ||
selection: ReadonlyArray<SceneNode>; | ||
selectedTextRange: { | ||
@@ -559,0 +558,0 @@ node: TextNode; |
@@ -1,25 +0,4 @@ | ||
/// <reference types="@figma/plugin-typings" /> | ||
import * as F from "./figma-json"; | ||
export { type DumpOptions as Options, type DumpOptions, dump, isVisible, } from "./read"; | ||
export { insert, fontsToLoad } from "./write"; | ||
export * from "./figma-json"; | ||
export { default as defaultLayers } from "./figma-default-layers"; | ||
export declare const writeBlacklist: Set<string>; | ||
export declare const fallbackFonts: F.FontName[]; | ||
export interface Options { | ||
skipInvisibleNodes: boolean; | ||
images: boolean; | ||
geometry: "none" | "paths"; | ||
styles: boolean; | ||
} | ||
export declare function dump(n: readonly SceneNode[], options?: Partial<Options>): Promise<F.DumpedFigma>; | ||
export declare function loadFonts(requestedFonts: F.FontName[], fallbackFonts: F.FontName[]): Promise<{ | ||
availableFonts: F.FontName[]; | ||
missingFonts: F.FontName[]; | ||
fontReplacements: Record<EncodedFont, EncodedFont>; | ||
}>; | ||
declare type EncodedFont = string; | ||
export declare function encodeFont({ family, style }: FontName): EncodedFont; | ||
export declare function decodeFont(f: EncodedFont): FontName; | ||
export declare function applyFontName(n: TextNode, fontName: F.TextNode["fontName"], fontReplacements: Record<EncodedFont, EncodedFont>): Promise<void>; | ||
export declare function getFontReplacement(missingFont: FontName, fallbackFonts: F.FontName[]): F.FontName; | ||
export declare function fontsToLoad(n: F.DumpedFigma): FontName[]; | ||
export declare function insert(n: F.DumpedFigma): Promise<SceneNode[]>; |
1197
dist/index.js
@@ -73,13 +73,7 @@ "use strict"; | ||
MixedValue: () => MixedValue, | ||
applyFontName: () => applyFontName, | ||
decodeFont: () => decodeFont, | ||
defaultLayers: () => figma_default_layers_default, | ||
dump: () => dump, | ||
encodeFont: () => encodeFont, | ||
fallbackFonts: () => fallbackFonts, | ||
fontsToLoad: () => fontsToLoad, | ||
getFontReplacement: () => getFontReplacement, | ||
insert: () => insert, | ||
loadFonts: () => loadFonts, | ||
writeBlacklist: () => writeBlacklist | ||
isVisible: () => isVisible | ||
}); | ||
@@ -104,2 +98,3 @@ module.exports = __toCommonJS(src_exports); | ||
"masterComponent", | ||
"selection", | ||
"playbackSettings", | ||
@@ -179,2 +174,177 @@ "listSpacing", | ||
// src/figmaState.ts | ||
var skipState; | ||
function saveFigmaState(skipInvisibleInstanceChildren) { | ||
if ("figma" in globalThis) { | ||
skipState = figma.skipInvisibleInstanceChildren; | ||
figma.skipInvisibleInstanceChildren = skipInvisibleInstanceChildren; | ||
} | ||
} | ||
function restoreFigmaState() { | ||
if ("figma" in globalThis && skipState !== void 0) { | ||
figma.skipInvisibleInstanceChildren = skipState; | ||
skipState = void 0; | ||
} | ||
} | ||
// src/read.ts | ||
function isVisible(n) { | ||
if (typeof n !== "object") { | ||
return true; | ||
} | ||
if (!("visible" in n) || typeof n.visible !== "boolean" || !("opacity" in n) || typeof n.opacity !== "number" || !("removed" in n) || typeof n.removed !== "boolean") { | ||
return true; | ||
} | ||
return n.visible && n.opacity > 1e-3 && !n.removed; | ||
} | ||
var defaultOptions = { | ||
skipInvisibleNodes: true, | ||
images: false, | ||
geometry: "none", | ||
styles: false, | ||
maxDepth: Infinity | ||
}; | ||
var DumpContext = class { | ||
constructor(options) { | ||
this.options = options; | ||
this.imageHashes = /* @__PURE__ */ new Set(); | ||
this.components = {}; | ||
this.componentSets = {}; | ||
this.styles = {}; | ||
this.depth = 0; | ||
} | ||
pushParent() { | ||
this.depth++; | ||
} | ||
popParent() { | ||
this.depth--; | ||
} | ||
}; | ||
function _dumpObject(n, keys, ctx) { | ||
return keys.reduce((o, k) => { | ||
var _a; | ||
const v = n[k]; | ||
if (k === "imageHash" && typeof v === "string") { | ||
ctx.imageHashes.add(v); | ||
} else if (k.endsWith("StyleId") && typeof v === "string" && v.length > 0 && ctx.options.styles) { | ||
const style = figma.getStyleById(v); | ||
if (style) { | ||
ctx.styles[style.id] = { | ||
key: style.key, | ||
name: style.name, | ||
styleType: style.type, | ||
remote: style.remote, | ||
description: style.description | ||
}; | ||
} else { | ||
console.warn(`Couldn't find style with id ${v}.`); | ||
} | ||
} else if (k === "mainComponent" && v) { | ||
const component = v; | ||
let componentSetId; | ||
if (((_a = component.parent) == null ? void 0 : _a.type) === "COMPONENT_SET") { | ||
const componentSet = component.parent; | ||
const { name: name2, description: description2, documentationLinks: documentationLinks2, key: key2, remote: remote2 } = componentSet; | ||
componentSetId = componentSet.id; | ||
ctx.componentSets[componentSet.id] = { | ||
key: key2, | ||
name: name2, | ||
description: description2, | ||
remote: remote2, | ||
documentationLinks: documentationLinks2 | ||
}; | ||
} | ||
const { name, key, description, documentationLinks, remote } = component; | ||
ctx.components[component.id] = { | ||
key, | ||
name, | ||
description, | ||
remote, | ||
componentSetId, | ||
documentationLinks | ||
}; | ||
o["componentId"] = v.id; | ||
return o; | ||
} else if (k === "children") { | ||
if (ctx.depth >= ctx.options.maxDepth) { | ||
if (ctx.options.childrenReplacement) { | ||
o[k] = [ctx.options.childrenReplacement]; | ||
} else { | ||
o[k] = []; | ||
} | ||
} else { | ||
ctx.pushParent(); | ||
o[k] = _dump(v, ctx); | ||
ctx.popParent(); | ||
} | ||
return o; | ||
} | ||
o[k] = _dump(v, ctx); | ||
return o; | ||
}, {}); | ||
} | ||
function _dump(n, ctx) { | ||
switch (typeof n) { | ||
case "object": { | ||
if (Array.isArray(n)) { | ||
return n.filter((v) => !ctx.options.skipInvisibleNodes || isVisible(v)).map((v) => _dump(v, ctx)); | ||
} else if (n === null) { | ||
return null; | ||
} else if (n.__proto__ !== void 0) { | ||
const blacklistKeys = conditionalReadBlacklist(n, ctx.options); | ||
const keys = [...Object.keys(n), ...Object.keys(n.__proto__)].filter( | ||
(k) => !blacklistKeys.has(k) | ||
); | ||
return _dumpObject(n, keys, ctx); | ||
} else { | ||
const keys = Object.keys(n); | ||
return _dumpObject(n, keys, ctx); | ||
} | ||
} | ||
case "function": | ||
return void 0; | ||
case "symbol": | ||
if (n === figma.mixed) { | ||
return "__Symbol(figma.mixed)__"; | ||
} else { | ||
return String(n); | ||
} | ||
default: | ||
return n; | ||
} | ||
} | ||
function requestImages(ctx) { | ||
return __async(this, null, function* () { | ||
const imageRequests = [...ctx.imageHashes].map((hash) => __async(this, null, function* () { | ||
const im = figma.getImageByHash(hash); | ||
if (im === null) { | ||
throw new Error(`Image not found: ${hash}`); | ||
} | ||
const dat = yield im.getBytesAsync(); | ||
return [hash, dat]; | ||
})); | ||
const r = yield Promise.all(imageRequests); | ||
return Object.fromEntries(r); | ||
}); | ||
} | ||
function dump(_0) { | ||
return __async(this, arguments, function* (n, options = {}) { | ||
const resolvedOptions = __spreadValues(__spreadValues({}, defaultOptions), options); | ||
const { skipInvisibleNodes } = resolvedOptions; | ||
saveFigmaState(skipInvisibleNodes); | ||
const ctx = new DumpContext(resolvedOptions); | ||
const objects = n.filter((v) => !skipInvisibleNodes || isVisible(v)).map((o) => _dump(o, ctx)); | ||
const images = resolvedOptions.images ? yield requestImages(ctx) : {}; | ||
restoreFigmaState(); | ||
const { components, componentSets, styles } = ctx; | ||
return { | ||
objects, | ||
components, | ||
componentSets, | ||
styles, | ||
images | ||
}; | ||
}); | ||
} | ||
// src/applyOverridesToChildren.ts | ||
@@ -249,17 +419,2 @@ function isSupported(property) { | ||
// src/figmaState.ts | ||
var skipState; | ||
function saveFigmaState(skipInvisibleInstanceChildren) { | ||
if ("figma" in globalThis) { | ||
skipState = figma.skipInvisibleInstanceChildren; | ||
figma.skipInvisibleInstanceChildren = skipInvisibleInstanceChildren; | ||
} | ||
} | ||
function restoreFigmaState() { | ||
if ("figma" in globalThis && skipState !== void 0) { | ||
figma.skipInvisibleInstanceChildren = skipState; | ||
skipState = void 0; | ||
} | ||
} | ||
// src/updateImageHashes.ts | ||
@@ -292,2 +447,429 @@ function updateImageHashes(n, updates) { | ||
// src/fallbackFonts.ts | ||
var fallbackFonts = [ | ||
{ family: "Inter", style: "Regular" }, | ||
{ family: "Inter", style: "Thin" }, | ||
{ family: "Inter", style: "Extra Light" }, | ||
{ family: "Inter", style: "Light" }, | ||
{ family: "Inter", style: "Medium" }, | ||
{ family: "Inter", style: "Semi Bold" }, | ||
{ family: "Inter", style: "Bold" }, | ||
{ family: "Inter", style: "Extra Bold" }, | ||
{ family: "Inter", style: "Black" } | ||
]; | ||
// src/write.ts | ||
var writeBlacklist = /* @__PURE__ */ new Set([ | ||
"id", | ||
"componentPropertyReferences", | ||
"variantProperties", | ||
"overlayPositionType", | ||
"overlayBackground", | ||
"overlayBackgroundInteraction", | ||
"fontWeight", | ||
"overrides", | ||
"componentProperties", | ||
"inferredAutoLayout", | ||
"componentId", | ||
"isAsset" | ||
]); | ||
function notUndefined(x) { | ||
return x !== void 0; | ||
} | ||
function loadFonts(requestedFonts, fallbackFonts2) { | ||
return __async(this, null, function* () { | ||
const availableFonts = []; | ||
const missingFonts = []; | ||
const fontReplacements = {}; | ||
const loadFontPromises = requestedFonts.map((fontName) => __async(this, null, function* () { | ||
try { | ||
yield figma.loadFontAsync(fontName); | ||
availableFonts.push(fontName); | ||
} catch (e) { | ||
console.warn(`Unable to load font: ${encodeFont(fontName)}`); | ||
missingFonts.push(fontName); | ||
const replacement = getFontReplacement(fontName, fallbackFonts2); | ||
console.log(`Trying font replacement: ${encodeFont(replacement)}`); | ||
try { | ||
yield figma.loadFontAsync(replacement); | ||
console.log(`Loaded font replacement: ${encodeFont(replacement)}`); | ||
fontReplacements[encodeFont(fontName)] = encodeFont(replacement); | ||
} catch (e2) { | ||
console.warn( | ||
`Unable to load font replacement: ${encodeFont(replacement)}` | ||
); | ||
fontReplacements[encodeFont(fontName)] = encodeFont(fallbackFonts2[0]); | ||
} | ||
} | ||
})); | ||
yield Promise.all(loadFontPromises); | ||
console.log("done loading fonts."); | ||
return { availableFonts, missingFonts, fontReplacements }; | ||
}); | ||
} | ||
function loadComponents(requestedComponents) { | ||
return __async(this, null, function* () { | ||
const availableComponents = {}; | ||
yield Promise.all( | ||
Object.entries(requestedComponents).map((_0) => __async(this, [_0], function* ([id, requested]) { | ||
try { | ||
const component = yield figma.importComponentByKeyAsync(requested.key); | ||
availableComponents[id] = component; | ||
} catch (e) { | ||
const node = figma.getNodeById(id); | ||
if (node && node.type === "COMPONENT") { | ||
availableComponents[id] = node; | ||
} else { | ||
console.log("error loading component:", e); | ||
} | ||
} | ||
})) | ||
); | ||
return { availableComponents }; | ||
}); | ||
} | ||
function loadStyles(requestedStyles) { | ||
return __async(this, null, function* () { | ||
yield Promise.all( | ||
Object.entries(requestedStyles).map((_0) => __async(this, [_0], function* ([id, requested]) { | ||
try { | ||
yield figma.importStyleByKeyAsync(requested.key); | ||
} catch (e) { | ||
console.log("error loading style:", e); | ||
} | ||
})) | ||
); | ||
}); | ||
} | ||
function encodeFont({ family, style }) { | ||
if (family.includes("|") || style.includes("|")) { | ||
throw new Error(`Cannot encode a font with "|" in the name.`); | ||
} | ||
return [family, style].join("|"); | ||
} | ||
function decodeFont(f) { | ||
const s = f.split("|"); | ||
if (s.length !== 2) { | ||
throw new Error(`Unable to decode font string: ${f}`); | ||
} | ||
const [family, style] = s; | ||
return { family, style }; | ||
} | ||
function applyFontName(n, fontName, fontReplacements) { | ||
return __async(this, null, function* () { | ||
if (fontName === "__Symbol(figma.mixed)__") { | ||
return; | ||
} | ||
const replacement = fontReplacements[encodeFont(fontName)]; | ||
if (replacement) { | ||
n.fontName = decodeFont(replacement); | ||
return; | ||
} | ||
n.fontName = fontName; | ||
}); | ||
} | ||
function getFontReplacement(missingFont, fallbackFonts2) { | ||
const replacement = fallbackFonts2.find((f) => f.style === missingFont.style); | ||
if (replacement) { | ||
return replacement; | ||
} | ||
return fallbackFonts2[0]; | ||
} | ||
function resizeOrLog(f, width, height, withoutConstraints) { | ||
if (width > 0.01 && height > 0.01) { | ||
if (withoutConstraints) { | ||
f.resizeWithoutConstraints(width, height); | ||
} else { | ||
f.resize(width, height); | ||
} | ||
} else { | ||
const generic = f; | ||
const { type } = generic; | ||
console.log( | ||
`Couldn't resize item: ${JSON.stringify({ | ||
type, | ||
width, | ||
height | ||
})}` | ||
); | ||
} | ||
} | ||
function fontsToLoad(n) { | ||
const fonts = /* @__PURE__ */ new Set(); | ||
const addFonts = (json) => { | ||
switch (json.type) { | ||
case "COMPONENT": | ||
case "INSTANCE": | ||
case "FRAME": | ||
case "GROUP": | ||
const { children = [] } = json; | ||
children.forEach(addFonts); | ||
return; | ||
case "TEXT": | ||
const { fontName } = json; | ||
if (typeof fontName === "object") { | ||
fonts.add(encodeFont(fontName)); | ||
} else if (fontName === "__Symbol(figma.mixed)__") { | ||
console.log("encountered mixed fontName: ", fontName); | ||
} | ||
} | ||
}; | ||
try { | ||
n.objects.forEach(addFonts); | ||
} catch (err) { | ||
console.error("error searching for fonts:", err); | ||
} | ||
const fontNames = [...fonts].map((fstr) => decodeFont(fstr)); | ||
return fontNames; | ||
} | ||
function safeAssign(n, dict) { | ||
for (let k in dict) { | ||
try { | ||
if (writeBlacklist.has(k)) { | ||
continue; | ||
} | ||
const v = dict[k]; | ||
if (v === MixedValue || v === void 0) { | ||
continue; | ||
} | ||
n[k] = v; | ||
} catch (error) { | ||
console.error("assignment failed for key", k, error); | ||
} | ||
} | ||
} | ||
function applyPluginData(n, pluginData) { | ||
if (pluginData === void 0) { | ||
return; | ||
} | ||
Object.entries(pluginData).map(([k, v]) => n.setPluginData(k, v)); | ||
} | ||
function safeApplyLayoutMode(f, dict) { | ||
const { layoutMode, itemReverseZIndex, strokesIncludedInLayout } = dict; | ||
f.layoutMode = layoutMode; | ||
if (f.layoutMode !== "NONE") { | ||
f.itemReverseZIndex = itemReverseZIndex; | ||
f.strokesIncludedInLayout = strokesIncludedInLayout; | ||
} | ||
} | ||
function insert(n) { | ||
return __async(this, null, function* () { | ||
const offset = { x: 0, y: 0 }; | ||
console.log("starting insert."); | ||
const [{ fontReplacements }, { availableComponents }] = yield Promise.all([ | ||
loadFonts(fontsToLoad(n), fallbackFonts), | ||
loadComponents(n.components), | ||
loadStyles(n.styles) | ||
]); | ||
console.log("creating images."); | ||
const jsonImages = Object.entries(n.images); | ||
const hashUpdates = /* @__PURE__ */ new Map(); | ||
const figim = jsonImages.map(([hash, bytes]) => { | ||
console.log("Adding with hash: ", hash); | ||
const im = figma.createImage(bytes); | ||
hashUpdates.set(hash, im.hash); | ||
return [hash, im]; | ||
}); | ||
console.log("updating figma based on new hashes."); | ||
const objects = n.objects.map((n2) => updateImageHashes(n2, hashUpdates)); | ||
console.log("inserting."); | ||
const insertSceneNode = (json, target) => { | ||
const factories = { | ||
RECTANGLE: () => figma.createRectangle(), | ||
LINE: () => figma.createLine(), | ||
ELLIPSE: () => figma.createEllipse(), | ||
POLYGON: () => figma.createPolygon(), | ||
STAR: () => figma.createStar(), | ||
VECTOR: () => figma.createVector(), | ||
TEXT: () => figma.createText(), | ||
FRAME: () => figma.createFrame(), | ||
COMPONENT: () => figma.createComponent(), | ||
INSTANCE: (componentId, availableComponents2) => { | ||
const component = availableComponents2[componentId]; | ||
if (!component) { | ||
throw new Error("Couldn't find component"); | ||
} | ||
return component.createInstance(); | ||
} | ||
}; | ||
const addToParent = (n3) => { | ||
if (n3 && n3.parent !== target) { | ||
target.appendChild(n3); | ||
} | ||
}; | ||
let n2; | ||
switch (json.type) { | ||
case "INSTANCE": | ||
const _a = json, { | ||
type, | ||
children = [], | ||
width, | ||
height, | ||
pluginData, | ||
layoutMode, | ||
itemReverseZIndex, | ||
strokesIncludedInLayout, | ||
componentId, | ||
overflowDirection, | ||
isExposedInstance, | ||
componentProperties | ||
} = _a, rest = __objRest(_a, [ | ||
"type", | ||
"children", | ||
"width", | ||
"height", | ||
"pluginData", | ||
"layoutMode", | ||
"itemReverseZIndex", | ||
"strokesIncludedInLayout", | ||
"componentId", | ||
"overflowDirection", | ||
"isExposedInstance", | ||
"componentProperties" | ||
]); | ||
let f; | ||
try { | ||
f = factories[type](componentId, availableComponents); | ||
} catch (e) { | ||
console.error("Couldn't create instance of component", componentId); | ||
break; | ||
} | ||
const properties = Object.fromEntries( | ||
Object.entries(componentProperties).map( | ||
([propertyName, { value }]) => [propertyName, value] | ||
) | ||
); | ||
f.setProperties(properties); | ||
applyOverridesToChildren(f, json); | ||
addToParent(f); | ||
safeApplyLayoutMode(f, { | ||
layoutMode, | ||
itemReverseZIndex, | ||
strokesIncludedInLayout | ||
}); | ||
resizeOrLog(f, width, height); | ||
safeAssign(f, rest); | ||
applyPluginData(f, pluginData); | ||
n2 = f; | ||
break; | ||
case "FRAME": | ||
case "COMPONENT": { | ||
const _b = json, { | ||
type: type2, | ||
children: children2 = [], | ||
width: width2, | ||
height: height2, | ||
strokeCap, | ||
strokeJoin, | ||
pluginData: pluginData2, | ||
layoutMode: layoutMode2, | ||
itemReverseZIndex: itemReverseZIndex2, | ||
strokesIncludedInLayout: strokesIncludedInLayout2 | ||
} = _b, rest2 = __objRest(_b, [ | ||
"type", | ||
"children", | ||
"width", | ||
"height", | ||
"strokeCap", | ||
"strokeJoin", | ||
"pluginData", | ||
"layoutMode", | ||
"itemReverseZIndex", | ||
"strokesIncludedInLayout" | ||
]); | ||
const f2 = factories[json.type](); | ||
addToParent(f2); | ||
safeApplyLayoutMode(f2, { | ||
layoutMode: layoutMode2, | ||
itemReverseZIndex: itemReverseZIndex2, | ||
strokesIncludedInLayout: strokesIncludedInLayout2 | ||
}); | ||
resizeOrLog(f2, width2, height2); | ||
safeAssign(f2, rest2); | ||
applyPluginData(f2, pluginData2); | ||
children2.forEach((c) => insertSceneNode(c, f2)); | ||
n2 = f2; | ||
break; | ||
} | ||
case "GROUP": { | ||
const _c = json, { | ||
type: type2, | ||
children: children2 = [], | ||
width: width2, | ||
height: height2, | ||
pluginData: pluginData2 | ||
} = _c, rest2 = __objRest(_c, [ | ||
"type", | ||
"children", | ||
"width", | ||
"height", | ||
"pluginData" | ||
]); | ||
const nodes = children2.map((c) => insertSceneNode(c, target)).filter(notUndefined); | ||
const f2 = figma.group(nodes, target); | ||
safeAssign(f2, rest2); | ||
n2 = f2; | ||
break; | ||
} | ||
case "BOOLEAN_OPERATION": { | ||
const _d = json, { type: type2, children: children2, width: width2, height: height2, pluginData: pluginData2 } = _d, rest2 = __objRest(_d, ["type", "children", "width", "height", "pluginData"]); | ||
const f2 = figma.createBooleanOperation(); | ||
safeAssign(f2, rest2); | ||
applyPluginData(f2, pluginData2); | ||
resizeOrLog(f2, width2, height2); | ||
n2 = f2; | ||
break; | ||
} | ||
case "RECTANGLE": | ||
case "ELLIPSE": | ||
case "LINE": | ||
case "POLYGON": | ||
case "VECTOR": { | ||
const _e = json, { type: type2, width: width2, height: height2, pluginData: pluginData2 } = _e, rest2 = __objRest(_e, ["type", "width", "height", "pluginData"]); | ||
const f2 = factories[json.type](); | ||
safeAssign( | ||
f2, | ||
rest2 | ||
); | ||
applyPluginData(f2, pluginData2); | ||
resizeOrLog(f2, width2, height2, true); | ||
n2 = f2; | ||
break; | ||
} | ||
case "TEXT": { | ||
const _f = json, { type: type2, width: width2, height: height2, fontName, pluginData: pluginData2 } = _f, rest2 = __objRest(_f, ["type", "width", "height", "fontName", "pluginData"]); | ||
const f2 = figma.createText(); | ||
applyFontName(f2, fontName, fontReplacements); | ||
safeAssign(f2, rest2); | ||
applyPluginData(f2, pluginData2); | ||
resizeOrLog(f2, width2, height2); | ||
n2 = f2; | ||
break; | ||
} | ||
default: { | ||
console.log(`element type not supported: ${json.type}`); | ||
break; | ||
} | ||
} | ||
if (n2) { | ||
target.appendChild(n2); | ||
} else { | ||
console.warn("Unable to do anything with", json); | ||
} | ||
return n2; | ||
}; | ||
return objects.map((o) => { | ||
const n2 = insertSceneNode(o, figma.currentPage); | ||
if (n2 !== void 0) { | ||
n2.x += offset.x; | ||
n2.y += offset.y; | ||
n2.name = `${n2.name} Copy`; | ||
} else { | ||
console.error("returned undefined for json", o); | ||
} | ||
return n2; | ||
}).filter(notUndefined); | ||
}); | ||
} | ||
// src/figma-default-layers.ts | ||
@@ -865,3 +1447,2 @@ var defaultLayers = { | ||
guides: [], | ||
selection: [], | ||
selectedTextRange: null, | ||
@@ -901,578 +1482,10 @@ backgrounds: [ | ||
var figma_default_layers_default = defaultLayers; | ||
// src/index.ts | ||
var writeBlacklist = /* @__PURE__ */ new Set([ | ||
"id", | ||
"componentPropertyReferences", | ||
"variantProperties", | ||
"overlayPositionType", | ||
"overlayBackground", | ||
"overlayBackgroundInteraction", | ||
"fontWeight", | ||
"overrides", | ||
"componentProperties", | ||
"inferredAutoLayout", | ||
"componentId", | ||
"isAsset" | ||
]); | ||
var fallbackFonts = [ | ||
{ family: "Inter", style: "Regular" }, | ||
{ family: "Inter", style: "Thin" }, | ||
{ family: "Inter", style: "Extra Light" }, | ||
{ family: "Inter", style: "Light" }, | ||
{ family: "Inter", style: "Medium" }, | ||
{ family: "Inter", style: "Semi Bold" }, | ||
{ family: "Inter", style: "Bold" }, | ||
{ family: "Inter", style: "Extra Bold" }, | ||
{ family: "Inter", style: "Black" } | ||
]; | ||
function notUndefined(x) { | ||
return x !== void 0; | ||
} | ||
function isVisible(n) { | ||
if (typeof n !== "object") { | ||
return true; | ||
} | ||
if (!("visible" in n) || typeof n.visible !== "boolean" || !("opacity" in n) || typeof n.opacity !== "number" || !("removed" in n) || typeof n.removed !== "boolean") { | ||
return true; | ||
} | ||
return n.visible && n.opacity > 1e-3 && !n.removed; | ||
} | ||
var defaultOptions = { | ||
skipInvisibleNodes: true, | ||
images: false, | ||
geometry: "none", | ||
styles: false | ||
}; | ||
var DumpContext = class { | ||
constructor(options) { | ||
this.options = options; | ||
this.imageHashes = /* @__PURE__ */ new Set(); | ||
this.components = {}; | ||
this.componentSets = {}; | ||
this.styles = {}; | ||
} | ||
}; | ||
function _dumpObject(n, keys, ctx) { | ||
return keys.reduce((o, k) => { | ||
var _a; | ||
const v = n[k]; | ||
if (k === "imageHash" && typeof v === "string") { | ||
ctx.imageHashes.add(v); | ||
} else if (k.endsWith("StyleId") && typeof v === "string" && v.length > 0 && ctx.options.styles) { | ||
const style = figma.getStyleById(v); | ||
if (style) { | ||
ctx.styles[style.id] = { | ||
key: style.key, | ||
name: style.name, | ||
styleType: style.type, | ||
remote: style.remote, | ||
description: style.description | ||
}; | ||
} else { | ||
console.warn(`Couldn't find style with id ${v}.`); | ||
} | ||
} else if (k === "mainComponent" && v) { | ||
const component = v; | ||
let componentSetId; | ||
if (((_a = component.parent) == null ? void 0 : _a.type) === "COMPONENT_SET") { | ||
const componentSet = component.parent; | ||
const { name: name2, description: description2, documentationLinks: documentationLinks2, key: key2, remote: remote2 } = componentSet; | ||
componentSetId = componentSet.id; | ||
ctx.componentSets[componentSet.id] = { | ||
key: key2, | ||
name: name2, | ||
description: description2, | ||
remote: remote2, | ||
documentationLinks: documentationLinks2 | ||
}; | ||
} | ||
const { name, key, description, documentationLinks, remote } = component; | ||
ctx.components[component.id] = { | ||
key, | ||
name, | ||
description, | ||
remote, | ||
componentSetId, | ||
documentationLinks | ||
}; | ||
o["componentId"] = v.id; | ||
return o; | ||
} | ||
o[k] = _dump(v, ctx); | ||
return o; | ||
}, {}); | ||
} | ||
function _dump(n, ctx) { | ||
switch (typeof n) { | ||
case "object": { | ||
if (Array.isArray(n)) { | ||
return n.filter((v) => !ctx.options.skipInvisibleNodes || isVisible(v)).map((v) => _dump(v, ctx)); | ||
} else if (n === null) { | ||
return null; | ||
} else if (n.__proto__ !== void 0) { | ||
const blacklistKeys = conditionalReadBlacklist(n, ctx.options); | ||
const keys = [...Object.keys(n), ...Object.keys(n.__proto__)].filter( | ||
(k) => !blacklistKeys.has(k) | ||
); | ||
return _dumpObject(n, keys, ctx); | ||
} else { | ||
const keys = Object.keys(n); | ||
return _dumpObject(n, keys, ctx); | ||
} | ||
} | ||
case "function": | ||
return void 0; | ||
case "symbol": | ||
if (n === figma.mixed) { | ||
return "__Symbol(figma.mixed)__"; | ||
} else { | ||
return String(n); | ||
} | ||
default: | ||
return n; | ||
} | ||
} | ||
function requestImages(ctx) { | ||
return __async(this, null, function* () { | ||
const imageRequests = [...ctx.imageHashes].map((hash) => __async(this, null, function* () { | ||
const im = figma.getImageByHash(hash); | ||
if (im === null) { | ||
throw new Error(`Image not found: ${hash}`); | ||
} | ||
const dat = yield im.getBytesAsync(); | ||
return [hash, dat]; | ||
})); | ||
const r = yield Promise.all(imageRequests); | ||
return Object.fromEntries(r); | ||
}); | ||
} | ||
function dump(_0) { | ||
return __async(this, arguments, function* (n, options = {}) { | ||
const resolvedOptions = __spreadValues(__spreadValues({}, defaultOptions), options); | ||
const { skipInvisibleNodes } = resolvedOptions; | ||
saveFigmaState(skipInvisibleNodes); | ||
const ctx = new DumpContext(resolvedOptions); | ||
const objects = n.filter((v) => !skipInvisibleNodes || isVisible(v)).map((o) => _dump(o, ctx)); | ||
const images = resolvedOptions.images ? yield requestImages(ctx) : {}; | ||
restoreFigmaState(); | ||
const { components, componentSets, styles } = ctx; | ||
return { | ||
objects, | ||
components, | ||
componentSets, | ||
styles, | ||
images | ||
}; | ||
}); | ||
} | ||
function loadFonts(requestedFonts, fallbackFonts2) { | ||
return __async(this, null, function* () { | ||
const availableFonts = []; | ||
const missingFonts = []; | ||
const fontReplacements = {}; | ||
const loadFontPromises = requestedFonts.map((fontName) => __async(this, null, function* () { | ||
try { | ||
yield figma.loadFontAsync(fontName); | ||
availableFonts.push(fontName); | ||
} catch (e) { | ||
console.warn(`Unable to load font: ${encodeFont(fontName)}`); | ||
missingFonts.push(fontName); | ||
const replacement = getFontReplacement(fontName, fallbackFonts2); | ||
console.log(`Trying font replacement: ${encodeFont(replacement)}`); | ||
try { | ||
yield figma.loadFontAsync(replacement); | ||
console.log(`Loaded font replacement: ${encodeFont(replacement)}`); | ||
fontReplacements[encodeFont(fontName)] = encodeFont(replacement); | ||
} catch (e2) { | ||
console.warn( | ||
`Unable to load font replacement: ${encodeFont(replacement)}` | ||
); | ||
fontReplacements[encodeFont(fontName)] = encodeFont(fallbackFonts2[0]); | ||
} | ||
} | ||
})); | ||
yield Promise.all(loadFontPromises); | ||
console.log("done loading fonts."); | ||
return { availableFonts, missingFonts, fontReplacements }; | ||
}); | ||
} | ||
function loadComponents(requestedComponents) { | ||
return __async(this, null, function* () { | ||
const availableComponents = {}; | ||
yield Promise.all( | ||
Object.entries(requestedComponents).map((_0) => __async(this, [_0], function* ([id, requested]) { | ||
try { | ||
const component = yield figma.importComponentByKeyAsync(requested.key); | ||
availableComponents[id] = component; | ||
} catch (e) { | ||
const node = figma.getNodeById(id); | ||
if (node && node.type === "COMPONENT") { | ||
availableComponents[id] = node; | ||
} else { | ||
console.log("error loading component:", e); | ||
} | ||
} | ||
})) | ||
); | ||
return { availableComponents }; | ||
}); | ||
} | ||
function loadStyles(requestedStyles) { | ||
return __async(this, null, function* () { | ||
yield Promise.all( | ||
Object.entries(requestedStyles).map((_0) => __async(this, [_0], function* ([id, requested]) { | ||
try { | ||
yield figma.importStyleByKeyAsync(requested.key); | ||
} catch (e) { | ||
console.log("error loading style:", e); | ||
} | ||
})) | ||
); | ||
}); | ||
} | ||
function encodeFont({ family, style }) { | ||
if (family.includes("|") || style.includes("|")) { | ||
throw new Error(`Cannot encode a font with "|" in the name.`); | ||
} | ||
return [family, style].join("|"); | ||
} | ||
function decodeFont(f) { | ||
const s = f.split("|"); | ||
if (s.length !== 2) { | ||
throw new Error(`Unable to decode font string: ${f}`); | ||
} | ||
const [family, style] = s; | ||
return { family, style }; | ||
} | ||
function applyFontName(n, fontName, fontReplacements) { | ||
return __async(this, null, function* () { | ||
if (fontName === "__Symbol(figma.mixed)__") { | ||
return; | ||
} | ||
const replacement = fontReplacements[encodeFont(fontName)]; | ||
if (replacement) { | ||
n.fontName = decodeFont(replacement); | ||
return; | ||
} | ||
n.fontName = fontName; | ||
}); | ||
} | ||
function getFontReplacement(missingFont, fallbackFonts2) { | ||
const replacement = fallbackFonts2.find((f) => f.style === missingFont.style); | ||
if (replacement) { | ||
return replacement; | ||
} | ||
return fallbackFonts2[0]; | ||
} | ||
function resizeOrLog(f, width, height, withoutConstraints) { | ||
if (width > 0.01 && height > 0.01) { | ||
if (withoutConstraints) { | ||
f.resizeWithoutConstraints(width, height); | ||
} else { | ||
f.resize(width, height); | ||
} | ||
} else { | ||
const generic = f; | ||
const { type } = generic; | ||
console.log( | ||
`Couldn't resize item: ${JSON.stringify({ | ||
type, | ||
width, | ||
height | ||
})}` | ||
); | ||
} | ||
} | ||
function fontsToLoad(n) { | ||
const fonts = /* @__PURE__ */ new Set(); | ||
const addFonts = (json) => { | ||
switch (json.type) { | ||
case "COMPONENT": | ||
case "INSTANCE": | ||
case "FRAME": | ||
case "GROUP": | ||
const { children = [] } = json; | ||
children.forEach(addFonts); | ||
return; | ||
case "TEXT": | ||
const { fontName } = json; | ||
if (typeof fontName === "object") { | ||
fonts.add(encodeFont(fontName)); | ||
} else if (fontName === "__Symbol(figma.mixed)__") { | ||
console.log("encountered mixed fontName: ", fontName); | ||
} | ||
} | ||
}; | ||
try { | ||
n.objects.forEach(addFonts); | ||
} catch (err) { | ||
console.error("error searching for fonts:", err); | ||
} | ||
const fontNames = [...fonts].map((fstr) => decodeFont(fstr)); | ||
return fontNames; | ||
} | ||
function safeAssign(n, dict) { | ||
for (let k in dict) { | ||
try { | ||
if (writeBlacklist.has(k)) { | ||
continue; | ||
} | ||
const v = dict[k]; | ||
if (v === MixedValue || v === void 0) { | ||
continue; | ||
} | ||
n[k] = v; | ||
} catch (error) { | ||
console.error("assignment failed for key", k, error); | ||
} | ||
} | ||
} | ||
function applyPluginData(n, pluginData) { | ||
if (pluginData === void 0) { | ||
return; | ||
} | ||
Object.entries(pluginData).map(([k, v]) => n.setPluginData(k, v)); | ||
} | ||
function safeApplyLayoutMode(f, dict) { | ||
const { layoutMode, itemReverseZIndex, strokesIncludedInLayout } = dict; | ||
f.layoutMode = layoutMode; | ||
if (f.layoutMode !== "NONE") { | ||
f.itemReverseZIndex = itemReverseZIndex; | ||
f.strokesIncludedInLayout = strokesIncludedInLayout; | ||
} | ||
} | ||
function insert(n) { | ||
return __async(this, null, function* () { | ||
const offset = { x: 0, y: 0 }; | ||
console.log("starting insert."); | ||
const [{ fontReplacements }, { availableComponents }] = yield Promise.all([ | ||
loadFonts(fontsToLoad(n), fallbackFonts), | ||
loadComponents(n.components), | ||
loadStyles(n.styles) | ||
]); | ||
console.log("creating images."); | ||
const jsonImages = Object.entries(n.images); | ||
const hashUpdates = /* @__PURE__ */ new Map(); | ||
const figim = jsonImages.map(([hash, bytes]) => { | ||
console.log("Adding with hash: ", hash); | ||
const im = figma.createImage(bytes); | ||
hashUpdates.set(hash, im.hash); | ||
return [hash, im]; | ||
}); | ||
console.log("updating figma based on new hashes."); | ||
const objects = n.objects.map((n2) => updateImageHashes(n2, hashUpdates)); | ||
console.log("inserting."); | ||
const insertSceneNode = (json, target) => { | ||
const factories = { | ||
RECTANGLE: () => figma.createRectangle(), | ||
LINE: () => figma.createLine(), | ||
ELLIPSE: () => figma.createEllipse(), | ||
POLYGON: () => figma.createPolygon(), | ||
STAR: () => figma.createStar(), | ||
VECTOR: () => figma.createVector(), | ||
TEXT: () => figma.createText(), | ||
FRAME: () => figma.createFrame(), | ||
COMPONENT: () => figma.createComponent(), | ||
INSTANCE: (componentId, availableComponents2) => { | ||
const component = availableComponents2[componentId]; | ||
if (!component) { | ||
throw new Error("Couldn't find component"); | ||
} | ||
return component.createInstance(); | ||
} | ||
}; | ||
const addToParent = (n3) => { | ||
if (n3 && n3.parent !== target) { | ||
target.appendChild(n3); | ||
} | ||
}; | ||
let n2; | ||
switch (json.type) { | ||
case "INSTANCE": | ||
const _a = json, { | ||
type, | ||
children = [], | ||
width, | ||
height, | ||
pluginData, | ||
layoutMode, | ||
itemReverseZIndex, | ||
strokesIncludedInLayout, | ||
componentId, | ||
overflowDirection, | ||
isExposedInstance, | ||
componentProperties | ||
} = _a, rest = __objRest(_a, [ | ||
"type", | ||
"children", | ||
"width", | ||
"height", | ||
"pluginData", | ||
"layoutMode", | ||
"itemReverseZIndex", | ||
"strokesIncludedInLayout", | ||
"componentId", | ||
"overflowDirection", | ||
"isExposedInstance", | ||
"componentProperties" | ||
]); | ||
let f; | ||
try { | ||
f = factories[type](componentId, availableComponents); | ||
} catch (e) { | ||
console.error("Couldn't create instance of component", componentId); | ||
break; | ||
} | ||
const properties = Object.fromEntries( | ||
Object.entries(componentProperties).map( | ||
([propertyName, { value }]) => [propertyName, value] | ||
) | ||
); | ||
f.setProperties(properties); | ||
applyOverridesToChildren(f, json); | ||
addToParent(f); | ||
safeApplyLayoutMode(f, { | ||
layoutMode, | ||
itemReverseZIndex, | ||
strokesIncludedInLayout | ||
}); | ||
resizeOrLog(f, width, height); | ||
safeAssign(f, rest); | ||
applyPluginData(f, pluginData); | ||
n2 = f; | ||
break; | ||
case "FRAME": | ||
case "COMPONENT": { | ||
const _b = json, { | ||
type: type2, | ||
children: children2 = [], | ||
width: width2, | ||
height: height2, | ||
strokeCap, | ||
strokeJoin, | ||
pluginData: pluginData2, | ||
layoutMode: layoutMode2, | ||
itemReverseZIndex: itemReverseZIndex2, | ||
strokesIncludedInLayout: strokesIncludedInLayout2 | ||
} = _b, rest2 = __objRest(_b, [ | ||
"type", | ||
"children", | ||
"width", | ||
"height", | ||
"strokeCap", | ||
"strokeJoin", | ||
"pluginData", | ||
"layoutMode", | ||
"itemReverseZIndex", | ||
"strokesIncludedInLayout" | ||
]); | ||
const f2 = factories[json.type](); | ||
addToParent(f2); | ||
safeApplyLayoutMode(f2, { | ||
layoutMode: layoutMode2, | ||
itemReverseZIndex: itemReverseZIndex2, | ||
strokesIncludedInLayout: strokesIncludedInLayout2 | ||
}); | ||
resizeOrLog(f2, width2, height2); | ||
safeAssign(f2, rest2); | ||
applyPluginData(f2, pluginData2); | ||
children2.forEach((c) => insertSceneNode(c, f2)); | ||
n2 = f2; | ||
break; | ||
} | ||
case "GROUP": { | ||
const _c = json, { | ||
type: type2, | ||
children: children2 = [], | ||
width: width2, | ||
height: height2, | ||
pluginData: pluginData2 | ||
} = _c, rest2 = __objRest(_c, [ | ||
"type", | ||
"children", | ||
"width", | ||
"height", | ||
"pluginData" | ||
]); | ||
const nodes = children2.map((c) => insertSceneNode(c, target)).filter(notUndefined); | ||
const f2 = figma.group(nodes, target); | ||
safeAssign(f2, rest2); | ||
n2 = f2; | ||
break; | ||
} | ||
case "BOOLEAN_OPERATION": { | ||
const _d = json, { type: type2, children: children2, width: width2, height: height2, pluginData: pluginData2 } = _d, rest2 = __objRest(_d, ["type", "children", "width", "height", "pluginData"]); | ||
const f2 = figma.createBooleanOperation(); | ||
safeAssign(f2, rest2); | ||
applyPluginData(f2, pluginData2); | ||
resizeOrLog(f2, width2, height2); | ||
n2 = f2; | ||
break; | ||
} | ||
case "RECTANGLE": | ||
case "ELLIPSE": | ||
case "LINE": | ||
case "POLYGON": | ||
case "VECTOR": { | ||
const _e = json, { type: type2, width: width2, height: height2, pluginData: pluginData2 } = _e, rest2 = __objRest(_e, ["type", "width", "height", "pluginData"]); | ||
const f2 = factories[json.type](); | ||
safeAssign( | ||
f2, | ||
rest2 | ||
); | ||
applyPluginData(f2, pluginData2); | ||
resizeOrLog(f2, width2, height2, true); | ||
n2 = f2; | ||
break; | ||
} | ||
case "TEXT": { | ||
const _f = json, { type: type2, width: width2, height: height2, fontName, pluginData: pluginData2 } = _f, rest2 = __objRest(_f, ["type", "width", "height", "fontName", "pluginData"]); | ||
const f2 = figma.createText(); | ||
applyFontName(f2, fontName, fontReplacements); | ||
safeAssign(f2, rest2); | ||
applyPluginData(f2, pluginData2); | ||
resizeOrLog(f2, width2, height2); | ||
n2 = f2; | ||
break; | ||
} | ||
default: { | ||
console.log(`element type not supported: ${json.type}`); | ||
break; | ||
} | ||
} | ||
if (n2) { | ||
target.appendChild(n2); | ||
} else { | ||
console.warn("Unable to do anything with", json); | ||
} | ||
return n2; | ||
}; | ||
return objects.map((o) => { | ||
const n2 = insertSceneNode(o, figma.currentPage); | ||
if (n2 !== void 0) { | ||
n2.x += offset.x; | ||
n2.y += offset.y; | ||
n2.name = `${n2.name} Copy`; | ||
} else { | ||
console.error("returned undefined for json", o); | ||
} | ||
return n2; | ||
}).filter(notUndefined); | ||
}); | ||
} | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
MixedValue, | ||
applyFontName, | ||
decodeFont, | ||
defaultLayers, | ||
dump, | ||
encodeFont, | ||
fallbackFonts, | ||
fontsToLoad, | ||
getFontReplacement, | ||
insert, | ||
loadFonts, | ||
writeBlacklist | ||
isVisible | ||
}); |
{ | ||
"name": "figma-json-plugin", | ||
"version": "0.0.5-alpha.15", | ||
"version": "0.0.5-alpha.16", | ||
"description": "Dump a hierarchy to JSON within a Figma document, or insert a dumped JSON hierarchy. Intended for use within Figma plugins.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
Sorry, the diff of this file is not supported yet
110993
20
3779