2026-03-18 16:03:17 +01:00

389 lines
9.9 KiB
TypeScript

import type {
ActiveUser,
Board,
Boolean,
BooleanType,
Color,
ColorShapeInfo,
Ellipse,
EventsMap,
File,
Flags,
FontsContext,
Group,
HistoryContext,
LibraryComponent,
LibraryContext,
LibraryVariantComponent,
LocalStorage,
Page,
Path,
Penpot,
Rectangle,
Shape,
SvgRaw,
Text,
Theme,
User,
VariantContainer,
Viewport,
} from '@penpot/plugin-types';
import { Permissions } from '../models/manifest.model.js';
import { OpenUIOptions } from '../models/open-ui-options.model.js';
import { z } from 'zod';
import { createPluginManager } from '../plugin-manager.js';
export const validEvents = [
'finish',
'pagechange',
'filechange',
'selectionchange',
'themechange',
'shapechange',
'contentsave',
] as const;
export function createApi(
plugin: Awaited<ReturnType<typeof createPluginManager>>,
) {
const checkPermission = (permission: Permissions) => {
if (!plugin.manifest.permissions.includes(permission)) {
throw new Error(`Permission ${permission} is not granted`);
}
};
const penpot: Penpot = {
ui: {
open: (name: string, url: string, options?: OpenUIOptions) => {
plugin.openModal(name, url, options);
},
get size() {
return plugin.getModal()?.size() || null;
},
resize: (width: number, height: number) => {
return plugin.resizeModal(width, height);
},
sendMessage(message: unknown) {
let cloneableMessage: unknown;
try {
cloneableMessage = structuredClone(message);
} catch (err) {
console.error(
'plugin sendMessage: the message could not be cloned. ' +
'Ensure the message does not contain functions, DOM nodes, or other non-serializable values.',
err,
);
return;
}
const event = new CustomEvent('message', {
detail: cloneableMessage,
});
plugin.getModal()?.dispatchEvent(event);
},
onMessage: <T>(callback: (message: T) => void) => {
z.function().parse(callback);
plugin.registerMessageCallback(callback as (message: unknown) => void);
},
},
utils: {
geometry: {
center(shapes: Shape[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (window as any).app.plugins.public_utils.centerShapes(shapes);
},
},
types: {
isBoard(shape: Shape): shape is Board {
return shape.type === 'board';
},
isGroup(shape: Shape): shape is Group {
return shape.type === 'group';
},
isMask(shape: Shape): shape is Group {
return shape.type === 'group' && shape.isMask();
},
isBool(shape: Shape): shape is Boolean {
return shape.type === 'boolean';
},
isRectangle(shape: Shape): shape is Rectangle {
return shape.type === 'rectangle';
},
isPath(shape: Shape): shape is Path {
return shape.type === 'path';
},
isText(shape: Shape): shape is Text {
return shape.type === 'text';
},
isEllipse(shape: Shape): shape is Ellipse {
return shape.type === 'ellipse';
},
isSVG(shape: Shape): shape is SvgRaw {
return shape.type === 'svg-raw';
},
isVariantContainer(shape: Shape): shape is VariantContainer {
return shape.type === 'board' && shape.isVariantContainer();
},
isVariantComponent(
component: LibraryComponent,
): component is LibraryVariantComponent {
return component.isVariant();
},
},
},
closePlugin: () => {
plugin.close();
},
on<T extends keyof EventsMap>(
type: T,
callback: (event: EventsMap[T]) => void,
props?: { [key: string]: unknown },
): symbol {
// z.function alter fn, so can't use it here
z.enum(validEvents).parse(type);
z.function().parse(callback);
// To suscribe to events needs the read permission
checkPermission('content:read');
return plugin.registerListener(type, callback, props);
},
off(eventId: symbol): void {
plugin.destroyListener(eventId);
},
// Penpot State API
get version(): string {
return plugin.context.version;
},
get root(): Shape | null {
checkPermission('content:read');
return plugin.context.root;
},
get currentFile(): File | null {
checkPermission('content:read');
return plugin.context.currentFile;
},
get currentPage(): Page | null {
checkPermission('content:read');
return plugin.context.currentPage;
},
get selection(): Shape[] {
checkPermission('content:read');
return plugin.context.selection;
},
set selection(value: Shape[]) {
checkPermission('content:read');
plugin.context.selection = value;
},
get viewport(): Viewport {
return plugin.context.viewport;
},
get history(): HistoryContext {
return plugin.context.history;
},
get library(): LibraryContext {
checkPermission('library:read');
return plugin.context.library;
},
get fonts(): FontsContext {
checkPermission('content:read');
return plugin.context.fonts;
},
get flags(): Flags {
return plugin.context.flags;
},
get currentUser(): User {
checkPermission('user:read');
return plugin.context.currentUser;
},
get activeUsers(): ActiveUser[] {
checkPermission('user:read');
return plugin.context.activeUsers;
},
shapesColors(shapes: Shape[]): (Color & ColorShapeInfo)[] {
checkPermission('content:read');
return plugin.context.shapesColors(shapes);
},
replaceColor(shapes: Shape[], oldColor: Color, newColor: Color) {
checkPermission('content:write');
return plugin.context.replaceColor(shapes, oldColor, newColor);
},
get theme(): Theme {
return plugin.context.theme;
},
get localStorage(): LocalStorage {
checkPermission('allow:localstorage');
return plugin.context.localStorage;
},
createBoard(): Board {
checkPermission('content:write');
return plugin.context.createBoard();
},
createRectangle(): Rectangle {
checkPermission('content:write');
return plugin.context.createRectangle();
},
createEllipse(): Ellipse {
checkPermission('content:write');
return plugin.context.createEllipse();
},
createText(text: string): Text | null {
checkPermission('content:write');
return plugin.context.createText(text);
},
createPath(): Path {
checkPermission('content:write');
return plugin.context.createPath();
},
createBoolean(boolType: BooleanType, shapes: Shape[]): Boolean | null {
checkPermission('content:write');
return plugin.context.createBoolean(boolType, shapes);
},
createShapeFromSvg(svgString: string): Group | null {
checkPermission('content:write');
return plugin.context.createShapeFromSvg(svgString);
},
createShapeFromSvgWithImages(svgString: string): Promise<Group | null> {
checkPermission('content:write');
return plugin.context.createShapeFromSvgWithImages(svgString);
},
group(shapes: Shape[]): Group | null {
checkPermission('content:write');
return plugin.context.group(shapes);
},
ungroup(group: Group, ...other: Group[]): void {
checkPermission('content:write');
plugin.context.ungroup(group, ...other);
},
uploadMediaUrl(name: string, url: string) {
checkPermission('content:write');
return plugin.context.uploadMediaUrl(name, url);
},
uploadMediaData(name: string, data: Uint8Array, mimeType: string) {
checkPermission('content:write');
return plugin.context.uploadMediaData(name, data, mimeType);
},
generateMarkup(
shapes: Shape[],
options?: { type?: 'html' | 'svg' },
): string {
checkPermission('content:read');
return plugin.context.generateMarkup(shapes, options);
},
generateStyle(
shapes: Shape[],
options?: {
type?: 'css';
withPrelude?: boolean;
includeChildren?: boolean;
},
): string {
checkPermission('content:read');
return plugin.context.generateStyle(shapes, options);
},
generateFontFaces(shapes: Shape[]): Promise<string> {
checkPermission('content:read');
return plugin.context.generateFontFaces(shapes);
},
openViewer(): void {
checkPermission('content:read');
plugin.context.openViewer();
},
createPage(): Page {
checkPermission('content:write');
return plugin.context.createPage();
},
openPage(page: Page | string, newWindow?: boolean): void {
checkPermission('content:read');
plugin.context.openPage(page, newWindow ?? false);
},
alignHorizontal(
shapes: Shape[],
direction: 'left' | 'center' | 'right',
): void {
checkPermission('content:write');
plugin.context.alignHorizontal(shapes, direction);
},
alignVertical(
shapes: Shape[],
direction: 'top' | 'center' | 'bottom',
): void {
checkPermission('content:write');
plugin.context.alignVertical(shapes, direction);
},
distributeHorizontal(shapes: Shape[]): void {
checkPermission('content:write');
plugin.context.distributeHorizontal(shapes);
},
distributeVertical(shapes: Shape[]): void {
checkPermission('content:write');
plugin.context.distributeVertical(shapes);
},
flatten(shapes: Shape[]): Path[] {
checkPermission('content:write');
return plugin.context.flatten(shapes);
},
createVariantFromComponents(shapes: Board[]): VariantContainer {
checkPermission('content:write');
return plugin.context.createVariantFromComponents(shapes);
},
};
return {
penpot,
};
}