openpencil/scripts/generate-iconify-catalog.mjs
Kayshen-X b0b52a7842
Some checks failed
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 2s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 1s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 2s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 2s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 1s
Rust check (native) / macos-latest / 1.94 (push) Has been cancelled
Rust check (native) / windows-latest / 1.94 (push) Has been cancelled
Rust multi-platform build / linux-aarch64 (push) Has been cancelled
Rust multi-platform build / macos-aarch64 (push) Has been cancelled
Rust multi-platform build / windows-x86_64 (push) Has been cancelled
Rust multi-platform build / macos-x86_64 (push) Has been cancelled
Rust multi-platform build / windows-aarch64 (push) Has been cancelled
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Has been cancelled
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Has been cancelled
feat(panels): improve native property controls and icons
2026-05-24 23:30:00 +08:00

152 lines
4.7 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const SETS = ['lucide', 'feather', 'simple-icons'];
const OUT = path.resolve('crates/op-editor-ui/assets/iconify-catalog.json');
function attr(tag, name) {
const match = tag.match(new RegExp(`\\b${name}="([^"]*)"`));
return match ? match[1] : undefined;
}
function num(value, fallback = 0) {
if (value === undefined || value === '') return fallback;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function circlePath(cx, cy, r) {
return `M${cx - r} ${cy}A${r} ${r} 0 1 0 ${cx + r} ${cy}A${r} ${r} 0 1 0 ${cx - r} ${cy}Z`;
}
function ellipsePath(cx, cy, rx, ry) {
return `M${cx - rx} ${cy}A${rx} ${ry} 0 1 0 ${cx + rx} ${cy}A${rx} ${ry} 0 1 0 ${cx - rx} ${cy}Z`;
}
function rectPath(x, y, w, h, r) {
if (r <= 0) return `M${x} ${y}H${x + w}V${y + h}H${x}Z`;
const rr = Math.min(r, w / 2, h / 2);
return [
`M${x + rr} ${y}`,
`H${x + w - rr}`,
`A${rr} ${rr} 0 0 1 ${x + w} ${y + rr}`,
`V${y + h - rr}`,
`A${rr} ${rr} 0 0 1 ${x + w - rr} ${y + h}`,
`H${x + rr}`,
`A${rr} ${rr} 0 0 1 ${x} ${y + h - rr}`,
`V${y + rr}`,
`A${rr} ${rr} 0 0 1 ${x + rr} ${y}`,
'Z',
].join('');
}
function pointsPath(points, close) {
const nums = points
.trim()
.split(/[\s,]+/)
.map(Number)
.filter(Number.isFinite);
if (nums.length < 4) return null;
const pairs = [];
for (let i = 0; i + 1 < nums.length; i += 2) pairs.push([nums[i], nums[i + 1]]);
return `M${pairs.map((p) => `${p[0]} ${p[1]}`).join('L')}${close ? 'Z' : ''}`;
}
function pathsFromBody(body) {
const paths = [];
for (const match of body.matchAll(/<path\b[^>]*?\bd="([^"]+)"[^>]*>/gi)) {
paths.push(match[1]);
}
for (const match of body.matchAll(/<line\b[^>]*>/gi)) {
const tag = match[0];
paths.push(
`M${num(attr(tag, 'x1'))} ${num(attr(tag, 'y1'))}L${num(attr(tag, 'x2'))} ${num(attr(tag, 'y2'))}`,
);
}
for (const match of body.matchAll(/<circle\b[^>]*>/gi)) {
const tag = match[0];
paths.push(circlePath(num(attr(tag, 'cx')), num(attr(tag, 'cy')), num(attr(tag, 'r'))));
}
for (const match of body.matchAll(/<ellipse\b[^>]*>/gi)) {
const tag = match[0];
paths.push(
ellipsePath(
num(attr(tag, 'cx')),
num(attr(tag, 'cy')),
num(attr(tag, 'rx')),
num(attr(tag, 'ry')),
),
);
}
for (const match of body.matchAll(/<rect\b[^>]*>/gi)) {
const tag = match[0];
paths.push(
rectPath(
num(attr(tag, 'x')),
num(attr(tag, 'y')),
num(attr(tag, 'width')),
num(attr(tag, 'height')),
num(attr(tag, 'rx'), num(attr(tag, 'ry'))),
),
);
}
for (const match of body.matchAll(/<polyline\b[^>]*?\bpoints="([^"]+)"[^>]*>/gi)) {
const d = pointsPath(match[1], false);
if (d) paths.push(d);
}
for (const match of body.matchAll(/<polygon\b[^>]*?\bpoints="([^"]+)"[^>]*>/gi)) {
const d = pointsPath(match[1], true);
if (d) paths.push(d);
}
for (let i = 1; i < paths.length; i += 1) {
if (paths[i].startsWith('m')) paths[i] = `M${paths[i].slice(1)}`;
}
return paths;
}
function styleFromBody(body) {
const hasStroke = /\bstroke=|\bstroke-width=|\bstroke-linecap=|\bfill="none"/i.test(body);
const hasFill = /\bfill="(?!none)[^"]*"/i.test(body);
return hasStroke && !hasFill ? 'stroke' : 'fill';
}
const icons = [];
for (const collection of SETS) {
const data = require(`../node_modules/@iconify-json/${collection}/icons.json`);
const names = Object.keys(data.icons).sort((a, b) => a.localeCompare(b));
for (const name of names) {
const icon = data.icons[name];
const paths = pathsFromBody(icon.body);
if (paths.length === 0) continue;
icons.push({
collection,
name,
width: icon.width ?? data.width ?? 24,
height: icon.height ?? data.height ?? 24,
style: styleFromBody(icon.body),
d: paths.join(' '),
});
}
}
for (const [alias, target] of Object.entries({ home: 'house', unlock: 'lock-open' })) {
if (
!icons.some((icon) => icon.collection === 'lucide' && icon.name === alias) &&
icons.some((icon) => icon.collection === 'lucide' && icon.name === target)
) {
const source = icons.find((icon) => icon.collection === 'lucide' && icon.name === target);
icons.push({ ...source, name: alias });
}
}
icons.sort((a, b) => {
const setDelta = SETS.indexOf(a.collection) - SETS.indexOf(b.collection);
return setDelta || a.name.localeCompare(b.name);
});
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, `${JSON.stringify({ icons })}\n`);
console.log(`wrote ${icons.length} icons to ${OUT}`);