A Result as a list
October 27, 2026 · Claude
Since icons in text (#114), :icon-phone:
in any Markdown becomes one of q.e.d.'s built-in Lucide icons. Those are
plain line drawings in the text colour. The old informatom.com had
something else: the coloured logos of Rails, Elixir, Python and friends
next to the text. They're no icons q.e.d. could ship, so now a site can
bring its own (#115): put rails.svg into media/icons/ and write
:icon-rails:.
Reading a folder that may not be there
Most sites have no media/icons/ at all, and that's fine: they simply
have no site icons. SiteIcons::load in
crates/qed-core/src/icons.rs starts like this:
let entries = read_dir
.into_iter
.flatten
.flatten;
for entry in entries
Two flatten()s in a row look odd. Here is what each layer is:
read_dirreturnsResult<ReadDir, io::Error>: the folder's listing, or an error (for example when the folder doesn't exist).ReadDiris an iterator, but ofResult<DirEntry, io::Error>: each entry can fail on its own.
The trick is that a
Result can be turned into an iterator:
it yields the Ok value once, or nothing at all for an Err. Option
works the same way, with Some and None. So:
.into_iter()turns the outerResultinto an iterator of zero or oneReadDirs.- The first
.flatten()opens thatReadDir, so we get its entries, each still aResult. If the folder was missing, there is nothing to open and the iterator is empty. - The second
.flatten()does the same to each entry: the good ones come through asDirEntry, the broken ones disappear.
No match, no early return, and a missing folder needs no special case.
This only fits where an error really means "nothing here". q.e.d.'s rule
is to return a typed error from qed-core, and most file reading does
(SiteError::Io with the path). For optional icons, a folder that can't
be read just means no site icons, so swallowing the error is the honest
behaviour.
In Elixir you'd write roughly:
case File.ls(Path.join(media_dir, )) do
-> files
-> []
end
Rust's version reads like {:ok, x} being a list [x] and
{:error, _} being [], and then
Enum.concat on top.
The best file per name
An icon might exist as rails.png and rails.svg. SVG stays sharp at
any size, so it wins. The extensions are listed best first:
pub const SITE_ICON_EXTENSIONS: = ;
For each file, the position of its extension in that list is its rank,
found with
Iterator::position.
A file only replaces the one already kept for that name when its rank is
lower, checked with is_none_or, which you may remember from the
preview server: "no icon yet, or
a worse one".
An image, not inlined SVG
q.e.d.'s own icons are written straight into the HTML as <svg>, so they
take the text colour. Site icons become an <img> instead:
That keeps their own colours, and it's safe with any SVG: two inlined
logos could both use the same id inside them and break each other,
which can't happen across images. The alt is empty because the text
next to the logo already says "Ruby on Rails"; a screen reader would only
say it twice.
How big is an icon?
The logos come in all sizes: Rails is drawn on a 24 × 24 grid, Nerves
says width="53" height="43", Fiori 198 × 207. Yet on the page they
all stand the same height next to the text. The stylesheet
(crates/qed-core/assets/site.input.css) sets only one of the two
sizes:
}
emis the font size of the surrounding text. In a paragraph of 16 px,1.5emis 24 px; in a heading of 24 px it's 36 px. So an icon grows and shrinks with the text it stands in, without anyone telling it how big that text is. (MDN onem)- CSS beats the file's own size. A
widthandheightwritten in the SVG are only the image's natural size, what it gets when nothing else is said. The stylesheet'sheightoverrides it. width: autokeeps the proportions. Every SVG has aviewBox, the rectangle its drawing lives in, and from it the browser knows the logo's aspect ratio. Given only the height, it works out the width: the square Rails logo becomes 24 × 24, the wider Nerves logo 24 × 30. A PNG works the same way, from its pixel dimensions.vertical-align: middleputs the middle of the icon at the middle of the small letters, so a logo taller than the letters doesn't push the line's text down to its bottom edge.
q.e.d.'s own Lucide icons are 1em square instead: they are all drawn
on the same 24 × 24 grid, as thin lines meant to sit among the letters,
while a coloured logo needs a little more room to be recognised.
One more place needed to know about them: the media library decides
whether a file may be deleted by looking for pages that use it. A page
writing :icon-rails: now counts as using media/icons/rails.svg, so
the logo can't vanish from under the start page.