Files
Endoleap/src/lib/canvas-icon-loader.ts
T

37 lines
2.3 KiB
TypeScript

import manifest from "./canvas-icon-assets.json" with { type: "json" };
import { createResourceLoader } from "./resource-loader.ts";
export type IconPaths = [string, Record<string, string | number>][];
export function iconAsset(name: string) {
let hash = 0;
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
const key = name.startsWith("Tabler") ? "tabler" : `lucide-${hash % 96}`;
return `${manifest.root}/${manifest.assets[key as keyof typeof manifest.assets]}`;
}
const allowedTags = new Set(["path", "circle", "ellipse", "rect", "line", "polyline", "polygon"]);
const allowedAttributes = new Set(["key", "d", "cx", "cy", "r", "rx", "ry", "x", "y", "x1", "x2", "y1", "y2", "width", "height", "points", "fill", "fillRule", "clipRule", "stroke", "strokeWidth", "strokeLinecap", "strokeLinejoin", "opacity", "transform"]);
export function validateIconShard(value: unknown): Record<string, IconPaths> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid icon data");
for (const [id, paths] of Object.entries(value)) {
if (!/^[A-Z][A-Za-z0-9]{0,63}$/.test(id) || !Array.isArray(paths) || !paths.length || paths.length > 100) throw new Error("Invalid icon paths");
for (const path of paths) {
if (!Array.isArray(path) || path.length !== 2 || !allowedTags.has(path[0]) || !path[1] || typeof path[1] !== "object" || Array.isArray(path[1])) throw new Error("Invalid icon shape");
for (const [key, attribute] of Object.entries(path[1])) {
if (!allowedAttributes.has(key) || !["string", "number"].includes(typeof attribute) || /url\s*\(|javascript:|data:/i.test(String(attribute))) throw new Error("Invalid icon attribute");
}
}
}
return value as Record<string, IconPaths>;
}
const resources = createResourceLoader(async (url, signal) => {
const response = await fetch(url, { signal, cache: "force-cache" });
if (!response.ok) throw new Error("图标加载失败");
return validateIconShard(await response.json());
});
export function peekIcon(name: string) { return resources.peek(iconAsset(name))?.[name]; }
export async function loadIcon(name: string, signal?: AbortSignal) {
const paths = (await resources.request(iconAsset(name), signal))[name];
if (!paths) throw new Error("未找到图标");
return paths;
}