Archilyzer · Source

archilyzer

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

commit 428699f16fc9c31be45e3ad196a3404f6f2bb1c9
parent 43a950af8402102fa2f1dab681a4f38d98cb7cd0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 20:49:43 -0400

common: the how-to-export hint only on a drawing program's leftovers; stale text (final review F3, F4)

The hint follows a style, <metadata> and an element or attribute in an
editor's namespace; an <a>, <image>, <title> with markup or src gets the
reason alone. MobileMenu's comment no longer names the Options dialog; the
older homepage changelog bullet reads as the end state; `featured`'s doc
drops "a narrow header shows fewer" (SETTINGS.md, SITE.md regenerated).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
MSETTINGS.md | 2+-
MSITE.md | 2+-
Mcommon/lib/normalizeSocialSvg.test.ts | 25+++++++++++++++++++++++++
Mcommon/lib/settingsSchema.ts | 2+-
Mcommon/lib/socialSvg.ts | 16++++++++++------
Mexport/app/components/MobileMenu.tsx | 5++---
Mhomepage/CHANGELOG.md | 16++++++++--------
7 files changed, 48 insertions(+), 20 deletions(-)

diff --git a/SETTINGS.md b/SETTINGS.md @@ -474,7 +474,7 @@ Per entry — each entry spells its own values. | `label` | The link's name: the icon's accessible name and its tooltip, never text beside it. Shown as text only in place of an icon that fails the check at render. | | `url` | Link target: http(s), mailto: or a site-relative path. | | `svg` | Inline SVG markup: ONE well-formed `<svg>` element, checked when it is saved new or edited and again every time it is rendered (a link whose icon fails at render shows its label instead; `archilyzer doctor` names it). It may contain shapes, groups, defs, gradients, patterns, clip paths, masks, filters, text and animate/animateTransform/set — no script, style block, foreignObject, a, image, title, desc or any HTML element (a title or desc holding text only is removed); SVG presentation attributes plus aria-*, data-* and xmlns:* — no event handler (on…); a `style` attribute of presentation properties only; an href or url(…) only to an id inside the icon, written plainly; no CSS escape, comment or function that loads anything (image-set, image, cross-fade, element, src, paint, @import); ids plain names. A leading XML declaration, a DOCTYPE without an internal subset and comments are removed. Normalized on save: width/height stripped, aria-hidden added, a single-colour icon's fills made fill="currentColor" (an icon of two or more colours keeps them). A root with no viewBox but a numeric width W and height H (unitless or px) is given `viewBox="0 0 W H"`, so a file pasted as downloaded is accepted. A refused save names why; export from a drawing program with presentation attributes rather than a style block (in Inkscape, save as Plain SVG). | -| `featured` | Show this link in a header. A header shows at most 4 links: the featured ones when any link is marked, else all of them; of those, the last 4 (a narrow header shows fewer). The footer shows every link. Written only when true. | +| `featured` | Show this link in a header. A header shows at most 4 links: the featured ones when any link is marked, else all of them; of those, the last 4. The footer shows every link. Written only when true. | Default: diff --git a/SITE.md b/SITE.md @@ -80,7 +80,7 @@ Per entry — each entry spells its own values. | `label` | The link's name: the icon's accessible name and its tooltip, never text beside it. Shown as text only in place of an icon that fails the check at render. | | `url` | Link target: http(s), mailto: or a site-relative path. | | `svg` | Inline SVG markup: ONE well-formed `<svg>` element, checked when it is saved new or edited and again every time it is rendered (a link whose icon fails at render shows its label instead; `archilyzer doctor` names it). It may contain shapes, groups, defs, gradients, patterns, clip paths, masks, filters, text and animate/animateTransform/set — no script, style block, foreignObject, a, image, title, desc or any HTML element (a title or desc holding text only is removed); SVG presentation attributes plus aria-*, data-* and xmlns:* — no event handler (on…); a `style` attribute of presentation properties only; an href or url(…) only to an id inside the icon, written plainly; no CSS escape, comment or function that loads anything (image-set, image, cross-fade, element, src, paint, @import); ids plain names. A leading XML declaration, a DOCTYPE without an internal subset and comments are removed. Normalized on save: width/height stripped, aria-hidden added, a single-colour icon's fills made fill="currentColor" (an icon of two or more colours keeps them). A root with no viewBox but a numeric width W and height H (unitless or px) is given `viewBox="0 0 W H"`, so a file pasted as downloaded is accepted. A refused save names why; export from a drawing program with presentation attributes rather than a style block (in Inkscape, save as Plain SVG). | -| `featured` | Show this link in a header. A header shows at most 4 links: the featured ones when any link is marked, else all of them; of those, the last 4 (a narrow header shows fewer). The footer shows every link. Written only when true. | +| `featured` | Show this link in a header. A header shows at most 4 links: the featured ones when any link is marked, else all of them; of those, the last 4. The footer shows every link. Written only when true. | ## `groups` diff --git a/common/lib/normalizeSocialSvg.test.ts b/common/lib/normalizeSocialSvg.test.ts @@ -367,6 +367,31 @@ test("elements an icon has no use for, named", () => { } }); +test("only a drawing program's leftovers say how to export: a style, metadata, its own namespace", () => { + const HINT = /presentation attributes rather than a style block \(in Inkscape, save as Plain SVG\)$/; + for (const inner of [ + `<style>path{fill:red}</style>`, + `<metadata><x/></metadata>`, + `<sodipodi:namedview/>`, + `<rect inkscape:label="x"/>`, + `<rect style="position:fixed"/>`, + ]) { + assert.match(socialSvgProblem(OK(inner)) ?? "", HINT, inner); + } + for (const inner of [ + `<a href="#a"><rect/></a>`, + `<image href="#a"/>`, + `<title><path/></title>`, + `<foreignObject/>`, + `<rect src="x"/>`, + `<rect formaction="x"/>`, + ]) { + const problem = socialSvgProblem(OK(inner)); + assert.ok(problem, inner); + assert.doesNotMatch(problem, /presentation attributes|Plain SVG/, inner); + } +}); + test("attributes an icon has no use for, named; a style with an escape or an import", () => { assert.equal(socialSvgProblem(OK(`<rect src="x"/>`)), SVG_PROBLEM.attribute("src")); assert.equal(socialSvgProblem(OK(`<rect formaction="x"/>`)), SVG_PROBLEM.attribute("formaction")); diff --git a/common/lib/settingsSchema.ts b/common/lib/settingsSchema.ts @@ -586,7 +586,7 @@ export const SOCIAL_LINK_FIELD_DOCS: FieldDocs<SocialLink> = { featured: "Show this link in a header. A header shows at most 4 links: the " + "featured ones when any link is marked, else all of them; of those, the " + - "last 4 (a narrow header shows fewer). The footer shows every link. " + + "last 4. The footer shows every link. " + "Written only when true.", }; diff --git a/common/lib/socialSvg.ts b/common/lib/socialSvg.ts @@ -108,12 +108,17 @@ const FRAGMENT_RE = /^#[A-Za-z_][\w.:-]*$/; // The reasons a pasted icon is refused, by class. The editor shows them after // the link's label, as text (React escapes it). None echoes the markup: a tag // or attribute NAME is reduced to letters, digits and `_.:-` and cut to 40 -// characters before it is named. The classes a drawing program's export -// causes (a style block, its own elements and attributes, an old DOCTYPE) say -// how to export an acceptable file. +// characters before it is named. Only the refusals a drawing program's export +// causes say how to export an acceptable file: a style (a `<style>` block or a +// `style` attribute), `<metadata>`, and an element or attribute in the +// program's own namespace (`inkscape:`, `sodipodi:`, …); an old DOCTYPE has a +// sentence of its own. An `<a>`, an `<image>`, a `<title>` with markup or a +// `src` is not something an export setting removes, so it gets no hint. const nameOf = (name: string) => name.replace(/[^A-Za-z0-9_.:-]/g, "").slice(0, 40) || "?"; const EXPORT_HINT = "export it with presentation attributes rather than a style block (in Inkscape, save as Plain SVG)"; +const fromDrawingProgram = (name: string) => /^(style|metadata)$/i.test(name) || name.includes(":"); +const withHint = (name: string) => (fromDrawingProgram(name) ? `; ${EXPORT_HINT}` : ""); export const SVG_PROBLEM = { markup: "it is not one well-formed <svg> element", doctype: "it has a DOCTYPE with an internal subset; export it again without one, or delete the DOCTYPE", @@ -123,9 +128,8 @@ export const SVG_PROBLEM = { style: `it has a style an icon cannot use; ${EXPORT_HINT}`, id: "it has an id an icon cannot use", viewBox: "it has no viewBox, and no numeric width and height to make one from", - element: (name: string) => `it has an element an icon has no use for (${nameOf(name)}); ${EXPORT_HINT}`, - attribute: (name: string) => - `it has an attribute an icon has no use for (${nameOf(name)}); ${EXPORT_HINT}`, + element: (name: string) => `it has an element an icon has no use for (${nameOf(name)})${withHint(name)}`, + attribute: (name: string) => `it has an attribute an icon has no use for (${nameOf(name)})${withHint(name)}`, } as const; // The only properties a `style` attribute may set: presentation, never layout diff --git a/export/app/components/MobileMenu.tsx b/export/app/components/MobileMenu.tsx @@ -23,9 +23,8 @@ import type { SwitcherGroup } from "./SiblingSwitcher"; // // Header is a server component, so the link groups arrive as plain serialisable // props. The base and accent lists are read from the client ThemeProvider and -// rendered as NATIVE radio groups ("Base", "Accent"; ThemeRadios, shared with -// the Options dialog), not by nesting -// ThemeMenu's DropdownMenu inside this dialog — one popover layer inside +// rendered as NATIVE radio groups ("Base", "Accent"; ThemeRadios), not by +// nesting ThemeMenu's DropdownMenu inside this dialog — one popover layer inside // another is a focus-trap fight, and the dropdown's `menuitemradio`s are a spec // hook that belongs to the md+ header. export default function MobileMenu({ diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md @@ -19,14 +19,14 @@ - **The e2e no longer needs the operator's data.** It reads a synthetic summary built by the real summary builder (`e2e/fixture-summary.ts`, six sites, deterministic) instead of `public/homepage-summary.json`, so a fresh clone runs every spec instead of skipping eight. `e2e/fixture-accents.ts` is gone. - **On the Dark ground the header mark's tile has a thin outline.** Its slate tile now has a 1-pixel ring just outside it, following its rounded corners, in the colour of the mark's unlit lines, so the tile's edge shows against the dark page. Light and Sepia are unchanged, and so are the icons. - **The lines inside the Archilyzer mark are easier to see.** The header mark's and the icons' three unlit lines now read at 3:1 against the slate tile instead of 2:1. -- **The homepage opens on the Dark ground in Signal, even with JavaScript off, and a reader can pick another ground or accent.** - The theme menu has **Base** (System, Light, Sepia, Dark) and **Accent** (the seven named - accents, Signal tagged *default*); the toggle cycles System → Light → Sepia → Dark. The old - Archilyzer theme family is gone, and so are the other four: the dark ground is now the warm ink - the archives used. Type is Archivo, IBM Plex Sans and IBM Plex Mono. The chart's third colour - is a violet, well clear of the "gone" red, and the chart stacks its instances in palette order, - so no two touching layers are hard to tell apart for a colour-blind reader. A stored theme from - before carries over once. The docs' *Operate* page describes the accent and the reader's menu. +- **The homepage opens on the Dark ground in Signal, even with JavaScript off, and a reader can pick another ground.** + The header's toggle cycles System → Light → Sepia → Dark; the accent is the homepage's own, + Signal. The old Archilyzer theme family is gone, and so are the other four: the dark ground is + now the warm ink the archives used. Type is Archivo, IBM Plex Sans and IBM Plex Mono. The + chart's third colour is a violet, well clear of the "gone" red, and the chart stacks its + instances in palette order, so no two touching layers are hard to tell apart for a colour-blind + reader. A stored theme from before carries over once. The docs' *Operate* page describes the + accent. - **The Found-line mark.** The header's CSS triangle is now the family's parent mark (bone on slate) and "ARCHILYZER" is the split wordmark "Archi|lyzer" — heavy lead, light suffix, no longer tracked uppercase; the link is still named "Archilyzer home". The