fresnel-icon 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +371 -154
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -18,8 +18,12 @@ var APPEARANCE_LABEL = {
|
|
|
18
18
|
function appearanceKeys(a) {
|
|
19
19
|
switch (a) {
|
|
20
20
|
case "dark":
|
|
21
|
-
case "clear-dark":
|
|
22
21
|
return ["dark"];
|
|
22
|
+
// Clear and Tinted (light and dark) are all rendered from Icon Composer's Mono, written as "tinted".
|
|
23
|
+
// Apple: "For mono, you can preview clear and tinted variants"; Assets.car keeps only light / dark /
|
|
24
|
+
// tintable renditions. A dark value does not carry over to Clear dark [推測: strong].
|
|
25
|
+
case "clear-light":
|
|
26
|
+
case "clear-dark":
|
|
23
27
|
case "tinted-light":
|
|
24
28
|
case "tinted-dark":
|
|
25
29
|
return ["tinted"];
|
|
@@ -135,6 +139,7 @@ function readProps(obj) {
|
|
|
135
139
|
function writeProps(props, order, structural = {}) {
|
|
136
140
|
const out = {};
|
|
137
141
|
const emitted = /* @__PURE__ */ new Set();
|
|
142
|
+
let fromFile = true;
|
|
138
143
|
const emit = (rawKey) => {
|
|
139
144
|
if (emitted.has(rawKey)) return;
|
|
140
145
|
if (rawKey in structural) {
|
|
@@ -142,17 +147,28 @@ function writeProps(props, order, structural = {}) {
|
|
|
142
147
|
emitted.add(rawKey);
|
|
143
148
|
return;
|
|
144
149
|
}
|
|
150
|
+
const literal = props[rawKey];
|
|
151
|
+
if (rawKey.endsWith(SPEC_SUFFIX) && literal?.plain) {
|
|
152
|
+
out[rawKey] = structuredClone(literal.plain.value);
|
|
153
|
+
emitted.add(rawKey);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
145
156
|
const isSpec = rawKey.endsWith(SPEC_SUFFIX);
|
|
146
157
|
const base = isSpec ? rawKey.slice(0, -SPEC_SUFFIX.length) : rawKey;
|
|
147
158
|
const p = props[base];
|
|
148
159
|
if (!p) return;
|
|
149
|
-
if (isSpec ? p.specs && p.specs.length > 0 : p.plain) {
|
|
160
|
+
if (isSpec ? p.specs && (p.specs.length > 0 || fromFile) : p.plain) {
|
|
150
161
|
out[rawKey] = structuredClone(isSpec ? p.specs : p.plain.value);
|
|
151
162
|
emitted.add(rawKey);
|
|
152
163
|
}
|
|
153
164
|
};
|
|
154
165
|
for (const k of order) emit(k);
|
|
166
|
+
fromFile = false;
|
|
155
167
|
for (const [base, p] of Object.entries(props)) {
|
|
168
|
+
if (base.endsWith(SPEC_SUFFIX) && p.plain) {
|
|
169
|
+
emit(base);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
156
172
|
if (p.plain) emit(base);
|
|
157
173
|
if (p.specs?.length) emit(base + SPEC_SUFFIX);
|
|
158
174
|
}
|
|
@@ -200,6 +216,11 @@ function baseValue(p) {
|
|
|
200
216
|
if (b) return b.value;
|
|
201
217
|
return p.plain?.value;
|
|
202
218
|
}
|
|
219
|
+
function variantValue(p, v) {
|
|
220
|
+
const vv = toVariant(v);
|
|
221
|
+
if (isEmpty(vv)) return baseValue(p);
|
|
222
|
+
return p?.specs?.find((e) => sameVariant(e, vv))?.value;
|
|
223
|
+
}
|
|
203
224
|
function variantsOf(p) {
|
|
204
225
|
return (p?.specs ?? []).filter((e) => !isBaseEntry(e)).map((e) => ({ ...e.appearance ? { appearance: e.appearance } : {}, ...e.idiom ? { idiom: e.idiom } : {} }));
|
|
205
226
|
}
|
|
@@ -392,6 +413,7 @@ function svgSize(svg) {
|
|
|
392
413
|
return { width: 1024, height: 1024 };
|
|
393
414
|
}
|
|
394
415
|
function pngSize(b) {
|
|
416
|
+
if (b.length < 24) return { width: 0, height: 0 };
|
|
395
417
|
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
396
418
|
return { width: dv.getUint32(16), height: dv.getUint32(20) };
|
|
397
419
|
}
|
|
@@ -403,6 +425,10 @@ function jpgSize(b) {
|
|
|
403
425
|
continue;
|
|
404
426
|
}
|
|
405
427
|
const marker = b[i + 1];
|
|
428
|
+
if (marker === 255) {
|
|
429
|
+
i++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
406
432
|
const len = b[i + 2] << 8 | b[i + 3];
|
|
407
433
|
if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
|
|
408
434
|
return { height: b[i + 5] << 8 | b[i + 6], width: b[i + 7] << 8 | b[i + 8] };
|
|
@@ -442,15 +468,26 @@ function expandInternalEntities(svg) {
|
|
|
442
468
|
if (v.length <= 2048 && !/[&<]/.test(v)) ents.set(m[1], v);
|
|
443
469
|
}
|
|
444
470
|
const body = svg.slice(0, doctype.index) + svg.slice(doctype.index + doctype[0].length);
|
|
445
|
-
|
|
471
|
+
let budget = Math.max(1 << 20, body.length * 4);
|
|
472
|
+
return body.replace(/&([\w.-]+);/g, (m, name) => {
|
|
473
|
+
const v = ents.get(name);
|
|
474
|
+
if (v === void 0 || v.length > budget) return m;
|
|
475
|
+
budget -= v.length;
|
|
476
|
+
return v;
|
|
477
|
+
});
|
|
446
478
|
}
|
|
479
|
+
var DATA_IMAGE = /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml)(?:;[\w.+-]+=[\w.+-]+)*;base64,[a-z0-9+/=\s]*$/i;
|
|
447
480
|
function safeUrlRef(v) {
|
|
448
481
|
const t = v.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
449
|
-
return t.startsWith("#") ||
|
|
482
|
+
return t.startsWith("#") || DATA_IMAGE.test(t);
|
|
450
483
|
}
|
|
484
|
+
function decodeXmlEntities(v) {
|
|
485
|
+
return v.replace(/&#x([0-9a-f]+);/gi, (_m, h) => String.fromCodePoint(parseInt(h, 16) % 1114112)).replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(Number(d) % 1114112)).replace(/&(lt|gt|quot|apos|amp);/g, (_m, n2) => ({ lt: "<", gt: ">", quot: '"', apos: "'", amp: "&" })[n2]);
|
|
486
|
+
}
|
|
487
|
+
var escapeXmlText = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
451
488
|
function cleanCss(css0) {
|
|
452
489
|
const css = css0.replace(/\\([0-9a-f]{1,6})\s?/gi, (_m, h) => String.fromCodePoint(parseInt(h, 16) % 1114112)).replace(/\\(.)/g, "$1");
|
|
453
|
-
return css.replace(/@import\b[^;]*;?/gi, "").replace(/url\s*\(([^)]*)\)/gi, (m, inner) => safeUrlRef(inner) ? m : "none").replace(/expression\s*\(/gi, "(").replace(/javascript\s*:/gi, "");
|
|
490
|
+
return css.replace(/@import\b[^;]*;?/gi, "").replace(/url\s*\(([^)]*)\)/gi, (m, inner) => safeUrlRef(inner) ? m : "none").replace(/(?:-webkit-)?image-set\s*\(|\bsrc\s*\(/gi, "(").replace(/expression\s*\(/gi, "(").replace(/javascript\s*:/gi, "");
|
|
454
491
|
}
|
|
455
492
|
function cleanAttrs(raw) {
|
|
456
493
|
const out = [];
|
|
@@ -471,8 +508,10 @@ function cleanAttrs(raw) {
|
|
|
471
508
|
if (/javascript:|vbscript:/.test(plain) || lname === "xml:base") continue;
|
|
472
509
|
let v = value;
|
|
473
510
|
if (lname === "style" || /url\(/.test(plain)) {
|
|
474
|
-
|
|
475
|
-
|
|
511
|
+
const css = cleanCss(decodeXmlEntities(v));
|
|
512
|
+
const refs = [...decodeForCheck(css).matchAll(/url\(([^)]*)\)/g)].map((m2) => m2[1]);
|
|
513
|
+
if (refs.some((r) => !safeUrlRef(r))) continue;
|
|
514
|
+
v = escapeXmlText(css);
|
|
476
515
|
}
|
|
477
516
|
out.push(`${name}="${v.replace(/"/g, """)}"`);
|
|
478
517
|
}
|
|
@@ -486,7 +525,7 @@ function sanitizeOnce(svg) {
|
|
|
486
525
|
while (i < svg.length) {
|
|
487
526
|
const lt = svg.indexOf("<", i);
|
|
488
527
|
const text2 = lt < 0 ? svg.slice(i) : svg.slice(i, lt);
|
|
489
|
-
if (!skip.length) out += inStyle ? cleanCss(text2) : text2;
|
|
528
|
+
if (!skip.length) out += inStyle ? escapeXmlText(cleanCss(decodeXmlEntities(text2))) : text2;
|
|
490
529
|
if (lt < 0) break;
|
|
491
530
|
if (svg.startsWith("<!--", lt)) {
|
|
492
531
|
const e = svg.indexOf("-->", lt + 4);
|
|
@@ -496,7 +535,7 @@ function sanitizeOnce(svg) {
|
|
|
496
535
|
if (svg.startsWith("<![CDATA[", lt)) {
|
|
497
536
|
const e = svg.indexOf("]]>", lt);
|
|
498
537
|
const body = svg.slice(lt + 9, e < 0 ? svg.length : e);
|
|
499
|
-
if (!skip.length) out += inStyle ?
|
|
538
|
+
if (!skip.length) out += inStyle ? escapeXmlText(cleanCss(body)) : `<![CDATA[${body}]]>`;
|
|
500
539
|
i = e < 0 ? svg.length : e + 3;
|
|
501
540
|
continue;
|
|
502
541
|
}
|
|
@@ -569,12 +608,12 @@ function sanitizeOnce(svg) {
|
|
|
569
608
|
}
|
|
570
609
|
function sanitizeSvg(svg) {
|
|
571
610
|
let s = expandInternalEntities(svg);
|
|
572
|
-
for (let n2 = 0; n2 <
|
|
611
|
+
for (let n2 = 0; n2 < 8; n2++) {
|
|
573
612
|
const next = sanitizeOnce(s);
|
|
574
|
-
if (next === s)
|
|
613
|
+
if (next === s) return s;
|
|
575
614
|
s = next;
|
|
576
615
|
}
|
|
577
|
-
return
|
|
616
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"/>';
|
|
578
617
|
}
|
|
579
618
|
function svgFacts(svg) {
|
|
580
619
|
const widths = [...svg.matchAll(/stroke-width\s*[:=]\s*["']?\s*([0-9.]+)/gi)].map((m) => Number(m[1])).filter((n2) => n2 > 0);
|
|
@@ -601,7 +640,15 @@ function assetText(a) {
|
|
|
601
640
|
function mimeOf(a) {
|
|
602
641
|
return a.kind === "svg" ? "image/svg+xml" : a.kind === "png" ? "image/png" : a.kind === "jpg" ? "image/jpeg" : "application/octet-stream";
|
|
603
642
|
}
|
|
643
|
+
var MAX_DECODE_PIXELS = 64e6;
|
|
604
644
|
function decodePng(b) {
|
|
645
|
+
try {
|
|
646
|
+
return decodePngUnchecked(b);
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function decodePngUnchecked(b) {
|
|
605
652
|
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
606
653
|
let off = 8;
|
|
607
654
|
let width = 0;
|
|
@@ -629,6 +676,7 @@ function decodePng(b) {
|
|
|
629
676
|
off += 12 + len;
|
|
630
677
|
}
|
|
631
678
|
if (depth !== 8 || interlace !== 0) return null;
|
|
679
|
+
if (!width || !height || width * height > MAX_DECODE_PIXELS) return null;
|
|
632
680
|
const ch = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[ctype];
|
|
633
681
|
if (!ch) return null;
|
|
634
682
|
const total = idat.reduce((a, x) => a + x.length, 0);
|
|
@@ -806,7 +854,7 @@ function isSafeBundlePath(rel) {
|
|
|
806
854
|
return rel.split("/").every((p) => p !== "" && p !== "." && p !== ".." && !p.startsWith(".") && p.length <= 255);
|
|
807
855
|
}
|
|
808
856
|
function readBundle(files) {
|
|
809
|
-
const jsonKey = Object.keys(files).
|
|
857
|
+
const jsonKey = Object.keys(files).filter((k) => k === "icon.json" || k.endsWith("/icon.json")).sort((a, b) => a.split("/").length - b.split("/").length || a.length - b.length)[0];
|
|
810
858
|
if (!jsonKey) throw new IconFormatError("icon.json not found in the bundle");
|
|
811
859
|
const prefix = jsonKey.slice(0, -"icon.json".length);
|
|
812
860
|
const base = parseIconJson(strFromU8(files[jsonKey]));
|
|
@@ -815,7 +863,7 @@ function readBundle(files) {
|
|
|
815
863
|
for (const [k, v] of Object.entries(files)) {
|
|
816
864
|
if (!k.startsWith(prefix) || k === jsonKey || k.endsWith("/")) continue;
|
|
817
865
|
const rel = k.slice(prefix.length);
|
|
818
|
-
if (!isSafeBundlePath(rel)) continue;
|
|
866
|
+
if (!isSafeBundlePath(rel) || rel.endsWith(".fresnel-tmp")) continue;
|
|
819
867
|
if (rel.startsWith("Assets/") && rel.split("/").length === 2) {
|
|
820
868
|
const name = rel.slice("Assets/".length);
|
|
821
869
|
if (name) assets[name] = makeAsset(name, v);
|
|
@@ -850,10 +898,12 @@ function syncFeatures(doc) {
|
|
|
850
898
|
}
|
|
851
899
|
function writeBundle(doc, extra = {}) {
|
|
852
900
|
const synced = { props: { ...doc.props }, groups: doc.groups };
|
|
901
|
+
if (synced.props.features) synced.props.features = structuredClone(synced.props.features);
|
|
853
902
|
syncFeatures(synced);
|
|
854
903
|
const files = { ...extra };
|
|
855
|
-
files["icon.json"] = strToU8(serializeIconJson({ ...doc, props: synced.props }));
|
|
856
904
|
for (const a of Object.values(doc.assets)) files[`Assets/${a.name}`] = a.bytes;
|
|
905
|
+
delete files["icon.json"];
|
|
906
|
+
files["icon.json"] = strToU8(serializeIconJson({ ...doc, props: synced.props }));
|
|
857
907
|
return files;
|
|
858
908
|
}
|
|
859
909
|
function unzipBundle(zip) {
|
|
@@ -902,8 +952,13 @@ function resolvePath(doc, path) {
|
|
|
902
952
|
const p = path.trim();
|
|
903
953
|
if (p === "" || p === "document" || p === "/") return { kind: "document" };
|
|
904
954
|
const whole = doc.groups.filter((g2) => nameOf(g2).toLowerCase() === p.toLowerCase());
|
|
905
|
-
if (whole.length === 1) return { kind: "group", id: whole[0].id };
|
|
906
955
|
const [gp, lp, ...rest] = p.split("/");
|
|
956
|
+
if (whole.length === 1) {
|
|
957
|
+
const g2 = lp !== void 0 && !rest.length ? doc.groups.find((x) => nameOf(x).toLowerCase() === gp.trim().toLowerCase()) : void 0;
|
|
958
|
+
if (g2?.layers.some((l2) => nameOf(l2).toLowerCase() === lp.trim().toLowerCase()))
|
|
959
|
+
throw new OpError(`"${path}" matches both a group and a layer; use #index (e.g. "#${doc.groups.indexOf(whole[0])}")`);
|
|
960
|
+
return { kind: "group", id: whole[0].id };
|
|
961
|
+
}
|
|
907
962
|
if (rest.length) throw new OpError(`Invalid target "${path}". Use "Group" or "Group/Layer".`);
|
|
908
963
|
const pick = (list, key, what) => {
|
|
909
964
|
const k = key.trim();
|
|
@@ -940,9 +995,10 @@ function cleanName(name) {
|
|
|
940
995
|
}
|
|
941
996
|
function uniqueName(existing, wanted0) {
|
|
942
997
|
const wanted = cleanName(wanted0) || "Untitled";
|
|
943
|
-
|
|
998
|
+
const taken = new Set(existing.map((n2) => n2.toLowerCase()));
|
|
999
|
+
if (!taken.has(wanted.toLowerCase())) return wanted;
|
|
944
1000
|
let i = 2;
|
|
945
|
-
while (
|
|
1001
|
+
while (taken.has(`${wanted} ${i}`.toLowerCase())) i++;
|
|
946
1002
|
return `${wanted} ${i}`;
|
|
947
1003
|
}
|
|
948
1004
|
function addGroup(doc, opts = {}) {
|
|
@@ -963,10 +1019,15 @@ function clampIndex(i, len) {
|
|
|
963
1019
|
function addAsset(doc, asset) {
|
|
964
1020
|
let name = asset.name.replace(/[\\/\u0000-\u001f:]/g, "_").replace(/^[.\s]+/, "").trim() || "image";
|
|
965
1021
|
const dot = name.lastIndexOf(".");
|
|
966
|
-
const
|
|
967
|
-
|
|
1022
|
+
const ext = dot > 0 ? name.slice(dot, dot + 16) : "";
|
|
1023
|
+
let stem = dot > 0 ? name.slice(0, dot) : name;
|
|
1024
|
+
while (new TextEncoder().encode(stem).length > 200) stem = stem.slice(0, -1);
|
|
1025
|
+
name = stem + ext;
|
|
1026
|
+
const clash = (n2) => Object.values(doc.assets).find((a) => a.name.toLowerCase() === n2.toLowerCase());
|
|
968
1027
|
let i = 2;
|
|
969
|
-
|
|
1028
|
+
for (let c = clash(name); c && !sameBytes(c.bytes, asset.bytes); c = clash(name)) name = `${stem} ${i++}${ext}`;
|
|
1029
|
+
const same = clash(name);
|
|
1030
|
+
if (same) return same.name;
|
|
970
1031
|
doc.assets[name] = { ...asset, name };
|
|
971
1032
|
return name;
|
|
972
1033
|
}
|
|
@@ -990,6 +1051,11 @@ function addLayer(doc, groupId, asset, opts = {}) {
|
|
|
990
1051
|
return l;
|
|
991
1052
|
}
|
|
992
1053
|
function setProp(doc, ref, key, value, appearance, detached) {
|
|
1054
|
+
const old = key === "image-name" && !detached && ref.kind === "layer" ? imagesOf([findLayer(doc, ref.id)?.layer].filter((l) => !!l)) : null;
|
|
1055
|
+
setPropRaw(doc, ref, key, value, appearance, detached);
|
|
1056
|
+
if (old?.size) pruneAssets(doc, old);
|
|
1057
|
+
}
|
|
1058
|
+
function setPropRaw(doc, ref, key, value, appearance, detached) {
|
|
993
1059
|
const node = detached ? { kind: "layer", props: detached.props } : nodeOf(doc, ref);
|
|
994
1060
|
if (key === "layers" || key === "groups") throw new OpError(`"${key}" can't be set directly`);
|
|
995
1061
|
if (key.endsWith("-specializations")) throw new OpError(`Set "${key.replace(/-specializations$/, "")}" with an appearance instead`);
|
|
@@ -1018,31 +1084,40 @@ function normalizeFillColors(f) {
|
|
|
1018
1084
|
const o = { ...f };
|
|
1019
1085
|
if (typeof o.solid === "string") o.solid = normalizeColorInput(o.solid);
|
|
1020
1086
|
if (typeof o["automatic-gradient"] === "string") o["automatic-gradient"] = normalizeColorInput(o["automatic-gradient"]);
|
|
1021
|
-
if (Array.isArray(o["linear-gradient"]))
|
|
1087
|
+
if (Array.isArray(o["linear-gradient"])) {
|
|
1088
|
+
if (!o["linear-gradient"].every((c) => typeof c === "string")) throw new OpError("linear-gradient takes a list of color strings");
|
|
1089
|
+
o["linear-gradient"] = o["linear-gradient"].map(normalizeColorInput);
|
|
1090
|
+
}
|
|
1022
1091
|
return o;
|
|
1023
1092
|
}
|
|
1024
1093
|
function removeNode(doc, ref) {
|
|
1025
1094
|
if (ref.kind === "document") throw new OpError("The document can't be removed");
|
|
1095
|
+
let removedImages;
|
|
1026
1096
|
if (ref.kind === "group") {
|
|
1027
1097
|
const i = doc.groups.findIndex((g) => g.id === ref.id);
|
|
1028
1098
|
if (i < 0) throw new OpError("Group not found");
|
|
1099
|
+
removedImages = imagesOf(doc.groups[i].layers);
|
|
1029
1100
|
doc.groups.splice(i, 1);
|
|
1030
1101
|
} else {
|
|
1031
1102
|
const f = findLayer(doc, ref.id);
|
|
1032
1103
|
if (!f) throw new OpError("Layer not found");
|
|
1104
|
+
removedImages = imagesOf([f.layer]);
|
|
1033
1105
|
f.group.layers.splice(f.index, 1);
|
|
1034
1106
|
}
|
|
1035
|
-
pruneAssets(doc);
|
|
1107
|
+
pruneAssets(doc, removedImages);
|
|
1036
1108
|
}
|
|
1037
|
-
function
|
|
1109
|
+
function imagesOf(layers) {
|
|
1038
1110
|
const used = /* @__PURE__ */ new Set();
|
|
1039
|
-
for (const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1111
|
+
for (const l of layers) {
|
|
1112
|
+
const p = l.props["image-name"];
|
|
1113
|
+
if (p?.plain && typeof p.plain.value === "string") used.add(p.plain.value);
|
|
1114
|
+
for (const s of p?.specs ?? []) if (typeof s.value === "string") used.add(s.value);
|
|
1115
|
+
}
|
|
1116
|
+
return used;
|
|
1117
|
+
}
|
|
1118
|
+
function pruneAssets(doc, only) {
|
|
1119
|
+
const used = imagesOf(doc.groups.flatMap((g) => g.layers));
|
|
1120
|
+
for (const k of Object.keys(doc.assets)) if (!used.has(k) && (!only || only.has(k))) delete doc.assets[k];
|
|
1046
1121
|
}
|
|
1047
1122
|
function moveNode(doc, ref, index, toGroupId) {
|
|
1048
1123
|
if (ref.kind === "document") throw new OpError("The document can't be moved");
|
|
@@ -1109,9 +1184,10 @@ function validate(doc) {
|
|
|
1109
1184
|
let anyTinted = false;
|
|
1110
1185
|
const scan = (props) => {
|
|
1111
1186
|
for (const p of Object.values(props)) {
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1187
|
+
for (const v of variantsOf(p)) {
|
|
1188
|
+
if (v.appearance === "dark") anyDark = true;
|
|
1189
|
+
if (v.appearance === "tinted") anyTinted = true;
|
|
1190
|
+
}
|
|
1115
1191
|
}
|
|
1116
1192
|
};
|
|
1117
1193
|
scan(doc.props);
|
|
@@ -1142,19 +1218,22 @@ function validate(doc) {
|
|
|
1142
1218
|
}
|
|
1143
1219
|
if (Math.abs(a.width - a.height) > 0.5) {
|
|
1144
1220
|
push("V-03", `"${n2}" is ${round(a.width)}\xD7${round(a.height)}. Apple recommends square, unmasked layers.`, lName, lref);
|
|
1145
|
-
} else if (a.kind !== "svg" &&
|
|
1146
|
-
push("V-03", `"${n2}"
|
|
1221
|
+
} else if (a.kind !== "svg" && pos.scale > 1.01) {
|
|
1222
|
+
push("V-03", `"${n2}" (${a.width}\xD7${a.height} px) is scaled up ${round(pos.scale)}\xD7 and may look soft. Use a larger image or a vector.`, lName, lref);
|
|
1147
1223
|
}
|
|
1148
1224
|
if (a.kind === "png" && a.width === a.height && a.width >= 256) {
|
|
1149
1225
|
const bmp = safeDecode(a.bytes);
|
|
1150
1226
|
if (bmp && looksPreMasked(bmp)) push("V-04", `"${n2}" seems to have rounded corners baked in. The system masks icons itself.`, lName, lref);
|
|
1151
1227
|
}
|
|
1152
1228
|
if (a.kind === "svg") {
|
|
1153
|
-
const
|
|
1229
|
+
const text2 = assetText(a);
|
|
1230
|
+
const f = svgFacts(text2);
|
|
1231
|
+
const vb = /<svg\b[^>]*\sviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)/i.exec(text2);
|
|
1232
|
+
const unit2 = vb && Number(vb[1]) > 0 ? svgSize(text2).width / Number(vb[1]) : 1;
|
|
1154
1233
|
if (f.hasText) push("V-07", `"${n2}" contains <text>. Convert text to outlines.`, lName, lref);
|
|
1155
1234
|
if (f.hasFilter) push("V-07", `"${n2}" uses SVG filters (blur, glow, shadow). Let the system draw effects.`, lName, lref);
|
|
1156
|
-
if (f.minStrokeWidth !== null && f.minStrokeWidth * pos.scale < 10)
|
|
1157
|
-
push("V-06", `"${n2}" has strokes as thin as ${f.minStrokeWidth}. They'll be under 0.16 px at 16 px.`, lName, lref);
|
|
1235
|
+
if (f.minStrokeWidth !== null && f.minStrokeWidth * unit2 * pos.scale < 10)
|
|
1236
|
+
push("V-06", `"${n2}" has strokes as thin as ${round(f.minStrokeWidth * unit2)} pt. They'll be under 0.16 px at 16 px.`, lName, lref);
|
|
1158
1237
|
}
|
|
1159
1238
|
}
|
|
1160
1239
|
}
|
|
@@ -1370,10 +1449,24 @@ function moveBy(doc, ref, dx, dy, ctx, variant) {
|
|
|
1370
1449
|
props = f.layer.props;
|
|
1371
1450
|
scale = resolveProp(f.group.props.position, ctx, null)?.scale ?? 1;
|
|
1372
1451
|
}
|
|
1373
|
-
const p = resolveProp(props.position, ctx, { scale: 1, "translation-in-points": [0, 0] }) ?? { scale: 1, "translation-in-points": [0, 0] };
|
|
1374
|
-
const [x, y] = p["translation-in-points"] ?? [0, 0];
|
|
1375
1452
|
const round2 = (v) => Math.round(v * 100) / 100;
|
|
1376
|
-
|
|
1453
|
+
const shift = (p) => {
|
|
1454
|
+
const q = p ?? { scale: 1, "translation-in-points": [0, 0] };
|
|
1455
|
+
const [x, y] = q["translation-in-points"] ?? [0, 0];
|
|
1456
|
+
return { ...q, "translation-in-points": [round2(x + dx / scale), round2(y + dy / scale)] };
|
|
1457
|
+
};
|
|
1458
|
+
const vv = toVariant(variant);
|
|
1459
|
+
if (vv.appearance !== void 0 || vv.idiom !== void 0) {
|
|
1460
|
+
const cur = variantValue(props.position, vv) ?? resolveProp(props.position, ctx, null);
|
|
1461
|
+
setProp(doc, ref, "position", shift(cur), variant);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const pos = props.position;
|
|
1465
|
+
setProp(doc, ref, "position", shift(baseValue(pos)), void 0);
|
|
1466
|
+
for (const e of pos?.specs ?? []) {
|
|
1467
|
+
if (e.appearance === void 0 && e.idiom === void 0) continue;
|
|
1468
|
+
if (e.value && typeof e.value === "object") e.value = shift(e.value);
|
|
1469
|
+
}
|
|
1377
1470
|
}
|
|
1378
1471
|
function alignNodes(doc, refs, how, ctx, opts = {}) {
|
|
1379
1472
|
const boxes = refs.map((r) => ({ ref: r, box: boxOf(doc, r, ctx) })).filter((x) => !!x.box);
|
|
@@ -1454,6 +1547,7 @@ async function saveIcon(path, doc) {
|
|
|
1454
1547
|
const assetsDir = join(root, "Assets");
|
|
1455
1548
|
await assertNoSymlink(root, assetsDir);
|
|
1456
1549
|
await mkdir(assetsDir, { recursive: true });
|
|
1550
|
+
const written = [];
|
|
1457
1551
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
1458
1552
|
const target = resolve(root, ...rel.split("/"));
|
|
1459
1553
|
await assertNoSymlink(root, target);
|
|
@@ -1463,16 +1557,20 @@ async function saveIcon(path, doc) {
|
|
|
1463
1557
|
await rm(tmp, { force: true });
|
|
1464
1558
|
await writeFile(tmp, bytes, { flag: "wx" });
|
|
1465
1559
|
await rename(tmp, target);
|
|
1560
|
+
written.push(target);
|
|
1466
1561
|
}
|
|
1467
1562
|
const keep = new Set(Object.keys(doc.assets));
|
|
1468
1563
|
await assertNoSymlink(root, assetsDir);
|
|
1469
1564
|
for (const e of await readdir(assetsDir, { withFileTypes: true })) {
|
|
1470
|
-
if (e.isFile()
|
|
1565
|
+
if (!e.isFile() || keep.has(e.name)) continue;
|
|
1566
|
+
if (e.name.endsWith(".fresnel-tmp") || isSafeBundlePath(`Assets/${e.name}`) && !e.name.startsWith(".")) await rm(join(assetsDir, e.name));
|
|
1471
1567
|
}
|
|
1568
|
+
return written;
|
|
1472
1569
|
}
|
|
1473
1570
|
async function writeFiles(dir, files) {
|
|
1474
1571
|
const written = [];
|
|
1475
1572
|
const root = resolve(dir);
|
|
1573
|
+
if ((await lstat(root).catch(() => null))?.isSymbolicLink()) throw new Error(`Refusing to write through a symbolic link: ${dir}`);
|
|
1476
1574
|
for (const [rel, bytes] of Object.entries(files)) {
|
|
1477
1575
|
const target = resolve(root, ...rel.split("/"));
|
|
1478
1576
|
const r = relative(root, target);
|
|
@@ -1523,7 +1621,7 @@ function contentBoxOf(asset) {
|
|
|
1523
1621
|
}
|
|
1524
1622
|
function svgAssetsNeedingBounds(doc) {
|
|
1525
1623
|
const out = /* @__PURE__ */ new Set();
|
|
1526
|
-
const hasGradient = (p) => [p?.plain?.value, ...(p?.specs ?? []).map((s) => s.value)].some((v) => !!v && typeof v === "object" && "linear-gradient" in v);
|
|
1624
|
+
const hasGradient = (p) => [p?.plain?.value, ...(p?.specs ?? []).map((s) => s.value)].some((v) => !!v && typeof v === "object" && ("linear-gradient" in v || "automatic-gradient" in v));
|
|
1527
1625
|
for (const g of doc.groups)
|
|
1528
1626
|
for (const l of g.layers) {
|
|
1529
1627
|
if (!hasGradient(l.props.fill)) continue;
|
|
@@ -1552,12 +1650,12 @@ function squirclePath(size = CANVAS, exponent = 4.4, steps = 192) {
|
|
|
1552
1650
|
}
|
|
1553
1651
|
var SQUIRCLE = squirclePath();
|
|
1554
1652
|
var C = CANVAS;
|
|
1555
|
-
function maskPath(shape) {
|
|
1653
|
+
function maskPath(shape, pad2 = 0) {
|
|
1556
1654
|
if (shape === "circle") {
|
|
1557
1655
|
const r = C / 2 + 32;
|
|
1558
1656
|
return `M${C / 2},${C / 2 - r} A${r},${r} 0 1 1 ${C / 2},${C / 2 + r} A${r},${r} 0 1 1 ${C / 2},${C / 2 - r}Z`;
|
|
1559
1657
|
}
|
|
1560
|
-
if (shape === "none") return `
|
|
1658
|
+
if (shape === "none") return `M${-pad2},${-pad2} H${C + pad2} V${C + pad2} H${-pad2}Z`;
|
|
1561
1659
|
return SQUIRCLE;
|
|
1562
1660
|
}
|
|
1563
1661
|
|
|
@@ -1583,6 +1681,17 @@ function hexLuma(hex) {
|
|
|
1583
1681
|
const v = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
|
|
1584
1682
|
return luminance({ r: v(0), g: v(2), b: v(4), a: 1 });
|
|
1585
1683
|
}
|
|
1684
|
+
var NAMED = { white: 1, black: 0 };
|
|
1685
|
+
function paintLuma(v) {
|
|
1686
|
+
if (v.startsWith("#")) return hexLuma(v);
|
|
1687
|
+
const m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)/i.exec(v);
|
|
1688
|
+
if (m) {
|
|
1689
|
+
const ch = (x) => Math.min(1, Math.max(0, x.endsWith("%") ? Number(x.slice(0, -1)) / 100 : Number(x) / 255));
|
|
1690
|
+
return luminance({ r: ch(m[1]), g: ch(m[2]), b: ch(m[3]), a: 1 });
|
|
1691
|
+
}
|
|
1692
|
+
const named = NAMED[v.toLowerCase()];
|
|
1693
|
+
return named ?? null;
|
|
1694
|
+
}
|
|
1586
1695
|
function assetLuma(a) {
|
|
1587
1696
|
const hit = cache2.get(a.bytes);
|
|
1588
1697
|
if (hit !== void 0) return hit;
|
|
@@ -1603,10 +1712,11 @@ function assetLuma(a) {
|
|
|
1603
1712
|
}
|
|
1604
1713
|
} else if (a.kind === "svg") {
|
|
1605
1714
|
const text2 = new TextDecoder().decode(a.display ?? a.bytes);
|
|
1606
|
-
const
|
|
1607
|
-
const
|
|
1608
|
-
const
|
|
1609
|
-
if (
|
|
1715
|
+
const paint = /(?:fill|stop-color)\s*[:=]\s*["']?\s*(rgba?\([^)]*\)|[^"';>\s]+)/gi;
|
|
1716
|
+
const values = [...text2.matchAll(paint)].map((m) => m[1].trim()).filter((v) => !/^(none|transparent|inherit|currentcolor|url\()/i.test(v));
|
|
1717
|
+
const ls = values.map(paintLuma).filter((x) => x !== null);
|
|
1718
|
+
if (ls.length) l = ls.reduce((x, y) => x + y, 0) / ls.length;
|
|
1719
|
+
else l = values.length ? 0.5 : 0;
|
|
1610
1720
|
}
|
|
1611
1721
|
cache2.set(a.bytes, l);
|
|
1612
1722
|
return l;
|
|
@@ -1624,6 +1734,7 @@ function rasterRenderOptions(ro) {
|
|
|
1624
1734
|
};
|
|
1625
1735
|
}
|
|
1626
1736
|
var C2 = CANVAS;
|
|
1737
|
+
var PAD = C2 / 4;
|
|
1627
1738
|
var H2 = C2 / 2;
|
|
1628
1739
|
var b64Cache = /* @__PURE__ */ new WeakMap();
|
|
1629
1740
|
var b64 = (bytes) => {
|
|
@@ -1642,11 +1753,32 @@ var b64 = (bytes) => {
|
|
|
1642
1753
|
};
|
|
1643
1754
|
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
1644
1755
|
var n = (x) => +x.toFixed(3);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1756
|
+
var DEFAULT_TINT = { r: 0.25, g: 0.65, b: 0.63, a: 1 };
|
|
1757
|
+
function tintToRGBA(input) {
|
|
1758
|
+
try {
|
|
1759
|
+
const c = parseColor(normalizeColorInput(input));
|
|
1760
|
+
return c ? { ...toRGBA(c), a: 1 } : DEFAULT_TINT;
|
|
1761
|
+
} catch {
|
|
1762
|
+
return DEFAULT_TINT;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
var num2 = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
1766
|
+
function safePosition(p) {
|
|
1767
|
+
if (!p || typeof p !== "object") return null;
|
|
1768
|
+
const o = p;
|
|
1769
|
+
const t = Array.isArray(o["translation-in-points"]) ? o["translation-in-points"] : [];
|
|
1770
|
+
const scale = num2(o.scale, 1);
|
|
1771
|
+
return { scale: scale > 0 ? scale : 1, tx: num2(t[0], 0), ty: num2(t[1], 0) };
|
|
1772
|
+
}
|
|
1773
|
+
function safeOrientation(o) {
|
|
1774
|
+
const pt = (v) => {
|
|
1775
|
+
const q = v;
|
|
1776
|
+
return q && typeof q === "object" && Number.isFinite(q.x) && Number.isFinite(q.y) ? { x: q.x, y: q.y } : null;
|
|
1777
|
+
};
|
|
1778
|
+
const r = o;
|
|
1779
|
+
const start = r && typeof r === "object" ? pt(r.start) : null;
|
|
1780
|
+
const stop = r && typeof r === "object" ? pt(r.stop) : null;
|
|
1781
|
+
return start && stop ? { start, stop } : void 0;
|
|
1650
1782
|
}
|
|
1651
1783
|
var colorOf = (s) => {
|
|
1652
1784
|
const c = typeof s === "string" ? parseColor(s) : null;
|
|
@@ -1693,10 +1825,10 @@ function paintFor(fill, defs, box, darkSystem) {
|
|
|
1693
1825
|
const c = colorOf(f["automatic-gradient"]);
|
|
1694
1826
|
return lin(shade(c, 0.22), shade(c, -0.08));
|
|
1695
1827
|
}
|
|
1696
|
-
if (Array.isArray(f["linear-gradient"])) {
|
|
1828
|
+
if (Array.isArray(f["linear-gradient"]) && f["linear-gradient"].length) {
|
|
1697
1829
|
const [a, b] = f["linear-gradient"];
|
|
1698
|
-
const o = f.orientation;
|
|
1699
|
-
return lin(colorOf(a), colorOf(b), o?.start, o?.stop);
|
|
1830
|
+
const o = safeOrientation(f.orientation);
|
|
1831
|
+
return lin(colorOf(a), colorOf(b ?? a), o?.start, o?.stop);
|
|
1700
1832
|
}
|
|
1701
1833
|
return null;
|
|
1702
1834
|
}
|
|
@@ -1709,22 +1841,25 @@ function renderSvg(doc, opts = {}) {
|
|
|
1709
1841
|
const dark = isDarkAppearance(appearance);
|
|
1710
1842
|
const isClear = appearance === "clear-light" || appearance === "clear-dark";
|
|
1711
1843
|
const isTinted = appearance === "tinted-light" || appearance === "tinted-dark";
|
|
1712
|
-
const tint =
|
|
1844
|
+
const tint = opts.tint ? tintToRGBA(opts.tint) : DEFAULT_TINT;
|
|
1845
|
+
const idioms = idiomsFor(ctx.idiom);
|
|
1846
|
+
const forPlatform = (s) => s.idiom === void 0 || idioms.includes(s.idiom);
|
|
1713
1847
|
const angle = (opts.lightAngle ?? 0) * Math.PI / 180;
|
|
1714
1848
|
const toLight = { x: Math.sin(angle), y: -Math.cos(angle) };
|
|
1715
1849
|
const defs = new Defs(opts.idPrefix ?? "");
|
|
1716
1850
|
const body = [];
|
|
1717
1851
|
const fillProp = doc.props.fill;
|
|
1718
1852
|
let bgFill = resolveProp(fillProp, ctx, "automatic");
|
|
1719
|
-
const darkOverridden = !!fillProp?.specs?.some((s) => s.appearance === "dark");
|
|
1853
|
+
const darkOverridden = !!fillProp?.specs?.some((s) => s.appearance === "dark" && forPlatform(s));
|
|
1720
1854
|
let darken = 0;
|
|
1721
1855
|
if (dark && !darkOverridden && bgFill !== "automatic" && bgFill !== "system-dark" && bgFill !== "none") darken = 0.62;
|
|
1722
1856
|
let bgPaint;
|
|
1723
|
-
const P = mask === "circle" ? 32 : 0;
|
|
1857
|
+
const P = mask === "circle" || mask === "none" && ctx.idiom === "watchOS" ? 32 : 0;
|
|
1724
1858
|
const full = { x: -P, y: -P, w: C2 + 2 * P, h: C2 + 2 * P };
|
|
1725
|
-
const monoOverride = (isClear || isTinted) && !!fillProp?.specs?.some((s) => s.appearance === "tinted"
|
|
1859
|
+
const monoOverride = (isClear || isTinted) && !!fillProp?.specs?.some((s) => s.appearance === "tinted" && forPlatform(s));
|
|
1726
1860
|
if (monoOverride) {
|
|
1727
|
-
|
|
1861
|
+
const mono = resolveProp(fillProp, { appearance: dark ? "tinted-dark" : "tinted-light", idiom: ctx.idiom }, "automatic");
|
|
1862
|
+
bgPaint = paintFor(mono ?? "automatic", defs, full, dark);
|
|
1728
1863
|
} else if (isClear) {
|
|
1729
1864
|
bgPaint = dark ? paintFor({ "linear-gradient": ["srgb:0.24,0.25,0.28,0.92", "srgb:0.11,0.12,0.13,0.92"] }, defs, full, true) : paintFor({ "linear-gradient": ["srgb:0.97,0.98,0.99,0.86", "srgb:0.84,0.87,0.91,0.86"] }, defs, full, false);
|
|
1730
1865
|
} else if (isTinted) {
|
|
@@ -1765,7 +1900,14 @@ function renderSvg(doc, opts = {}) {
|
|
|
1765
1900
|
plainId = plain;
|
|
1766
1901
|
}
|
|
1767
1902
|
function drawGroup(g, backdropId) {
|
|
1768
|
-
const
|
|
1903
|
+
const gp = safePosition(resolveProp(g.props.position, ctx, null));
|
|
1904
|
+
const gs = gp?.scale ?? 1;
|
|
1905
|
+
const gtx = gp?.tx ?? 0;
|
|
1906
|
+
const gty = gp?.ty ?? 0;
|
|
1907
|
+
const gx0 = H2 + (-PAD - H2 - gtx) / gs;
|
|
1908
|
+
const gy0 = H2 + (-PAD - H2 - gty) / gs;
|
|
1909
|
+
const gw = (C2 + 2 * PAD) / gs;
|
|
1910
|
+
const localRegion = `x="${n(gx0)}" y="${n(gy0)}" width="${n(gw)}" height="${n(gw)}"`;
|
|
1769
1911
|
const layers = FRONT_FIRST ? [...g.layers].reverse() : [...g.layers];
|
|
1770
1912
|
const placed = [];
|
|
1771
1913
|
for (const l of layers) {
|
|
@@ -1773,14 +1915,23 @@ function renderSvg(doc, opts = {}) {
|
|
|
1773
1915
|
const name = resolveProp(l.props["image-name"], ctx, "");
|
|
1774
1916
|
const asset = doc.assets[name];
|
|
1775
1917
|
if (!asset || asset.kind === "pdf" || asset.kind === "other") continue;
|
|
1776
|
-
|
|
1918
|
+
if (!(asset.width > 0 && asset.height > 0 && Number.isFinite(asset.width) && Number.isFinite(asset.height))) continue;
|
|
1919
|
+
const pos = safePosition(resolveProp(l.props.position, ctx, null));
|
|
1777
1920
|
const s = pos?.scale ?? 1;
|
|
1778
|
-
const [tx, ty] = pos?.["translation-in-points"] ?? [0, 0];
|
|
1779
1921
|
const w = asset.width * s;
|
|
1780
1922
|
const h = asset.height * s;
|
|
1781
|
-
|
|
1923
|
+
const x = H2 + (pos?.tx ?? 0) - w / 2;
|
|
1924
|
+
const y = H2 + (pos?.ty ?? 0) - h / 2;
|
|
1925
|
+
const cx0 = H2 + gtx + (x - H2) * gs;
|
|
1926
|
+
const cy0 = H2 + gty + (y - H2) * gs;
|
|
1927
|
+
const cx1 = cx0 + w * gs;
|
|
1928
|
+
const cy1 = cy0 + h * gs;
|
|
1929
|
+
if (cx1 < -P || cy1 < -P || cx0 > C2 + P || cy0 > C2 + P) continue;
|
|
1930
|
+
placed.push({ layer: l, asset, x, y, w, h });
|
|
1782
1931
|
}
|
|
1783
1932
|
if (!placed.length) return null;
|
|
1933
|
+
const groupOpacity = Math.min(1, Math.max(0, num2(resolveProp(g.props.opacity, ctx, 1), 1)));
|
|
1934
|
+
if (groupOpacity <= 0) return null;
|
|
1784
1935
|
const imgId = /* @__PURE__ */ new Map();
|
|
1785
1936
|
const layerEls = [];
|
|
1786
1937
|
const glassEls = [];
|
|
@@ -1797,7 +1948,7 @@ function renderSvg(doc, opts = {}) {
|
|
|
1797
1948
|
const s = p.w / p.asset.width;
|
|
1798
1949
|
const place = `transform="translate(${n(p.x)} ${n(p.y)}) scale(${n(s)})"`;
|
|
1799
1950
|
const fill = resolveProp(p.layer.props.fill, ctx, null);
|
|
1800
|
-
const opacity = resolveProp(p.layer.props.opacity, ctx, 1);
|
|
1951
|
+
const opacity = Math.min(1, Math.max(0, num2(resolveProp(p.layer.props.opacity, ctx, 1), 1)));
|
|
1801
1952
|
const blend = cssBlend(resolveProp(p.layer.props["blend-mode"], ctx, "normal"));
|
|
1802
1953
|
const cb = contentBoxOf(p.asset);
|
|
1803
1954
|
const fbox = cb ? { x: p.x + cb.x * p.w, y: p.y + cb.y * p.h, w: cb.w * p.w, h: cb.h * p.h } : { x: p.x, y: p.y, w: p.w, h: p.h };
|
|
@@ -1805,7 +1956,7 @@ function renderSvg(doc, opts = {}) {
|
|
|
1805
1956
|
let el;
|
|
1806
1957
|
if (paint) {
|
|
1807
1958
|
const mid = defs.id("lm");
|
|
1808
|
-
defs.add(`<mask id="${mid}" mask-type="alpha" style="mask-type:alpha" maskUnits="userSpaceOnUse"
|
|
1959
|
+
defs.add(`<mask id="${mid}" mask-type="alpha" style="mask-type:alpha" maskUnits="userSpaceOnUse" ${localRegion}><use href="#${iid}" ${place}/></mask>`);
|
|
1809
1960
|
el = `<rect x="${n(p.x)}" y="${n(p.y)}" width="${n(p.w)}" height="${n(p.h)}" fill="${paint}" mask="url(#${mid})"/>`;
|
|
1810
1961
|
} else {
|
|
1811
1962
|
el = `<use href="#${iid}" ${place}/>`;
|
|
@@ -1817,43 +1968,53 @@ function renderSvg(doc, opts = {}) {
|
|
|
1817
1968
|
glassLumas.push(fill && fill !== "automatic" ? fillLuma(fill, dark) ?? assetLuma(p.asset) : assetLuma(p.asset));
|
|
1818
1969
|
}
|
|
1819
1970
|
}
|
|
1820
|
-
const gTransform =
|
|
1971
|
+
const gTransform = gp ? ` transform="translate(${n(H2 + gtx)} ${n(H2 + gty)}) scale(${n(gs)}) translate(${-H2} ${-H2})"` : "";
|
|
1972
|
+
const canvasRegion = `filterUnits="userSpaceOnUse" x="${-PAD}" y="${-PAD}" width="${C2 + 2 * PAD}" height="${C2 + 2 * PAD}"`;
|
|
1973
|
+
const groupRegion = `filterUnits="userSpaceOnUse" ${localRegion}`;
|
|
1821
1974
|
const contentId = defs.id("gc");
|
|
1822
1975
|
defs.add(`<g id="${contentId}"${gTransform}>${layerEls.join("")}</g>`);
|
|
1823
1976
|
contentIds.push(contentId);
|
|
1824
1977
|
const out = [];
|
|
1825
|
-
const
|
|
1978
|
+
const shadow0 = resolveProp(g.props.shadow, ctx, { kind: "neutral", opacity: 0.5 });
|
|
1979
|
+
const shadow = shadow0 && typeof shadow0 === "object" ? { kind: shadow0.kind, opacity: Math.min(1, Math.max(0, num2(shadow0.opacity, 0.5))) } : null;
|
|
1826
1980
|
if (effects && shadow && shadow.kind !== "none" && shadow.opacity > 0) {
|
|
1827
1981
|
const fid = defs.id("sh");
|
|
1828
1982
|
if (shadow.kind === "neutral") {
|
|
1829
1983
|
defs.add(
|
|
1830
|
-
`<filter id="${fid}"
|
|
1984
|
+
`<filter id="${fid}" ${canvasRegion}><feGaussianBlur in="SourceAlpha" stdDeviation="16"/><feOffset dy="14" result="b"/><feFlood flood-color="#000" flood-opacity="0.45"/><feComposite in2="b" operator="in"/></filter>`
|
|
1831
1985
|
);
|
|
1832
1986
|
} else {
|
|
1833
1987
|
defs.add(
|
|
1834
|
-
`<filter id="${fid}"
|
|
1988
|
+
`<filter id="${fid}" ${canvasRegion}><feGaussianBlur in="SourceGraphic" stdDeviation="18"/><feOffset dy="16"/><feColorMatrix type="matrix" values="0.75 0 0 0 0 0 0.75 0 0 0 0 0 0.75 0 0 0 0 0 0.8 0"/></filter>`
|
|
1835
1989
|
);
|
|
1836
1990
|
}
|
|
1837
1991
|
out.push(`<use href="#${contentId}" filter="url(#${fid})" opacity="${n(shadow.opacity)}"/>`);
|
|
1838
1992
|
}
|
|
1839
|
-
const
|
|
1840
|
-
const
|
|
1993
|
+
const tr0 = resolveProp(g.props.translucency, ctx, { enabled: true, value: 0.5 });
|
|
1994
|
+
const tr = tr0 && typeof tr0 === "object" ? { enabled: !!tr0.enabled, value: num2(tr0.value, 0.5) } : null;
|
|
1995
|
+
const blurRaw = resolveProp(g.props["blur-material"], ctx, void 0);
|
|
1996
|
+
const blurMat = blurRaw === null ? null : num2(blurRaw, 0.2);
|
|
1841
1997
|
const translucent = effects && !!tr?.enabled && tr.value > 0;
|
|
1842
|
-
const
|
|
1998
|
+
const refr0 = resolveProp(g.props.refractivity, ctx, null);
|
|
1999
|
+
const refr = refr0 && typeof refr0 === "object" ? { enabled: !!refr0.enabled, strength: num2(refr0.strength, 0), depth: num2(refr0.depth, 0) } : null;
|
|
1843
2000
|
const refracts = effects && !!refr?.enabled && glassEls.length > 0;
|
|
1844
2001
|
if (translucent || refracts) {
|
|
1845
2002
|
const maskSrc = translucent ? `<use href="#${contentId}"/>` : `<g${gTransform}>${glassEls.join("")}</g>`;
|
|
1846
2003
|
const mid = defs.id("gm");
|
|
1847
|
-
defs.add(`<mask id="${mid}" mask-type="alpha" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2004
|
+
defs.add(`<mask id="${mid}" mask-type="alpha" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="${-PAD}" y="${-PAD}" width="${C2 + 2 * PAD}" height="${C2 + 2 * PAD}">${maskSrc}</mask>`);
|
|
2005
|
+
const blurPx = Math.max(0, (blurMat === null ? 0 : 4 + blurMat * 40) + (refracts ? refr.depth * 10 : 0));
|
|
2006
|
+
let filterAttr = "";
|
|
2007
|
+
if (blurPx > 0) {
|
|
2008
|
+
const fid = defs.id("bk");
|
|
2009
|
+
defs.add(`<filter id="${fid}" ${canvasRegion}><feGaussianBlur stdDeviation="${n(blurPx)}"/></filter>`);
|
|
2010
|
+
filterAttr = ` filter="url(#${fid})"`;
|
|
2011
|
+
}
|
|
2012
|
+
const zoom = 1.02 + (refracts ? refr.strength * 0.12 : 0.015);
|
|
1852
2013
|
out.push(
|
|
1853
|
-
`<g mask="url(#${mid})"><use href="#${backdropId}"
|
|
2014
|
+
`<g mask="url(#${mid})"><use href="#${backdropId}"${filterAttr} transform="translate(${H2} ${H2}) scale(${n(zoom)}) translate(${-H2} ${-H2})"/></g>`
|
|
1854
2015
|
);
|
|
1855
2016
|
}
|
|
1856
|
-
const gOpacity =
|
|
2017
|
+
const gOpacity = refracts && !translucent ? 1 - Math.min(1, Math.max(0, refr.strength)) * 0.3 : 1;
|
|
1857
2018
|
let fadeMask = "";
|
|
1858
2019
|
if (translucent) {
|
|
1859
2020
|
const v = Math.min(1, Math.max(0, tr.value));
|
|
@@ -1879,15 +2040,15 @@ function renderSvg(doc, opts = {}) {
|
|
|
1879
2040
|
const fid = defs.id("sp");
|
|
1880
2041
|
if (specular === "outside") {
|
|
1881
2042
|
defs.add(
|
|
1882
|
-
`<filter id="${fid}"
|
|
2043
|
+
`<filter id="${fid}" ${groupRegion}><feMorphology in="SourceAlpha" operator="dilate" radius="6" result="d"/><feComposite in="d" in2="SourceAlpha" operator="out" result="band"/><feOffset in="SourceAlpha" dx="${n(-dx * 1.6)}" dy="${n(-dy * 1.6)}" result="lit"/><feComposite in="band" in2="lit" operator="in" result="edge"/><feGaussianBlur in="edge" stdDeviation="1.6" result="soft"/><feFlood flood-color="#fff" flood-opacity="0.8"/><feComposite in2="soft" operator="in"/></filter>`
|
|
1883
2044
|
);
|
|
1884
2045
|
} else defs.add(
|
|
1885
|
-
`<filter id="${fid}"
|
|
2046
|
+
`<filter id="${fid}" ${groupRegion}><feOffset in="SourceAlpha" dx="${dx}" dy="${dy}" result="sh"/><feComposite in="SourceAlpha" in2="sh" operator="out" result="edge"/><feGaussianBlur in="edge" stdDeviation="1.6" result="soft"/><feFlood flood-color="#fff" flood-opacity="0.85"/><feComposite in2="soft" operator="in" result="hi"/><feOffset in="SourceAlpha" dx="${n(-dx)}" dy="${n(-dy)}" result="sh2"/><feComposite in="SourceAlpha" in2="sh2" operator="out" result="edge2"/><feGaussianBlur in="edge2" stdDeviation="2" result="soft2"/><feFlood flood-color="#fff" flood-opacity="0.28"/><feComposite in2="soft2" operator="in" result="lo"/><feMerge><feMergeNode in="hi"/><feMergeNode in="lo"/></feMerge></filter>`
|
|
1886
2047
|
);
|
|
1887
2048
|
const glass = individual ? glassEls.map((e) => `<g filter="url(#${fid})">${e}</g>`).join("") : `<g filter="url(#${fid})">${glassEls.join("")}</g>`;
|
|
1888
2049
|
out.push(`<g${gTransform}>${glass}</g>`);
|
|
1889
2050
|
}
|
|
1890
|
-
return out.join("");
|
|
2051
|
+
return groupOpacity < 1 ? `<g opacity="${n(groupOpacity)}">${out.join("")}</g>` : out.join("");
|
|
1891
2052
|
}
|
|
1892
2053
|
let top = `<use href="#${stackId}"/>`;
|
|
1893
2054
|
if ((isClear || isTinted) && contentIds.length) {
|
|
@@ -1909,18 +2070,19 @@ function renderSvg(doc, opts = {}) {
|
|
|
1909
2070
|
}
|
|
1910
2071
|
const mid = defs.id("fm");
|
|
1911
2072
|
defs.add(
|
|
1912
|
-
`<mask id="${mid}" mask-type="alpha" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="
|
|
2073
|
+
`<mask id="${mid}" mask-type="alpha" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="${-PAD}" y="${-PAD}" width="${C2 + 2 * PAD}" height="${C2 + 2 * PAD}">` + contentIds.map((id) => `<use href="#${id}"/>`).join("") + `</mask>`
|
|
1913
2074
|
);
|
|
1914
2075
|
top = `<use href="#${bgStackId}"/><g filter="url(#${fid})"><use href="#${stackId}" mask="url(#${mid})"/></g>`;
|
|
1915
2076
|
}
|
|
1916
2077
|
const clipId = defs.id("clip");
|
|
1917
|
-
const shape = maskPath(mask);
|
|
2078
|
+
const shape = maskPath(mask, P);
|
|
1918
2079
|
defs.add(`<clipPath id="${clipId}"><path d="${shape}"/></clipPath>`);
|
|
1919
|
-
const
|
|
2080
|
+
const inset = Math.min(C2 / 2 - 1, Math.max(0, num2(opts.inset, 0)));
|
|
2081
|
+
const M = inset > 0 ? inset * C2 / (C2 - 2 * inset) : 0;
|
|
1920
2082
|
if (M > 0) {
|
|
1921
2083
|
const sid = defs.id("ish");
|
|
1922
2084
|
defs.add(
|
|
1923
|
-
`<filter id="${sid}" x="-
|
|
2085
|
+
`<filter id="${sid}" filterUnits="userSpaceOnUse" x="${-(M + 64)}" y="${-(M + 64)}" width="${C2 + 2 * (M + 64)}" height="${C2 + 2 * (M + 64)}"><feGaussianBlur in="SourceAlpha" stdDeviation="${n(C2 * 0.012)}"/><feOffset dy="${n(C2 * 0.012)}" result="b"/><feFlood flood-color="#000" flood-opacity="0.3"/><feComposite in2="b" operator="in" result="s"/><feMerge><feMergeNode in="s"/><feMergeNode in="SourceGraphic"/></feMerge></filter>`
|
|
1924
2086
|
);
|
|
1925
2087
|
body.push(`<g filter="url(#${sid})"><g clip-path="url(#${clipId})">${top}</g></g>`);
|
|
1926
2088
|
} else body.push(`<g clip-path="url(#${clipId})">${top}</g>`);
|
|
@@ -1935,8 +2097,9 @@ function renderSvg(doc, opts = {}) {
|
|
|
1935
2097
|
}
|
|
1936
2098
|
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${size}" height="${size}" viewBox="${-(P + M)} ${-(P + M)} ${C2 + 2 * (P + M)} ${C2 + 2 * (P + M)}"><title>${esc(appearance)}</title><defs>${defs.toString()}</defs>${body.join("")}</svg>`;
|
|
1937
2099
|
}
|
|
2100
|
+
var KNOWN_BLEND = new Set(BLEND_MODES);
|
|
1938
2101
|
function cssBlend(mode) {
|
|
1939
|
-
if (!mode || mode === "normal") return null;
|
|
2102
|
+
if (typeof mode !== "string" || !KNOWN_BLEND.has(mode) || mode === "normal") return null;
|
|
1940
2103
|
if (mode === "plus-lighter") return "screen";
|
|
1941
2104
|
if (mode === "plus-darker") return "multiply";
|
|
1942
2105
|
return mode;
|
|
@@ -1979,6 +2142,7 @@ async function resvg() {
|
|
|
1979
2142
|
}
|
|
1980
2143
|
}
|
|
1981
2144
|
async function svgToPng(svg, width) {
|
|
2145
|
+
if (!(width >= 2)) throw new Error(`Image size must be at least 2 px (got ${width})`);
|
|
1982
2146
|
const { Resvg } = await resvg();
|
|
1983
2147
|
const r = new Resvg(svg, { fitTo: { mode: "width", value: Math.round(width) }, font: { loadSystemFonts: true } });
|
|
1984
2148
|
return new Uint8Array(r.render().asPng());
|
|
@@ -2107,7 +2271,7 @@ function convert(k, raw, flag) {
|
|
|
2107
2271
|
switch (k.type) {
|
|
2108
2272
|
case "number": {
|
|
2109
2273
|
const n2 = Number(raw);
|
|
2110
|
-
if (
|
|
2274
|
+
if (!/^\s*[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?\s*$/i.test(raw) || Number.isNaN(n2)) throw new Error(`${flag} expects a number, got "${raw}"`);
|
|
2111
2275
|
return n2;
|
|
2112
2276
|
}
|
|
2113
2277
|
case "array":
|
|
@@ -2122,59 +2286,89 @@ function convert(k, raw, flag) {
|
|
|
2122
2286
|
return raw;
|
|
2123
2287
|
}
|
|
2124
2288
|
}
|
|
2289
|
+
var BOOL_TRUE = /^(true|1|yes|on)$/i;
|
|
2290
|
+
var BOOL_FALSE = /^(false|0|no|off)$/i;
|
|
2125
2291
|
function parseToolArgv(t, argv) {
|
|
2126
2292
|
const infos = argsOf(t);
|
|
2127
2293
|
const byFlag = new Map(infos.map((a) => [a.flag, a]));
|
|
2128
2294
|
const out = {};
|
|
2129
2295
|
let base = {};
|
|
2296
|
+
const roots = [];
|
|
2297
|
+
let help = false;
|
|
2130
2298
|
const positionals = [];
|
|
2299
|
+
let optionsDone = false;
|
|
2131
2300
|
for (let i = 0; i < argv.length; i++) {
|
|
2132
2301
|
const tok = argv[i];
|
|
2133
|
-
if (
|
|
2302
|
+
if (optionsDone || tok === "-" || !tok.startsWith("-")) {
|
|
2134
2303
|
positionals.push(tok);
|
|
2135
2304
|
continue;
|
|
2136
2305
|
}
|
|
2306
|
+
if (tok === "--") {
|
|
2307
|
+
optionsDone = true;
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
if (tok === "--help" || tok === "-h") {
|
|
2311
|
+
help = true;
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (tok === "-r") {
|
|
2315
|
+
const v2 = argv[++i];
|
|
2316
|
+
if (v2 === void 0) throw new Error("-r needs a directory");
|
|
2317
|
+
roots.push(v2);
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
if (!tok.startsWith("--")) throw new Error(`Unknown option ${tok}. Run "fresnel ${cliCommand(t)} --help".`);
|
|
2137
2321
|
const eq = tok.indexOf("=");
|
|
2138
2322
|
const flag = eq > 0 ? tok.slice(0, eq) : tok;
|
|
2139
2323
|
const inline = eq > 0 ? tok.slice(eq + 1) : void 0;
|
|
2324
|
+
const value = () => {
|
|
2325
|
+
const raw = inline ?? argv[++i];
|
|
2326
|
+
if (raw === void 0) throw new Error(`${flag} needs a value`);
|
|
2327
|
+
return raw;
|
|
2328
|
+
};
|
|
2329
|
+
if (flag === "--root") {
|
|
2330
|
+
roots.push(value());
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2140
2333
|
if (flag === "--args") {
|
|
2141
|
-
const
|
|
2142
|
-
if (
|
|
2143
|
-
|
|
2144
|
-
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("--args must be a JSON object");
|
|
2145
|
-
base = v;
|
|
2334
|
+
const v2 = convert({ type: "json" }, value(), "--args");
|
|
2335
|
+
if (!v2 || typeof v2 !== "object" || Array.isArray(v2)) throw new Error("--args must be a JSON object");
|
|
2336
|
+
base = { ...base, ...v2 };
|
|
2146
2337
|
continue;
|
|
2147
2338
|
}
|
|
2148
2339
|
const neg = flag.startsWith("--no-") ? byFlag.get(`--${flag.slice(5)}`) : void 0;
|
|
2149
|
-
if (neg && neg.kind.type === "boolean") {
|
|
2340
|
+
if (neg && neg.kind.type === "boolean" && inline === void 0) {
|
|
2150
2341
|
out[neg.name] = false;
|
|
2151
2342
|
continue;
|
|
2152
2343
|
}
|
|
2153
2344
|
const info = byFlag.get(flag);
|
|
2154
2345
|
if (!info) throw new Error(`Unknown option ${flag} for ${t.id}. Run "fresnel ${cliCommand(t)} --help".`);
|
|
2155
2346
|
if (info.kind.type === "boolean") {
|
|
2156
|
-
|
|
2347
|
+
if (inline === void 0) out[info.name] = true;
|
|
2348
|
+
else if (BOOL_TRUE.test(inline)) out[info.name] = true;
|
|
2349
|
+
else if (BOOL_FALSE.test(inline)) out[info.name] = false;
|
|
2350
|
+
else throw new Error(`${flag} expects true or false, got "${inline}"`);
|
|
2157
2351
|
continue;
|
|
2158
2352
|
}
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2161
|
-
out[info.name] = convert(info.kind, raw, flag);
|
|
2353
|
+
const v = convert(info.kind, value(), flag);
|
|
2354
|
+
out[info.name] = info.kind.type === "array" && Array.isArray(out[info.name]) ? [...out[info.name], ...v] : v;
|
|
2162
2355
|
}
|
|
2163
2356
|
const pos = t.positional ?? [];
|
|
2164
2357
|
if (positionals.length > pos.length) throw new Error(`Unexpected argument "${positionals[pos.length]}". Run "fresnel ${cliCommand(t)} --help".`);
|
|
2165
2358
|
positionals.forEach((v, i) => {
|
|
2166
2359
|
const info = infos.find((a) => a.name === pos[i]);
|
|
2167
|
-
if (out[info.name]
|
|
2360
|
+
if (out[info.name] !== void 0) throw new Error(`<${info.name}> was given twice (as an argument and as ${info.flag})`);
|
|
2361
|
+
out[info.name] = convert(info.kind, v, `<${info.name}>`);
|
|
2168
2362
|
});
|
|
2169
|
-
return { ...base, ...out };
|
|
2363
|
+
return { args: { ...base, ...out }, roots, help };
|
|
2170
2364
|
}
|
|
2171
2365
|
|
|
2172
2366
|
// ../mcp/src/contract.ts
|
|
2173
2367
|
import { z as z3 } from "zod";
|
|
2174
2368
|
|
|
2175
2369
|
// ../mcp/src/spec.ts
|
|
2176
|
-
import {
|
|
2177
|
-
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
2370
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
2371
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, sep as sep2 } from "path";
|
|
2178
2372
|
import { z as z2 } from "zod";
|
|
2179
2373
|
var CONTRACT_VERSION = "1.0";
|
|
2180
2374
|
var tool = (def) => def;
|
|
@@ -2189,6 +2383,7 @@ var groupsOutline = z2.array(z2.looseObject({ group: z2.string(), layers: z2.arr
|
|
|
2189
2383
|
function describeProps(props) {
|
|
2190
2384
|
const base = {};
|
|
2191
2385
|
const overrides = {};
|
|
2386
|
+
const variants = /* @__PURE__ */ new Map();
|
|
2192
2387
|
for (const [k, p] of Object.entries(props)) {
|
|
2193
2388
|
const b = p.specs?.find((s) => s.appearance === void 0 && s.idiom === void 0);
|
|
2194
2389
|
if (b) base[k] = b.value;
|
|
@@ -2197,21 +2392,32 @@ function describeProps(props) {
|
|
|
2197
2392
|
const s = p.specs.find((e) => e.appearance === a && e.idiom === void 0);
|
|
2198
2393
|
(overrides[a] ??= {})[k] = s.value;
|
|
2199
2394
|
}
|
|
2395
|
+
for (const s of p.specs ?? []) {
|
|
2396
|
+
if (s.idiom === void 0) continue;
|
|
2397
|
+
const key = `${s.appearance ?? ""}|${s.idiom}`;
|
|
2398
|
+
if (!variants.has(key)) variants.set(key, { ...s.appearance ? { appearance: s.appearance } : {}, platform: s.idiom, values: {} });
|
|
2399
|
+
variants.get(key).values[k] = s.value;
|
|
2400
|
+
}
|
|
2200
2401
|
}
|
|
2201
|
-
return
|
|
2402
|
+
return {
|
|
2403
|
+
...base,
|
|
2404
|
+
...Object.keys(overrides).length ? { overrides } : {},
|
|
2405
|
+
...variants.size ? { variants: [...variants.values()] } : {}
|
|
2406
|
+
};
|
|
2202
2407
|
}
|
|
2203
2408
|
function inspect(doc) {
|
|
2204
2409
|
return {
|
|
2205
2410
|
order: "groups and layers are listed front to back (index 0 = frontmost)",
|
|
2206
2411
|
document: describeProps(doc.props),
|
|
2412
|
+
// attributes first, so an unknown key in the file (e.g. "target") can't replace these fields
|
|
2207
2413
|
groups: doc.groups.map((g, gi) => ({
|
|
2414
|
+
...describeProps(g.props),
|
|
2208
2415
|
target: nameOf(g) || `#${gi}`,
|
|
2209
2416
|
index: gi,
|
|
2210
|
-
...describeProps(g.props),
|
|
2211
2417
|
layers: g.layers.map((l, li) => ({
|
|
2418
|
+
...describeProps(l.props),
|
|
2212
2419
|
target: `${nameOf(g) || `#${gi}`}/${nameOf(l) || `#${li}`}`,
|
|
2213
|
-
index: li
|
|
2214
|
-
...describeProps(l.props)
|
|
2420
|
+
index: li
|
|
2215
2421
|
}))
|
|
2216
2422
|
})),
|
|
2217
2423
|
assets: Object.values(doc.assets).map((a) => ({ name: a.name, kind: a.kind, width: a.width, height: a.height }))
|
|
@@ -2229,9 +2435,7 @@ function targetLabel(doc, ref) {
|
|
|
2229
2435
|
}
|
|
2230
2436
|
async function saveFile(ws, out, bytes) {
|
|
2231
2437
|
const p = await ws.resolve(out);
|
|
2232
|
-
await
|
|
2233
|
-
await writeFile2(p, bytes);
|
|
2234
|
-
return p;
|
|
2438
|
+
return (await writeFiles(dirname2(p), { [basename(p)]: bytes }))[0];
|
|
2235
2439
|
}
|
|
2236
2440
|
var inspectOutput = loose({ order: z2.string(), document: z2.record(z2.string(), z2.unknown()), groups: z2.array(z2.looseObject({ target: z2.string(), index: z2.number() })), assets: z2.array(z2.looseObject({ name: z2.string() })) });
|
|
2237
2441
|
var EX_STAR = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M512 212 L594 420 L812 430 L640 566 L700 780 L512 660 L324 780 L384 566 L212 430 L430 420 Z" fill="#ffffff"/></svg>';
|
|
@@ -2256,7 +2460,9 @@ var TOOLS = [
|
|
|
2256
2460
|
},
|
|
2257
2461
|
output: loose({ created: z2.string(), dry_run: z2.boolean().optional(), groups: z2.array(z2.unknown()) }),
|
|
2258
2462
|
mutates_input: true,
|
|
2463
|
+
writes_files: true,
|
|
2259
2464
|
idempotency_hint: "not-idempotent",
|
|
2465
|
+
destructive: true,
|
|
2260
2466
|
supports_dry_run: true,
|
|
2261
2467
|
verification: ["icon_inspect shows the document fill", "icon_render_sheet shows the background in all appearances"],
|
|
2262
2468
|
requires_visual_verification: false,
|
|
@@ -2264,14 +2470,16 @@ var TOOLS = [
|
|
|
2264
2470
|
async run(ws, { path, background, dark_background, watch, overwrite, dry_run }) {
|
|
2265
2471
|
if (/\.zip$/i.test(path.trim())) throw new Error("icon_create writes an .icon folder; pass a path without .zip (use icon_export to make a zip)");
|
|
2266
2472
|
const full = await ws.iconPath(path);
|
|
2267
|
-
const exists = await stat2(full).then(() => true, () => false);
|
|
2268
|
-
if (exists && !overwrite) throw new Error(`${path} already exists. Pass overwrite: true to replace it.`);
|
|
2269
2473
|
const doc = createDocument({
|
|
2270
2474
|
...background ? { background } : {},
|
|
2271
2475
|
platforms: watch === false ? { squares: "shared" } : { circles: ["watchOS"], squares: "shared" }
|
|
2272
2476
|
});
|
|
2273
2477
|
if (dark_background) setProp(doc, { kind: "document" }, "fill", dark_background, "dark");
|
|
2274
|
-
|
|
2478
|
+
await ws.withLock(full, async () => {
|
|
2479
|
+
const exists = await stat2(full).then(() => true, () => false);
|
|
2480
|
+
if (exists && !overwrite) throw new Error(`${path} already exists. Pass overwrite: true to replace it.`);
|
|
2481
|
+
if (!dry_run) await saveIcon(full, doc);
|
|
2482
|
+
});
|
|
2275
2483
|
return { data: { created: full, ...dry_run ? { dry_run: true } : {}, ...inspect(doc) } };
|
|
2276
2484
|
}
|
|
2277
2485
|
}),
|
|
@@ -2287,6 +2495,7 @@ var TOOLS = [
|
|
|
2287
2495
|
input: { path: pathArg },
|
|
2288
2496
|
output: inspectOutput,
|
|
2289
2497
|
mutates_input: false,
|
|
2498
|
+
writes_files: false,
|
|
2290
2499
|
idempotency_hint: "idempotent",
|
|
2291
2500
|
supports_dry_run: false,
|
|
2292
2501
|
verification: [],
|
|
@@ -2315,6 +2524,7 @@ var TOOLS = [
|
|
|
2315
2524
|
},
|
|
2316
2525
|
output: loose({ added: z2.string(), groups: z2.array(z2.string()) }),
|
|
2317
2526
|
mutates_input: true,
|
|
2527
|
+
writes_files: false,
|
|
2318
2528
|
idempotency_hint: "not-idempotent",
|
|
2319
2529
|
supports_dry_run: true,
|
|
2320
2530
|
verification: ["icon_inspect lists the new group"],
|
|
@@ -2357,6 +2567,7 @@ var TOOLS = [
|
|
|
2357
2567
|
},
|
|
2358
2568
|
output: loose({ added: z2.string(), size: z2.string(), warnings: z2.array(z2.string()) }),
|
|
2359
2569
|
mutates_input: true,
|
|
2570
|
+
writes_files: false,
|
|
2360
2571
|
idempotency_hint: "not-idempotent",
|
|
2361
2572
|
supports_dry_run: true,
|
|
2362
2573
|
verification: ["Check `warnings` (V-01\u2026V-10)", "icon_render_sheet to see the layer in every appearance"],
|
|
@@ -2432,6 +2643,7 @@ var TOOLS = [
|
|
|
2432
2643
|
},
|
|
2433
2644
|
output: loose({ target: z2.string(), now: z2.record(z2.string(), z2.unknown()) }),
|
|
2434
2645
|
mutates_input: true,
|
|
2646
|
+
writes_files: false,
|
|
2435
2647
|
idempotency_hint: "idempotent",
|
|
2436
2648
|
supports_dry_run: true,
|
|
2437
2649
|
verification: ["`now` shows the stored values", "icon_render_sheet for the appearances you changed"],
|
|
@@ -2467,6 +2679,7 @@ var TOOLS = [
|
|
|
2467
2679
|
input: { path: pathArg, target: z2.string(), index: z2.number().int().min(0), to_group: z2.string().optional(), dry_run: dryRunArg },
|
|
2468
2680
|
output: loose({ groups: groupsOutline }),
|
|
2469
2681
|
mutates_input: true,
|
|
2682
|
+
writes_files: false,
|
|
2470
2683
|
idempotency_hint: "idempotent",
|
|
2471
2684
|
supports_dry_run: true,
|
|
2472
2685
|
verification: ["`groups` lists the new order (front to back)"],
|
|
@@ -2480,6 +2693,7 @@ var TOOLS = [
|
|
|
2480
2693
|
const ref = resolvePath(doc, target);
|
|
2481
2694
|
let gid;
|
|
2482
2695
|
if (to_group) {
|
|
2696
|
+
if (ref.kind !== "layer") throw new Error("to_group only applies to layers; groups are reordered with index");
|
|
2483
2697
|
const g = resolvePath(doc, to_group);
|
|
2484
2698
|
if (g.kind !== "group") throw new Error("to_group must be a group");
|
|
2485
2699
|
gid = g.id;
|
|
@@ -2511,6 +2725,7 @@ var TOOLS = [
|
|
|
2511
2725
|
},
|
|
2512
2726
|
output: loose({ moved: z2.number(), positions: z2.array(z2.looseObject({ target: z2.string() })) }),
|
|
2513
2727
|
mutates_input: true,
|
|
2728
|
+
writes_files: false,
|
|
2514
2729
|
idempotency_hint: "idempotent",
|
|
2515
2730
|
supports_dry_run: true,
|
|
2516
2731
|
verification: ["`positions` gives each bounding box in canvas points", "icon_render to look at the result"],
|
|
@@ -2546,6 +2761,7 @@ var TOOLS = [
|
|
|
2546
2761
|
input: { path: pathArg, target: z2.string(), dry_run: dryRunArg },
|
|
2547
2762
|
output: loose({ removed: z2.string(), groups: groupsOutline }),
|
|
2548
2763
|
mutates_input: true,
|
|
2764
|
+
writes_files: false,
|
|
2549
2765
|
idempotency_hint: "not-idempotent",
|
|
2550
2766
|
destructive: true,
|
|
2551
2767
|
supports_dry_run: true,
|
|
@@ -2582,6 +2798,7 @@ var TOOLS = [
|
|
|
2582
2798
|
issues: z2.array(z2.looseObject({ id: z2.string(), severity: z2.enum(["error", "warning", "info"]), message: z2.string() }))
|
|
2583
2799
|
}),
|
|
2584
2800
|
mutates_input: false,
|
|
2801
|
+
writes_files: false,
|
|
2585
2802
|
idempotency_hint: "idempotent",
|
|
2586
2803
|
supports_dry_run: false,
|
|
2587
2804
|
verification: ['No issue with severity "error"'],
|
|
@@ -2608,12 +2825,13 @@ var TOOLS = [
|
|
|
2608
2825
|
platform: z2.enum(["iOS", "macOS", "watchOS"]).optional().describe("Default: iOS. watchOS renders a circle"),
|
|
2609
2826
|
size: z2.number().int().min(16).max(2048).optional().describe("Pixels, default 512"),
|
|
2610
2827
|
mask: maskEnum.optional(),
|
|
2611
|
-
tint: z2.string().optional().describe('Tint color for tinted appearances, "#RRGGBB"'),
|
|
2828
|
+
tint: z2.string().regex(/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i, 'tint must be a color like "#33AAFF"').optional().describe('Tint color for tinted appearances, "#RRGGBB"'),
|
|
2612
2829
|
light_angle: z2.number().min(-180).max(180).optional().describe("Light direction in degrees, 0 = top (default), -45 = top-left"),
|
|
2613
2830
|
out: z2.string().optional().describe("Save the PNG to this path")
|
|
2614
2831
|
},
|
|
2615
2832
|
output: loose({ appearance: z2.string(), saved: z2.string().optional() }),
|
|
2616
2833
|
mutates_input: false,
|
|
2834
|
+
writes_files: true,
|
|
2617
2835
|
idempotency_hint: "idempotent",
|
|
2618
2836
|
supports_dry_run: false,
|
|
2619
2837
|
verification: ["Look at the returned image"],
|
|
@@ -2646,11 +2864,12 @@ var TOOLS = [
|
|
|
2646
2864
|
input: {
|
|
2647
2865
|
path: pathArg,
|
|
2648
2866
|
cell: z2.number().int().min(96).max(600).optional().describe("Cell size in pixels, default 240"),
|
|
2649
|
-
tint: z2.string().optional(),
|
|
2867
|
+
tint: z2.string().regex(/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i, 'tint must be a color like "#33AAFF"').optional().describe('Tint color for tinted appearances, "#RRGGBB"'),
|
|
2650
2868
|
out: z2.string().optional()
|
|
2651
2869
|
},
|
|
2652
2870
|
output: loose({ appearances: z2.array(z2.string()), saved: z2.string().optional() }),
|
|
2653
2871
|
mutates_input: false,
|
|
2872
|
+
writes_files: true,
|
|
2654
2873
|
idempotency_hint: "idempotent",
|
|
2655
2874
|
supports_dry_run: false,
|
|
2656
2875
|
verification: ["Look at the returned image: every appearance should stay legible"],
|
|
@@ -2676,12 +2895,14 @@ var TOOLS = [
|
|
|
2676
2895
|
path: pathArg,
|
|
2677
2896
|
out_dir: z2.string(),
|
|
2678
2897
|
formats: z2.array(z2.enum(["icon", "icns", "appiconset", "png"])).min(1),
|
|
2679
|
-
sizes: z2.array(z2.number().int().min(16).max(2048)).optional().describe("PNG sizes, default [1024, 512, 256, 128, 64, 32, 16]"),
|
|
2680
|
-
appearances: z2.array(appearanceEnum).optional().describe("PNG appearances, default all six")
|
|
2898
|
+
sizes: z2.array(z2.number().int().min(16).max(2048)).min(1).optional().describe("PNG sizes, default [1024, 512, 256, 128, 64, 32, 16]"),
|
|
2899
|
+
appearances: z2.array(appearanceEnum).min(1).optional().describe("PNG appearances, default all six")
|
|
2681
2900
|
},
|
|
2682
2901
|
output: loose({ out_dir: z2.string(), files: z2.number(), written: z2.array(z2.string()) }),
|
|
2683
2902
|
mutates_input: false,
|
|
2903
|
+
writes_files: true,
|
|
2684
2904
|
idempotency_hint: "idempotent",
|
|
2905
|
+
destructive: true,
|
|
2685
2906
|
supports_dry_run: false,
|
|
2686
2907
|
verification: ["`written` lists every file", "Open the .icon in Icon Composer / Xcode 26 on a Mac for the final check"],
|
|
2687
2908
|
requires_visual_verification: false,
|
|
@@ -2689,6 +2910,9 @@ var TOOLS = [
|
|
|
2689
2910
|
async run(ws, { path, out_dir, formats, sizes, appearances }) {
|
|
2690
2911
|
const full = await ws.iconPath(path, { mustExist: true });
|
|
2691
2912
|
const outDir = await ws.resolve(out_dir);
|
|
2913
|
+
const inside = relative2(full, outDir);
|
|
2914
|
+
if (inside === "" || !inside.startsWith(`..${sep2}`) && inside !== ".." && !isAbsolute2(inside))
|
|
2915
|
+
throw new Error("out_dir is inside the icon bundle; choose a folder outside it");
|
|
2692
2916
|
const doc = await ws.read(full);
|
|
2693
2917
|
const name = basename(full).replace(/\.icon(\.zip)?$/, "");
|
|
2694
2918
|
const raster = nodeRasterizer(doc);
|
|
@@ -2697,16 +2921,9 @@ var TOOLS = [
|
|
|
2697
2921
|
throw new Error(`These visible layers can't be rendered and would be missing from the images: ${missing.join(", ")}. Fix them first (icon_validate).`);
|
|
2698
2922
|
const written = [];
|
|
2699
2923
|
if (formats.includes("icon")) {
|
|
2700
|
-
|
|
2701
|
-
await saveIcon(dest, doc);
|
|
2702
|
-
written.push(join2(dest, "icon.json"), ...Object.keys(doc.assets).map((a) => join2(dest, "Assets", a)));
|
|
2703
|
-
}
|
|
2704
|
-
if (formats.includes("icns")) {
|
|
2705
|
-
const p = join2(outDir, `${name}.icns`);
|
|
2706
|
-
await mkdir2(dirname2(p), { recursive: true });
|
|
2707
|
-
await writeFile2(p, await buildIcns(raster));
|
|
2708
|
-
written.push(p);
|
|
2924
|
+
written.push(...await saveIcon(join2(outDir, `${name}.icon`), doc));
|
|
2709
2925
|
}
|
|
2926
|
+
if (formats.includes("icns")) written.push(...await writeFiles(outDir, { [`${name}.icns`]: await buildIcns(raster) }));
|
|
2710
2927
|
if (formats.includes("appiconset")) written.push(...await writeFiles(join2(outDir, "AppIcon.appiconset"), await buildAppIconSet(raster, { watch: targetsWatch(doc) })));
|
|
2711
2928
|
if (formats.includes("png"))
|
|
2712
2929
|
written.push(
|
|
@@ -2725,7 +2942,7 @@ async function runTool(ws, t, args) {
|
|
|
2725
2942
|
}
|
|
2726
2943
|
|
|
2727
2944
|
// ../mcp/src/version.ts
|
|
2728
|
-
var VERSION = "0.2.
|
|
2945
|
+
var VERSION = "0.2.2";
|
|
2729
2946
|
|
|
2730
2947
|
// ../mcp/src/contract.ts
|
|
2731
2948
|
var RESOURCES = [
|
|
@@ -2762,6 +2979,7 @@ function toolSpec(t) {
|
|
|
2762
2979
|
input_schema: jsonSchema(z3.object(t.input).strict(), "input"),
|
|
2763
2980
|
output_schema: jsonSchema(t.output, "output"),
|
|
2764
2981
|
mutates_input: t.mutates_input,
|
|
2982
|
+
writes_files: t.writes_files,
|
|
2765
2983
|
idempotency_hint: t.idempotency_hint,
|
|
2766
2984
|
destructive: !!t.destructive,
|
|
2767
2985
|
supports_dry_run: t.supports_dry_run,
|
|
@@ -2794,7 +3012,7 @@ function buildContract() {
|
|
|
2794
3012
|
rule: "Breaking changes bump contract_version to the next major. The frozen surface (packages/mcp/spec/surface.json) makes CI fail otherwise."
|
|
2795
3013
|
},
|
|
2796
3014
|
tools: TOOLS.map(toolSpec),
|
|
2797
|
-
capability_map: Object.fromEntries(Object.entries(capability_map).sort(([a], [b]) => a
|
|
3015
|
+
capability_map: Object.fromEntries(Object.entries(capability_map).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
|
|
2798
3016
|
resources: RESOURCES,
|
|
2799
3017
|
prompts: PROMPTS,
|
|
2800
3018
|
icon_json: { document: attrTable(DOCUMENT_ATTRS), group: attrTable(GROUP_ATTRS), layer: attrTable(LAYER_ATTRS) },
|
|
@@ -2892,10 +3110,11 @@ function createServer(ws) {
|
|
|
2892
3110
|
{
|
|
2893
3111
|
title: t.title,
|
|
2894
3112
|
description: t.description,
|
|
2895
|
-
|
|
3113
|
+
// strict: a misspelt argument (dryRun for dry_run) is an error, not silently dropped
|
|
3114
|
+
inputSchema: z4.object(t.input).strict(),
|
|
2896
3115
|
outputSchema: t.output,
|
|
2897
3116
|
annotations: {
|
|
2898
|
-
readOnlyHint: !t.mutates_input,
|
|
3117
|
+
readOnlyHint: !t.mutates_input && !t.writes_files,
|
|
2899
3118
|
destructiveHint: !!t.destructive,
|
|
2900
3119
|
idempotentHint: t.idempotency_hint === "idempotent",
|
|
2901
3120
|
openWorldHint: false
|
|
@@ -2969,8 +3188,8 @@ Use the fresnel tools:
|
|
|
2969
3188
|
}
|
|
2970
3189
|
|
|
2971
3190
|
// ../mcp/src/workspace.ts
|
|
2972
|
-
import { realpath, stat as stat3 } from "fs/promises";
|
|
2973
|
-
import { dirname as dirname3, isAbsolute as
|
|
3191
|
+
import { lstat as lstat2, realpath, stat as stat3 } from "fs/promises";
|
|
3192
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
|
|
2974
3193
|
var Workspace = class {
|
|
2975
3194
|
roots;
|
|
2976
3195
|
locks = /* @__PURE__ */ new Map();
|
|
@@ -2983,13 +3202,19 @@ var Workspace = class {
|
|
|
2983
3202
|
/** Resolves a user path (relative paths are relative to the first root) and checks it's allowed. */
|
|
2984
3203
|
async resolve(p, opts = {}) {
|
|
2985
3204
|
if (!p || typeof p !== "string") throw new Error("A path is required");
|
|
2986
|
-
const abs =
|
|
3205
|
+
const abs = isAbsolute3(p) ? resolve2(p) : resolve2(this.roots[0], p);
|
|
2987
3206
|
let probe = abs;
|
|
2988
3207
|
let suffix = "";
|
|
2989
3208
|
for (; ; ) {
|
|
2990
3209
|
try {
|
|
2991
3210
|
const real = await realpath(probe);
|
|
2992
3211
|
const full = suffix ? resolve2(real, suffix) : real;
|
|
3212
|
+
if (suffix) {
|
|
3213
|
+
const first = resolve2(real, suffix.split(sep3)[0]);
|
|
3214
|
+
const st = await lstat2(first).catch(() => null);
|
|
3215
|
+
if (st?.isSymbolicLink()) throw new Error(`Access denied: ${p} goes through a symbolic link that points nowhere`);
|
|
3216
|
+
if (st) throw new Error(`Invalid path: ${p} (${suffix.split(sep3)[0]} is not a folder)`);
|
|
3217
|
+
}
|
|
2993
3218
|
if (!await this.allowed(full)) throw new Error(`Access denied: ${p} is outside the allowed roots (${this.roots.join(", ")})`);
|
|
2994
3219
|
if (opts.mustExist && full !== real) throw new Error(`Not found: ${p}`);
|
|
2995
3220
|
return full;
|
|
@@ -2998,7 +3223,7 @@ var Workspace = class {
|
|
|
2998
3223
|
if (opts.mustExist) throw new Error(`Not found: ${p}`);
|
|
2999
3224
|
const parent = dirname3(probe);
|
|
3000
3225
|
if (parent === probe) throw new Error(`Invalid path: ${p}`);
|
|
3001
|
-
suffix = suffix ? `${probe.slice(parent.length + 1)}${
|
|
3226
|
+
suffix = suffix ? `${probe.slice(parent.length + 1)}${sep3}${suffix}` : probe.slice(parent.length + 1);
|
|
3002
3227
|
probe = parent;
|
|
3003
3228
|
}
|
|
3004
3229
|
}
|
|
@@ -3011,15 +3236,17 @@ var Workspace = class {
|
|
|
3011
3236
|
root = await realpath(r);
|
|
3012
3237
|
} catch {
|
|
3013
3238
|
}
|
|
3014
|
-
const rel =
|
|
3015
|
-
if (rel === "" || !rel.startsWith(
|
|
3239
|
+
const rel = relative3(root, full);
|
|
3240
|
+
if (rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel)) return true;
|
|
3016
3241
|
}
|
|
3017
3242
|
return false;
|
|
3018
3243
|
}
|
|
3019
3244
|
/** Normalizes an icon path: accepts "Foo", "Foo.icon", ".../Foo.icon/icon.json". */
|
|
3020
3245
|
async iconPath(p, opts = {}) {
|
|
3021
|
-
let q = p.trim();
|
|
3246
|
+
let q = (typeof p === "string" ? p : "").trim().replace(/[\\/]+$/, "");
|
|
3022
3247
|
if (q.endsWith("/icon.json") || q.endsWith("\\icon.json")) q = q.slice(0, -"/icon.json".length);
|
|
3248
|
+
const base = q.split(/[\\/]/).pop() ?? "";
|
|
3249
|
+
if (!base || base === ".icon" || base === ".icon.zip" || base === "." || base === "..") throw new Error(`Invalid icon path "${p}": give a name such as "MyApp.icon"`);
|
|
3023
3250
|
if (!q.endsWith(".icon") && !q.endsWith(".icon.zip")) q = `${q}.icon`;
|
|
3024
3251
|
const full = await this.resolve(q, opts);
|
|
3025
3252
|
if (opts.mustExist) {
|
|
@@ -3081,23 +3308,13 @@ Appearances: ${APPEARANCES.join(", ")}
|
|
|
3081
3308
|
Docs: https://github.com/kajisho5/fresnel
|
|
3082
3309
|
`;
|
|
3083
3310
|
async function callTool(t, argv) {
|
|
3084
|
-
|
|
3311
|
+
const { args, roots, help } = parseToolArgv(t, argv);
|
|
3312
|
+
if (help) {
|
|
3085
3313
|
process.stdout.write(toolHelp(t));
|
|
3086
3314
|
return;
|
|
3087
3315
|
}
|
|
3088
|
-
const roots = [];
|
|
3089
|
-
const rest = [];
|
|
3090
|
-
for (let i = 0; i < argv.length; i++) {
|
|
3091
|
-
const a = argv[i];
|
|
3092
|
-
if (a === "--root" || a === "-r") {
|
|
3093
|
-
const v = argv[++i];
|
|
3094
|
-
if (!v) throw new Error("--root needs a directory");
|
|
3095
|
-
roots.push(v);
|
|
3096
|
-
} else if (a.startsWith("--root=")) roots.push(a.slice(7));
|
|
3097
|
-
else rest.push(a);
|
|
3098
|
-
}
|
|
3099
3316
|
const ws = new Workspace(roots, { unrestricted: roots.length === 0 });
|
|
3100
|
-
const { data, image } = await runTool(ws, t,
|
|
3317
|
+
const { data, image } = await runTool(ws, t, args);
|
|
3101
3318
|
if (image && !data.saved) process.stderr.write("fresnel: the PNG was not saved; pass --out <file.png> to keep it\n");
|
|
3102
3319
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3103
3320
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fresnel-icon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "fresnel CLI: build, validate, render and export Liquid Glass app icons (.icon bundles) from the command line or CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "9.0.0",
|
|
40
40
|
"tsx": "4.23.15",
|
|
41
|
-
"@fresnel/core": "0.2.
|
|
42
|
-
"@fresnel/render": "0.2.
|
|
41
|
+
"@fresnel/core": "0.2.2",
|
|
42
|
+
"@fresnel/render": "0.2.2"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup",
|