37 lines
2.3 KiB
TypeScript
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;
|
|
}
|