Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/lib/normalizeSocialSvg.test.ts | 35++++++++++++++++++++++++++++++++++-
Mcommon/lib/settingsSchema.ts | 14+++++++++-----
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