import manifest from "./canvas-icon-assets.json" with { type: "json" }; import { createResourceLoader } from "./resource-loader.ts"; export type IconPaths = [string, Record][]; 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 { 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; } 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; }