@cf-wasm/og
Version:
Generate Open Graph Images dynamically from HTML/CSS without a browser.
401 lines (398 loc) • 14.3 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/figma/figma.ts
var figma_exports = {};
__export(figma_exports, {
FigmaImageResponse: () => FigmaImageResponse,
assertValue: () => assertValue,
getFigmaSvg: () => getFigmaSvg,
getSvgDimensions: () => getSvgDimensions,
getTextNodes: () => getTextNodes,
getTspanNodes: () => getTspanNodes,
isComplexTemplate: () => isComplexTemplate,
loadTextNodeFonts: () => loadTextNodeFonts,
parseFigmaUrl: () => parseFigmaUrl,
parseTextNode: () => parseTextNode,
parseTspanNode: () => parseTspanNode,
removeTextNodes: () => removeTextNodes,
renderFigma: () => renderFigma,
svgToBase64: () => svgToBase64
});
module.exports = __toCommonJS(figma_exports);
var import_detect_runtime = require("@cf-wasm/internals/detect-runtime");
var import_core = require("../core/index.js");
var import_errors = require("../core/errors.js");
var import_render = require("../core/render.js");
var import_response = require("../core/response.js");
var import_entities = require("../core/utils/entities.js");
if (!import_core.modules.isUsable()) {
throw new Error(
`Modules are not initialized! Please import the main module based on runtime (i.e. '@cf-wasm/og', '@cf-wasm/og/next', etc.) before importing the 'figma' submodule (i.e. '@cf-wasm/og/figma').
For example:
------------
import '@cf-wasm/og/next'; // <- import main module based on runtime first
import { FigmaImageResponse } from '@cf-wasm/og/figma' // <- now you can import figma submodule
// ...
------------`
);
}
var parseFigmaUrl = (figmaUrl) => {
const regex = /\/file\/([^/]+)\/[^?]+\?[^#]*node-id=([^&#]+)/;
const match2 = figmaUrl.match(regex);
let fileId = "";
let nodeId = "";
if (match2) {
fileId = match2[1] || "";
nodeId = match2[2] || "";
}
return { fileId, nodeId };
};
var assertValue = (value, errorMessage) => {
if (typeof value === "undefined") {
throw new Error(errorMessage);
}
return value;
};
var match = (string, matcher, index = 1, defaultValue = "") => {
const matches = string.match(matcher);
if (matches && typeof matches[index] === "string") {
return matches[index];
}
return defaultValue;
};
var isComplexTemplate = (template) => typeof template !== "string" && template !== void 0 && "value" in template;
var svgToBase64 = (svg) => {
let base64;
if (typeof Buffer !== "undefined") {
base64 = Buffer.from(svg).toString("base64");
} else if (typeof btoa === "function") {
base64 = btoa(svg);
} else {
throw new Error("Base64 encoding is not supported in this environment.");
}
return `data:image/svg+xml;base64,${base64}`;
};
var getSvgDimensions = (svg) => {
const widthMatch = svg.match(/width="(\d+)/);
const heightMatch = svg.match(/height="(\d+)/);
if (widthMatch && heightMatch) {
const width = Number(widthMatch[1]);
const height = Number(heightMatch[1]);
return { width, height };
}
return { width: 0, height: 0 };
};
var getTextNodes = (svg) => {
const regex = /<text[^>]*>(.*?)<\/text>/g;
let execArray = regex.exec(svg);
const matches = [];
while (execArray !== null) {
matches.push(execArray[0]);
execArray = regex.exec(svg);
}
return matches;
};
var getTspanNodes = (text) => {
const regex = /<tspan[^>]*>(.*?)<\/tspan>/g;
let execArray = regex.exec(text);
const matches = [];
while (execArray !== null) {
matches.push(execArray[0]);
execArray = regex.exec(text);
}
return matches;
};
var parseTspanNode = (tspanNode) => ({
x: match(tspanNode, /x="([^"]*)"/),
y: match(tspanNode, /y="([^"]*)"/),
content: (0, import_entities.replaceEntities)(match(tspanNode, /<tspan[^>]*>([^<]*)<\/tspan>/))
});
var parseTextNode = (textNode) => {
const tspanNodesAttributes = getTspanNodes(textNode).map(parseTspanNode);
return {
id: match(textNode, /id="([^"]*)"/),
fill: match(textNode, /fill="([^"]*)"/),
fontFamily: match(textNode, /font-family="([^"]*)"/),
fontSize: match(textNode, /font-size="([^"]*)"/),
fontWeight: match(textNode, /font-weight="([^"]*)"/).replace("normal", "400").replace("bold", "700") || void 0,
fontStyle: match(textNode, /font-style="([^"]*)"/) || void 0,
letterSpacing: match(textNode, /letter-spacing="([^"]*)"/),
x: tspanNodesAttributes[0].x,
y: tspanNodesAttributes[0].y,
children: tspanNodesAttributes
};
};
var removeTextNodes = (svg) => svg.replace(/<text[^>]*>(.*?)<\/text>/g, "");
var getFigmaSvg = async (figmaOptions) => {
const { url, token } = figmaOptions;
const { fileId, nodeId } = parseFigmaUrl(url);
assertValue(url, "(@cf-wasm/og) [ERROR] 'url' field is required");
assertValue(token, "(@cf-wasm/og) [ERROR] 'token' field is required");
const apiUrl = `https://api.figma.com/v1/images/${fileId}?ids=${nodeId}&svg_outline_text=false&format=svg&svg_include_id=true`;
const figmaResponse = await fetch(apiUrl, {
method: "GET",
headers: {
"X-FIGMA-TOKEN": token
},
// This is to prevent errors on workerd runtime
...(0, import_detect_runtime.detectRuntime)() === "workerd" ? void 0 : { cache: "no-store" }
}).then((res) => {
if (!res.ok) {
throw new import_errors.FetchError(`Response was not successful (status: ${res.status}, statusText: ${res.statusText})`, { response: res });
}
return res;
}).catch((e) => {
throw new import_errors.FetchError(`Failed to fetch figma image. An error ocurred while fetching ${apiUrl}`, {
cause: e,
response: e instanceof import_errors.FetchError ? e.response : void 0
});
});
const figmaResponseJson = await figmaResponse.json();
const svgDownloadPath = figmaResponseJson.images[nodeId.replace("-", ":")];
const svgResponse = await fetch(
svgDownloadPath,
// This is to prevent errors on workerd runtime
(0, import_detect_runtime.detectRuntime)() === "workerd" ? void 0 : { cache: "no-store" }
);
const response = new Response(svgResponse.body, svgResponse);
response.headers.set("Content-Type", "text/xml");
const svg = await svgResponse.text();
return svg;
};
var loadTextNodeFonts = async (nodeAttributes, loadFonts) => (await Promise.all(
nodeAttributes.filter(
(value, index, self) => index === self.findIndex((t) => t?.fontFamily === value.fontFamily && t.fontWeight === value.fontWeight && t.fontStyle === value.fontStyle)
).map(async (attrs) => {
const { family, weight, style } = {
family: attrs.fontFamily,
weight: attrs.fontWeight ? Number(attrs.fontWeight) : void 0,
style: attrs.fontStyle || void 0
};
const awaited = await loadFonts(family, weight, style);
if (awaited) {
return {
name: family,
weight,
style,
get data() {
return typeof awaited === "object" && "data" in awaited ? awaited.data : awaited;
}
};
}
return void 0;
})
)).filter(Boolean);
var renderFigma = (figmaOptions, renderOptions) => {
const getFigmaData = async () => {
const { template } = figmaOptions;
const svg = await getFigmaSvg(figmaOptions);
const { width, height } = getSvgDimensions(svg);
const textNodes = getTextNodes(svg);
const textNodeAttributes = textNodes.map(parseTextNode);
const dynamicFonts = typeof renderOptions?.loadFonts === "function" ? await loadTextNodeFonts(textNodeAttributes, renderOptions.loadFonts) : [];
const element = {
key: "0",
type: "div",
props: {
style: { display: "flex" },
children: [
{
type: "img",
props: {
style: { position: "absolute" },
alt: "",
width,
height,
src: svgToBase64(removeTextNodes(svg))
}
},
{
type: "div",
props: {
style: {
display: "flex",
position: "relative",
width: "100%"
},
children: textNodeAttributes.map((textNode) => {
const t = template[textNode.id];
let value = "";
if (t === void 0) {
value = textNode.children;
} else if (isComplexTemplate(t)) {
value = t.value;
} else {
value = t;
}
let cssProps = {};
let centerHorizontally = false;
if (isComplexTemplate(t)) {
const complexTemplate = t;
if (complexTemplate.props) {
const { centerHorizontally: centerHorizontallyProp, ...otherCSSProps } = complexTemplate.props;
cssProps = otherCSSProps;
centerHorizontally = centerHorizontallyProp || false;
}
}
const children = Array.isArray(value) ? value.map(
(e, i) => ({
key: String(i),
type: "span",
props: {
style: {
position: "absolute",
left: `${Number(e.x) - Number(textNode.x)}px`,
top: `${Number(e.y) - Number(textNode.y) - Number(textNode.fontSize) / 2}px`
},
children: e.content
}
})
) : value;
if (centerHorizontally) {
return {
type: "div",
props: {
style: {
position: "absolute",
display: "flex",
justifyContent: "center",
width: "100%"
},
children: {
type: "span",
props: {
style: {
color: textNode.fill,
marginTop: `${Number(textNode.y) - Number(textNode.fontSize) / 2}px`,
fontWeight: textNode.fontWeight || "400",
fontSize: textNode.fontSize,
fontFamily: textNode.fontFamily,
letterSpacing: textNode.letterSpacing,
textAlign: "center",
...cssProps
},
children
}
}
}
};
}
return {
type: "span",
props: {
style: {
position: "absolute",
color: textNode.fill,
left: `${textNode.x}px`,
top: `${Number(textNode.y) - Number(textNode.fontSize) / 2}px`,
fontWeight: textNode.fontWeight || "400",
fontSize: textNode.fontSize,
fontFamily: textNode.fontFamily,
letterSpacing: textNode.letterSpacing,
...cssProps
},
children
}
};
})
}
}
]
}
};
return { element, width, height, fonts: dynamicFonts };
};
const data = {
renderer: null,
element: null,
svg: null,
png: null
};
const asElement = async () => {
if (!data.element) {
data.element = await getFigmaData();
}
return data.element;
};
const getRenderer = async () => {
if (!data.renderer) {
const elementData = await asElement();
data.renderer = (0, import_render.render)(elementData.element, {
...renderOptions,
width: elementData.width,
height: elementData.height,
fonts: [...elementData.fonts, ...renderOptions?.fonts || []]
});
}
return data.renderer;
};
const asSvg = async () => {
if (!data.svg) {
data.svg = await (await getRenderer()).asSvg();
}
return data.svg;
};
const asPng = async () => {
if (!data.png) {
data.png = await (await getRenderer()).asPng();
}
return data.png;
};
return { asElement, asSvg, asPng };
};
var FigmaImageResponse = class extends import_response.BaseResponse {
/**
* Creates an instance of {@link FigmaImageResponse}
*
* @param figmaOptions Figma options {@link FigmaOptions}
* @param responseOptions The same as {@link ImageResponseOptions} except `width` and `height`. `width` and `height` are automatically set from the Figma frame's size.
*/
constructor(figmaOptions, responseOptions) {
super(async () => {
const renderer = renderFigma(figmaOptions, responseOptions);
const elementData = await renderer.asElement();
return [
elementData.element,
{
...responseOptions,
width: elementData.width,
height: elementData.height,
fonts: [...elementData.fonts, ...responseOptions?.fonts || []]
}
];
}, responseOptions);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FigmaImageResponse,
assertValue,
getFigmaSvg,
getSvgDimensions,
getTextNodes,
getTspanNodes,
isComplexTemplate,
loadTextNodeFonts,
parseFigmaUrl,
parseTextNode,
parseTspanNode,
removeTextNodes,
renderFigma,
svgToBase64
});