commit 3cc3f46f5218c55c6024797de9fce7d0490eace1
parent a0026d3f077aafd69dd7fe1c97611a8252bde013
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 03:40:33 -0400
common: normalizeSocialSvg passes over a <clipPath> like a <mask> — a Figma one-colour icon (a shape clipped by a white rect) is themed
A clip never paints, so its fills are neither counted nor changed; block and
self-closing forms, the self-closing one first so an empty `<clipPath …/>`
cannot swallow what follows. The Figma-shape test fails on the previous rule
(['none', 'black', 'white']: two colours, nothing themed); x.com and Kiwi
Farms are byte-for-byte as before; idempotent (O1 review, re-read low).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/common/lib/normalizeSocialSvg.test.ts b/common/lib/normalizeSocialSvg.test.ts
@@ -145,8 +145,41 @@ test("an empty self-closing <mask/> does not swallow what follows it", () => {
assert.ok(out.includes(`<mask id="b"><rect fill="white" width="8" height="8"/></mask>`));
});
+// Figma's export of a one-colour icon: the shape in a group clipped to the
+// frame, and the clip a white rect in <defs>. A clip never paints, so its white
+// is no second colour (release 11, O2b; O1's review, "a <clipPath>'s fill").
+const FIGMA = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1_2)">
+<path d="M12 2L2 22h20L12 2z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_1_2">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>`;
+
+test("Figma: a clip's white is no colour — the black shape is themed, the clip kept", () => {
+ const out = normalized(FIGMA);
+ assert.deepEqual(fills(out), ["none", "currentColor", "white"]);
+ assert.equal(
+ out,
+ FIGMA.replace(' width="24" height="24" viewBox', ' viewBox')
+ .replace("<svg ", '<svg aria-hidden="true" ')
+ .replace('fill="black"', 'fill="currentColor"'),
+ );
+ // A self-closing clipPath passes over just itself; a clip with two colours
+ // inside still counts as none of the icon's.
+ const selfClosed =
+ `<svg viewBox="0 0 8 8"><clipPath id="c"/><path fill="#fff" d="M0 0"/>` +
+ `<clipPath id="d"><rect fill="white"/><circle fill="#f00"/></clipPath></svg>`;
+ const out2 = normalized(selfClosed);
+ assert.ok(out2.includes(`<clipPath id="c"/><path fill="currentColor" d="M0 0"/>`));
+ assert.ok(out2.includes(`<clipPath id="d"><rect fill="white"/><circle fill="#f00"/></clipPath>`));
+});
+
test("idempotent: a stored icon normalizes to itself", () => {
- for (const raw of [X_COM, KIWI_FARMS, FIXTURE, BARE, YOUTUBE, BADGE]) {
+ for (const raw of [X_COM, KIWI_FARMS, FIXTURE, BARE, YOUTUBE, BADGE, FIGMA]) {
const once = normalized(raw);
assert.equal(normalized(once), once);
}
diff --git a/common/lib/settingsSchema.ts b/common/lib/settingsSchema.ts
@@ -1341,11 +1341,15 @@ function mapTagFills(tag: string, paint: (value: string) => string): string {
// The children, tag by tag. Passed over whole, never read or changed:
// - a <mask> (its white and black say how much shows through, not what
-// colour) — self-closing first, so an empty `<mask …/>` cannot swallow
-// everything up to the next `</mask>`;
+// colour) and a <clipPath> (a clip never paints: only its shape counts —
+// Figma exports almost every icon as a path clipped by a
+// `<clipPath><rect fill="white"/></clipPath>`, release 11, O2b) —
+// self-closing first, so an empty `<mask …/>` or `<clipPath …/>` cannot
+// swallow everything up to the next closing tag;
// - an animation tag, whose `fill="freeze"` / `fill="remove"` is timing.
-const CHILD_TAG = /<mask\b[^>]*\/>|<mask\b[\s\S]*?<\/mask\s*>|<[a-zA-Z][^>]*>/gi;
-const PASSED_OVER = /^<(?:mask|animate\w*|set)\b/i;
+const CHILD_TAG =
+ /<(?:mask|clipPath)\b[^>]*\/>|<mask\b[\s\S]*?<\/mask\s*>|<clipPath\b[\s\S]*?<\/clipPath\s*>|<[a-zA-Z][^>]*>/gi;
+const PASSED_OVER = /^<(?:mask|clipPath|animate\w*|set)\b/i;
function mapChildFills(body: string, paint: (value: string) => string): string {
return body.replace(CHILD_TAG, (m) => (PASSED_OVER.test(m) ? m : mapTagFills(m, paint)));
@@ -1357,7 +1361,7 @@ function mapChildFills(body: string, paint: (value: string) => string): string {
// + aria-hidden on the root <svg>. Requires a viewBox so the icon scales.
//
// A SINGLE-COLOUR icon follows the theme: when its fills — the root's and its
-// children's together, outside a <mask> — hold at most one solid colour, each
+// children's together, outside a <mask> or <clipPath> — hold at most one solid colour, each
// becomes `currentColor`, the footer link's colour. Drawn for one background,
// such an icon vanishes on another (X's official logo is a `<path
// fill="white">`: 1.11:1 on the light footer). An icon of TWO or more colours