Files
paseo/packages/plugin/src/icons.ts
T
Mohamed Boudra b6dde61a7f feat(plugins): accept inline SVG and component icons (#7368)
Every contribution `icon` now takes a Lucide name, `{ svg }`, or a
component. Attachment sources take only the first two because composer
drafts persist the source icon.

The daemon's provider-icon check and the app's inline-SVG check are the
same function in the shared SDK root, so a plugin sees the same reasons
in both places. The app parses each icon once at registration and fails
the plugin with that reason. Rendering goes through one module that
turns any form into a component, so surfaces never branch on the form.
2026-10-19 15:36:04 +07:00

39 lines
1.6 KiB
TypeScript

/** What a component icon receives from the surface that draws it. */
export interface PluginIconProps {
size: number;
color: string;
}
/** Inline SVG markup. Paseo validates it when the contribution registers. */
export interface PluginSvgIcon {
svg: string;
}
/** An icon that can be stored or sent: a Lucide icon name, or inline SVG. */
export type PluginSerializableIcon = string | PluginSvgIcon;
export const PLUGIN_SVG_ICON_MAX_BYTES = 64 * 1024;
const EXTERNAL_HREF = /\s(?:href|xlink:href)\s*=\s*(?:(["'])(.*?)\1|([^\s>]+))/gi;
/**
* Why Paseo refuses to render this SVG, or null when it is a self-contained document.
* The daemon applies it to provider and usage-source icon files; the app to inline plugin icons.
*/
export function findSvgIconProblem(svg: string): string | null {
if (new TextEncoder().encode(svg).byteLength > PLUGIN_SVG_ICON_MAX_BYTES) {
return "exceeds 64 KiB";
}
if (!/^\s*<svg(?:\s|>)/i.test(svg)) return "not an SVG document";
if (/<script(?:\s|>)/i.test(svg)) return "script elements are not allowed";
if (/<foreignObject(?:\s|>)/i.test(svg)) return "foreignObject elements are not allowed";
if (/<style(?:\s|>)/i.test(svg)) return "style elements are not allowed";
if (/\son[a-z0-9_-]*\s*=/i.test(svg)) return "event-handler attributes are not allowed";
if (/javascript\s*:/i.test(svg)) return "javascript URLs are not allowed";
for (const match of svg.matchAll(EXTERNAL_HREF)) {
const href = match[2] ?? match[3] ?? "";
if (!href.startsWith("#")) return "external href references are not allowed";
}
return null;
}