Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-docbook/src/svgz.rs

//! `.svgz` — the compressed storage form of a rendered book page.
//!
//! A typst-rendered page is enormous and enormously redundant. typst outlines
//! every glyph once into a per-page `<defs>` of `<symbol>` paths and then places
//! each occurrence as a `<use xlink:href="#g…">`, four-space-indented, with the
//! coordinates printed straight off `f64` — so a page is thousands of near-identical
//! lines carrying values like `x="32.592000000000006"`. Measured over korp's real
//! 84-page manual on 2026-09-02:
//!
//! | stage                                        | bytes    |
//! |----------------------------------------------|----------|
//! | typst as emitted                              | 35.21 MB |
//! | + [`optimize_svg`] (indentation + f64 noise)  | 24.17 MB |
//! | + [`to_svgz`] (gzip)                          | **4.49 MB** |
//!
//! 7.8×. That is the difference between a manual that can be embedded in a shipped
//! binary and published on crates.io, and one that cannot.
//!
//! Both steps live HERE, in the shared render engine, and not in any one repo's
//! xtask — every book (dwarves', korp's, any future one) inherits the same bytes
//! from the same writer, and a committed page can never disagree with the emitter
//! that produced it.
//!
//! `.svgz` is not an invention of this crate: it is the standard gzip-compressed
//! SVG container that every SVG consumer already understands. A reader needs a
//! stock gzip decoder and nothing from this crate — which is the point, because the
//! GUI that renders the pages must not link the typst stack to read them.

use std::io::Write as _;
use std::path::Path;

/// The file extension of a compressed page. Pair it with the page-number stem the
/// caller already uses (`page-001.svgz`).
pub const SVGZ_EXTENSION: &str = "svgz";

/// Fractional digits [`optimize_svg`] keeps.
///
/// A book page is ~595 pt wide and is rasterized at roughly 1–3 px/pt, so 1e-4 pt
/// is ~3e-4 px — four orders of magnitude below the sub-pixel grid a rasterizer
/// antialiases on. That argument is *not* the proof; the proof is
/// `tests::optimize_svg_does_not_move_a_single_pixel`, which renders a real typst
/// page before and after through resvg and compares the RGBA buffers pixel by
/// pixel. Lowering this constant to 0 makes that test fail, which is how we know
/// it can see a moved glyph at all.
const KEPT_DECIMALS: usize = 4;

/// Losslessly shrink one typst-emitted SVG page, in the two ways its emitter is
/// wasteful. Nothing here changes the rendered image: the transform is text-level
/// and structure-preserving — no element is dropped, merged, reordered or renamed.
///
/// 1. **Inter-element indentation.** typst pretty-prints with four-space nesting.
///    A typst page has NO `<text>` node — every glyph is a `<use>` of a `<symbol>`
///    outline — so whitespace between a `>` and the next `<` is XML-insignificant
///    throughout the file. (The single newline after the final `</svg>` is not
///    between elements and is left alone.)
///
///    **That is a precondition, not a description**, and 0.1.2 gave it an edge to
///    fall off: [`crate::export::TextLayer`] puts real `<text>` elements over a
///    column render, where the space between two words IS significant. The layer
///    is therefore added *after* this pass and never fed through it. Anything
///    else that grows a `<text>` node must do the same, or this function will
///    quietly delete the spaces between the words.
/// 2. **`f64` print noise.** Coordinates are printed straight off `f64`, so a glyph
///    advance arrives as `x="32.592000000000006"` and a page height as
///    `841.8897637795276`. A literal carrying more than [`KEPT_DECIMALS`] fractional
///    digits is re-printed rounded, with trailing zeros trimmed.
///
/// A literal is rewritten only when it is a whole numeric token — the byte before it
/// is neither a digit nor a `.` — and only when the shortened form is strictly
/// shorter. typst's `#g<32 hex>` glyph ids contain no `.`, so they can never match;
/// `tests::optimize_svg_leaves_glyph_ids_alone` states that rather than trusting it.
///
/// The function is **idempotent**: `optimize_svg(optimize_svg(x)) == optimize_svg(x)`.
/// That is what lets a committed page be checked against this emitter without
/// keeping the pre-image around — inflate it, re-optimize, and the bytes must not move.
pub fn optimize_svg(src: &str) -> String {
    optimize_svg_owned(src.to_owned())
}

/// The same pass, taking the page **by value and rewriting it in its own
/// buffer** — no second copy of the page is ever live.
///
/// This is the whole of [`optimize_svg`]; the borrowing door copies once and
/// calls here, so there is one implementation of the transform and not two.
///
/// # Why it can be done in place at all
///
/// Both rewrites are **strictly shrinking**: whitespace between two elements is
/// deleted, and a numeric literal is replaced only when the rounded form is
/// *strictly shorter* than what was there. So the write cursor is never ahead of
/// the read cursor and no byte is overwritten before it has been read. That is a
/// property of the transform, not an assumption about the input, and
/// `tests::the_in_place_pass_is_byte_for_byte_the_pass_it_replaced` compares it
/// to 0.1.4's copying version on a real page rather than trusting the argument.
///
/// # Why it exists
///
/// MEASURED 2026-09-12 on oden, one 3 MB markdown document through
/// [`crate::export_svg_column`]: `typst_svg` hands over a **512 MB** page and
/// the optimizer answered with a **313 MB** copy of it, so 825 MB of the render
/// was two spellings of the same page. `examples/mem.rs` is the instrument and
/// carries the table.
pub fn optimize_svg_owned(svg: String) -> String {
    let mut buf = svg.into_bytes();
    let len = shrink_in_place(&mut buf);
    buf.truncate(len);
    // Only whole ASCII tokens were replaced by ASCII, so UTF-8 boundaries are intact.
    String::from_utf8(buf).expect("the transform only rewrites ASCII numeric tokens")
}

/// Apply both rewrites to `buf` in place and answer the new length.
///
/// `w <= i` is the invariant that makes this sound: every branch writes at most
/// as many bytes as it consumed. A byte at `i - 1` — which `starts_token` reads
/// — is therefore always still the ORIGINAL byte, because the writer has only
/// reached `w <= i` and anything at or past `w` that is below `i` is untouched.
fn shrink_in_place(buf: &mut [u8]) -> usize {
    let n = buf.len();
    let (mut i, mut w) = (0usize, 0usize);
    while i < n {
        let b = buf[i];
        // (1) `>` whitespace… `<`  →  `><`
        if b == b'>' {
            let mut j = i + 1;
            while j < n && buf[j].is_ascii_whitespace() {
                j += 1;
            }
            buf[w] = b'>';
            w += 1;
            i = if j > i + 1 && j < n && buf[j] == b'<' { j } else { i + 1 };
            continue;
        }
        // (2) a numeric token with runaway fractional digits
        if b == b'-' || b.is_ascii_digit() {
            let prev = if i == 0 { None } else { Some(buf[i - 1]) };
            let starts_token = !matches!(prev, Some(p) if p.is_ascii_digit() || p == b'.');
            if starts_token {
                if let Some(end) = noisy_number_end(buf, i) {
                    if let Some(short) = shorter_form(&buf[i..end]) {
                        buf[w..w + short.len()].copy_from_slice(short.as_bytes());
                        w += short.len();
                        i = end;
                        continue;
                    }
                }
            }
        }
        buf[w] = b;
        w += 1;
        i += 1;
    }
    w
}

/// The rounded spelling of a scanned numeric token, when it is strictly
/// shorter than the token itself. `None` leaves the token alone.
fn shorter_form(token: &[u8]) -> Option<String> {
    // Every byte scanned by `noisy_number_end` is ASCII `-`, digit or `.`.
    let text = std::str::from_utf8(token).expect("ascii numeric token");
    let short = trim_fraction(text.parse::<f64>().expect("scanned literal parses"));
    (short.len() < text.len()).then_some(short)
}

/// If a `[-]?<digits>.<digits>` literal starts at `i` and its fraction is longer than
/// [`KEPT_DECIMALS`], return the index one past its last digit. `None` otherwise —
/// integers included, since they carry no noise to trim.
fn noisy_number_end(src: &[u8], i: usize) -> Option<usize> {
    let mut j = i;
    if src[j] == b'-' {
        j += 1;
    }
    let int_start = j;
    while j < src.len() && src[j].is_ascii_digit() {
        j += 1;
    }
    if j == int_start || j >= src.len() || src[j] != b'.' {
        return None;
    }
    j += 1;
    let frac_start = j;
    while j < src.len() && src[j].is_ascii_digit() {
        j += 1;
    }
    (j - frac_start > KEPT_DECIMALS).then_some(j)
}

/// `v` printed with at most [`KEPT_DECIMALS`] fractional digits, trailing zeros and a
/// bare trailing `.` removed: `841.8897637795276` → `841.8898`,
/// `32.592000000000006` → `32.592`, `1.000004` → `1`.
fn trim_fraction(v: f64) -> String {
    let mut s = format!("{v:.KEPT$}", KEPT = KEPT_DECIMALS);
    if s.contains('.') {
        while s.ends_with('0') {
            s.pop();
        }
        if s.ends_with('.') {
            s.pop();
        }
    }
    s
}

/// One rendered page as the bytes a `.svgz` file is written with: [`optimize_svg`]
/// then gzip — **byte-stable across runs, machines and clocks**.
///
/// Determinism is not decoration here. These pages are COMMITTED (korp's manual is
/// 84 blobs in git and inside the shipped binary), so a gzip header carrying the
/// build clock would make every re-render of unchanged documentation a full-book
/// diff. The two header fields that are not a function of the input are pinned:
/// MTIME to 0 and the OS byte to `0xFF` ("unknown").
///
/// The result is an ordinary gzip stream. A consumer inflates it with any stock
/// gzip reader — it does not need this crate, and a GUI must not link typst to read
/// a page.
pub fn to_svgz(svg: &str) -> Vec<u8> {
    gzip(&optimize_svg(svg))
}

/// The pinned gzip stream of an ALREADY optimized page.
///
/// [`to_svgz`] is this plus the optimizer, which is how the header stays pinned
/// in one place. It is separate so that a caller holding the optimized page —
/// [`SvgzBook::write`], which needs its length for the stats line — can compress
/// what it is already holding instead of re-running the pass. It ran the pass
/// TWICE per page until 2026-09-12: once inside `to_svgz` and once for
/// `optimized_bytes`, for a whole second copy of every page and a whole second
/// scan of it.
fn gzip(optimized: &str) -> Vec<u8> {
    let mut enc = flate2::GzBuilder::new()
        .mtime(0)
        .operating_system(255)
        .write(Vec::new(), flate2::Compression::best());
    enc.write_all(optimized.as_bytes()).expect("gzip into a Vec cannot fail");
    enc.finish().expect("gzip into a Vec cannot fail")
}

/// Inflate a `.svgz` page back to SVG text — the inverse of [`to_svgz`], for tools
/// that already depend on this crate (a reader that only renders pages should use a
/// stock gzip decoder instead of taking the whole typst stack as a dependency).
pub fn from_svgz(gz: &[u8]) -> anyhow::Result<String> {
    use std::io::Read as _;
    let mut svg = String::new();
    flate2::read::GzDecoder::new(gz)
        .read_to_string(&mut svg)
        .map_err(|e| anyhow::anyhow!("not a readable .svgz page: {e}"))?;
    Ok(svg)
}

/// A committed `.svgz` page set on disk — the ONE door between a render and the
/// bytes that land in git.
///
/// Both directions live here, and that is the whole point:
///
/// * [`SvgzBook::write`] is how pages are produced (optimize + deterministic gzip,
///   stale files swept), so a re-render always yields the committed shape;
/// * [`SvgzBook::check`] renders nothing and writes nothing — it answers whether
///   what is on disk is *exactly* what `write` would produce for the pages it is
///   handed. That is the gate a docs door runs, and it is only possible because
///   [`to_svgz`] pins the gzip header (see its notes): with a clock-stamped MTIME
///   every page would compare stale one second after it was written.
///
/// Ordinary builds never come near either one. They embed the committed bytes; only
/// the docs verb renders, and only the docs verb checks.
pub struct SvgzBook<'a> {
    /// Directory holding `page-NNN.svgz`.
    pub dir: &'a Path,
    /// Zero-padding width of the page number (korp's book uses 3 → `page-001.svgz`).
    pub digits: usize,
    /// Sidecar file names in `dir` that are NOT pages and must be left alone
    /// (`toc.tsv`, `manifest.json`, per-page `.txt` search sidecars…).
    pub keep: &'a [&'a str],
}

/// What [`SvgzBook::check`] found. `is_empty()` is the green.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SvgzMismatch {
    /// Pages the render produced that are not on disk at all.
    pub missing: Vec<String>,
    /// Pages on disk whose bytes differ from what the render produces — the doc was
    /// edited and the book was not re-rendered (or was edited by hand).
    pub stale: Vec<String>,
    /// Files on disk the render does not produce: pages past the end of a shortened
    /// book, and anything not named in [`SvgzBook::keep`].
    pub extra: Vec<String>,
    /// Uncompressed `*.svg` pages sitting in the book directory. Never a warning:
    /// they are a second copy of a page riding into every binary and crate that
    /// embeds the folder.
    pub uncompressed: Vec<String>,
}

impl SvgzMismatch {
    /// True when the committed book is exactly what the emitter produces.
    pub fn is_empty(&self) -> bool {
        self.missing.is_empty()
            && self.stale.is_empty()
            && self.extra.is_empty()
            && self.uncompressed.is_empty()
    }
}

impl std::fmt::Display for SvgzMismatch {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut section = |label: &str, items: &[String]| -> std::fmt::Result {
            if items.is_empty() {
                return Ok(());
            }
            writeln!(f, "  {} ({}): {}", label, items.len(), items.join(", "))
        };
        section("stale", &self.stale)?;
        section("missing", &self.missing)?;
        section("extra", &self.extra)?;
        section("uncompressed", &self.uncompressed)
    }
}

/// What [`SvgzBook::write`] wrote, in bytes — the numbers a docs verb prints.
#[derive(Debug, Default, Clone, Copy)]
pub struct SvgzStats {
    pub pages: usize,
    /// Total size of the pages as typst emitted them.
    pub raw_bytes: usize,
    /// Total size after [`optimize_svg`], before gzip.
    pub optimized_bytes: usize,
    /// Total size actually committed.
    pub svgz_bytes: usize,
}

impl<'a> SvgzBook<'a> {
    /// The file name page `i` (0-based) is stored under.
    pub fn page_name(&self, i: usize) -> String {
        format!("page-{:0width$}.{SVGZ_EXTENSION}", i + 1, width = self.digits)
    }

    /// Write the page set, sweeping every stale page first — including any
    /// uncompressed `*.svg` left by an older render, which would otherwise sit next
    /// to its own `.svgz` as a silent duplicate.
    pub fn write(&self, pages: &[String]) -> anyhow::Result<SvgzStats> {
        self.sweep()?;
        std::fs::create_dir_all(self.dir)?;
        let mut stats = SvgzStats { pages: pages.len(), ..Default::default() };
        for (i, page) in pages.iter().enumerate() {
            // ONE optimizer pass per page, and the compressor is handed what it
            // produced. `to_svgz(page)` plus `optimize_svg(page).len()` ran the
            // pass twice and held a second full copy of every page to do it.
            let opt = optimize_svg(page);
            let gz = gzip(&opt);
            stats.raw_bytes += page.len();
            stats.optimized_bytes += opt.len();
            stats.svgz_bytes += gz.len();
            drop(opt);
            std::fs::write(self.dir.join(self.page_name(i)), &gz)?;
        }
        Ok(stats)
    }

    /// Answer whether the committed book already IS this page set. Renders nothing,
    /// writes nothing, touches nothing.
    pub fn check(&self, pages: &[String]) -> anyhow::Result<SvgzMismatch> {
        let mut out = SvgzMismatch::default();
        let expected: Vec<String> = (0..pages.len()).map(|i| self.page_name(i)).collect();
        for (name, page) in expected.iter().zip(pages) {
            match std::fs::read(self.dir.join(name)) {
                Err(_) => out.missing.push(name.clone()),
                Ok(on_disk) if on_disk != to_svgz(page) => out.stale.push(name.clone()),
                Ok(_) => {}
            }
        }
        for name in self.other_files()? {
            if name.ends_with(".svg") {
                out.uncompressed.push(name);
            } else if !expected.contains(&name) {
                out.extra.push(name);
            }
        }
        out.missing.sort();
        out.stale.sort();
        out.extra.sort();
        out.uncompressed.sort();
        Ok(out)
    }

    /// Every file in `dir` that is not a `keep` sidecar.
    fn other_files(&self) -> anyhow::Result<Vec<String>> {
        if !self.dir.exists() {
            return Ok(Vec::new());
        }
        let mut names = Vec::new();
        for entry in std::fs::read_dir(self.dir)?.flatten() {
            let name = entry.file_name().to_string_lossy().into_owned();
            if !self.keep.contains(&name.as_str()) {
                names.push(name);
            }
        }
        Ok(names)
    }

    /// Delete every page (compressed or not) so a shortened render leaves no orphans.
    fn sweep(&self) -> anyhow::Result<()> {
        for name in self.other_files()? {
            if name.ends_with(".svg") || name.ends_with(".svgz") {
                let _ = std::fs::remove_file(self.dir.join(name));
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Read as _;

    /// A REAL typst page as this crate emits it — korp's `assets/manual-svg/page-001.svg`
    /// captured 2026-09-02, before the book started shipping compressed. It is the
    /// pre-image the optimizer is measured against. Re-capture it from a fresh
    /// uncompressed render if typst's output shape ever changes.
    fn raw_page() -> String {
        let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/typst-page-raw.svg");
        std::fs::read_to_string(&p)
            .unwrap_or_else(|e| panic!("fixture {} must exist: {e}", p.display()))
    }

    /// Rasterize a page through resvg — the SAME engine facett-docview gives every
    /// shipped Manual tab — and return its straight-RGBA pixels.
    fn pixels(svg: &str) -> ((u32, u32), Vec<[u8; 4]>) {
        let opts = usvg::Options::default();
        let tree = usvg::Tree::from_data(svg.as_bytes(), &opts).expect("page parses");
        let size = tree.size();
        // 2 px/pt: enough that a glyph is tens of pixels wide, so a moved outline
        // shows up rather than vanishing into a coarse grid.
        let (w, h) = ((size.width() * 2.0) as u32, (size.height() * 2.0) as u32);
        let mut pixmap = tiny_skia::Pixmap::new(w, h).expect("pixmap");
        resvg::render(&tree, tiny_skia::Transform::from_scale(2.0, 2.0), &mut pixmap.as_mut());
        let px = pixmap
            .pixels()
            .iter()
            .map(|p| {
                let c = p.demultiply();
                [c.red(), c.green(), c.blue(), c.alpha()]
            })
            .collect();
        ((w, h), px)
    }

    /// **The lossless proof.** Not "it still parses", not "it got smaller" — the
    /// optimized page must rasterize to a bit-identical image. A changed glyph is a
    /// red, and the only thing that can see a changed glyph is the pixels.
    ///
    /// The comparison is guarded against being hollow two ways: the BEFORE raster
    /// must carry thousands of dark pixels (two blank sheets also compare equal),
    /// and setting `KEPT_DECIMALS` to 0 makes this test fail — verified 2026-09-02,
    /// so it is known to be able to go red.
    #[test]
    fn optimize_svg_does_not_move_a_single_pixel() {
        let raw = raw_page();
        let opt = optimize_svg(&raw);
        assert!(opt.len() < raw.len(), "the optimizer must actually remove bytes");

        let (before_size, before) = pixels(&raw);
        let (after_size, after) = pixels(&opt);
        assert_eq!(before_size, after_size, "the page keeps its size");

        let ink = before.iter().filter(|p| p[3] > 0 && p[0] < 200).count();
        assert!(
            ink > 1_000,
            "the BEFORE raster must carry real ink or this comparison proves nothing: \
             {ink} dark px in {before_size:?}",
        );

        let moved = before.iter().zip(&after).filter(|(a, b)| a != b).count();
        assert_eq!(
            moved,
            0,
            "{moved} of {} pixels changed ({} B raw -> {} B optimized)",
            before.len(),
            raw.len(),
            opt.len(),
        );
    }

    /// **The in-place pass is the pass it replaced.** 0.1.4 built the optimized
    /// page into a second buffer; 0.1.5 rewrites it in the page's own. That is a
    /// change to how every committed `.svgz` in the constellation is produced,
    /// so it is not checked against itself.
    ///
    /// `reference_optimize_svg` below is 0.1.4's function, verbatim, kept here
    /// as the ORACLE — the one case where a second implementation is the point
    /// rather than a duplicate. Compared on a real typst page, byte for byte.
    ///
    /// Seen RED 2026-09-12 by making `shrink_in_place` skip its `w += 1` on the
    /// `>` branch — every `>` in the page is then eaten by the byte after it.
    /// 15 tests went red with it, the pixel proof included.
    #[test]
    fn the_in_place_pass_is_byte_for_byte_the_pass_it_replaced() {
        let raw = raw_page();
        let reference = reference_optimize_svg(&raw);
        assert!(
            reference.len() < raw.len() && reference.len() > raw.len() / 4,
            "the oracle must actually have optimized something: {} B -> {} B",
            raw.len(),
            reference.len(),
        );
        assert_eq!(optimize_svg_owned(raw.clone()), reference, "the owned door");
        assert_eq!(optimize_svg(&raw), reference, "the borrowing door");
    }

    /// 0.1.4's `optimize_svg`, unchanged, as the oracle for the in-place pass.
    /// Do not "tidy" this into a call to the real one — that is the whole point
    /// of it.
    fn reference_optimize_svg(src: &str) -> String {
        let src = src.as_bytes();
        let mut out: Vec<u8> = Vec::with_capacity(src.len());
        let mut i = 0usize;
        while i < src.len() {
            let b = src[i];
            if b == b'>' {
                let mut j = i + 1;
                while j < src.len() && src[j].is_ascii_whitespace() {
                    j += 1;
                }
                out.push(b'>');
                i = if j > i + 1 && j < src.len() && src[j] == b'<' { j } else { i + 1 };
                continue;
            }
            if b == b'-' || b.is_ascii_digit() {
                let prev = if i == 0 { None } else { Some(src[i - 1]) };
                let starts_token = !matches!(prev, Some(p) if p.is_ascii_digit() || p == b'.');
                if starts_token {
                    if let Some(end) = noisy_number_end(src, i) {
                        let text = std::str::from_utf8(&src[i..end]).expect("ascii numeric token");
                        let short =
                            trim_fraction(text.parse::<f64>().expect("scanned literal parses"));
                        if short.len() < text.len() {
                            out.extend_from_slice(short.as_bytes());
                            i = end;
                            continue;
                        }
                    }
                }
            }
            out.push(b);
            i += 1;
        }
        String::from_utf8(out).expect("ascii only")
    }

    /// The optimizer is a fixed point. This is what lets a committed `.svgz` be
    /// audited against the emitter with no pre-image: inflate, re-optimize, compare.
    #[test]
    fn optimize_svg_is_idempotent() {
        let once = optimize_svg(&raw_page());
        assert_eq!(optimize_svg(&once), once);
    }

    /// Both wasteful shapes are actually gone — stated in bytes, not as a feeling.
    #[test]
    fn optimize_svg_removes_indentation_and_float_noise() {
        let raw = raw_page();
        assert!(raw.contains(">\n "), "the fixture must still be indented emitter output");
        assert!(raw.contains("32.592000000000006\""), "the fixture must still carry f64 noise");

        let opt = optimize_svg(&raw);
        let b = opt.as_bytes();
        let mut between = 0usize;
        for i in 0..b.len() {
            if b[i] != b'>' {
                continue;
            }
            let mut j = i + 1;
            while j < b.len() && b[j].is_ascii_whitespace() {
                j += 1;
            }
            if j > i + 1 && j < b.len() && b[j] == b'<' {
                between += 1;
            }
        }
        assert_eq!(between, 0, "no whitespace survives between two elements");
        assert_eq!(noisy_literals(&opt), 0, "no literal keeps more than {KEPT_DECIMALS} decimals");
        assert!(noisy_literals(&raw) > 100, "the fixture had noise to remove");
    }

    /// Count the `[-]?d+.d{KEPT+1,}` literals [`optimize_svg`] targets.
    fn noisy_literals(src: &str) -> usize {
        let b = src.as_bytes();
        (0..b.len())
            .filter(|&i| {
                if b[i] != b'-' && !b[i].is_ascii_digit() {
                    return false;
                }
                let prev = if i == 0 { None } else { Some(b[i - 1]) };
                if matches!(prev, Some(p) if p.is_ascii_digit() || p == b'.') {
                    return false;
                }
                noisy_number_end(b, i).is_some()
            })
            .count()
    }

    /// A glyph id is never mistaken for a coordinate. typst's `#g<32 hex>` ids share
    /// the digit alphabet with numbers; the `.`-plus-5-decimals requirement is what
    /// keeps them apart, and this states it rather than trusting it.
    #[test]
    fn optimize_svg_leaves_glyph_ids_alone() {
        let src = r##"<use xlink:href="#g6C14D6BACFB5D2F655FD69CF7DA72632" x="17.164000000000001"/>"##;
        let out = optimize_svg(src);
        assert!(out.contains("#g6C14D6BACFB5D2F655FD69CF7DA72632"), "id intact: {out}");
        assert!(out.contains(r##"x="17.164""##), "coordinate trimmed: {out}");
    }

    /// The committed bytes must be REPRODUCIBLE: two encodes of one page are the same
    /// file, and the header fields that would otherwise vary — MTIME and the OS byte —
    /// are pinned. Without this, every re-render churns the whole book in git.
    #[test]
    fn svgz_is_byte_stable_across_runs() {
        let page = raw_page();
        let a = to_svgz(&page);
        assert_eq!(a, to_svgz(&page), "two encodes of the same page must be identical");
        assert_eq!(&a[0..2], b"\x1f\x8b", "gzip magic");
        assert_eq!(&a[4..8], &[0, 0, 0, 0], "MTIME is pinned to 0, not the build clock");
        assert_eq!(a[9], 0xFF, "the OS byte is pinned to `unknown`, not the builder's OS");
    }

    /// A `.svgz` is a real gzip stream that inflates to the OPTIMIZED page — and it
    /// is what a stock decoder reads, not only what [`from_svgz`] reads.
    #[test]
    fn svgz_round_trips_through_a_stock_gzip_reader() {
        let page = raw_page();
        let gz = to_svgz(&page);
        let mut stock = String::new();
        flate2::read::GzDecoder::new(&gz[..]).read_to_string(&mut stock).unwrap();
        assert_eq!(stock, optimize_svg(&page), "a stock reader gets the optimized page");
        assert_eq!(from_svgz(&gz).unwrap(), stock, "and `from_svgz` agrees with it");
        assert!(stock.contains("<svg"), "which is SVG");
    }

    /// Garbage is refused rather than handed on as a "page".
    #[test]
    fn from_svgz_rejects_something_that_is_not_a_page() {
        assert!(from_svgz(b"<svg>not compressed at all</svg>").is_err());
        assert!(from_svgz(&[0x1f, 0x8b, 0x08, 0x00, 0x00]).is_err(), "a truncated stream");
    }

    /// The whole point, as one number on a real page.
    #[test]
    fn a_real_page_ships_much_smaller() {
        let raw = raw_page();
        let gz = to_svgz(&raw);
        assert!(
            raw.len() >= gz.len() * 5,
            "{} B raw -> {} B .svgz is only {:.1}x",
            raw.len(),
            gz.len(),
            raw.len() as f64 / gz.len() as f64,
        );
    }
    // ── the GATE: `SvgzBook::check` ─────────────────────────────────────────────
    //
    // The owner, 2026-09-02: "a build-time or test-time GATE keeps them honest … plant
    // one uncompressed page and one stale page and prove each fails." Every case
    // below plants the fault and asserts the SPECIFIC field that must name it — a
    // check that merely returned "not ok" would pass a much weaker test than these.

    fn book(dir: &std::path::Path) -> SvgzBook<'_> {
        SvgzBook { dir, digits: 3, keep: &["toc.tsv"] }
    }

    /// Two real pages, so the gate is exercised on genuine typst output rather than
    /// on a stub whose every byte is the same.
    fn two_pages() -> Vec<String> {
        let p = raw_page();
        vec![p.clone(), p.replace("841.8897637795276", "841.8897637795277")]
    }

    /// A freshly written book checks clean, and it is written COMPRESSED under the
    /// expected names. (The green half — without it, every red below could be a
    /// check that never returns clean.)
    #[test]
    fn a_freshly_written_book_checks_clean() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let pages = two_pages();
        let stats = b.write(&pages).unwrap();
        assert_eq!(stats.pages, 2);
        assert!(stats.svgz_bytes * 5 < stats.raw_bytes, "the write actually compressed");
        assert!(tmp.path().join("page-001.svgz").exists(), "zero-padded to `digits`");

        let m = b.check(&pages).unwrap();
        assert!(m.is_empty(), "a book just written must be in sync:\n{m}");
    }

    /// **RED 1 — a STALE page.** One page's source changed and nobody re-rendered.
    /// The gate must name that page, and only that page.
    #[test]
    fn the_gate_catches_a_stale_page() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let mut pages = two_pages();
        b.write(&pages).unwrap();

        pages[1] = pages[1].replace("841.8897637795277", "800.1234567890123");
        let m = b.check(&pages).unwrap();
        assert!(!m.is_empty(), "an edited doc with an un-re-rendered book is a RED");
        assert_eq!(m.stale, vec!["page-002.svgz".to_string()], "the changed page is named");
        assert!(m.missing.is_empty() && m.extra.is_empty() && m.uncompressed.is_empty());
    }

    /// **RED 2 — an UNCOMPRESSED page.** A `page-NNN.svg` planted next to the book is
    /// a second copy of a page riding into every binary that embeds the folder.
    #[test]
    fn the_gate_catches_an_uncompressed_page() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let pages = two_pages();
        b.write(&pages).unwrap();
        std::fs::write(tmp.path().join("page-001.svg"), raw_page()).unwrap();

        let m = b.check(&pages).unwrap();
        assert!(!m.is_empty(), "an uncompressed page in the book is a RED");
        assert_eq!(m.uncompressed, vec!["page-001.svg".to_string()]);
        assert!(m.stale.is_empty(), "the compressed page itself is still fine");
    }

    /// **RED 3 — a page the render no longer produces.** The book got shorter; the
    /// orphan must be named, not silently embedded forever.
    #[test]
    fn the_gate_catches_an_orphan_page() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let pages = two_pages();
        b.write(&pages).unwrap();

        let m = b.check(&pages[..1]).unwrap();
        assert_eq!(m.extra, vec!["page-002.svgz".to_string()], "the orphan is named");
        assert!(m.stale.is_empty() && m.missing.is_empty());
    }

    /// **RED 4 — a MISSING page.** A page was deleted (or never written).
    #[test]
    fn the_gate_catches_a_missing_page() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let pages = two_pages();
        b.write(&pages).unwrap();
        std::fs::remove_file(tmp.path().join("page-002.svgz")).unwrap();

        let m = b.check(&pages).unwrap();
        assert_eq!(m.missing, vec!["page-002.svgz".to_string()]);
    }

    /// The gate is not fooled by the CLOCK. This is the reason [`to_svgz`] pins the
    /// gzip MTIME: a check that compares bytes would call every page stale a second
    /// after it was written if the header carried the build time.
    #[test]
    fn the_gate_is_not_a_timestamp_comparison() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let pages = two_pages();
        b.write(&pages).unwrap();
        // Re-write the same pages: byte-identical files, so still clean.
        b.write(&pages).unwrap();
        assert!(b.check(&pages).unwrap().is_empty(), "re-rendering unchanged docs is a no-op");
    }

    /// `write` sweeps what `check` would have called an orphan — the two halves of
    /// the door agree, so a re-render never leaves a book the gate then rejects.
    #[test]
    fn write_sweeps_stale_and_uncompressed_pages() {
        let tmp = tempfile::tempdir().unwrap();
        let b = book(tmp.path());
        let pages = two_pages();
        b.write(&pages).unwrap();
        std::fs::write(tmp.path().join("page-009.svg"), raw_page()).unwrap();
        std::fs::write(tmp.path().join("toc.tsv"), "0\tOne\n").unwrap();

        b.write(&pages[..1]).unwrap();
        assert!(b.check(&pages[..1]).unwrap().is_empty(), "the sweep left a clean book");
        assert!(!tmp.path().join("page-002.svgz").exists(), "the orphan is gone");
        assert!(!tmp.path().join("page-009.svg").exists(), "the uncompressed page is gone");
        assert_eq!(
            std::fs::read_to_string(tmp.path().join("toc.tsv")).unwrap(),
            "0\tOne\n",
            "a `keep` sidecar is never swept",
        );
    }
}