~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
1239 lines · 44.8 KB Raw History
//! Social cards: the picture a link to a page unfurls into, in a chat or a
//! post. Each is an SVG written here, as the pages are, and rasterized in
//! process: no browser, and no fonts but those compiled in, Inter, Noto
//! Color Emoji for what Inter lacks, and JetBrains Mono for the name, cut to
//! its letters, so a card looks the same on every host.
//!
//! A card's URL carries a hash of its SVG, so the picture can be cached
//! forever: when what it shows changes, so does the URL its page names.
//! A person's picture is named in the SVG by its address, which changes
//! with the picture, and its bytes are handed to the renderer beside it.

use crate::languages::Language;
use resvg::{tiny_skia, usvg};
use sha2::{Digest, Sha256};
use skrifa::MetadataProvider;
use std::collections::{HashMap, VecDeque};
use std::fmt::Write;
use std::sync::{Arc, LazyLock, Mutex};

/// Icons, each a 16-unit path: GitHub's Octicons, as the pages draw them.
mod icon {
    pub const ISSUE: &str = "M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z";
    pub const PULL: &str = "M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z";
    pub const COMMENTS: &str = "M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z";
    pub const REVIEW: &str = "M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 13H8.061l-2.574 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25v-8.5C0 1.784.784 1 1.75 1ZM1.5 2.75v8.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Zm5.28 1.72a.75.75 0 0 1 0 1.06L5.31 7l1.47 1.47a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-2-2a.75.75 0 0 1 0-1.06l2-2a.75.75 0 0 1 1.06 0Zm2.44 0a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L10.69 7 9.22 5.53a.75.75 0 0 1 0-1.06Z";
    pub const FILES: &str = "M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z";
}

fn esc(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            c => out.push(c),
        }
    }
    out
}

pub const WIDTH: u32 = 1200;
pub const HEIGHT: u32 = 630;
const PAD: f32 = 80.0;
/// Where the text of the last row sits.
const FOOT: f32 = 550.0;

const FG: &str = "#1c1d20";
const MUTE: &str = "#6a6d75";
const ACCENT: &str = "#2f6af5";

static REGULAR: &[u8] = include_bytes!("../fonts/InterDisplay-Regular.ttf");
static SEMIBOLD: &[u8] = include_bytes!("../fonts/InterDisplay-SemiBold.ttf");
static EMOJI: &[u8] = include_bytes!("../fonts/NotoColorEmoji-COLRv1.ttf");
/// Only what `~ knotless` needs, as the site's header sets it.
static MONO: &[u8] = include_bytes!("../fonts/JetBrainsMono-SemiBold.ttf");

/// Read where they are compiled in, not copied.
static FONTS: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
    let mut db = usvg::fontdb::Database::new();
    for data in [REGULAR, SEMIBOLD, EMOJI, MONO] {
        db.load_font_source(usvg::fontdb::Source::Binary(Arc::new(data)));
    }
    Arc::new(db)
});

fn emoji() -> skrifa::FontRef<'static> {
    skrifa::FontRef::new(EMOJI).expect("the fonts compiled in parse")
}

#[derive(Clone, Copy)]
enum Weight {
    Regular,
    Semibold,
}

impl Weight {
    fn font(self) -> skrifa::FontRef<'static> {
        let data = match self {
            Weight::Regular => REGULAR,
            Weight::Semibold => SEMIBOLD,
        };
        skrifa::FontRef::new(data).expect("the fonts compiled in parse")
    }

    fn css(self) -> u16 {
        match self {
            Weight::Regular => 400,
            Weight::Semibold => 600,
        }
    }
}

/// Whether `c` draws as part of the character before it, `prev`, rather
/// than on its own: the joiners, selectors, skin tones and tags of an emoji
/// sequence, a keycap, what a joiner joins, and the second letter of a flag
/// (`flag` being whether `prev` is a flag's first).
fn joins(prev: Option<char>, c: char, flag: bool) -> bool {
    let Some(prev) = prev else {
        return false;
    };
    matches!(
        c,
        '\u{200D}' | '\u{FE00}'..='\u{FE0F}' | '\u{20E3}' | '\u{1F3FB}'..='\u{1F3FF}' | '\u{E0020}'..='\u{E007F}'
    ) || prev == '\u{200D}'
        || (flag && is_flag_letter(c))
}

fn is_flag_letter(c: char) -> bool {
    ('\u{1F1E6}'..='\u{1F1FF}').contains(&c)
}

/// `text` split where a line may break inside a word: between the
/// characters drawn apart, each with the sequence it heads.
fn clusters(text: &str) -> Vec<&str> {
    let mut out = Vec::new();
    let mut start = 0;
    let mut prev = None;
    let mut flag = false;
    for (i, c) in text.char_indices() {
        if joins(prev, c, flag) {
            flag = false;
        } else {
            if i > start {
                out.push(&text[start..i]);
            }
            start = i;
            flag = is_flag_letter(c);
        }
        prev = Some(c);
    }
    if start < text.len() {
        out.push(&text[start..]);
    }
    out
}

/// How wide `text` is set at `size`: each cluster as wide as its first
/// character, in Inter, or the emoji if Inter lacks it. Kerning is left
/// out, which errs wide.
fn measure(text: &str, size: f32, weight: Weight) -> f32 {
    let size = skrifa::instance::Size::new(size);
    let at = skrifa::instance::LocationRef::default();
    let (text_font, emoji) = (weight.font(), emoji());
    let (text_map, emoji_map) = (text_font.charmap(), emoji.charmap());
    let text_metrics = text_font.glyph_metrics(size, at);
    let emoji_metrics = emoji.glyph_metrics(size, at);
    clusters(text)
        .into_iter()
        .map(|cluster| {
            let c = cluster.chars().next().expect("a cluster is not empty");
            let advance = match (text_map.map(c), emoji_map.map(c)) {
                (None, Some(glyph)) => emoji_metrics.advance_width(glyph),
                // What Inter lacks and the emoji too is drawn as Inter's box.
                (glyph, _) => text_metrics.advance_width(glyph.unwrap_or_default()),
            };
            advance.unwrap_or_default()
        })
        .sum()
}

/// `text` broken into at most `max` lines no wider than `width`, the last
/// ending in an ellipsis if it had to be cut.
fn wrap(text: &str, size: f32, weight: Weight, width: f32, max: usize) -> Vec<String> {
    let fits = |s: &str| measure(s, size, weight) <= width;
    let mut lines: Vec<String> = Vec::new();
    let mut line = String::new();
    for word in text.split_whitespace() {
        if lines.len() > max {
            break;
        }
        let joined = format!("{line} {word}");
        let joined = joined.trim_start();
        if fits(joined) {
            line = joined.to_string();
            continue;
        }
        if !line.is_empty() {
            lines.push(std::mem::take(&mut line));
        }
        // A word wider than a line is broken where it overflows, between
        // what is drawn apart.
        for cluster in clusters(word) {
            let alone = line.is_empty();
            line.push_str(cluster);
            if !alone && !fits(&line) {
                line.truncate(line.len() - cluster.len());
                lines.push(std::mem::replace(&mut line, cluster.to_string()));
            }
        }
    }
    if !line.is_empty() {
        lines.push(line);
    }
    if lines.len() > max {
        lines.truncate(max);
        let last = lines.last_mut().expect("max is at least one");
        while !fits(&format!("{last}…")) && !last.is_empty() {
            let end = clusters(last).last().expect("not empty").len();
            last.truncate(last.len() - end);
        }
        let kept = last.trim_end().len();
        last.truncate(kept);
        last.push('…');
    }
    lines
}

/// Whether `c` is drawn from the emoji, as Inter lacks it.
fn is_emoji(c: char) -> bool {
    Weight::Regular.font().charmap().map(c).is_none() && emoji().charmap().map(c).is_some()
}

/// `text`, left on `x`, escaped: in Inter, and each run of what Inter
/// lacks in the emoji. Each run is placed on its own, so it is shaped in
/// its font alone: the renderer, given runs in two fonts to shape together,
/// drops sequences of emoji, or fails on them.
fn spans(text: &str, x: f32, size: f32, weight: Weight) -> String {
    // Inter has all of ASCII.
    if text.is_ascii() {
        return esc(text);
    }
    let mut runs: Vec<(bool, String)> = Vec::new();
    for cluster in clusters(text) {
        let emoji = is_emoji(cluster.chars().next().expect("a cluster is not empty"));
        match runs.last_mut() {
            Some((was, run)) if *was == emoji => run.push_str(cluster),
            _ => runs.push((emoji, cluster.to_string())),
        }
    }
    if runs.iter().all(|(emoji, _)| !emoji) {
        return esc(text);
    }
    let mut out = String::new();
    let mut x = x;
    for (emoji, run) in runs {
        let family = if emoji {
            r#" font-family="Noto Color Emoji""#
        } else {
            ""
        };
        write!(out, r#"<tspan x="{x}"{family}>{}</tspan>"#, esc(&run)).unwrap();
        x += measure(&run, size, weight);
    }
    out
}

/// Lines of text, `leading` apart, the first on `y`.
fn lines(
    out: &mut String,
    lines: &[String],
    x: f32,
    y: f32,
    leading: f32,
    (size, weight, fill): (f32, Weight, &str),
) {
    for (i, line) in lines.iter().enumerate() {
        write!(
            out,
            r#"<text x="{x}" y="{}" {}>{}</text>"#,
            y + leading * i as f32,
            style(size, weight, fill),
            spans(line, x, size, weight)
        )
        .unwrap();
    }
}

fn style(size: f32, weight: Weight, fill: &str) -> String {
    format!(
        r#"font-size="{size}" font-weight="{}" fill="{fill}""#,
        weight.css()
    )
}

/// A 16-unit icon, `size` across, its top left at `x`, `y`.
fn icon(out: &mut String, path: &str, x: f32, y: f32, size: f32, fill: &str) {
    write!(
        out,
        r#"<path transform="translate({x} {y}) scale({})" fill="{fill}" d="{path}"/>"#,
        size / 16.0
    )
    .unwrap();
}

/// The site's name as its header sets it, `~ knotless` in a semibold mono,
/// `size` high, its left at `x`.
fn brand(out: &mut String, x: f32, baseline: f32, size: f32) {
    write!(
        out,
        r#"<text x="{x}" y="{baseline}" font-family="JetBrains Mono NL" {}>~ knotless</text>"#,
        style(size, Weight::Semibold, FG)
    )
    .unwrap();
}

/// How wide `brand` draws at `size`.
fn brand_width(size: f32) -> f32 {
    let font = skrifa::FontRef::new(MONO).expect("the fonts compiled in parse");
    let metrics = font.glyph_metrics(
        skrifa::instance::Size::new(size),
        skrifa::instance::LocationRef::default(),
    );
    let map = font.charmap();
    "~ knotless"
        .chars()
        .filter_map(|c| metrics.advance_width(map.map(c)?))
        .sum()
}

/// The hue a name's mark is drawn in.
pub(crate) fn hue(name: &str) -> u32 {
    name.bytes()
        .fold(7u32, |h, b| h.wrapping_mul(31).wrapping_add(b as u32))
        % 360
}

/// The letter a name's mark shows.
pub(crate) fn initial(name: &str) -> char {
    name.chars().find(|c| c.is_alphanumeric()).unwrap_or('?')
}

/// A name's round mark, `r` in radius, centered on `cx`, `cy`: its
/// initial, and over it the picture at `picture`, cut to the circle. A
/// picture the renderer is not given, or cannot read, leaves the initial.
fn avatar(out: &mut String, name: &str, picture: Option<&str>, (cx, cy, r): (f32, f32, f32)) {
    write!(
        out,
        r#"<circle cx="{cx}" cy="{cy}" r="{r}" fill="hsl({}, 45%, 52%)"/><text x="{cx}" y="{}" text-anchor="middle" {}>{}</text>"#,
        hue(name),
        cy + r * 0.36,
        style(r, Weight::Semibold, "#fff"),
        esc(&initial(name).to_uppercase().to_string())
    )
    .unwrap();
    let Some(picture) = picture else {
        return;
    };
    write!(
        out,
        r#"<clipPath id="avatar"><circle cx="{cx}" cy="{cy}" r="{r}"/></clipPath><image href="{}" x="{}" y="{}" width="{}" height="{}" preserveAspectRatio="xMidYMid slice" clip-path="url(#avatar)"/>"#,
        esc(picture),
        cx - r,
        cy - r,
        r * 2.0,
        r * 2.0
    )
    .unwrap();
}

/// The day `at`, a timestamp, falls on in UTC, as `November 1, 2026`;
/// none if it is no timestamp.
fn date(at: &str) -> Option<String> {
    let at: jiff::Timestamp = at.parse().ok()?;
    Some(
        at.to_zoned(jiff::tz::TimeZone::UTC)
            .strftime("%B %-d, %Y")
            .to_string(),
    )
}

fn plural(n: usize, one: &str, many: &str) -> String {
    format!("{n} {}", if n == 1 { one } else { many })
}

/// What a card shows.
#[derive(Debug, Clone, PartialEq)]
pub enum Card {
    /// The front page.
    Home,
    Repo {
        owner: String,
        /// The address of the owner's picture, if it has one.
        avatar: Option<String>,
        name: String,
        description: Option<String>,
        /// Open pulls and issues.
        pulls: usize,
        issues: usize,
        /// What it is written in, drawn along the foot.
        languages: Vec<Language>,
    },
    /// A pull or an issue.
    Item {
        owner: String,
        /// The address of the owner's picture, if it has one.
        avatar: Option<String>,
        name: String,
        pull: bool,
        title: String,
        author: String,
        /// When it was opened, as its record says.
        opened: Option<String>,
        comments: usize,
        /// A pull's reviews; none for an issue.
        reviews: usize,
        /// What a pull changes, if its commits could be read.
        size: Option<Size>,
    },
}

/// What a pull changes, as its card counts it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Size {
    pub commits: usize,
    pub files: usize,
    pub added: usize,
    pub removed: usize,
}

impl Size {
    /// Of five squares, how many are green for lines added and how many
    /// red for lines removed, as GitHub draws them; the rest are grey.
    /// Each side that changed anything shows at least one.
    fn squares(self) -> (usize, usize) {
        let all = self.added + self.removed;
        if all == 0 {
            return (0, 0);
        }
        let red = (5 * self.removed).div_ceil(all);
        let red = red.min(5 - usize::from(self.added > 0));
        (5 - red, red)
    }
}

const ADDED: &str = "#1a7f37";
const REMOVED: &str = "#cf222e";
const UNCHANGED: &str = "#d0d7de";

impl Card {
    /// The address of the picture it shows, if any.
    pub fn picture(&self) -> Option<&str> {
        match self {
            Card::Home => None,
            Card::Repo { avatar, .. } | Card::Item { avatar, .. } => avatar.as_deref(),
        }
    }

    pub fn svg(&self) -> String {
        let mut out = format!(
            r##"<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}"><rect width="{WIDTH}" height="{HEIGHT}" fill="#fff"/>"##
        );
        let wide = WIDTH as f32 - PAD * 2.0;
        let bar = match self {
            Card::Home => {
                brand(&mut out, PAD, 168.0, 68.0);
                let head = wrap(
                    "Git that lives in your atmosphere account.",
                    80.0,
                    Weight::Semibold,
                    wide,
                    2,
                );
                let at = 330.0;
                lines(&mut out, &head, PAD, at, 92.0, (80.0, Weight::Semibold, FG));
                let sub = wrap(
                    "Repositories as records on your own PDS. Clone and push with plain git; agents read, review and merge over MCP.",
                    36.0,
                    Weight::Regular,
                    wide,
                    2,
                );
                lines(
                    &mut out,
                    &sub,
                    PAD,
                    at + 92.0 + 80.0,
                    50.0,
                    (36.0, Weight::Regular, MUTE),
                );
                ACCENT.to_string()
            }
            Card::Repo {
                owner,
                avatar,
                name,
                description,
                pulls,
                issues,
                ..
            } => {
                top(&mut out, (owner, avatar.as_deref()), None);
                let title = wrap(name, 84.0, Weight::Semibold, wide, 2);
                lines(
                    &mut out,
                    &title,
                    PAD,
                    268.0,
                    96.0,
                    (84.0, Weight::Semibold, FG),
                );
                let below = 268.0 + 96.0 * (title.len() - 1) as f32 + 72.0;
                // As many lines as fit above the last row.
                let room = ((470.0 - below) / 48.0) as usize + 1;
                let about = wrap(
                    description.as_deref().unwrap_or_default(),
                    36.0,
                    Weight::Regular,
                    wide,
                    room,
                );
                lines(
                    &mut out,
                    &about,
                    PAD,
                    below,
                    48.0,
                    (36.0, Weight::Regular, MUTE),
                );
                brand(&mut out, PAD, FOOT, 44.0);
                // The counts, from the right edge leftward.
                let mut right = WIDTH as f32 - PAD;
                for (path, text) in [
                    (icon::PULL, plural(*pulls, "open pull", "open pulls")),
                    (icon::ISSUE, plural(*issues, "open issue", "open issues")),
                ] {
                    let w = measure(&text, 32.0, Weight::Regular);
                    write!(
                        out,
                        r#"<text x="{right}" y="{FOOT}" text-anchor="end" {}>{}</text>"#,
                        style(32.0, Weight::Regular, MUTE),
                        text
                    )
                    .unwrap();
                    icon(&mut out, path, right - w - 42.0, FOOT - 28.0, 32.0, MUTE);
                    right -= w + 42.0 + 40.0;
                }
                format!("hsl({}, 45%, 52%)", hue(owner))
            }
            Card::Item {
                owner,
                avatar,
                name,
                pull,
                title,
                author,
                opened,
                comments,
                reviews,
                size,
            } => {
                top(&mut out, (owner, avatar.as_deref()), Some(name));
                // A pull's counts take a row above the last, so a title
                // three lines long is set smaller to leave it room.
                let mut head = wrap(title, 72.0, Weight::Semibold, wide, 3);
                let (mut set, mut at) = ((72.0, 86.0), 262.0);
                if *pull && head.len() == 3 {
                    head = wrap(title, 58.0, Weight::Semibold, wide, 3);
                    (set, at) = ((58.0, 68.0), 238.0);
                }
                lines(
                    &mut out,
                    &head,
                    PAD,
                    at,
                    set.1,
                    (set.0, Weight::Semibold, FG),
                );
                if *pull {
                    counts(&mut out, *comments, *reviews, *size);
                }
                let mark = brand_width(34.0);
                // A pull's comments are counted above; its commits here.
                let mut said = vec![author.clone()];
                said.extend(opened.as_deref().and_then(date));
                said.extend(match (*pull, size) {
                    (false, _) => Some(plural(*comments, "comment", "comments")),
                    (true, Some(size)) => Some(plural(size.commits, "commit", "commits")),
                    (true, None) => None,
                });
                let room = WIDTH as f32 - PAD - mark - 48.0 - PAD;
                let by = wrap(&said.join(" · "), 32.0, Weight::Regular, room, 1);
                lines(&mut out, &by, PAD, FOOT, 0.0, (32.0, Weight::Regular, MUTE));
                brand(&mut out, WIDTH as f32 - PAD - mark, FOOT, 34.0);
                format!("hsl({}, 45%, 52%)", hue(owner))
            }
        };
        write!(
            out,
            r#"<rect y="{}" width="{WIDTH}" height="14" fill="{bar}"/>"#,
            HEIGHT - 14
        )
        .unwrap();
        if let Card::Repo { languages, .. } = self {
            language_bar(&mut out, languages);
        }
        out.push_str("</svg>");
        out
    }
}

/// Where a pull's counts sit, above the last row.
const COUNTS: f32 = 456.0;

/// A pull's counts, left to right as GitHub's cards draw them: its
/// comments, reviews and files, then the lines it adds and removes and
/// five squares of their share.
fn counts(out: &mut String, comments: usize, reviews: usize, size: Option<Size>) {
    let (text, gap) = (30.0, 36.0);
    let mut x = PAD;
    let mut counted = vec![
        (icon::COMMENTS, plural(comments, "comment", "comments")),
        (icon::REVIEW, plural(reviews, "review", "reviews")),
    ];
    if let Some(size) = size {
        counted.push((icon::FILES, plural(size.files, "file", "files")));
    }
    for (path, said) in counted {
        icon(out, path, x, COUNTS - 25.0, 30.0, MUTE);
        x += 30.0 + 10.0;
        write!(
            out,
            r#"<text x="{x}" y="{COUNTS}" {}>{}</text>"#,
            style(text, Weight::Regular, MUTE),
            esc(&said)
        )
        .unwrap();
        x += measure(&said, text, Weight::Regular) + gap;
    }
    let Some(size) = size else { return };
    for (said, fill) in [
        (format!("+{}", size.added), ADDED),
        (format!("-{}", size.removed), REMOVED),
    ] {
        write!(
            out,
            r#"<text x="{x}" y="{COUNTS}" {}>{said}</text>"#,
            style(text, Weight::Semibold, fill),
        )
        .unwrap();
        x += measure(&said, text, Weight::Semibold) + 10.0;
    }
    let (green, red) = size.squares();
    for i in 0..5 {
        let fill = match i {
            i if i < green => ADDED,
            i if i < green + red => REMOVED,
            _ => UNCHANGED,
        };
        write!(
            out,
            r#"<rect x="{}" y="{}" width="18" height="18" rx="2" fill="{fill}"/>"#,
            x + 4.0 + 24.0 * i as f32,
            COUNTS - 19.0
        )
        .unwrap();
    }
}

/// A repository's languages along the foot, over its color, as GitHub's
/// cards draw them: each as wide as its share of the bytes, its edges on
/// whole pixels, so none shows a seam.
fn language_bar(out: &mut String, languages: &[Language]) {
    let total: u64 = languages.iter().map(|l| l.bytes).sum();
    let edge = |bytes: u64| (WIDTH as u64 * bytes + total / 2) / total;
    let mut before = 0;
    for l in languages {
        let (x, end) = (edge(before), edge(before + l.bytes));
        write!(
            out,
            r#"<rect x="{x}" y="{}" width="{}" height="14" fill="{}"/>"#,
            HEIGHT - 14,
            end - x,
            esc(&l.color)
        )
        .unwrap();
        before += l.bytes;
    }
}

/// The first row: the owner's mark, or picture, and name, and the
/// repository's after.
fn top(out: &mut String, (owner, picture): (&str, Option<&str>), name: Option<&str>) {
    avatar(out, owner, picture, (PAD + 32.0, 112.0, 32.0));
    let x = PAD + 84.0;
    let room = WIDTH as f32 - PAD - x;
    let Some(name) = name else {
        let shown = wrap(owner, 36.0, Weight::Regular, room, 1);
        lines(out, &shown, x, 125.0, 0.0, (36.0, Weight::Regular, MUTE));
        return;
    };
    let owner = wrap(&format!("{owner}/"), 36.0, Weight::Regular, room / 2.0, 1);
    let after = x + measure(&owner[0], 36.0, Weight::Regular);
    let name = wrap(name, 36.0, Weight::Semibold, WIDTH as f32 - PAD - after, 1);
    lines(out, &owner, x, 125.0, 0.0, (36.0, Weight::Regular, MUTE));
    lines(out, &name, after, 125.0, 0.0, (36.0, Weight::Semibold, FG));
}

/// What names a card's picture: a hash of its SVG.
pub fn version(svg: &str) -> String {
    let mut out = String::with_capacity(16);
    for b in &Sha256::digest(svg)[..8] {
        write!(out, "{b:02x}").unwrap();
    }
    out
}

/// The bytes of a picture as the renderer takes them, if they are of a
/// kind it reads: told by their first bytes, as what serves them may not
/// say.
fn image(bytes: Arc<Vec<u8>>) -> Option<usvg::ImageKind> {
    match bytes.as_slice() {
        [0x89, b'P', b'N', b'G', ..] => Some(usvg::ImageKind::PNG(bytes)),
        [0xFF, 0xD8, 0xFF, ..] => Some(usvg::ImageKind::JPEG(bytes)),
        [b'G', b'I', b'F', b'8', ..] => Some(usvg::ImageKind::GIF(bytes)),
        [
            b'R',
            b'I',
            b'F',
            b'F',
            _,
            _,
            _,
            _,
            b'W',
            b'E',
            b'B',
            b'P',
            ..,
        ] => Some(usvg::ImageKind::WEBP(bytes)),
        _ => None,
    }
}

/// The SVG drawn, its picture, if it names one, from `picture`: never
/// read from a file or the network here.
pub fn raster(svg: &str, picture: Option<Arc<Vec<u8>>>) -> tiny_skia::Pixmap {
    let resolve = move |_: &str, _: &usvg::Options| picture.clone().and_then(image);
    let options = usvg::Options {
        font_family: "Inter Display".into(),
        fontdb: FONTS.clone(),
        image_href_resolver: usvg::ImageHrefResolver {
            resolve_data: Box::new(|_, _, _| None),
            resolve_string: Box::new(resolve),
        },
        ..Default::default()
    };
    let tree = usvg::Tree::from_str(svg, &options).expect("a card's SVG parses");
    let mut pixmap = tiny_skia::Pixmap::new(WIDTH, HEIGHT).expect("a card has a size");
    resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
    pixmap
}

/// The drawing as a PNG. A card is a few flat colors and the edges between
/// them, not 256 in all, so it is written exactly with a palette: a third
/// of the bytes to compress, and a fast compressor makes it smaller than
/// a thorough one makes RGB. With more colors, it is RGB.
pub fn encode(pixmap: &tiny_skia::Pixmap) -> Vec<u8> {
    let (w, h) = (pixmap.width(), pixmap.height());
    // Opaque, so its premultiplied colors are its colors.
    let pixels = pixmap
        .data()
        .as_chunks::<4>()
        .0
        .iter()
        .map(|p| [p[0], p[1], p[2]]);
    let mut palette: Vec<u8> = Vec::new();
    let mut known: HashMap<[u8; 3], u8> = HashMap::new();
    let mut indexed = Vec::with_capacity((w * h) as usize);
    // Neighbours are mostly the same color: it is looked up once a run.
    let mut last = None;
    for rgb in pixels.clone() {
        let i = match last {
            Some((seen, i)) if seen == rgb => i,
            _ => {
                let i = match known.get(&rgb) {
                    Some(&i) => i,
                    None => {
                        let Ok(i) = u8::try_from(known.len()) else {
                            let rgb: Vec<u8> = pixels.flatten().collect();
                            return png(w, h, png::ColorType::Rgb, None, &rgb);
                        };
                        known.insert(rgb, i);
                        palette.extend_from_slice(&rgb);
                        i
                    }
                };
                last = Some((rgb, i));
                i
            }
        };
        indexed.push(i);
    }
    png(w, h, png::ColorType::Indexed, Some(palette), &indexed)
}

fn png(w: u32, h: u32, color: png::ColorType, palette: Option<Vec<u8>>, data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() / 16);
    let mut encoder = png::Encoder::new(&mut out, w, h);
    encoder.set_color(color);
    encoder.set_depth(png::BitDepth::Eight);
    if let Some(palette) = palette {
        encoder.set_palette(palette);
    }
    encoder.set_compression(png::Compression::Fast);
    encoder.set_filter(png::Filter::Adaptive);
    let mut writer = encoder.write_header().expect("writing to memory");
    writer.write_image_data(data).expect("writing to memory");
    writer.finish().expect("writing to memory");
    out
}

/// The SVG as a PNG, with the bytes of the picture it names.
pub fn render(svg: &str, picture: Option<Arc<Vec<u8>>>) -> Vec<u8> {
    encode(&raster(svg, picture))
}

/// Values by key, the oldest dropped past a number of them, or past a
/// number of bytes when each is weighed.
pub struct Newest<V> {
    kept: Mutex<Entries<V>>,
    cap: usize,
    weigh: fn(&V) -> usize,
}

struct Entries<V> {
    by_key: HashMap<String, V>,
    /// Oldest first.
    order: VecDeque<String>,
    /// What all of them weigh together.
    weight: usize,
}

impl<V: Clone> Newest<V> {
    /// Keeping `cap` of them.
    pub fn new(cap: usize) -> Newest<V> {
        Newest::weighed(cap, |_| 1)
    }

    /// Keeping as many as `weigh` says weigh `cap` together, and always the
    /// newest.
    pub fn weighed(cap: usize, weigh: fn(&V) -> usize) -> Newest<V> {
        let keys = Entries {
            by_key: HashMap::new(),
            order: VecDeque::new(),
            weight: 0,
        };
        Newest {
            kept: Mutex::new(keys),
            cap,
            weigh,
        }
    }

    pub fn get(&self, key: &str) -> Option<V> {
        self.kept.lock().unwrap().by_key.get(key).cloned()
    }

    pub fn put(&self, key: &str, value: V) {
        let mut kept = self.kept.lock().unwrap();
        let Entries {
            by_key,
            order,
            weight,
        } = &mut *kept;
        *weight += (self.weigh)(&value);
        match by_key.insert(key.to_string(), value) {
            Some(was) => *weight -= (self.weigh)(&was),
            None => order.push_back(key.to_string()),
        }
        while *weight > self.cap && order.len() > 1 {
            let oldest = order.pop_front().expect("more than one");
            let was = by_key.remove(&oldest).expect("each in order is kept");
            *weight -= (self.weigh)(&was);
        }
    }

    pub fn len(&self) -> usize {
        self.kept.lock().unwrap().order.len()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

/// Cards drawn, by version, the oldest dropped past a number of them; and
/// a cap on how many are drawn at once, as each takes a core.
pub struct Cache {
    pub drawn: Newest<Arc<[u8]>>,
    drawing: tokio::sync::Semaphore,
}

impl Cache {
    pub fn new(cap: usize) -> Cache {
        let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
        Cache {
            drawn: Newest::new(cap),
            drawing: tokio::sync::Semaphore::new(cores),
        }
    }

    /// The PNG of `svg`, whose version is `version`, drawn with the bytes
    /// of the picture it names: kept under its version only if `whole`,
    /// as a card drawn without a picture it names may yet be drawn with it.
    pub async fn draw(
        &self,
        version: &str,
        svg: String,
        picture: Option<Arc<Vec<u8>>>,
        whole: bool,
    ) -> Arc<[u8]> {
        let _turn = self.drawing.acquire().await.expect("never closed");
        let png: Arc<[u8]> = tokio::task::spawn_blocking(move || render(&svg, picture))
            .await
            .expect("drawing a card does not panic")
            .into();
        if whole {
            self.drawn.put(version, png.clone());
        }
        png
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn newest_by_weight() {
        let kept: Newest<Vec<u8>> = Newest::weighed(10, Vec::len);
        kept.put("a", vec![0; 4]);
        kept.put("b", vec![0; 4]);
        assert_eq!(kept.len(), 2);
        // Put again, it weighs what it weighs now, not both.
        kept.put("b", vec![0; 6]);
        assert_eq!((kept.len(), kept.get("b").map(|b| b.len())), (2, Some(6)));
        // Past the cap, the oldest go.
        kept.put("c", vec![0; 3]);
        assert!(kept.get("a").is_none());
        assert_eq!(kept.len(), 2);
        // One heavier than the cap is still kept, alone.
        kept.put("d", vec![0; 20]);
        assert_eq!((kept.len(), kept.get("d").map(|d| d.len())), (1, Some(20)));
    }

    #[test]
    fn wrapping() {
        let w = |t: &str, width: f32, max: usize| wrap(t, 40.0, Weight::Regular, width, max);
        assert_eq!(w("one two three", 2000.0, 1), ["one two three"]);
        assert_eq!(w("one two three", 170.0, 3), ["one two", "three"]);
        let cut = w("one two three four five six", 170.0, 2);
        assert_eq!(cut.len(), 2);
        assert!(cut[1].ends_with('…'), "{cut:?}");
        // A word longer than a line breaks where it overflows.
        let long = w(&"x".repeat(40), 200.0, 5);
        assert!(long.len() > 1 && long.concat().len() == 40, "{long:?}");
        assert!(
            long.iter()
                .all(|l| measure(l, 40.0, Weight::Regular) <= 200.0)
        );
        assert!(w("", 100.0, 2).is_empty());
        assert_eq!(w("a b c d e f g h", 1.0, 1), ["…"]);
        // Narrower than one letter, a line still takes one.
        assert_eq!(w("ab", 1.0, 3), ["a", "b"]);
    }

    #[test]
    fn marks() {
        assert_eq!(
            esc(r#"<a href="x">'&'</a>"#),
            "&lt;a href=&quot;x&quot;&gt;&#39;&amp;&#39;&lt;/a&gt;"
        );
        assert_eq!(initial("_alice"), 'a');
        assert_eq!(initial("…"), '?');
        assert!(hue("alice") < 360);
        assert_eq!(
            date("2026-11-01T23:59:59Z").as_deref(),
            Some("November 1, 2026")
        );
        assert_eq!(date("yesterday"), None);
        // Both weights are one family, so asking for 600 gets the bolder.
        let faces: Vec<(String, u16)> = FONTS
            .faces()
            .map(|f| (f.families[0].0.clone(), f.weight.0))
            .collect();
        let family = || "Inter Display".to_string();
        let emoji = ("Noto Color Emoji".to_string(), 400);
        let mono = ("JetBrains Mono NL".to_string(), 600);
        assert_eq!(faces, [(family(), 400), (family(), 600), emoji, mono]);
        assert!(measure("W", 40.0, Weight::Semibold) > measure("i", 40.0, Weight::Semibold));
        // A letter the fonts lack is measured as the box drawn for it.
        assert!(measure("漢", 40.0, Weight::Regular) > 0.0);
    }

    #[test]
    fn emoji() {
        let one = measure("😀", 40.0, Weight::Regular);
        // As wide as Noto draws it, not as Inter's box.
        assert!(one > measure("W", 40.0, Weight::Regular), "{one}");
        assert_ne!(one, measure("漢", 40.0, Weight::Regular));
        // A sequence is drawn as one, however many characters it takes.
        for one_of in [
            "👩‍💻",
            "👋🏽",
            "❤️",
            "🇫🇷",
            "#️⃣",
            "🏴\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}",
            "👨‍👩‍👧‍👦",
        ] {
            assert_eq!(clusters(one_of), [one_of]);
        }
        assert_eq!(measure("👨‍👩‍👧‍👦", 40.0, Weight::Regular), one);
        // Flags pair off; a letter left over stands alone.
        assert_eq!(clusters("🇫🇷🇩🇪🇮"), ["🇫🇷", "🇩🇪", "🇮"]);
        assert_eq!(clusters("a😀b"), ["a", "😀", "b"]);
        // A selector with nothing before it stands alone.
        assert_eq!(clusters("\u{FE0F}x"), ["\u{FE0F}", "x"]);
        // Lines break, and are cut, between sequences, never inside one.
        let family = "👨‍👩‍👧‍👦".repeat(12);
        let broken = wrap(&family, 40.0, Weight::Regular, one * 5.0, 2);
        assert_eq!(broken[0], "👨‍👩‍👧‍👦".repeat(5));
        assert!(broken[1].ends_with('…'), "{broken:?}");
        assert!(clusters(&broken[1]).iter().all(|c| *c == "👨‍👩‍👧‍👦" || *c == "…"));
        let repo = |description: &str| {
            render(
                &Card::Repo {
                    owner: "alice.test".into(),
                    avatar: None,
                    name: "party".into(),
                    description: Some(description.into()),
                    pulls: 0,
                    issues: 0,
                    languages: Vec::new(),
                }
                .svg(),
                None,
            )
        };
        // Drawn in Noto's colors, not as boxes in the text's.
        let drawn = repo("Flags 🇫🇷🇯🇵, people 👩🏽‍🔬 and hearts ❤️");
        assert!(drawn.len() > repo("Flags ab, people c and hearts d").len());
        // What no keyboard makes draws too, rather than failing the card.
        for odd in [
            "😀\u{200D}a",
            "a\u{200D}😀",
            "\u{200D}\u{FE0F}",
            "🇫 x",
            "#\u{FE0F}\u{20E3}x",
            "😀\u{200D}漢 é 😀",
            "\u{1F3FD}\u{1F3FD}",
            "<😀&>",
        ] {
            repo(odd);
        }
    }

    fn item(opened: Option<&str>, pull: bool) -> Card {
        Card::Item {
            owner: "alice.test".into(),
            avatar: None,
            name: "tracked".into(),
            pull,
            title: "Broken <thing> & more".into(),
            author: "bob.test".into(),
            opened: opened.map(str::to_string),
            comments: 1,
            reviews: usize::from(pull),
            size: pull.then_some(Size {
                commits: 2,
                files: 3,
                added: 181,
                removed: 18,
            }),
        }
    }

    #[test]
    fn squares() {
        let size = |added, removed| Size {
            added,
            removed,
            ..Size::default()
        };
        assert_eq!(size(0, 0).squares(), (0, 0));
        assert_eq!(size(181, 18).squares(), (4, 1));
        assert_eq!(size(10, 0).squares(), (5, 0));
        assert_eq!(size(0, 3).squares(), (0, 5));
        assert_eq!(size(1, 1).squares(), (2, 3));
        assert_eq!(size(1, 1000).squares(), (1, 4));
        assert_eq!(size(1000, 1).squares(), (4, 1));
    }

    #[test]
    fn every_card_draws() {
        let cards = [
            Card::Home,
            Card::Repo {
                owner: "alice.test".into(),
                avatar: None,
                name: "a-repository-name-long-enough-to-wrap-onto-two-lines".into(),
                description: Some("Tracks <things>.".into()),
                pulls: 1,
                issues: 2,
                languages: crate::languages::breakdown([(0, 700), (1, 290), (2, 6), (3, 4)]),
            },
            Card::Repo {
                owner: "alice.test".into(),
                avatar: None,
                name: "bare".into(),
                description: None,
                pulls: 0,
                issues: 0,
                languages: Vec::new(),
            },
            item(Some("2026-11-01T12:00:00.000Z"), true),
            item(None, true),
            item(Some("2026-11-01T12:00:00.000Z"), false),
            item(None, false),
            // A long title set smaller, over a pull whose commits were
            // not read.
            Card::Item {
                owner: "alice.test".into(),
                avatar: None,
                name: "tracked".into(),
                pull: true,
                title: "A title long enough that it wraps onto three lines at the size titles are set on a card".into(),
                author: "bob.test".into(),
                opened: Some("not a time".into()),
                comments: 0,
                reviews: 0,
                size: None,
            },
        ];
        let mut versions = std::collections::HashSet::new();
        for card in &cards {
            let svg = card.svg();
            assert!(!svg.contains("<thing>"), "{svg}");
            assert!(versions.insert(version(&svg)));
            let png = render(&svg, None);
            assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
        }
        assert_eq!(version("x").len(), 16);
    }

    #[test]
    fn more_than_a_palette_holds() {
        let mut pixmap = tiny_skia::Pixmap::new(32, 32).unwrap();
        for (i, p) in pixmap
            .data_mut()
            .as_chunks_mut::<4>()
            .0
            .iter_mut()
            .enumerate()
        {
            p.copy_from_slice(&[i as u8, (i >> 8) as u8, 0, 255]);
        }
        let rgb = encode(&pixmap);
        // The header's color type: 2 is RGB, 3 a palette.
        assert_eq!(rgb[25], 2);
        assert_eq!(render(&Card::Home.svg(), None)[25], 3);
    }

    #[tokio::test]
    async fn the_cache_keeps_the_newest() {
        let cache = Cache::new(2);
        assert!(cache.drawn.is_empty());
        let svg = Card::Home.svg();
        let first = cache.draw("a", svg.clone(), None, true).await;
        assert!(Arc::ptr_eq(&first, &cache.drawn.get("a").unwrap()));
        cache.draw("b", svg.clone(), None, true).await;
        cache.draw("b", svg.clone(), None, true).await;
        cache.draw("c", svg.clone(), None, true).await;
        assert_eq!(cache.drawn.len(), 2);
        assert!(cache.drawn.get("a").is_none());
        // Drawn without the picture it names, a card is not kept.
        cache.draw("d", svg, None, false).await;
        assert!(cache.drawn.get("d").is_none());
    }

    /// A PNG of one color.
    fn plain(rgb: [u8; 3]) -> Arc<Vec<u8>> {
        let mut pixmap = tiny_skia::Pixmap::new(8, 8).unwrap();
        pixmap.fill(tiny_skia::Color::from_rgba8(rgb[0], rgb[1], rgb[2], 255));
        Arc::new(encode(&pixmap))
    }

    #[test]
    fn pictures() {
        let kind = |bytes: &[u8]| image(Arc::new(bytes.to_vec()));
        assert!(matches!(
            kind(&plain([1, 2, 3])),
            Some(usvg::ImageKind::PNG(_))
        ));
        assert!(matches!(
            kind(b"\xFF\xD8\xFF\xE0"),
            Some(usvg::ImageKind::JPEG(_))
        ));
        assert!(matches!(kind(b"GIF89a"), Some(usvg::ImageKind::GIF(_))));
        assert!(matches!(
            kind(b"RIFF\0\0\0\0WEBPVP8 "),
            Some(usvg::ImageKind::WEBP(_))
        ));
        assert!(kind(b"<svg/>").is_none());
        let card = |avatar: Option<&str>| Card::Repo {
            owner: "alice.test".into(),
            avatar: avatar.map(str::to_string),
            name: "party".into(),
            description: None,
            pulls: 0,
            issues: 0,
            languages: Vec::new(),
        };
        assert_eq!(Card::Home.picture(), None);
        let pictured = card(Some("https://cdn.example/a?b&c"));
        assert_eq!(pictured.picture(), Some("https://cdn.example/a?b&c"));
        // Its address is in what the version covers: a new picture is a
        // new card.
        let svg = pictured.svg();
        assert!(
            svg.contains(r#"href="https://cdn.example/a?b&amp;c""#),
            "{svg}"
        );
        assert_ne!(
            version(&svg),
            version(&card(Some("https://cdn.example/b")).svg())
        );
        // Without a picture, the card is as it ever was.
        assert!(!card(None).svg().contains("<image"));
        // The picture, cut to the circle, over the initial; the corner of
        // the box it is drawn in is the card's white.
        let at = |pixmap: &tiny_skia::Pixmap, x: u32, y: u32| {
            let p = pixmap.pixel(x, y).unwrap();
            [p.red(), p.green(), p.blue()]
        };
        let drawn = raster(&svg, Some(plain([200, 10, 20])));
        assert_eq!(at(&drawn, 112, 90), [200, 10, 20]);
        assert_eq!(at(&drawn, 82, 82), [255, 255, 255]);
        // Not given it, or given what is no picture, it leaves the initial.
        let initial = at(&raster(&card(None).svg(), None), 112, 90);
        assert_ne!(initial, [200, 10, 20]);
        assert_eq!(at(&raster(&svg, None), 112, 90), initial);
        let junk = Some(Arc::new(b"\x89PNG\r\n\x1a\nmine".to_vec()));
        assert_eq!(at(&raster(&svg, junk), 112, 90), initial);
    }
}