heartwood every commit a ring

Replace Chromium PDF pipeline with embedded Typst

0024761f by Isaac Bythewood · 2 days ago

Replace Chromium PDF pipeline with embedded Typst

Drops the chrome-headless-shell subprocess in favour of an in-process
Typst compiler (typst + typst-pdf + typst-kit), mirroring the migration
already done on blog.bythewood.me. Cuts the chromium apk + the
CHROMIUM_BIN/playwright-browsers lookup chain out of the runtime path.

The dashboard's only chart (events-over-time) was already a server-side
SVG polyline, so it survives unchanged via image(bytes(...), format:
"svg") inside the new templates/properties/property_report.typ. The
template adds a Y-axis with peak-count and 0 labels, plus a per-page
footer with brand line and Page X of Y. New typst_str/typst_md filters
in templates.rs keep user-controlled labels safe inside Typst string
literals and content blocks. LTO is dropped so release builds stay
tractable on the 1-vCPU VPS once typst-library entered the dep tree.
modified Cargo.lock
@@ -47,7 +47,6 @@ dependencies = [ "sha2", "sqlx", "tar", "tempfile", "thiserror", "tokio", "tower",
@@ -55,6 +54,9 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", "typst", "typst-kit", "typst-pdf", "uaparser", "urlencoding", "uuid",
@@ -75,6 +77,36 @@ version = "1.0.102"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"[[package]]name = "approx"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"dependencies = [ "num-traits",][[package]]name = "ar_archive_writer"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"dependencies = [ "object",][[package]]name = "arrayref"version = "0.3.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"[[package]]name = "arrayvec"version = "0.7.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"[[package]]name = "atoi"version = "2.0.0"
@@ -160,6 +192,12 @@ dependencies = [ "syn",][[package]]name = "az"version = "1.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7"[[package]]name = "base64"version = "0.22.1"
@@ -172,6 +210,50 @@ version = "1.8.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"[[package]]name = "biblatex"version = "0.11.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "53d0c374feba1b9a59042a7c1cf00ce7c34b977b9134fe7c42b08e5183729f66"dependencies = [ "paste", "roman-numerals-rs", "strum", "unic-langid", "unicode-normalization", "unscanny",][[package]]name = "bincode"version = "1.3.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"dependencies = [ "serde",][[package]]name = "bit-set"version = "0.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"dependencies = [ "bit-vec",][[package]]name = "bit-vec"version = "0.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"[[package]]name = "bitflags"version = "1.3.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"[[package]]name = "bitflags"version = "2.11.1"
@@ -196,12 +278,44 @@ version = "3.20.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"[[package]]name = "by_address"version = "1.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"[[package]]name = "bytemuck"version = "1.25.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"dependencies = [ "bytemuck_derive",][[package]]name = "bytemuck_derive"version = "1.10.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "byteorder"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"[[package]]name = "byteorder-lite"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"[[package]]name = "bytes"version = "1.11.1"
@@ -230,6 +344,24 @@ version = "0.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"[[package]]name = "chinese-number"version = "0.7.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b"dependencies = [ "chinese-variant", "enum-ordinalize", "num-bigint", "num-traits",][[package]]name = "chinese-variant"version = "1.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49"[[package]]name = "chrono"version = "0.4.44"
@@ -251,7 +383,89 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"dependencies = [ "chrono", "phf", "phf 0.12.1",][[package]]name = "ciborium"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"dependencies = [ "ciborium-io", "ciborium-ll", "serde",][[package]]name = "ciborium-io"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"[[package]]name = "ciborium-ll"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"dependencies = [ "ciborium-io", "half",][[package]]name = "citationberg"version = "0.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1f6597e8bdbca37f1f56e5a80d15857b0932aead21a78d20de49e99e74933046"dependencies = [ "quick-xml 0.38.4", "serde",][[package]]name = "cobs"version = "0.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"dependencies = [ "thiserror",][[package]]name = "codex"version = "0.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9589e1effc5cacbea347899645c654158b03b2053d24bb426fd3128ced6e423c"[[package]]name = "color_quant"version = "1.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"[[package]]name = "comemo"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3c963350b2b08aa4b725d7802593245380ab53dacfedcaa971385fc33306c0d4"dependencies = [ "comemo-macros", "parking_lot", "rustc-hash", "siphasher", "slab",][[package]]name = "comemo-macros"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a3c400139ba1389ef9e20ad2d87cda68b437a66483aa0da616bdf2cea7413853"dependencies = [ "proc-macro2", "quote", "syn",][[package]]
@@ -297,6 +511,15 @@ version = "0.8.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"[[package]]name = "core_maths"version = "0.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"dependencies = [ "libm",][[package]]name = "cpufeatures"version = "0.2.17"
@@ -330,6 +553,25 @@ dependencies = [ "cfg-if",][[package]]name = "crossbeam-deque"version = "0.8.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"dependencies = [ "crossbeam-epoch", "crossbeam-utils",][[package]]name = "crossbeam-epoch"version = "0.9.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"dependencies = [ "crossbeam-utils",][[package]]name = "crossbeam-queue"version = "0.3.12"
@@ -345,6 +587,12 @@ version = "0.8.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"[[package]]name = "crunchy"version = "0.2.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"[[package]]name = "crypto-common"version = "0.1.7"
@@ -355,6 +603,33 @@ dependencies = [ "typenum",][[package]]name = "csv"version = "1.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"dependencies = [ "csv-core", "itoa", "ryu", "serde_core",][[package]]name = "csv-core"version = "0.1.13"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"dependencies = [ "memchr",][[package]]name = "data-url"version = "0.3.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"[[package]]name = "der"version = "0.7.10"
@@ -417,6 +692,15 @@ version = "0.15.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"[[package]]name = "ecow"version = "0.2.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02"dependencies = [ "serde",][[package]]name = "either"version = "1.15.0"
@@ -426,6 +710,38 @@ dependencies = [ "serde",][[package]]name = "embedded-io"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"[[package]]name = "embedded-io"version = "0.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"[[package]]name = "enum-ordinalize"version = "4.3.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"dependencies = [ "enum-ordinalize-derive",][[package]]name = "enum-ordinalize-derive"version = "4.3.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "equivalent"version = "1.0.2"
@@ -453,6 +769,15 @@ dependencies = [ "windows-sys 0.48.0",][[package]]name = "euclid"version = "0.22.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"dependencies = [ "num-traits",][[package]]name = "event-listener"version = "5.4.1"
@@ -464,12 +789,38 @@ dependencies = [ "pin-project-lite",][[package]]name = "fancy-regex"version = "0.16.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"dependencies = [ "bit-set", "regex-automata", "regex-syntax",][[package]]name = "fast-srgb8"version = "1.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"[[package]]name = "fastrand"version = "2.4.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"[[package]]name = "fdeflate"version = "0.3.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"dependencies = [ "simd-adler32",][[package]]name = "filetime"version = "0.2.27"
@@ -495,6 +846,22 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"dependencies = [ "crc32fast", "miniz_oxide", "zlib-rs",][[package]]name = "float-cmp"version = "0.9.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"[[package]]name = "float-cmp"version = "0.10.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"dependencies = [ "num-traits",][[package]]
@@ -508,12 +875,50 @@ dependencies = [ "spin",][[package]]name = "fnv"version = "1.0.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"[[package]]name = "foldhash"version = "0.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"[[package]]name = "font-types"version = "0.10.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"dependencies = [ "bytemuck",][[package]]name = "fontconfig-parser"version = "0.5.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"dependencies = [ "roxmltree",][[package]]name = "fontdb"version = "0.23.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"dependencies = [ "fontconfig-parser", "log", "memmap2", "slotmap", "tinyvec", "ttf-parser",][[package]]name = "form_urlencoded"version = "1.2.2"
@@ -656,6 +1061,43 @@ dependencies = [ "wasip3",][[package]]name = "gif"version = "0.13.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"dependencies = [ "color_quant", "weezl",][[package]]name = "gif"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"dependencies = [ "color_quant", "weezl",][[package]]name = "glidesort"version = "0.1.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0"[[package]]name = "half"version = "2.7.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"dependencies = [ "cfg-if", "crunchy", "zerocopy",][[package]]name = "hashbrown"version = "0.15.5"
@@ -682,6 +1124,109 @@ dependencies = [ "hashbrown 0.15.5",][[package]]name = "hayagriva"version = "0.9.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1cb69425736f184173b3ca6e27fcba440a61492a790c786b1c6af7e06a03e575"dependencies = [ "biblatex", "ciborium", "citationberg", "indexmap", "paste", "roman-numerals-rs", "serde", "serde_yaml", "thiserror", "unic-langid", "unicode-segmentation", "unscanny", "url",][[package]]name = "hayro"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "048488ba88552bb0fb2a7e4001c64d5bed65d1a92167186a1bb9151571f32e60"dependencies = [ "bytemuck", "hayro-interpret", "image", "kurbo 0.12.0",][[package]]name = "hayro-font"version = "0.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "10e7e97ce840a6a70e7901e240ec65ba61106b66b37a4a1b899a2ce484248463"dependencies = [ "log", "phf 0.13.1",][[package]]name = "hayro-interpret"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "56204c972d08e844f3db13b1e14be769f846e576699b46d4f4637cc4f8f70102"dependencies = [ "bitflags 2.11.1", "hayro-font", "hayro-syntax", "kurbo 0.12.0", "log", "moxcms 0.7.11", "phf 0.13.1", "rustc-hash", "siphasher", "skrifa", "smallvec", "yoke 0.8.2",][[package]]name = "hayro-svg"version = "0.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e8c673304cec6e0dfd3b4f71fccecd45646899aa70279b62d3f933842abc4ac5"dependencies = [ "base64", "hayro-interpret", "image", "kurbo 0.12.0", "siphasher", "xmlwriter",][[package]]name = "hayro-syntax"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3f9e5c7dbc0f11dc42775d1a6cc00f5f5137b90b6288dd7fe5f71d17b14d10be"dependencies = [ "flate2", "kurbo 0.12.0", "log", "rustc-hash", "smallvec", "zune-jpeg 0.4.21",][[package]]name = "hayro-write"version = "0.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cc05d8b4bc878b9aee48d980ecb25ed08f1dd9fad6da5ab4d9b7c56ec03a0cf6"dependencies = [ "flate2", "hayro-syntax", "log", "pdf-writer",][[package]]name = "heck"version = "0.5.0"
@@ -832,6 +1377,12 @@ dependencies = [ "tracing",][[package]]name = "hypher"version = "0.1.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bef68590049bab63a464eee1a1158ac04c6f6613a546d8d90f78636b8b94f171"[[package]]name = "iana-time-zone"version = "0.1.65"
@@ -856,6 +1407,19 @@ dependencies = [ "cc",][[package]]name = "icu_collections"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"dependencies = [ "displaydoc", "serde", "yoke 0.7.5", "zerofrom", "zerovec 0.10.4",][[package]]name = "icu_collections"version = "2.2.0"
@@ -865,9 +1429,9 @@ dependencies = [ "displaydoc", "potential_utf", "utf8_iter", "yoke", "yoke 0.8.2", "zerofrom", "zerovec", "zerovec 0.11.6",][[package]]
@@ -877,24 +1441,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", "litemap 0.8.2", "tinystr 0.8.3", "writeable 0.6.3", "zerovec 0.11.6",][[package]]name = "icu_locid"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"dependencies = [ "displaydoc", "litemap 0.7.5", "tinystr 0.7.6", "writeable 0.5.5", "zerovec 0.10.4",][[package]]name = "icu_locid_transform"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider 1.5.0", "tinystr 0.7.6", "zerovec 0.10.4",][[package]]name = "icu_locid_transform_data"version = "1.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"[[package]]name = "icu_normalizer"version = "2.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"dependencies = [ "icu_collections", "icu_collections 2.2.0", "icu_normalizer_data", "icu_properties", "icu_provider", "icu_properties 2.2.0", "icu_provider 2.2.0", "smallvec", "zerovec", "zerovec 0.11.6",][[package]]
@@ -903,26 +1500,67 @@ version = "2.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"[[package]]name = "icu_properties"version = "1.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"dependencies = [ "displaydoc", "icu_collections 1.5.0", "icu_locid_transform", "icu_properties_data 1.5.1", "icu_provider 1.5.0", "serde", "tinystr 0.7.6", "zerovec 0.10.4",][[package]]name = "icu_properties"version = "2.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"dependencies = [ "icu_collections", "icu_collections 2.2.0", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", "icu_properties_data 2.2.0", "icu_provider 2.2.0", "zerotrie 0.2.4", "zerovec 0.11.6",][[package]]name = "icu_properties_data"version = "1.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"[[package]]name = "icu_properties_data"version = "2.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"[[package]]name = "icu_provider"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "postcard", "serde", "stable_deref_trait", "tinystr 0.7.6", "writeable 0.5.5", "yoke 0.7.5", "zerofrom", "zerovec 0.10.4",][[package]]name = "icu_provider"version = "2.2.0"
@@ -931,13 +1569,74 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "writeable 0.6.3", "yoke 0.8.2", "zerofrom", "zerotrie", "zerovec", "zerotrie 0.2.4", "zerovec 0.11.6",][[package]]name = "icu_provider_adapters"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d6324dfd08348a8e0374a447ebd334044d766b1839bb8d5ccf2482a99a77c0bc"dependencies = [ "icu_locid", "icu_locid_transform", "icu_provider 1.5.0", "tinystr 0.7.6", "zerovec 0.10.4",][[package]]name = "icu_provider_blob"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c24b98d1365f55d78186c205817631a4acf08d7a45bdf5dc9dcf9c5d54dccf51"dependencies = [ "icu_provider 1.5.0", "postcard", "serde", "writeable 0.5.5", "zerotrie 0.1.3", "zerovec 0.10.4",][[package]]name = "icu_provider_macros"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "icu_segmenter"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de"dependencies = [ "core_maths", "displaydoc", "icu_collections 1.5.0", "icu_locid", "icu_provider 1.5.0", "icu_segmenter_data", "serde", "utf8_iter", "zerovec 0.10.4",][[package]]name = "icu_segmenter_data"version = "1.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a1e52775179941363cc594e49ce99284d13d6948928d8e72c755f55e98caa1eb"[[package]]name = "id-arena"version = "2.3.0"
@@ -962,9 +1661,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"dependencies = [ "icu_normalizer", "icu_properties", "icu_properties 2.2.0",][[package]]name = "image"version = "0.25.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"dependencies = [ "bytemuck", "byteorder-lite", "color_quant", "gif 0.14.2", "image-webp", "moxcms 0.8.1", "num-traits", "png 0.18.1", "zune-core 0.5.1", "zune-jpeg 0.5.15",][[package]]name = "image-webp"version = "0.2.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"dependencies = [ "byteorder-lite", "quick-error",][[package]]name = "imagesize"version = "0.13.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"[[package]]name = "imagesize"version = "0.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"[[package]]name = "indexmap"version = "2.14.0"
@@ -977,6 +1716,12 @@ dependencies = [ "serde_core",][[package]]name = "infer"version = "0.19.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"[[package]]name = "ipnet"version = "2.12.0"
@@ -1010,6 +1755,84 @@ dependencies = [ "wasm-bindgen",][[package]]name = "kamadak-exif"version = "0.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"dependencies = [ "mutate_once",][[package]]name = "krilla"version = "0.6.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a0ddfec86fec13d068075e14f22a7e217c281f3ed69ddcb427bf3f5d504fd674"dependencies = [ "base64", "bumpalo", "comemo", "flate2", "float-cmp 0.10.0", "gif 0.13.3", "hayro-write", "image-webp", "imagesize 0.14.0", "indexmap", "once_cell", "pdf-writer", "png 0.17.16", "rayon", "rustc-hash", "rustybuzz", "siphasher", "skrifa", "smallvec", "subsetter", "tiny-skia-path", "xmp-writer", "yoke 0.8.2", "zune-jpeg 0.5.15",][[package]]name = "krilla-svg"version = "0.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f485e1a850201a01dcd8d73e7cf09f2cd4c4cc85c2cd296359094d49336d8ef7"dependencies = [ "flate2", "fontdb", "krilla", "png 0.17.16", "resvg", "tiny-skia", "usvg",][[package]]name = "kurbo"version = "0.11.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"dependencies = [ "arrayvec", "euclid", "smallvec",][[package]]name = "kurbo"version = "0.12.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32"dependencies = [ "arrayvec", "euclid", "smallvec",][[package]]name = "lazy_static"version = "1.5.0"
@@ -1043,7 +1866,7 @@ version = "0.1.16"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"dependencies = [ "bitflags", "bitflags 2.11.1", "libc", "plain", "redox_syscall 0.7.5",
@@ -1060,12 +1883,37 @@ dependencies = [ "vcpkg",][[package]]name = "linked-hash-map"version = "0.5.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"[[package]]name = "linux-raw-sys"version = "0.12.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"[[package]]name = "lipsum"version = "0.9.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064"dependencies = [ "rand 0.8.6", "rand_chacha 0.3.1",][[package]]name = "litemap"version = "0.7.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"dependencies = [ "serde",][[package]]name = "litemap"version = "0.8.2"
@@ -1136,6 +1984,15 @@ version = "2.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"[[package]]name = "memmap2"version = "0.9.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"dependencies = [ "libc",][[package]]name = "memo-map"version = "0.3.3"
@@ -1190,6 +2047,32 @@ dependencies = [ "windows-sys 0.61.2",][[package]]name = "moxcms"version = "0.7.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"dependencies = [ "num-traits", "pxfm",][[package]]name = "moxcms"version = "0.8.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"dependencies = [ "num-traits", "pxfm",][[package]]name = "mutate_once"version = "0.1.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af"[[package]]name = "nu-ansi-term"version = "0.50.3"
@@ -1199,6 +2082,16 @@ dependencies = [ "windows-sys 0.61.2",][[package]]name = "num-bigint"version = "0.4.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"dependencies = [ "num-integer", "num-traits",][[package]]name = "num-bigint-dig"version = "0.8.6"
@@ -1251,12 +2144,45 @@ dependencies = [ "libm",][[package]]name = "object"version = "0.37.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"dependencies = [ "memchr",][[package]]name = "once_cell"version = "1.21.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"[[package]]name = "palette"version = "0.7.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"dependencies = [ "approx", "fast-srgb8", "libm", "palette_derive",][[package]]name = "palette_derive"version = "0.7.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"dependencies = [ "by_address", "proc-macro2", "quote", "syn",][[package]]name = "parking"version = "2.2.1"
@@ -1286,6 +2212,24 @@ dependencies = [ "windows-link",][[package]]name = "paste"version = "1.0.15"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"[[package]]name = "pdf-writer"version = "0.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "92a79477295a713c2ed425aa82a8b5d20cec3fdee203706cbe6f3854880c1c81"dependencies = [ "bitflags 2.11.1", "itoa", "memchr", "ryu",][[package]]name = "pem-rfc7468"version = "0.7.0"
@@ -1307,7 +2251,41 @@ version = "0.12.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"dependencies = [ "phf_shared", "phf_shared 0.12.1",][[package]]name = "phf"version = "0.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"dependencies = [ "phf_macros", "phf_shared 0.13.1", "serde",][[package]]name = "phf_generator"version = "0.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"dependencies = [ "fastrand", "phf_shared 0.13.1",][[package]]name = "phf_macros"version = "0.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"dependencies = [ "phf_generator", "phf_shared 0.13.1", "proc-macro2", "quote", "syn",][[package]]
@@ -1319,6 +2297,21 @@ dependencies = [ "siphasher",][[package]]name = "phf_shared"version = "0.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"dependencies = [ "siphasher",][[package]]name = "pico-args"version = "0.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"[[package]]name = "pin-project-lite"version = "0.2.17"
@@ -1358,13 +2351,70 @@ version = "0.2.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"[[package]]name = "plist"version = "1.9.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"dependencies = [ "base64", "indexmap", "quick-xml 0.39.4", "serde", "time",][[package]]name = "png"version = "0.17.16"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide",][[package]]name = "png"version = "0.18.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"dependencies = [ "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", "miniz_oxide",][[package]]name = "portable-atomic"version = "1.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"[[package]]name = "postcard"version = "1.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", "serde",][[package]]name = "potential_utf"version = "0.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"dependencies = [ "zerovec", "zerovec 0.11.6",][[package]]
@@ -1392,6 +2442,12 @@ dependencies = [ "syn",][[package]]name = "proc-macro-hack"version = "0.5.20+deprecated"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"[[package]]name = "proc-macro2"version = "1.0.106"
@@ -1401,6 +2457,53 @@ dependencies = [ "unicode-ident",][[package]]name = "psm"version = "0.1.31"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"dependencies = [ "ar_archive_writer", "cc",][[package]]name = "pxfm"version = "0.1.29"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"[[package]]name = "qcms"version = "0.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa"[[package]]name = "quick-error"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"[[package]]name = "quick-xml"version = "0.38.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"dependencies = [ "memchr", "serde",][[package]]name = "quick-xml"version = "0.39.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"dependencies = [ "memchr",][[package]]name = "quinn"version = "0.11.9"
@@ -1536,13 +2639,43 @@ dependencies = [ "getrandom 0.3.4",][[package]]name = "rayon"version = "1.12.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"dependencies = [ "either", "rayon-core",][[package]]name = "rayon-core"version = "1.13.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"dependencies = [ "crossbeam-deque", "crossbeam-utils",][[package]]name = "read-fonts"version = "0.35.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358"dependencies = [ "bytemuck", "font-types",][[package]]name = "redox_syscall"version = "0.5.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"dependencies = [ "bitflags", "bitflags 2.11.1",][[package]]
@@ -1551,7 +2684,7 @@ version = "0.7.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b"dependencies = [ "bitflags", "bitflags 2.11.1",][[package]]
@@ -1624,6 +2757,32 @@ dependencies = [ "webpki-roots",][[package]]name = "resvg"version = "0.45.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43"dependencies = [ "gif 0.13.3", "image-webp", "log", "pico-args", "rgb", "svgtypes", "tiny-skia", "usvg", "zune-jpeg 0.4.21",][[package]]name = "rgb"version = "0.8.53"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"dependencies = [ "bytemuck",][[package]]name = "ring"version = "0.17.14"
@@ -1638,6 +2797,18 @@ dependencies = [ "windows-sys 0.52.0",][[package]]name = "roman-numerals-rs"version = "3.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e"[[package]]name = "roxmltree"version = "0.20.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"[[package]]name = "rsa"version = "0.9.10"
@@ -1658,6 +2829,16 @@ dependencies = [ "zeroize",][[package]]name = "rust_decimal"version = "1.42.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995"dependencies = [ "arrayvec", "num-traits",][[package]]name = "rustc-hash"version = "2.1.2"
@@ -1679,7 +2860,7 @@ version = "1.1.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"dependencies = [ "bitflags", "bitflags 2.11.1", "errno", "libc", "linux-raw-sys",
@@ -1727,12 +2908,39 @@ version = "1.0.22"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"[[package]]name = "rustybuzz"version = "0.20.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"dependencies = [ "bitflags 2.11.1", "bytemuck", "core_maths", "log", "smallvec", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", "unicode-script",][[package]]name = "ryu"version = "1.0.23"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"[[package]]name = "same-file"version = "1.0.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"dependencies = [ "winapi-util",][[package]]name = "scopeguard"version = "1.2.0"
@@ -1799,6 +3007,15 @@ dependencies = [ "serde_core",][[package]]name = "serde_spanned"version = "0.6.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"dependencies = [ "serde",][[package]]name = "serde_urlencoded"version = "0.7.1"
@@ -1887,17 +3104,45 @@ version = "0.3.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"[[package]]name = "simplecss"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"dependencies = [ "log",][[package]]name = "siphasher"version = "1.0.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"[[package]]name = "skrifa"version = "0.37.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841"dependencies = [ "bytemuck", "read-fonts",][[package]]name = "slab"version = "0.4.12"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"[[package]]name = "slotmap"version = "1.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"dependencies = [ "version_check",][[package]]name = "smallvec"
@@ -2032,7 +3277,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"dependencies = [ "atoi", "base64", "bitflags", "bitflags 2.11.1", "byteorder", "bytes", "chrono",
@@ -2076,7 +3321,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"dependencies = [ "atoi", "base64", "bitflags", "bitflags 2.11.1", "byteorder", "chrono", "crc",
@@ -2139,6 +3384,28 @@ version = "1.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"[[package]]name = "stacker"version = "0.1.24"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190"dependencies = [ "cc", "cfg-if", "libc", "psm", "windows-sys 0.61.2",][[package]]name = "strict-num"version = "0.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"dependencies = [ "float-cmp 0.9.0",][[package]]name = "stringprep"version = "0.1.5"
@@ -2150,12 +3417,55 @@ dependencies = [ "unicode-properties",][[package]]name = "strum"version = "0.27.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"dependencies = [ "strum_macros",][[package]]name = "strum_macros"version = "0.27.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"dependencies = [ "heck", "proc-macro2", "quote", "syn",][[package]]name = "subsetter"version = "0.2.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0"dependencies = [ "kurbo 0.12.0", "rustc-hash", "skrifa", "write-fonts",][[package]]name = "subtle"version = "2.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"[[package]]name = "svgtypes"version = "0.15.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"dependencies = [ "kurbo 0.11.3", "siphasher",][[package]]name = "syn"version = "2.0.117"
@@ -2187,6 +3497,27 @@ dependencies = [ "syn",][[package]]name = "syntect"version = "5.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"dependencies = [ "bincode", "fancy-regex", "flate2", "fnv", "once_cell", "plist", "regex-syntax", "serde", "serde_derive", "serde_json", "thiserror", "walkdir", "yaml-rust",][[package]]name = "tar"version = "0.4.45"
@@ -2199,17 +3530,10 @@ dependencies = [][[package]]name = "tempfile"version = "3.27.0"name = "thin-vec"version = "0.2.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2",]checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482"[[package]]name = "thiserror"
@@ -2271,6 +3595,43 @@ dependencies = [ "time-core",][[package]]name = "tiny-skia"version = "0.11.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"dependencies = [ "arrayref", "arrayvec", "bytemuck", "cfg-if", "log", "png 0.17.16", "tiny-skia-path",][[package]]name = "tiny-skia-path"version = "0.11.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"dependencies = [ "arrayref", "bytemuck", "strict-num",][[package]]name = "tinystr"version = "0.7.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"dependencies = [ "displaydoc", "serde", "zerovec 0.10.4",][[package]]name = "tinystr"version = "0.8.3"
@@ -2278,7 +3639,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"dependencies = [ "displaydoc", "zerovec", "serde_core", "zerovec 0.11.6",][[package]]
@@ -2358,6 +3720,47 @@ dependencies = [ "tokio",][[package]]name = "toml"version = "0.8.23"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit",][[package]]name = "toml_datetime"version = "0.6.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"dependencies = [ "serde",][[package]]name = "toml_edit"version = "0.22.27"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "toml_write", "winnow",][[package]]name = "toml_write"version = "0.1.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"[[package]]name = "tower"version = "0.5.3"
@@ -2396,7 +3799,7 @@ version = "0.6.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"dependencies = [ "bitflags", "bitflags 2.11.1", "bytes", "futures-core", "futures-util",
@@ -2457,52 +3860,390 @@ dependencies = [name = "tracing-core"version = "0.1.36"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"dependencies = [ "once_cell", "valuable",][[package]]name = "tracing-log"version = "0.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"dependencies = [ "log", "once_cell", "tracing-core",][[package]]name = "tracing-subscriber"version = "0.3.23"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log",][[package]]name = "try-lock"version = "0.2.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"[[package]]name = "ttf-parser"version = "0.25.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"dependencies = [ "core_maths",][[package]]name = "two-face"version = "0.4.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2"dependencies = [ "serde", "serde_derive", "syntect",][[package]]name = "typed-arena"version = "2.0.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"[[package]]name = "typenum"version = "1.20.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"[[package]]name = "typst"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1f6511ee598476f4f322b4d13891083d96dbacb8f9c2b908604c7094ba390653"dependencies = [ "comemo", "ecow", "rustc-hash", "typst-eval", "typst-html", "typst-layout", "typst-library", "typst-macros", "typst-realize", "typst-syntax", "typst-timing", "typst-utils",][[package]]name = "typst-assets"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5613cb719a6222fe9b74027c3625d107767ec187bff26b8fc931cf58942c834f"[[package]]name = "typst-eval"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "687757487dfc0c1e941344d5024cf7a28364e70c3e304faad89ac65597f62526"dependencies = [ "comemo", "ecow", "indexmap", "rustc-hash", "stacker", "toml", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils", "unicode-segmentation",][[package]]name = "typst-html"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e29f8da4f964d4c90739c3c1e0288b0ba1bccc3cc50623a6d558300b86ca8aad"dependencies = [ "bumpalo", "comemo", "ecow", "palette", "rustc-hash", "time", "typst-assets", "typst-library", "typst-macros", "typst-svg", "typst-syntax", "typst-timing", "typst-utils",][[package]]name = "typst-kit"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "31476ec753e080ffdd543a0e74b6d319355449ff3eca3f216634f31cfd09a92a"dependencies = [ "ecow", "fontdb", "once_cell", "serde", "serde_json", "typst-assets", "typst-library", "typst-syntax", "typst-timing", "typst-utils",][[package]]name = "typst-layout"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4cab0200105831a9158e63718a0f6141c78cb2c1722ed17d19ad28941e3b8491"dependencies = [ "az", "bumpalo", "codex", "comemo", "ecow", "either", "hypher", "icu_properties 1.5.1", "icu_provider 1.5.0", "icu_provider_adapters", "icu_provider_blob", "icu_segmenter", "kurbo 0.12.0", "memchr", "rustc-hash", "rustybuzz", "smallvec", "ttf-parser", "typst-assets", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils", "unicode-bidi", "unicode-math-class", "unicode-script", "unicode-segmentation",][[package]]name = "typst-library"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e276a5de53020c43efe2111ec236252e54ea4480b5ac18063e663dfbe03d9d1b"dependencies = [ "az", "bitflags 2.11.1", "bumpalo", "chinese-number", "ciborium", "codex", "comemo", "csv", "ecow", "flate2", "fontdb", "glidesort", "hayagriva", "hayro-syntax", "icu_properties 1.5.1", "icu_provider 1.5.0", "icu_provider_blob", "image", "indexmap", "kamadak-exif", "kurbo 0.12.0", "lipsum", "memchr", "palette", "phf 0.13.1", "png 0.17.16", "qcms", "rayon", "regex", "regex-syntax", "roxmltree", "rust_decimal", "rustc-hash", "rustybuzz", "serde", "serde_json", "serde_yaml", "siphasher", "smallvec", "syntect", "time", "toml", "ttf-parser", "two-face", "typed-arena", "typst-assets", "typst-macros", "typst-syntax", "typst-timing", "typst-utils", "unicode-math-class", "unicode-normalization", "unicode-segmentation", "unscanny", "usvg", "utf8_iter", "wasmi", "xmlwriter",][[package]]name = "typst-macros"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "141cbd1027129fbf6bda1013f52a264df7befc7388cc8f47767d65e803fd3a59"dependencies = [ "heck", "proc-macro2", "quote", "syn",][[package]]name = "typst-pdf"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "37c8a4630754767cd10d48e8b8186e7dc784631a30a3a93521edf7d77aebd0c0"dependencies = [ "az", "bytemuck", "comemo", "ecow", "image", "indexmap", "infer", "krilla", "krilla-svg", "rustc-hash", "serde", "smallvec", "typst-assets", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils",][[package]]name = "typst-realize"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f7ffe964757fb93d2e98978aa2a74ee85b0f94c8643e8f3550737258b58f39d8"dependencies = [ "once_cell", "valuable", "arrayvec", "bumpalo", "comemo", "ecow", "regex", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils",][[package]]name = "tracing-log"version = "0.2.0"name = "typst-svg"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"checksum = "e46b811837ade1f0243ef0d8bf3fb06d166443090eac22c28643f374c2ccdc9d"dependencies = [ "log", "once_cell", "tracing-core", "base64", "comemo", "ecow", "flate2", "hayro", "hayro-svg", "image", "rustc-hash", "ttf-parser", "typst-assets", "typst-library", "typst-macros", "typst-timing", "typst-utils", "xmlparser", "xmlwriter",][[package]]name = "tracing-subscriber"version = "0.3.23"name = "typst-syntax"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"checksum = "a95d9192060e23b1e491b0b94dff676acddc92a4d672aeb8ca3890a5a734e879"dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", "ecow", "rustc-hash", "serde", "toml", "typst-timing", "typst-utils", "unicode-ident", "unicode-math-class", "unicode-script", "unicode-segmentation", "unscanny",][[package]]name = "try-lock"version = "0.2.5"name = "typst-timing"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"checksum = "7be94f8faf19841b49574ef5c7fd7a12e2deb7c3d8deba5a596f35d2222024cd"dependencies = [ "parking_lot", "serde", "serde_json",][[package]]name = "typenum"version = "1.20.0"name = "typst-utils"version = "0.14.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"checksum = "a3966c92e8fa48c7ce898130d07000d985f18206d92b250f0f939287fbccdee3"dependencies = [ "once_cell", "portable-atomic", "rayon", "rustc-hash", "siphasher", "thin-vec", "unicode-math-class",][[package]]name = "uaparser"
@@ -2518,6 +4259,50 @@ dependencies = [ "serde_yaml",][[package]]name = "unic-langid"version = "0.9.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"dependencies = [ "unic-langid-impl", "unic-langid-macros",][[package]]name = "unic-langid-impl"version = "0.9.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"dependencies = [ "serde", "tinystr 0.8.3",][[package]]name = "unic-langid-macros"version = "0.9.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25"dependencies = [ "proc-macro-hack", "tinystr 0.8.3", "unic-langid-impl", "unic-langid-macros-impl",][[package]]name = "unic-langid-macros-impl"version = "0.9.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5"dependencies = [ "proc-macro-hack", "quote", "syn", "unic-langid-impl",][[package]]name = "unicase"version = "2.9.0"
@@ -2530,12 +4315,30 @@ version = "0.3.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"[[package]]name = "unicode-bidi-mirroring"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"[[package]]name = "unicode-ccc"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"[[package]]name = "unicode-ident"version = "1.0.24"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"[[package]]name = "unicode-math-class"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65"[[package]]name = "unicode-normalization"version = "0.1.25"
@@ -2551,6 +4354,24 @@ version = "0.1.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"[[package]]name = "unicode-script"version = "0.5.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"[[package]]name = "unicode-segmentation"version = "1.13.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"[[package]]name = "unicode-vo"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"[[package]]name = "unicode-xid"version = "0.2.6"
@@ -2563,6 +4384,12 @@ version = "0.2.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"[[package]]name = "unscanny"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47"[[package]]name = "untrusted"version = "0.9.0"
@@ -2579,6 +4406,7 @@ dependencies = [ "idna", "percent-encoding", "serde", "serde_derive",][[package]]
@@ -2587,6 +4415,33 @@ version = "2.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"[[package]]name = "usvg"version = "0.45.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"dependencies = [ "base64", "data-url", "flate2", "fontdb", "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", "roxmltree", "rustybuzz", "simplecss", "siphasher", "strict-num", "svgtypes", "tiny-skia-path", "unicode-bidi", "unicode-script", "unicode-vo", "xmlwriter",][[package]]name = "utf8_iter"version = "1.0.4"
@@ -2623,6 +4478,16 @@ version = "0.9.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"[[package]]name = "walkdir"version = "2.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"dependencies = [ "same-file", "winapi-util",][[package]]name = "want"version = "0.3.1"
@@ -2724,7 +4589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"dependencies = [ "leb128fmt", "wasmparser", "wasmparser 0.244.0",][[package]]
@@ -2736,7 +4601,7 @@ dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", "wasmparser 0.244.0",][[package]]
@@ -2752,13 +4617,59 @@ dependencies = [ "web-sys",][[package]]name = "wasmi"version = "0.51.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff"dependencies = [ "spin", "wasmi_collections", "wasmi_core", "wasmi_ir", "wasmparser 0.228.0",][[package]]name = "wasmi_collections"version = "0.51.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e9b8e98e45a2a534489f8225e765cbf1cb9a3078072605e58158910cf4749172"[[package]]name = "wasmi_core"version = "0.51.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c25f375c0cdf14810eab07f532f61f14d4966f09c747a55067fdf3196e8512e6"dependencies = [ "libm",][[package]]name = "wasmi_ir"version = "0.51.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "624e2a68a4293ecb8f564260b68394b29cf3b3edba6bce35532889a2cb33c3d9"dependencies = [ "wasmi_core",][[package]]name = "wasmparser"version = "0.228.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"dependencies = [ "bitflags 2.11.1",][[package]]name = "wasmparser"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"dependencies = [ "bitflags", "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver",
@@ -2793,6 +4704,12 @@ dependencies = [ "rustls-pki-types",][[package]]name = "weezl"version = "0.1.12"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"[[package]]name = "whoami"version = "1.6.1"
@@ -2803,6 +4720,15 @@ dependencies = [ "wasite",][[package]]name = "winapi-util"version = "0.1.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"dependencies = [ "windows-sys 0.61.2",][[package]]name = "windows-core"version = "0.62.2"
@@ -3084,6 +5010,15 @@ version = "0.53.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"[[package]]name = "winnow"version = "0.7.15"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"dependencies = [ "memchr",][[package]]name = "wit-bindgen"version = "0.51.0"
@@ -3148,7 +5083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"dependencies = [ "anyhow", "bitflags", "bitflags 2.11.1", "indexmap", "log", "serde",
@@ -3156,7 +5091,7 @@ dependencies = [ "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wasmparser 0.244.0", "wit-parser",]
@@ -3175,9 +5110,28 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", "wasmparser", "wasmparser 0.244.0",][[package]]name = "write-fonts"version = "0.43.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "886614b5ce857341226aa091f3c285e450683894acaaa7887f366c361efef79d"dependencies = [ "font-types", "indexmap", "kurbo 0.12.0", "log", "read-fonts",][[package]]name = "writeable"version = "0.5.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"[[package]]name = "writeable"version = "0.6.3"
@@ -3194,6 +5148,45 @@ dependencies = [ "rustix",][[package]]name = "xmlparser"version = "0.13.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"[[package]]name = "xmlwriter"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"[[package]]name = "xmp-writer"version = "0.3.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"[[package]]name = "yaml-rust"version = "0.4.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"dependencies = [ "linked-hash-map",][[package]]name = "yoke"version = "0.7.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"dependencies = [ "serde", "stable_deref_trait", "yoke-derive 0.7.5", "zerofrom",][[package]]name = "yoke"version = "0.8.2"
@@ -3201,10 +5194,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"dependencies = [ "stable_deref_trait", "yoke-derive", "yoke-derive 0.8.2", "zerofrom",][[package]]name = "yoke-derive"version = "0.7.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"dependencies = [ "proc-macro2", "quote", "syn", "synstructure",][[package]]name = "yoke-derive"version = "0.8.2"
@@ -3264,6 +5269,18 @@ version = "1.8.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"[[package]]name = "zerotrie"version = "0.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f"dependencies = [ "displaydoc", "litemap 0.7.5", "serde", "zerovec 0.10.4",][[package]]name = "zerotrie"version = "0.2.4"
@@ -3271,8 +5288,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"dependencies = [ "displaydoc", "yoke", "yoke 0.8.2", "zerofrom",][[package]]name = "zerovec"version = "0.10.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"dependencies = [ "serde", "yoke 0.7.5", "zerofrom", "zerovec-derive 0.10.3",][[package]]
@@ -3281,9 +5310,21 @@ version = "0.11.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"dependencies = [ "yoke", "serde", "yoke 0.8.2", "zerofrom", "zerovec-derive", "zerovec-derive 0.11.3",][[package]]name = "zerovec-derive"version = "0.10.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"dependencies = [ "proc-macro2", "quote", "syn",][[package]]
@@ -3297,8 +5338,44 @@ dependencies = [ "syn",][[package]]name = "zlib-rs"version = "0.6.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"[[package]]name = "zmij"version = "1.0.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"[[package]]name = "zune-core"version = "0.4.12"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"[[package]]name = "zune-core"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"[[package]]name = "zune-jpeg"version = "0.4.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"dependencies = [ "zune-core 0.4.12",][[package]]name = "zune-jpeg"version = "0.5.15"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"dependencies = [ "zune-core 0.5.1",]
modified Cargo.toml
@@ -32,12 +32,14 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] }flate2 = "1"tar = "0.4"tempfile = "3"mime_guess = "2"urlencoding = "2"futures-util = "0.3"typst = "0.14"typst-pdf = "0.14"typst-kit = { version = "0.14", default-features = false, features = ["fonts", "embed-fonts"] }[profile.release]lto = truecodegen-units = 1# Skip LTO so release builds stay tractable on the 1-vCPU alpine VPS once# typst-library entered the dep tree. codegen-units defaults to 16.strip = true
modified Dockerfile
@@ -23,7 +23,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \# ----- runtime -----FROM alpine:3.23RUN apk add --no-cache chromium font-jetbrains-mono ttf-dejavuRUN apk add --no-cache \    font-jetbrains-mono ttf-dejavu ttf-liberation fontconfigWORKDIR /app
modified src/app.rs
@@ -8,6 +8,7 @@ use tower_http::services::ServeDir;use tower_http::set_header::SetResponseHeaderLayer;use crate::geoip::{self, GeoIp};use crate::pdf::PdfRenderer;use crate::routes;use crate::ua::{self, UaParser};use crate::{db, middleware, templates};
@@ -20,6 +21,7 @@ pub struct AppState {    pub geoip: Arc<GeoIp>,    pub ua: Arc<UaParser>,    pub config: Arc<Config>,    pub pdf_renderer: Arc<PdfRenderer>,}#[derive(Debug, Clone)]
@@ -68,6 +70,8 @@ impl AppState {        let manifest_path = root.join("dist/.vite/manifest.json");        let env = Arc::new(templates::build_env(&templates_dir, &manifest_path));        let pdf_renderer = Arc::new(PdfRenderer::new(root.clone()));        let config = Arc::new(Config {            root,            data_dir,
@@ -83,6 +87,7 @@ impl AppState {            geoip,            ua,            config,            pdf_renderer,        })    }}
modified src/pdf.rs
@@ -1,106 +1,148 @@use std::path::PathBuf;use std::process::Command;use tempfile::{Builder, NamedTempFile};use std::path::{Path, PathBuf};use std::sync::Arc;/// Render an HTML string to a PDF using headless Chromium.////// Spawns chrome-headless-shell with --print-to-pdf. The HTML is written to a/// tempfile and loaded as a file:// URL so relative URLs (e.g. /content/images/...)/// can be rewritten to absolute http:// URLs pointing at the live server.pub fn html_to_pdf(html: &str, server_base: &str) -> anyhow::Result<Vec<u8>> {    // Rewrite relative absolute paths (/content/, /static/) to point at the    // running server so chromium can fetch them.    let rewritten = html        .replace("src=\"/content/", &format!("src=\"{server_base}/content/"))        .replace("href=\"/content/", &format!("href=\"{server_base}/content/"))        .replace("src=\"/static/", &format!("src=\"{server_base}/static/"))        .replace("href=\"/static/", &format!("href=\"{server_base}/static/"));use chrono::{Datelike, Local};use typst::{    diag::{FileError, FileResult, SourceDiagnostic},    foundations::{Bytes, Datetime},    layout::PagedDocument,    syntax::{FileId, Source, VirtualPath},    text::{Font, FontBook},    utils::LazyHash,    Library, LibraryExt, World,};use typst_kit::fonts::{FontSearcher, FontSlot, Fonts};    // Suffix matters: chromium decides HTML vs plain-text rendering by extension.    // Without .html the page is shown as raw source text instead of being rendered.    let mut html_file = Builder::new().suffix(".html").tempfile()?;    {        use std::io::Write;        html_file.write_all(rewritten.as_bytes())?;        html_file.flush()?;    }    let html_path = html_file.path().to_path_buf();    let pdf_file = NamedTempFile::new()?;    let pdf_path = pdf_file.path().to_path_buf();    let url = format!("file://{}", html_path.display());    let print_arg = format!("--print-to-pdf={}", pdf_path.display());    run_chromium(&url, &print_arg)?;    let bytes = std::fs::read(&pdf_path)?;    Ok(bytes)/// Pre-built renderer state. Fonts and the standard library are loaded once at/// startup and shared across renders.pub struct PdfRenderer {    library: Arc<LazyHash<Library>>,    book: Arc<LazyHash<FontBook>>,    fonts: Arc<Vec<FontSlot>>,    root: PathBuf,}fn run_chromium(url: &str, print_arg: &str) -> anyhow::Result<()> {    let bin = find_chromium().ok_or_else(|| {        anyhow::anyhow!("could not locate chromium; set CHROMIUM_BIN or install chromium on PATH")    })?;    let output = Command::new(&bin)        .arg("--headless=new")        .arg("--no-sandbox")        .arg("--disable-gpu")        .arg("--no-pdf-header-footer")        .arg("--hide-scrollbars")        .arg(print_arg)        .arg(url)        .output()?;    if !output.status.success() {        anyhow::bail!(            "chromium ({}) exited {}: {}",            bin.display(),            output.status,            String::from_utf8_lossy(&output.stderr)        );impl PdfRenderer {    /// Discover system + embedded fonts and build the renderer. `root` is the    /// project root that absolute paths in the Typst source resolve against    /// (e.g. `image("/templates/foo.svg")` -> `<root>/templates/foo.svg`).    pub fn new(root: PathBuf) -> Self {        let Fonts { book, fonts } = FontSearcher::new()            .include_system_fonts(true)            .search();        Self {            library: Arc::new(LazyHash::new(Library::default())),            book: Arc::new(LazyHash::new(book)),            fonts: Arc::new(fonts),            root,        }    }    /// Compile `source` (Typst markup) into a PDF.    pub fn render(&self, source: String) -> anyhow::Result<Vec<u8>> {        let main_id = FileId::new(None, VirtualPath::new("/main.typ"));        let main = Source::new(main_id, source);        let world = PdfWorld {            library: self.library.clone(),            book: self.book.clone(),            fonts: self.fonts.clone(),            root: self.root.clone(),            main,        };        let warned = typst::compile::<PagedDocument>(&world);        let document = warned            .output            .map_err(|errs| format_diagnostics("compile", &errs))?;        let bytes = typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())            .map_err(|errs| format_diagnostics("pdf export", &errs))?;        Ok(bytes)    }    Ok(())}/// Locate a chromium binary across environments:/// 1. `CHROMIUM_BIN` env var (explicit override)/// 2. PATH search for common chromium binary names (production install)/// 3. Glob under `/opt/playwright-browsers/` (dev container with Playwright)fn find_chromium() -> Option<PathBuf> {    if let Ok(p) = std::env::var("CHROMIUM_BIN") {        let path = PathBuf::from(p);        if path.is_file() {            return Some(path);fn format_diagnostics(stage: &str, errs: &[SourceDiagnostic]) -> anyhow::Error {    let mut s = String::new();    for e in errs {        if !s.is_empty() {            s.push('\n');        }        s.push_str(&e.message);        for h in &e.hints {            s.push_str("\n  hint: ");            s.push_str(h);        }    }    anyhow::anyhow!("typst {stage}: {s}")}    let names = [        "chromium",        "chromium-browser",        "chrome-headless-shell",        "google-chrome",        "chrome",    ];    if let Some(path_var) = std::env::var_os("PATH") {        for dir in std::env::split_paths(&path_var) {            for name in &names {                let candidate = dir.join(name);                if candidate.is_file() {                    return Some(candidate);                }            }struct PdfWorld {    library: Arc<LazyHash<Library>>,    book: Arc<LazyHash<FontBook>>,    fonts: Arc<Vec<FontSlot>>,    root: PathBuf,    main: Source,}impl World for PdfWorld {    fn library(&self) -> &LazyHash<Library> {        &self.library    }    fn book(&self) -> &LazyHash<FontBook> {        &self.book    }    fn main(&self) -> FileId {        self.main.id()    }    fn source(&self, id: FileId) -> FileResult<Source> {        if id == self.main.id() {            return Ok(self.main.clone());        }        let path = self.resolve(id)?;        let text =            std::fs::read_to_string(&path).map_err(|err| FileError::from_io(err, &path))?;        Ok(Source::new(id, text))    }    fn file(&self, id: FileId) -> FileResult<Bytes> {        let path = self.resolve(id)?;        let bytes = std::fs::read(&path).map_err(|err| FileError::from_io(err, &path))?;        Ok(Bytes::new(bytes))    }    fn font(&self, index: usize) -> Option<Font> {        self.fonts.get(index)?.get()    }    fn today(&self, _offset: Option<i64>) -> Option<Datetime> {        let now = Local::now();        Datetime::from_ymd(now.year(), now.month() as u8, now.day() as u8)    }}    if let Ok(entries) = std::fs::read_dir("/opt/playwright-browsers") {        for entry in entries.flatten() {            let candidate = entry                .path()                .join("chrome-headless-shell-linux64/chrome-headless-shell");            if candidate.is_file() {                return Some(candidate);            }impl PdfWorld {    fn resolve(&self, id: FileId) -> FileResult<PathBuf> {        if id.package().is_some() {            return Err(FileError::Other(Some(                "remote packages not supported".into(),            )));        }        id.vpath()            .resolve(&self.root)            .ok_or(FileError::AccessDenied)            .and_then(|p| {                if path_within(&p, &self.root) {                    Ok(p)                } else {                    Err(FileError::AccessDenied)                }            })    }}    Nonefn path_within(path: &Path, root: &Path) -> bool {    let canon = match path.canonicalize() {        Ok(p) => p,        Err(_) => return false,    };    let canon_root = match root.canonicalize() {        Ok(p) => p,        Err(_) => return false,    };    canon.starts_with(canon_root)}
modified src/routes/dashboard.rs
@@ -309,9 +309,9 @@ pub async fn property(            return (StatusCode::OK, h, body).into_response();        }        if fmt == "pdf" {            let html = match render_to_string(            let typst_source = match render_to_string(                &state,                "properties/property_print.html",                "properties/property_report.typ",                &path,                authed,                extra,
@@ -319,10 +319,9 @@ pub async fn property(                Ok(b) => b,                Err(resp) => return resp,            };            let server_base = state.config.base_url.clone();            let renderer = state.pdf_renderer.clone();            let pdf_res =                tokio::task::spawn_blocking(move || crate::pdf::html_to_pdf(&html, &server_base))                    .await;                tokio::task::spawn_blocking(move || renderer.render(typst_source)).await;            match pdf_res {                Ok(Ok(bytes)) => {                    let mut h = axum::http::HeaderMap::new();
modified src/templates.rs
@@ -120,10 +120,47 @@ pub fn build_env(templates_dir: &Path, manifest_path: &Path) -> Environment<'sta    env.add_function("url_for", url_for);    env.add_filter("naturaltime", naturaltime_filter);    env.add_filter("urlencode", urlencode_filter);    env.add_filter("typst_str", typst_str_filter);    env.add_filter("typst_md", typst_md_filter);    env}/// Escape a value for inclusion inside a `"..."` Typst string literal.fn typst_str_filter(value: Value) -> Result<String, Error> {    let s = value.as_str().map(|s| s.to_string()).unwrap_or_else(|| value.to_string());    let mut out = String::with_capacity(s.len());    for c in s.chars() {        match c {            '\\' => out.push_str("\\\\"),            '"' => out.push_str("\\\""),            '\n' => out.push_str("\\n"),            '\r' => out.push_str("\\r"),            '\t' => out.push_str("\\t"),            _ => out.push(c),        }    }    Ok(out)}/// Escape a value for inclusion inside a Typst content block `[...]`./// Backslash-escapes the markup specials so a label like `*foo*` renders as/// literal text, not bolded.fn typst_md_filter(value: Value) -> Result<String, Error> {    let s = value.as_str().map(|s| s.to_string()).unwrap_or_else(|| value.to_string());    let mut out = String::with_capacity(s.len());    for c in s.chars() {        match c {            '\\' | '[' | ']' | '*' | '_' | '`' | '#' | '$' | '<' | '@' | '~' => {                out.push('\\');                out.push(c);            }            _ => out.push(c),        }    }    Ok(out)}fn urlencode_filter(value: Value) -> Result<String, Error> {    let s = value.as_str().map(|s| s.to_string()).unwrap_or_else(|| value.to_string());    Ok(urlencoding::encode(&s).into_owned())
deleted templates/properties/property_print.html
@@ -1,467 +0,0 @@<html lang="en"><head><meta charset="utf-8"><title>{{ property.name }} · Analytics report</title><style>  @page { size: A4; margin: 14mm; }  @page :first { margin-top: 16mm; }  * { box-sizing: border-box; }  html, body {    background: #fff;    color: #000;    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;    font-size: 9.5pt;    line-height: 1.45;    margin: 0;    padding: 0;    -webkit-print-color-adjust: exact;    print-color-adjust: exact;  }  h1, h2, h3 { color: #000; margin: 0; font-weight: 700; }  h1 { font-size: 22pt; letter-spacing: -0.01em; }  h2 {    font-size: 11pt;    text-transform: uppercase;    letter-spacing: 0.08em;    margin: 16pt 0 6pt;    padding-bottom: 3pt;    border-bottom: 1px solid #000;  }  h3 {    font-size: 8.5pt;    text-transform: uppercase;    letter-spacing: 0.08em;    margin: 8pt 0 3pt;    color: #333;  }  .header {    display: flex;    justify-content: space-between;    align-items: flex-start;    margin-bottom: 4pt;  }  .header .brand {    font-size: 8.5pt;    text-transform: uppercase;    letter-spacing: 0.12em;    color: #555;  }  .header .generated {    font-size: 8pt;    color: #555;    text-align: right;  }  .meta {    font-size: 9pt;    margin-top: 4pt;    border-top: 1px solid #000;    border-bottom: 1px solid #000;    padding: 6pt 0;    display: grid;    grid-template-columns: repeat(2, 1fr);    gap: 2pt 16pt;  }  .meta dt { color: #555; font-size: 8pt; text-transform: uppercase; letter-spacing: 0.05em; margin: 0; }  .meta dd { margin: 0 0 4pt; font-weight: 600; }  .meta dd code { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 8.5pt; font-weight: 400; }  .metrics {    display: grid;    grid-template-columns: repeat(4, 1fr);    gap: 4pt;  }  .metric {    border: 1px solid #000;    padding: 5pt 7pt;    page-break-inside: avoid;  }  .metric .label {    font-size: 7.5pt;    text-transform: uppercase;    letter-spacing: 0.05em;    color: #555;    line-height: 1.2;  }  .metric .value {    font-size: 14pt;    font-weight: 700;    margin-top: 2pt;    font-variant-numeric: tabular-nums;  }  .metric .delta {    font-size: 7.5pt;    color: #333;    margin-top: 1pt;    font-variant-numeric: tabular-nums;  }  .metric .delta.up::before { content: "▲ "; }  .metric .delta.down::before { content: "▼ "; }  .metric .delta.flat::before { content: "· "; }  .chart-wrap {    margin: 4pt 0 0;    page-break-inside: avoid;  }  .chart {    width: 100%;    height: 90pt;    display: block;  }  .chart-axis {    display: flex;    justify-content: space-between;    font-size: 7.5pt;    color: #555;    margin-top: 2pt;    font-variant-numeric: tabular-nums;  }  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt 14pt; }  .grid-2 > section { page-break-inside: avoid; }  .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8pt 10pt; }  .grid-3 > section { page-break-inside: avoid; min-width: 0; }  table { width: 100%; border-collapse: collapse; font-size: 7.5pt; table-layout: fixed; }  th, td {    text-align: left;    padding: 2pt 3pt;    border-bottom: 1px solid #ddd;    vertical-align: top;    overflow-wrap: break-word;  }  th {    font-weight: 700;    border-bottom: 1px solid #000;    text-transform: uppercase;    letter-spacing: 0.04em;    font-size: 6.5pt;    color: #555;  }  td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; width: 22%; }  td.pct, th.pct { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; color: #555; width: 18%; }  td.code, td .code {    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;    font-size: 7pt;    word-break: break-all;  }  tr:last-child td { border-bottom: none; }  .empty { color: #888; font-style: italic; font-size: 8pt; }  footer.report-footer {    margin-top: 18pt;    padding-top: 6pt;    border-top: 1px solid #000;    font-size: 7.5pt;    color: #555;    display: flex;    justify-content: space-between;  }  .filter-banner {    display: inline-block;    border: 1px solid #000;    padding: 1pt 6pt;    font-size: 8pt;    margin-top: 4pt;  }  .filter-banner strong { font-weight: 700; }</style></head><body><div class="header">  <div class="brand">// Analytics · property report</div>  <div class="generated">Generated {{ generated_at }}</div></div><h1>{{ property.name }}</h1><dl class="meta">  <div><dt>Property ID</dt><dd><code>{{ property.id }}</code></dd></div>  <div><dt>Operator</dt><dd>operator</dd></div>  <div><dt>Date range</dt><dd>{{ date_start }} → {{ date_end }} <span style="color:#555;font-weight:400">({{ date_range }} days)</span></dd></div>  <div><dt>Live users · last 30m</dt><dd>{{ total_live_users }}</dd></div></dl>{% if filter_url %}<div class="filter-banner"><strong>Filter · url:</strong> <code>{{ filter_url }}</code></div>{% endif %}<h2>Metrics · period vs previous</h2><div class="metrics">  {% for card in event_cards %}  <div class="metric">    <div class="label">{{ card.name }}</div>    <div class="value">{{ card.value }}</div>    {% if card.percent_change == 0 %}      <div class="delta flat">no change</div>    {% elif card.percent_change < 0 %}      <div class="delta down">{{ card.percent_change }}% vs previous</div>    {% else %}      <div class="delta up">+{{ card.percent_change }}% vs previous</div>    {% endif %}  </div>  {% endfor %}</div><h2>Events over time</h2><div class="chart-wrap">  {% if chart_polyline %}  <svg class="chart" viewBox="0 0 600 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Events over time">    <line x1="0" y1="99.5" x2="600" y2="99.5" stroke="#000" stroke-width="0.4" />    <polyline fill="none" stroke="#000" stroke-width="0.9" stroke-linejoin="round" stroke-linecap="round" points="{{ chart_polyline }}" />  </svg>  <div class="chart-axis">    <span>{{ chart_label_start }}</span>    <span>peak {{ chart_peak_count }} · {{ chart_peak_label }}</span>    <span>{{ chart_label_end }}</span>  </div>  {% else %}  <div class="empty">No events in this range.</div>  {% endif %}</div><div class="grid-3">  {% if total_page_views_by_page_url %}  <section>    <h3>Top pages · page views</h3>    <table>      <thead><tr><th>URL</th><th class="num">Views</th></tr></thead>      <tbody>        {% for item in total_page_views_by_page_url %}        <tr><td class="code">{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_events_by_page_url %}  <section>    <h3>Top pages · all events</h3>    <table>      <thead><tr><th>URL</th><th class="num">Events</th></tr></thead>      <tbody>        {% for item in total_events_by_page_url %}        <tr><td class="code">{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_session_starts_by_referrer %}  <section>    <h3>Top referrers</h3>    <table>      <thead><tr><th>Referrer</th><th class="num">Sessions</th></tr></thead>      <tbody>        {% for item in total_session_starts_by_referrer %}        <tr><td class="code">{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_events_by_custom_event %}  <section>    <h3>Top custom events</h3>    <table>      <thead><tr><th>Event</th><th class="num">Count</th></tr></thead>      <tbody>        {% for item in total_events_by_custom_event %}        <tr><td>{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}</div><h2>Visitor breakdown</h2><div class="grid-3">  {% if total_events_by_device %}  <section>    <h3>Device</h3>    <table>      <thead><tr><th>Type</th><th class="num">Events</th><th class="pct">%</th></tr></thead>      <tbody>        {% for item in total_events_by_device %}        <tr>          <td>{{ item.label }}</td>          <td class="num">{{ item.count }}</td>          <td class="pct">{{ (item.count * 100 / breakdown_totals.device) | int }}%</td>        </tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_events_by_browser %}  <section>    <h3>Browser</h3>    <table>      <thead><tr><th>Name</th><th class="num">Events</th><th class="pct">%</th></tr></thead>      <tbody>        {% for item in total_events_by_browser %}        <tr>          <td>{{ item.label }}</td>          <td class="num">{{ item.count }}</td>          <td class="pct">{{ (item.count * 100 / breakdown_totals.browser) | int }}%</td>        </tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_events_by_platform %}  <section>    <h3>Platform</h3>    <table>      <thead><tr><th>Name</th><th class="num">Events</th><th class="pct">%</th></tr></thead>      <tbody>        {% for item in total_events_by_platform %}        <tr>          <td>{{ item.label }}</td>          <td class="num">{{ item.count }}</td>          <td class="pct">{{ (item.count * 100 / breakdown_totals.platform) | int }}%</td>        </tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_events_by_screen_size %}  <section>    <h3>Screen size</h3>    <table>      <thead><tr><th>Size</th><th class="num">Events</th><th class="pct">%</th></tr></thead>      <tbody>        {% for item in total_events_by_screen_size %}        <tr>          <td>{{ item.label }}</td>          <td class="num">{{ item.count }}</td>          <td class="pct">{{ (item.count * 100 / breakdown_totals.screen_size) | int }}%</td>        </tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}</div>{% if top_countries %}<h2>Geography</h2><div class="grid-2">  <section>    <h3>Top countries</h3>    <table>      <thead><tr><th>Country</th><th class="num">Sessions</th></tr></thead>      <tbody>        {% for item in top_countries %}        <tr><td>{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section></div>{% endif %}{% if total_page_views_by_utm_source or total_page_views_by_utm_medium or total_page_views_by_utm_campaign %}<h2>UTM attribution</h2><div class="grid-3">  {% if total_page_views_by_utm_source %}  <section>    <h3>Source</h3>    <table>      <thead><tr><th>Source</th><th class="num">Views</th></tr></thead>      <tbody>        {% for item in total_page_views_by_utm_source %}        <tr><td>{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_page_views_by_utm_medium %}  <section>    <h3>Medium</h3>    <table>      <thead><tr><th>Medium</th><th class="num">Views</th></tr></thead>      <tbody>        {% for item in total_page_views_by_utm_medium %}        <tr><td>{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}  {% if total_page_views_by_utm_campaign %}  <section>    <h3>Campaign</h3>    <table>      <thead><tr><th>Campaign</th><th class="num">Views</th></tr></thead>      <tbody>        {% for item in total_page_views_by_utm_campaign %}        <tr><td>{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}</div>{% endif %}<h2>Bot traffic <span style="font-size:8pt;font-weight:400;text-transform:none;letter-spacing:0;color:#555;">excluded from metrics above</span></h2>{% if bot_traffic.total > 0 %}<div class="grid-2">  <section>    <h3>Top bots</h3>    <table>      <thead><tr><th>Bot</th><th class="num">Events</th></tr></thead>      <tbody>        <tr><td><strong>Total</strong></td><td class="num"><strong>{{ bot_traffic.total }}</strong></td></tr>        {% for item in bot_traffic.top_bots %}        <tr><td>{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% if bot_traffic.top_pages %}  <section>    <h3>Top pages hit by bots</h3>    <table>      <thead><tr><th>URL</th><th class="num">Events</th></tr></thead>      <tbody>        {% for item in bot_traffic.top_pages %}        <tr><td class="code">{{ item.label }}</td><td class="num">{{ item.count }}</td></tr>        {% endfor %}      </tbody>    </table>  </section>  {% endif %}</div>{% else %}<div class="empty">No bot events in this range.</div>{% endif %}<footer class="report-footer">  <span>Analytics · self-hosted · {{ BASE_URL }}</span>  <span>{{ property.name }} · {{ date_start }} → {{ date_end }}</span></footer></body></html>
added templates/properties/property_report.typ
@@ -0,0 +1,409 @@// Property report rendered by minijinja into Typst markup, then compiled to// PDF by src/pdf.rs::PdfRenderer. Mirrors the shape of property_print.html// (the chromium-era template) so the dashboard ?report=pdf output stays// recognisable.#let dim = rgb("#555")#let muted = rgb("#888")#let mono = ("JetBrains Mono", "DejaVu Sans Mono", "Liberation Mono")#set page(  paper: "a4",  margin: (top: 14mm, bottom: 18mm, left: 14mm, right: 14mm),  footer: context {    set text(size: 7.5pt, fill: dim)    grid(      columns: (1fr, auto),      align: (left + horizon, right + horizon),      [Analytics · self-hosted{% if base_url %} · {{ base_url | typst_md }}{% endif %} · {{ property.name | typst_md }} · {{ date_start | typst_md }} → {{ date_end | typst_md }}],      [Page #counter(page).display() of #counter(page).final().first()],    )  },)#set text(  font: ("DejaVu Sans", "Liberation Sans", "Arial"),  size: 9.5pt,  fill: black,)#set par(leading: 0.5em, justify: false)#show heading.where(level: 1): set text(size: 22pt, weight: "bold")#show heading.where(level: 1): set block(above: 8pt, below: 4pt)#show heading.where(level: 2): it => block(  above: 16pt,  below: 6pt,  width: 100%,  stroke: (bottom: 0.6pt + black),  inset: (bottom: 3pt),)[#text(size: 11pt, weight: "bold", tracking: 0.6pt, upper(it.body))]#show heading.where(level: 3): it => block(  above: 8pt,  below: 3pt,)[#text(size: 8.5pt, weight: "bold", tracking: 0.5pt, fill: rgb("#333"), upper(it.body))]// Header strip#grid(  columns: (1fr, auto),  align: (left + top, right + top),  text(size: 8.5pt, tracking: 0.9pt, fill: dim, upper("// Analytics · property report")),  text(size: 8pt, fill: dim)[Generated {{ generated_at | typst_md }}],)= {{ property.name | typst_md }}// Meta dl: 2-col, with thin rules above and below.#block(  above: 8pt,  below: 0pt,  width: 100%,  stroke: (top: 0.6pt + black, bottom: 0.6pt + black),  inset: (top: 6pt, bottom: 6pt),)[  #grid(    columns: (1fr, 1fr),    column-gutter: 16pt,    row-gutter: 4pt,    [      #text(size: 7.5pt, tracking: 0.4pt, fill: dim, upper("Property ID")) \      #text(font: mono, size: 8.5pt)[{{ property.id | typst_md }}]    ],    [      #text(size: 7.5pt, tracking: 0.4pt, fill: dim, upper("Operator")) \      #text(weight: "semibold")[operator]    ],    [      #text(size: 7.5pt, tracking: 0.4pt, fill: dim, upper("Date range")) \      #text(weight: "semibold")[{{ date_start | typst_md }} → {{ date_end | typst_md }}] #text(fill: dim)[ ({{ date_range }} days)]    ],    [      #text(size: 7.5pt, tracking: 0.4pt, fill: dim, upper("Live users · last 30m")) \      #text(weight: "semibold")[{{ total_live_users }}]    ],  )]{% if filter_url %}#v(4pt)#box(stroke: 0.6pt + black, inset: (x: 6pt, y: 2pt))[  #text(size: 8pt)[*Filter · url:* #text(font: mono)[{{ filter_url | typst_md }}]]]{% endif %}== Metrics · period vs previous#grid(  columns: (1fr, 1fr, 1fr, 1fr),  gutter: 4pt,  {% for card in event_cards %}  rect(width: 100%, stroke: 0.6pt + black, inset: 6pt)[    #text(size: 7.5pt, tracking: 0.4pt, fill: dim)[{{ card.name | typst_md }}]    #v(2pt)    #text(size: 14pt, weight: "bold")[{{ card.value | typst_md }}]    #v(1pt)    {% if card.percent_change == 0 %}    #text(size: 7.5pt, fill: rgb("#333"))[· no change]    {% elif card.percent_change < 0 %}    #text(size: 7.5pt, fill: rgb("#333"))[▼ {{ card.percent_change }}% vs previous]    {% else %}    #text(size: 7.5pt, fill: rgb("#333"))[▲ +{{ card.percent_change }}% vs previous]    {% endif %}  ],  {% endfor %})== Events over time{% if chart_polyline %}// Two-row grid: row 1 is Y-axis labels (peak count top, 0 bottom) next to// the SVG line; row 2 is the X-axis date strip aligned under the SVG only.#grid(  columns: (auto, 1fr),  column-gutter: 4pt,  rows: (90pt, auto),  align: (right + horizon, left + horizon),  block(height: 90pt)[    #place(top + right)[#text(size: 6.5pt, fill: dim)[{{ chart_peak_count }}]]    #place(bottom + right)[#text(size: 6.5pt, fill: dim)[0]]  ],  image(    bytes("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 100' preserveAspectRatio='none'><line x1='0' y1='99.5' x2='600' y2='99.5' stroke='black' stroke-width='0.4'/><polyline fill='none' stroke='black' stroke-width='0.9' stroke-linejoin='round' stroke-linecap='round' points='{{ chart_polyline }}'/></svg>"),    format: "svg",    width: 100%,    height: 90pt,  ),  [],  grid(    columns: (1fr, 1fr, 1fr),    align: (left, center, right),    text(size: 7.5pt, fill: dim)[{{ chart_label_start | typst_md }}],    text(size: 7.5pt, fill: dim)[peak {{ chart_peak_count }} · {{ chart_peak_label | typst_md }}],    text(size: 7.5pt, fill: dim)[{{ chart_label_end | typst_md }}],  ),){% else %}#text(size: 8pt, fill: muted, style: "italic")[No events in this range.]{% endif %}// breakable: false keeps each column's table atomic across pages -- a column// either fits on the current page or moves to the next as a unit. Avoids the// orphan-row-with-re-printed-headers we got when Typst broke a table across// page boundaries inside a multi-column grid.#let label_count_table(items, label_header: "Label", count_header: "Count", label_mono: false) = {  block(breakable: false, table(    columns: (1fr, auto),    align: (left + top, right + top),    inset: (x: 3pt, y: 2pt),    stroke: (x, y) => if y == 0 { (bottom: 0.8pt + black) } else { (bottom: 0.3pt + rgb("#ddd")) },    table.header(      text(size: 6.5pt, tracking: 0.3pt, fill: dim, weight: "bold", upper(label_header)),      text(size: 6.5pt, tracking: 0.3pt, fill: dim, weight: "bold", upper(count_header)),    ),    ..items,  ))}#grid(  columns: (1fr, 1fr, 1fr),  column-gutter: 10pt,  row-gutter: 8pt,  {% if total_page_views_by_page_url %}  [    === Top pages · page views    #label_count_table(      label_header: "URL", count_header: "Views",      (        {% for item in total_page_views_by_page_url %}        text(font: mono, size: 7pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_events_by_page_url %}  [    === Top pages · all events    #label_count_table(      label_header: "URL", count_header: "Events",      (        {% for item in total_events_by_page_url %}        text(font: mono, size: 7pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_session_starts_by_referrer %}  [    === Top referrers    #label_count_table(      label_header: "Referrer", count_header: "Sessions",      (        {% for item in total_session_starts_by_referrer %}        text(font: mono, size: 7pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_events_by_custom_event %}  [    === Top custom events    #label_count_table(      label_header: "Event", count_header: "Count",      (        {% for item in total_events_by_custom_event %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %})== Visitor breakdown#let breakdown_table(items, label_header: "Type", count_header: "Events", total: 1) = {  block(breakable: false, table(    columns: (1fr, auto, auto),    align: (left + top, right + top, right + top),    inset: (x: 3pt, y: 2pt),    stroke: (x, y) => if y == 0 { (bottom: 0.8pt + black) } else { (bottom: 0.3pt + rgb("#ddd")) },    table.header(      text(size: 6.5pt, tracking: 0.3pt, fill: dim, weight: "bold", upper(label_header)),      text(size: 6.5pt, tracking: 0.3pt, fill: dim, weight: "bold", upper(count_header)),      text(size: 6.5pt, tracking: 0.3pt, fill: dim, weight: "bold", upper("%")),    ),    ..items,  ))}#grid(  columns: (1fr, 1fr, 1fr),  column-gutter: 10pt,  row-gutter: 8pt,  {% if total_events_by_device %}  [    === Device    #breakdown_table(      label_header: "Type", count_header: "Events",      (        {% for item in total_events_by_device %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}], text(size: 7.5pt, fill: dim)[{{ ((item.count * 100) / breakdown_totals.device) | int }}%],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_events_by_browser %}  [    === Browser    #breakdown_table(      label_header: "Name", count_header: "Events",      (        {% for item in total_events_by_browser %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}], text(size: 7.5pt, fill: dim)[{{ ((item.count * 100) / breakdown_totals.browser) | int }}%],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_events_by_platform %}  [    === Platform    #breakdown_table(      label_header: "Name", count_header: "Events",      (        {% for item in total_events_by_platform %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}], text(size: 7.5pt, fill: dim)[{{ ((item.count * 100) / breakdown_totals.platform) | int }}%],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_events_by_screen_size %}  [    === Screen size    #breakdown_table(      label_header: "Size", count_header: "Events",      (        {% for item in total_events_by_screen_size %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}], text(size: 7.5pt, fill: dim)[{{ ((item.count * 100) / breakdown_totals.screen_size) | int }}%],        {% endfor %}      ),    )  ],  {% endif %}){% if top_countries %}== Geography#grid(  columns: (1fr, 1fr),  column-gutter: 14pt,  [    === Top countries    #label_count_table(      label_header: "Country", count_header: "Sessions",      (        {% for item in top_countries %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  [],){% endif %}{% if total_page_views_by_utm_source or total_page_views_by_utm_medium or total_page_views_by_utm_campaign %}== UTM attribution#grid(  columns: (1fr, 1fr, 1fr),  column-gutter: 10pt,  {% if total_page_views_by_utm_source %}  [    === Source    #label_count_table(      label_header: "Source", count_header: "Views",      (        {% for item in total_page_views_by_utm_source %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_page_views_by_utm_medium %}  [    === Medium    #label_count_table(      label_header: "Medium", count_header: "Views",      (        {% for item in total_page_views_by_utm_medium %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %}  {% if total_page_views_by_utm_campaign %}  [    === Campaign    #label_count_table(      label_header: "Campaign", count_header: "Views",      (        {% for item in total_page_views_by_utm_campaign %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% endif %}){% endif %}== Bot traffic#v(-4pt)#text(size: 8pt, fill: dim)[Excluded from metrics above.]{% if bot_traffic.total > 0 %}#grid(  columns: (1fr, 1fr),  column-gutter: 14pt,  [    === Top bots    #label_count_table(      label_header: "Bot", count_header: "Events",      (        text(size: 7.5pt, weight: "bold")[Total], text(size: 7.5pt, weight: "bold")[{{ bot_traffic.total }}],        {% for item in bot_traffic.top_bots %}        text(size: 7.5pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% if bot_traffic.top_pages %}  [    === Top pages hit by bots    #label_count_table(      label_header: "URL", count_header: "Events",      (        {% for item in bot_traffic.top_pages %}        text(font: mono, size: 7pt)[{{ item.label | typst_md }}], text(size: 7.5pt)[{{ item.count }}],        {% endfor %}      ),    )  ],  {% else %}  [],  {% endif %}){% else %}#text(size: 8pt, fill: muted, style: "italic")[No bot events in this range.]{% endif %}