commit 6ebe505a07a43b70459a26f2e7d7f44859296230 Author: Dan Frumin Date: Mon May 25 17:05:15 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..da7f0f1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3187 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bzip2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 1.0.18", + "phf 0.11.3", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ebookm-cli" +version = "0.1.0" +dependencies = [ + "clap", + "ebookm-core", + "miette", + "serde_json", +] + +[[package]] +name = "ebookm-core" +version = "0.1.0" +dependencies = [ + "chrono", + "indexmap", + "kuchiki", + "miette", + "quick-xml", + "regex", + "reqwest", + "scraper", + "serde", + "serde_json", + "serde_yaml", + "sha1", + "tempfile", + "thiserror", + "url", + "uuid", + "zip", +] + +[[package]] +name = "ego-tree" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log", + "mac", + "markup5ever 0.10.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever 0.35.0", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa 1.0.18", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa 1.0.18", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[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 = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kuchiki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +dependencies = [ + "cssparser 0.27.2", + "html5ever 0.25.2", + "matches", + "selectors 0.22.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "liblzma" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6033b77c21d1f56deeae8014eb9fbe7bdf1765185a6c508b5ca82eeaed7f899" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a60851d15cd8c5346eca4ab8babff585be2ae4bc8097c067291d3ffe2add3b6" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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 = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.3", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f3a24d916e78954af99281a455168d4a9515d65eca99a18da1b813689c4ad9" +dependencies = [ + "cssparser 0.35.0", + "ego-tree", + "getopts", + "html5ever 0.35.0", + "precomputed-hash", + "selectors 0.31.0", + "tendril", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.27.2", + "derive_more 0.99.20", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.1.1", + "smallvec", + "thin-slice", +] + +[[package]] +name = "selectors" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5685b6ae43bfcf7d2e7dfcfb5d8e8f61b46442c902531e41a32a9a8bf0ee0fb6" +dependencies = [ + "bitflags 2.11.1", + "cssparser 0.35.0", + "derive_more 2.1.1", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.11.3", + "phf_codegen 0.11.3", + "precomputed-hash", + "servo_arc 0.4.3", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa 1.0.18", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.18", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa 1.0.18", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +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", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "liblzma", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[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 = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..53e25e0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = ["ebookm-core", "ebookm-cli"] +resolver = "2" + +[workspace.package] +edition = "2024" +license = "MIT" +version = "0.1.0" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cc732c --- /dev/null +++ b/README.md @@ -0,0 +1,482 @@ +# ebookm + +`ebookm` is a Rust command-line tool that compiles a set of Substack posts and local HTML files into a single EPUB. + +## Current Scope + +`v0.1` supports: + +- YAML manifests +- Public Substack post URLs +- Local HTML files +- Manifest-defined section order and TOC structure +- Per-entry metadata and TOC overrides +- Basic internal link rewriting between included entries +- EPUB generation with bundled article assets + +## Build + +```bash +cargo build +``` + +## Run + +Use the CLI through Cargo: + +```bash +cargo run -p ebookm-cli -- +``` + +Available commands: + +- `build -m ` +- `validate -m ` +- `inspect ` +- `init` + +## Quick Start + +This repository includes a runnable example manifest and local HTML fixture in `examples/`. + +Validate the example manifest: + +```bash +cargo run -p ebookm-cli -- validate -m examples/example-book.yaml +``` + +Build the example EPUB: + +```bash +cargo run -p ebookm-cli -- build -m examples/example-book.yaml +``` + +The output EPUB will be written to: + +```text +examples/dist/example-book.epub +``` + +Inspect a local HTML file: + +```bash +cargo run -p ebookm-cli -- inspect examples/articles/intro.html +``` + +Generate a starter manifest: + +```bash +cargo run -p ebookm-cli -- init +``` + +## Validate The Output EPUB + +There are two useful levels of validation. + +Quick XML/XHTML validation for generated chapter files: + +```bash +unzip -p path/to/book.epub OEBPS/text/chapter.xhtml | xmllint --noout - +``` + +If you want to check every generated XHTML file in the EPUB: + +```bash +mkdir -p /tmp/ebookm-check +unzip -o path/to/book.epub -d /tmp/ebookm-check +find /tmp/ebookm-check/OEBPS -name '*.xhtml' -print -exec xmllint --noout {} \; +``` + +Full EPUB validation: + +Use `epubcheck`, which validates the EPUB package itself, including metadata, navigation files, manifest/spine consistency, and XHTML correctness. + +```bash +epubcheck path/to/book.epub +``` + +Practical guidance: + +- Use `xmllint` when you want to quickly confirm that generated XHTML is well-formed XML. +- Use `epubcheck` when you want proper EPUB-level validation before distributing the file. +- If an EPUB reader only shows part of a chapter, malformed XHTML is a common cause, so `xmllint` on the generated chapter files is a good first check. + +## Manifest Reference + +The top-level manifest keys are: + +- `book`: EPUB metadata +- `output`: output path and optional cover image +- `defaults`: shared normalization and metadata defaults +- `sections`: ordered TOC and reading-order groups +- `entries`: source definitions and per-entry overrides +- `link_rules`: cross-link rewriting behavior + +Minimal example: + +```yaml +book: + title: "My Book" + author: "Editor" + language: "en" + identifier: "urn:uuid:my-book" + +output: + path: "dist/my-book.epub" + +sections: + - id: "part-1" + title: "Part 1" + entries: + - "essay" + +entries: + essay: + source: + kind: "html" + path: "articles/essay.html" + +link_rules: + mode: "auto" +``` + +### Top-Level Fields + +- `book`: + EPUB metadata block. Required. +- `output`: + Output configuration block. Required. +- `defaults`: + Shared defaults applied across entries. Optional. +- `sections`: + Ordered list of sections. Required in practice for a useful build. +- `entries`: + Map of entry IDs to entry definitions. Required in practice. +- `link_rules`: + Global link rewriting policy. Optional. + +### `book` + +- `book.title`: + Required string. Book title. +- `book.author`: + Optional string. Book-level author. +- `book.language`: + Optional string. Defaults to `en`. +- `book.identifier`: + Optional string. Defaults to a generated `urn:uuid:...`. +- `book.description`: + Optional string. Written into the EPUB package metadata. + +Example: + +```yaml +book: + title: "Collected Essays" + author: "Jane Doe" + language: "en" + identifier: "urn:uuid:collected-essays" + description: "A single-volume EPUB generated by ebookm" +``` + +### `output` + +- `output.path`: + Required string. Output EPUB path. Resolved relative to the manifest file. +- `output.cover_image`: + Optional string. Path to a cover image, resolved relative to the manifest file. + +Example: + +```yaml +output: + path: "dist/book.epub" + cover_image: "assets/cover.jpg" +``` + +### `defaults` + +- `defaults.fetch_images`: + Optional boolean. Defaults to `true`. When enabled, image assets referenced from article HTML are fetched and bundled into the EPUB. +- `defaults.normalize_substack_embeds`: + Optional boolean. Defaults to `true`. Currently removes iframe embeds during normalization. +- `defaults.metadata`: + Optional metadata override block applied after extracted source metadata and before per-entry overrides. +- `defaults.processing`: + Optional shared article-processing and chapter-header defaults. + +Example: + +```yaml +defaults: + fetch_images: true + normalize_substack_embeds: true + processing: + include_author: true + include_date: true + include_source_url: true + skip_first_paragraphs: 0 + metadata: + author: "Editorial Team" +``` + +### `defaults.processing` and `entries..processing` + +These fields control chapter-header rendering and article trimming. + +For `defaults.processing`, all fields are concrete values with defaults. +For `entries..processing`, the same fields are optional overrides. + +- `include_author`: + Boolean. Defaults to `true`. Controls whether the extracted or overridden author name is shown at the start of the chapter. +- `include_date`: + Boolean. Defaults to `true`. Controls whether the extracted or overridden publication date is shown at the start of the chapter. +- `include_source_url`: + Boolean. Defaults to `true`. Controls whether the canonical article URL is shown at the start of the chapter. +- `skip_first_paragraphs`: + Integer. Defaults to `0`. Removes the first `n` paragraph elements from the extracted article body before EPUB generation. + +Example: + +```yaml +defaults: + processing: + include_author: true + include_date: false + include_source_url: false + skip_first_paragraphs: 0 +``` + +### `defaults.metadata` and `entries..metadata` + +These fields use the same shape: + +- `author`: + Optional string. +- `published`: + Optional date in `YYYY-MM-DD` format. +- `subtitle`: + Optional string. Accepted by the parser but not yet emitted into the EPUB output. +- `summary`: + Optional string. Accepted by the parser but not yet emitted into the EPUB output. +- `tags`: + Optional list of strings. Accepted by the parser but not yet used in link or EPUB output logic. + +Example: + +```yaml +metadata: + author: "Jane Doe" + published: "2025-01-10" + subtitle: "Notebook entry" + summary: "A short summary" + tags: ["essay", "history"] +``` + +### `sections` + +`sections` is an ordered list. Section order controls reading order and TOC grouping. + +Each section supports: + +- `id`: + Required string. Stable section identifier. +- `title`: + Required string. Section title shown in the TOC. +- `entries`: + Optional list of entry IDs in reading order. Usually should not be empty. + +Example: + +```yaml +sections: + - id: "part-1" + title: "Part 1" + entries: + - "opening-post" + - "notes" +``` + +### `entries` + +`entries` is a map from entry ID to entry definition. Entry IDs are referenced from `sections` and link rules. + +Each entry supports: + +- `source`: + Required source definition block. +- `title`: + Optional string. Overrides the extracted article title. +- `metadata`: + Optional metadata override block. +- `toc`: + Optional TOC override block. +- `links`: + Optional per-entry link-policy block. +- `processing`: + Optional per-entry processing override block. + +Example: + +```yaml +entries: + opening-post: + source: + kind: "substack" + url: "https://example.substack.com/p/opening-post" + title: "Opening Post" + metadata: + published: "2025-01-10" + processing: + include_source_url: false + skip_first_paragraphs: 1 + toc: + title: "Introduction" + links: + mode: "explicit" + allow_to: ["notes"] +``` + +### `entries..source` + +Two source kinds are supported: + +- `kind: "substack"`: + Use a public Substack article URL. + Fields: + `url` required string. +- `kind: "html"`: + Use a local HTML file. + Fields: + `path` required string, resolved relative to the manifest file. + +Examples: + +```yaml +source: + kind: "substack" + url: "https://example.substack.com/p/my-post" +``` + +```yaml +source: + kind: "html" + path: "articles/local-post.html" +``` + +You can mix Substack URLs and local HTML files in the same manifest: + +```yaml +entries: + remote-post: + source: + kind: "substack" + url: "https://example.substack.com/p/remote-post" + + local-post: + source: + kind: "html" + path: "articles/local-post.html" +``` + +### `entries..toc` + +- `title`: + Optional string. Overrides the chapter label used in the TOC. +- `hidden`: + Optional boolean. Defaults to `false`. When `true`, the entry is omitted from the TOC. + +Example: + +```yaml +toc: + title: "Appendix" + hidden: false +``` + +### `entries..links` + +- `mode`: + Optional string. One of: + `auto`, `explicit`, `none`. + If omitted, the global `link_rules.mode` is used. +- `allow_to`: + Optional list of entry IDs. If set, rewritten internal links are limited to these targets. +- `block_to`: + Optional list of entry IDs. These targets are excluded from rewriting. + +Example: + +```yaml +links: + mode: "explicit" + allow_to: ["intro", "appendix"] + block_to: ["draft-notes"] +``` + +### `link_rules` + +- `link_rules.mode`: + Optional string. One of: + `auto`, `explicit`, `none`. + Defaults to `auto`. +- `link_rules.rewrite_external_substack_links`: + Optional boolean. Defaults to `true`. Accepted by the manifest parser, but not currently used to change behavior in `v0.1`. +- `link_rules.preserve_other_external_links`: + Optional boolean. Defaults to `true`. Accepted by the manifest parser, but not currently used to change behavior in `v0.1`. +- `link_rules.rules`: + Optional list of explicit link rules. + +Example: + +```yaml +link_rules: + mode: "explicit" + rewrite_external_substack_links: true + preserve_other_external_links: true + rules: + - from: ["notes"] + to: ["intro"] + match_mode: "canonical-url" +``` + +### `link_rules.rules[]` + +Each rule supports: + +- `from`: + Required list of selectors describing where the rule applies. +- `to`: + Required list of selectors describing eligible targets. +- `match_mode`: + Optional string. One of: + `canonical-url`, `source-url`, `disabled`. + Defaults to `canonical-url`. + +Supported selectors in `from` and `to`: + +- `*`: + Match all entries. +- ``: + Match one entry by ID. +- `section:`: + Match all entries referenced by that section. + +Example: + +```yaml +link_rules: + mode: "explicit" + rules: + - from: ["section:essays"] + to: ["section:essays"] + match_mode: "canonical-url" +``` + +## Notes + +- Output paths are resolved relative to the manifest file location. +- Local HTML paths are also resolved relative to the manifest file location. +- `sections` and `entries` are deserialized with empty defaults, but `validate` and `build` expect them to be meaningfully populated. +- `subtitle`, `summary`, `tags`, `rewrite_external_substack_links`, and `preserve_other_external_links` are accepted today but only partially wired into runtime behavior. +- For Substack sources, `v0.1` assumes public posts. Subscriber-only/session-based fetching is not implemented. diff --git a/ageofpeace/age_of_peace_cover.jpg b/ageofpeace/age_of_peace_cover.jpg new file mode 100644 index 0000000..0165d0c Binary files /dev/null and b/ageofpeace/age_of_peace_cover.jpg differ diff --git a/ageofpeace/ageofpeace.yaml b/ageofpeace/ageofpeace.yaml new file mode 100644 index 0000000..e00d0b9 --- /dev/null +++ b/ageofpeace/ageofpeace.yaml @@ -0,0 +1,76 @@ +book: + title: "Age of Peace" + author: "John Gu" + language: "en" + identifier: "urn:uuid:ageofpeace:johngu" + description: "Age of Peace: a novel" + +output: + path: "AgeOfPeace.epub" + cover_image: "age_of_peace_cover.jpg" + +defaults: + metadata: + author: "John Gu" + fetch_images: true + normalize_substack_embeds: true + processing: + include_author: false + include_date: false + include_source_url: false + +sections: + - id: "part-0" + title: "Prelude" + entries: + - "intro" + - id: "part-1" + title: "Overture" + entries: + - "contested_island" + - id: "part-2" + title: "The High Castle" + entries: + - "vira" + - "madelyna" + - "biridana" + - id: "part-3" + title: "Nameless Country" + entries: [] + + +entries: + intro: + source: + kind: "html" + path: "ageofpeace/introduction.html" + contested_island: + source: + kind: "substack" + url: "https://ageofpeace.substack.com/p/a-contested-island" + toc: + title: "A Contested Island" + processing: + skip_first_paragraphs: 1 + vira: + source: + kind: "substack" + url: "https://ageofpeace.substack.com/p/vira" + toc: + title: "Vira" + madelyna: + source: + kind: "substack" + url: "https://ageofpeace.substack.com/p/madelyna" + toc: + title: "Madelỳna" + biridana: + source: + kind: "substack" + url: "https://ageofpeace.substack.com/p/biridana" + toc: + title: "Biridana" + + +link_rules: + mode: "auto" diff --git a/ageofpeace/introduction.html b/ageofpeace/introduction.html new file mode 100644 index 0000000..538e5b9 --- /dev/null +++ b/ageofpeace/introduction.html @@ -0,0 +1,33 @@ + + + + Introduction + + + + + +
+

After securing a teaching job at a foreign university on “the thinnest set of credentials,” a young man sets off for life in Varrenia, an impoverished eastern kingdom still emerging from the shadow of a decades-long dictatorship. Years later, living in the decadent capital of Garamdal, our protagonist watches a war unfold in the republic’s restive eastern provinces and reflects on what he has gained — and lost — in a life of travel.

+

About the author

+

+In my late twenties, I did a short stint as a grad student in mathematical logic at the University of Amsterdam, where I learned that I am not smart enough to be a mathematician. After dropping out of grad school, I ended up staying in Europe for five years. This novel, greatly influenced by that experience, is my love letter to Europe and to that time. +

+ John Gu +

I am also very proud to be able say that I grew up in Houston — I actually come from the same neighborhood as Lizzo, Mo Amer, and Tila Tequila, cultural luminaries all.

+

Previous publications

+

TBD

+ +

Influences

+

Some readers of my work have remarked that it shares an affinity with the following writers and books. In some cases, these figures represent inspirations that I have leaned into, in others, the similarities (of theme, style, subject matter) are more coincidental:

+
    +
  • In Patagonia, Bruce Chatwin
  • +
  • Waiting for the Barbarians, J.M. Coetzee
  • +
  • Balkan Ghosts, Robert Kaplan
  • +
  • A Bend in the River, V.S. Naipaul
  • +
  • Paul Bowles
  • +
  • Milan Kundera
  • +
+
+ + diff --git a/ageofpeace/johngu.jpg b/ageofpeace/johngu.jpg new file mode 100644 index 0000000..0782c12 Binary files /dev/null and b/ageofpeace/johngu.jpg differ diff --git a/ebookm-cli/Cargo.toml b/ebookm-cli/Cargo.toml new file mode 100644 index 0000000..3c5c18c --- /dev/null +++ b/ebookm-cli/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ebookm-cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +ebookm-core = { path = "../ebookm-core" } +miette = { version = "7.2", features = ["fancy"] } +serde_json = "1.0" diff --git a/ebookm-cli/src/main.rs b/ebookm-cli/src/main.rs new file mode 100644 index 0000000..ec9dfca --- /dev/null +++ b/ebookm-cli/src/main.rs @@ -0,0 +1,89 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; +use ebookm_core::{ + build_epub, inspect_source, load_manifest, render_init_manifest, validate_manifest, +}; +use miette::{Context, IntoDiagnostic}; + +#[derive(Debug, Parser)] +#[command( + name = "ebookm", + version, + about = "Compile Substack articles into a single EPUB" +)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + Build { + #[arg(short, long)] + manifest: PathBuf, + #[arg(short, long)] + output: Option, + }, + Validate { + #[arg(short, long)] + manifest: PathBuf, + }, + Inspect { + source: String, + #[arg(long, default_value = "json")] + format: String, + }, + Init, +} + +fn main() -> miette::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Build { manifest, output } => { + let mut loaded = load_manifest(&manifest).into_diagnostic()?; + if let Some(output) = output { + loaded.output.path = output.display().to_string(); + } + let warnings = validate_manifest(&loaded).into_diagnostic()?; + for warning in warnings { + eprintln!("warning: {warning}"); + } + build_epub(&loaded, &manifest).into_diagnostic()?; + println!("{}", loaded.output.path); + } + Commands::Validate { manifest } => { + let loaded = load_manifest(&manifest).into_diagnostic()?; + let warnings = validate_manifest(&loaded).into_diagnostic()?; + for warning in warnings { + println!("warning: {warning}"); + } + println!("manifest is valid"); + } + Commands::Inspect { source, format } => { + let result = inspect_source(&source).into_diagnostic()?; + if format == "json" { + println!( + "{}", + serde_json::to_string_pretty(&result) + .into_diagnostic() + .wrap_err("failed to encode JSON")? + ); + } else { + println!("title: {}", result.title.unwrap_or_default()); + println!("author: {}", result.author.unwrap_or_default()); + println!("published: {}", result.published.unwrap_or_default()); + println!( + "canonical_url: {}", + result.canonical_url.unwrap_or_default() + ); + } + } + Commands::Init => { + print!("{}", render_init_manifest()); + } + } + + Ok(()) +} diff --git a/ebookm-core/Cargo.toml b/ebookm-core/Cargo.toml new file mode 100644 index 0000000..b1c46f6 --- /dev/null +++ b/ebookm-core/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ebookm-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +indexmap = { version = "2.7", features = ["serde"] } +kuchiki = "0.8" +miette = { version = "7.2", features = ["fancy"] } +quick-xml = "0.38" +regex = "1.11" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } +scraper = "0.24" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" +sha1 = "0.10" +thiserror = "2.0" +url = { version = "2.5", features = ["serde"] } +uuid = { version = "1.18", features = ["v4"] } +zip = "4.6" + +[dev-dependencies] +tempfile = "3.15" diff --git a/ebookm-core/src/epub.rs b/ebookm-core/src/epub.rs new file mode 100644 index 0000000..b419387 --- /dev/null +++ b/ebookm-core/src/epub.rs @@ -0,0 +1,298 @@ +use std::collections::BTreeSet; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use quick_xml::escape::escape; +use zip::CompressionMethod; +use zip::write::{SimpleFileOptions, ZipWriter}; + +use crate::error::{EbookmError, Result}; +use crate::pipeline::BuiltEntry; + +pub fn write_epub( + manifest: &crate::manifest::Manifest, + built: &[BuiltEntry], + output_path: &Path, + cover_bytes: Option<(String, Vec)>, +) -> Result<()> { + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent).map_err(|source| EbookmError::Io { + path: parent.display().to_string(), + source, + })?; + } + + let file = File::create(output_path).map_err(|source| EbookmError::Io { + path: output_path.display().to_string(), + source, + })?; + let mut zip = ZipWriter::new(file); + + let stored = SimpleFileOptions::default().compression_method(CompressionMethod::Stored); + zip.start_file("mimetype", stored) + .map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + zip.write_all(b"application/epub+zip") + .map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + + let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); + write_file(&mut zip, "META-INF/container.xml", deflated, CONTAINER_XML)?; + write_file(&mut zip, "OEBPS/styles/book.css", deflated, DEFAULT_STYLES)?; + + let nav = build_nav(manifest, built); + let ncx = build_ncx(manifest, built); + let opf = build_opf( + manifest, + built, + cover_bytes.as_ref().map(|(href, _)| href.as_str()), + ); + + write_file(&mut zip, "OEBPS/nav.xhtml", deflated, &nav)?; + write_file(&mut zip, "OEBPS/toc.ncx", deflated, &ncx)?; + write_file(&mut zip, "OEBPS/content.opf", deflated, &opf)?; + + if let Some((href, bytes)) = cover_bytes { + write_bytes(&mut zip, &format!("OEBPS/{href}"), deflated, &bytes)?; + } + + let mut seen_assets = BTreeSet::new(); + for entry in built { + write_file( + &mut zip, + &format!("OEBPS/text/{}.xhtml", entry.id), + deflated, + &entry.chapter.xhtml, + )?; + for asset in &entry.assets { + if seen_assets.insert(asset.href.clone()) { + write_bytes( + &mut zip, + &format!("OEBPS/{}", asset.href), + deflated, + &asset.bytes, + )?; + } + } + } + + zip.finish().map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + Ok(()) +} + +fn write_file( + zip: &mut ZipWriter, + path: &str, + options: SimpleFileOptions, + contents: &str, +) -> Result<()> { + write_bytes(zip, path, options, contents.as_bytes()) +} + +fn write_bytes( + zip: &mut ZipWriter, + path: &str, + options: SimpleFileOptions, + contents: &[u8], +) -> Result<()> { + zip.start_file(path, options) + .map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + zip.write_all(contents).map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + Ok(()) +} + +fn build_nav(manifest: &crate::manifest::Manifest, built: &[BuiltEntry]) -> String { + let mut nav_points = String::new(); + for section in &manifest.sections { + let section_target = section + .entries + .iter() + .find_map(|entry_id| built.iter().find(|candidate| &candidate.id == entry_id)) + .map(|entry| format!("text/{}.xhtml", entry.id)); + nav_points.push_str("
  • "); + if let Some(target) = section_target { + nav_points.push_str(&format!( + "{}
      ", + escape(&target), + escape(§ion.title) + )); + } else { + nav_points.push_str(&format!("{}
        ", escape(§ion.title))); + } + for entry_id in §ion.entries { + if let Some(entry) = built.iter().find(|candidate| &candidate.id == entry_id) { + if entry.hidden_from_toc { + continue; + } + nav_points.push_str(&format!( + "
      1. {}
      2. ", + escape(&entry.id), + escape(&entry.chapter.nav_title) + )); + } + } + nav_points.push_str("
      "); + } + + format!( + r#" + + + {} + + + + + +"#, + escape(&manifest.book.title), + escape(&manifest.book.title), + nav_points + ) +} + +fn build_ncx(manifest: &crate::manifest::Manifest, built: &[BuiltEntry]) -> String { + let mut play_order = 1usize; + let mut nav_points = String::new(); + for section in &manifest.sections { + let section_entries: Vec<_> = section + .entries + .iter() + .filter_map(|entry_id| built.iter().find(|candidate| &candidate.id == entry_id)) + .filter(|entry| !entry.hidden_from_toc) + .collect(); + + if section_entries.is_empty() { + continue; + } + + let section_play_order = play_order; + play_order += 1; + + let mut child_points = String::new(); + for entry in §ion_entries { + child_points.push_str(&format!( + "{}", + escape(&entry.id), + play_order, + escape(&entry.chapter.nav_title), + escape(&entry.id) + )); + play_order += 1; + } + + nav_points.push_str(&format!( + "{}{}", + escape(§ion.id), + section_play_order, + escape(§ion.title), + escape(§ion_entries[0].id), + child_points + )); + } + + format!( + r#" + + + + + {} + {} +"#, + escape(&manifest.book.identifier), + escape(&manifest.book.title), + nav_points + ) +} + +fn build_opf( + manifest: &crate::manifest::Manifest, + built: &[BuiltEntry], + cover_href: Option<&str>, +) -> String { + let mut manifest_items = String::from( + r#" + +"#, + ); + let mut spine_items = String::new(); + + for entry in built { + manifest_items.push_str(&format!( + "", + escape(&entry.id), + escape(&entry.id) + )); + spine_items.push_str(&format!("", escape(&entry.id))); + for asset in &entry.assets { + manifest_items.push_str(&format!( + "", + escape(&asset.id), + escape(&asset.href), + escape(&asset.media_type) + )); + } + } + + if let Some(cover_href) = cover_href { + manifest_items.push_str(&format!( + "", + escape(cover_href) + )); + } + + let author = manifest + .book + .author + .clone() + .unwrap_or_else(|| "Unknown".to_string()); + let description = manifest.book.description.clone().unwrap_or_default(); + format!( + r#" + + + {} + {} + {} + {} + {} + + {} + {} +"#, + escape(&manifest.book.identifier), + escape(&manifest.book.title), + escape(&author), + escape(&manifest.book.language), + escape(&description), + manifest_items, + spine_items + ) +} + +const CONTAINER_XML: &str = r#" + + + + +"#; + +const DEFAULT_STYLES: &str = r#"body { font-family: serif; line-height: 1.5; margin: 5%; } +h1 { margin-bottom: 0.2em; } +.chapter-meta { color: #555; font-size: 0.9em; margin-bottom: 1.5em; } +img { max-width: 100%; height: auto; } +a { color: #0b4f7a; text-decoration: none; } +"#; diff --git a/ebookm-core/src/error.rs b/ebookm-core/src/error.rs new file mode 100644 index 0000000..a35bb7d --- /dev/null +++ b/ebookm-core/src/error.rs @@ -0,0 +1,39 @@ +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum EbookmError { + #[error("failed to read file {path}: {source}")] + Io { + path: String, + #[source] + source: std::io::Error, + }, + #[error("failed to parse manifest {path}: {source}")] + ManifestParse { + path: String, + #[source] + source: serde_yaml::Error, + }, + #[error("manifest validation failed: {issues:?}")] + Validation { issues: Vec }, + #[error("network request failed for {url}: {source}")] + Request { + url: String, + #[source] + source: reqwest::Error, + }, + #[error("invalid source path: {path}")] + InvalidSourcePath { path: String }, + #[error("failed to parse URL {value}: {source}")] + UrlParse { + value: String, + #[source] + source: url::ParseError, + }, + #[error("article extraction failed for {input}")] + Extraction { input: String }, + #[error("EPUB generation failed: {message}")] + Epub { message: String }, +} diff --git a/ebookm-core/src/extract.rs b/ebookm-core/src/extract.rs new file mode 100644 index 0000000..d2a4ac9 --- /dev/null +++ b/ebookm-core/src/extract.rs @@ -0,0 +1,268 @@ +use chrono::{DateTime, NaiveDate}; +use scraper::{Html, Selector}; +use serde_json::Value; +use url::Url; + +use crate::error::{EbookmError, Result}; +use crate::source::{LoadedSource, SourceOrigin}; + +#[derive(Debug, Clone)] +pub struct ExtractedArticle { + pub title: String, + pub author: Option, + pub published: Option, + pub canonical_url: Option, + pub body_html: String, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct InspectResult { + pub title: Option, + pub author: Option, + pub published: Option, + pub canonical_url: Option, +} + +pub fn extract_article(loaded: &LoadedSource) -> Result { + let document = Html::parse_document(&loaded.html); + let json_ld = extract_primary_json_ld(&document); + let title = select_content( + &document, + &[ + r#"meta[property="og:title"]"#, + r#"article .post-title"#, + ".post-title", + "h1", + "title", + ], + "content", + ) + .or_else(|| { + select_text( + &document, + &[r#"article .post-title"#, ".post-title", "h1", "title"], + ) + }) + .or_else(|| json_ld_string(&json_ld, "headline")) + .ok_or_else(|| EbookmError::Extraction { + input: origin_label(&loaded.origin), + })?; + + let author = select_content( + &document, + &[ + r#"meta[name="author"]"#, + r#"meta[property="article:author"]"#, + ], + "content", + ) + .or_else(|| { + select_text( + &document, + &[ + "[data-testid='author-name']", + ".byline", + ".byline-wrapper a", + "address", + ], + ) + }) + .or_else(|| json_ld_author(&json_ld)); + + let published = select_content( + &document, + &[r#"meta[property="article:published_time"]"#, "time"], + "content", + ) + .or_else(|| select_attr(&document, &["time"], "datetime")) + .or_else(|| json_ld_string(&json_ld, "datePublished")) + .and_then(parse_date); + + let canonical_url = select_attr(&document, &[r#"link[rel="canonical"]"#], "href") + .or_else(|| match &loaded.origin { + SourceOrigin::Remote(url) => Some(url.to_string()), + SourceOrigin::LocalFile(_) => None, + }) + .and_then(|raw| Url::parse(&raw).ok()); + + let body_html = select_html( + &document, + &[ + ".available-content .body.markup", + ".available-content .markup", + "article .body.markup", + ".newsletter-post .body.markup", + "article", + "main", + "body", + ], + ) + .ok_or_else(|| EbookmError::Extraction { + input: origin_label(&loaded.origin), + })?; + + Ok(ExtractedArticle { + title, + author, + published, + canonical_url, + body_html, + }) +} + +pub fn inspect_article(loaded: &LoadedSource) -> Result { + let extracted = extract_article(loaded)?; + Ok(InspectResult { + title: Some(extracted.title), + author: extracted.author, + published: extracted.published.map(|date| date.to_string()), + canonical_url: extracted.canonical_url.map(|url| url.to_string()), + }) +} + +fn select_content(document: &Html, selectors: &[&str], attr: &str) -> Option { + selectors.iter().find_map(|selector| { + let selector = Selector::parse(selector).ok()?; + document + .select(&selector) + .next() + .and_then(|node| node.value().attr(attr)) + .map(clean_text) + }) +} + +fn select_text(document: &Html, selectors: &[&str]) -> Option { + selectors.iter().find_map(|selector| { + let selector = Selector::parse(selector).ok()?; + document + .select(&selector) + .next() + .map(|node| clean_text(&node.text().collect::())) + }) +} + +fn select_attr(document: &Html, selectors: &[&str], attr: &str) -> Option { + selectors.iter().find_map(|selector| { + let selector = Selector::parse(selector).ok()?; + document + .select(&selector) + .next() + .and_then(|node| node.value().attr(attr)) + .map(clean_text) + }) +} + +fn select_html(document: &Html, selectors: &[&str]) -> Option { + selectors.iter().find_map(|selector| { + let selector = Selector::parse(selector).ok()?; + document + .select(&selector) + .next() + .map(|node| node.inner_html()) + }) +} + +fn clean_text(value: &str) -> String { + value.split_whitespace().collect::>().join(" ") +} + +fn parse_date(value: String) -> Option { + DateTime::parse_from_rfc3339(&value) + .map(|parsed| parsed.date_naive()) + .ok() + .or_else(|| NaiveDate::parse_from_str(&value, "%Y-%m-%d").ok()) + .or_else(|| NaiveDate::parse_from_str(&value, "%b %d, %Y").ok()) +} + +fn origin_label(origin: &SourceOrigin) -> String { + match origin { + SourceOrigin::Remote(url) => url.to_string(), + SourceOrigin::LocalFile(path) => path.display().to_string(), + } +} + +fn extract_primary_json_ld(document: &Html) -> Option { + let selector = Selector::parse(r#"script[type="application/ld+json"]"#).ok()?; + for node in document.select(&selector) { + let raw = node.inner_html(); + let Ok(value) = serde_json::from_str::(&raw) else { + continue; + }; + if value.get("@type").and_then(Value::as_str).is_some() { + return Some(value); + } + } + None +} + +fn json_ld_string(json_ld: &Option, key: &str) -> Option { + json_ld + .as_ref()? + .get(key)? + .as_str() + .map(|value| value.to_string()) +} + +fn json_ld_author(json_ld: &Option) -> Option { + let author = json_ld.as_ref()?.get("author")?; + if let Some(author_name) = author.get(0).and_then(|entry| entry.get("name")).and_then(Value::as_str) { + return Some(author_name.to_string()); + } + if let Some(author_name) = author.get("name").and_then(Value::as_str) { + return Some(author_name.to_string()); + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::source::LoadedSource; + + #[test] + fn extracts_substack_article_body_without_page_chrome() { + let html = r#" + + + + + + + + + + +"#; + + let loaded = LoadedSource { + origin: SourceOrigin::Remote( + Url::parse("https://ageofpeace.substack.com/p/a-contested-island").expect("url"), + ), + html: html.to_string(), + }; + + let extracted = extract_article(&loaded).expect("extract article"); + assert_eq!(extracted.title, "A Contested Island"); + assert_eq!(extracted.author.as_deref(), Some("John Gu")); + assert_eq!( + extracted.published, + Some(NaiveDate::from_ymd_opt(2026, 3, 3).expect("date")) + ); + assert!(extracted.body_html.contains("First paragraph.")); + assert!(!extracted.body_html.contains("post-header")); + assert!(!extracted.body_html.contains("Share")); + } +} diff --git a/ebookm-core/src/graph.rs b/ebookm-core/src/graph.rs new file mode 100644 index 0000000..6ab847e --- /dev/null +++ b/ebookm-core/src/graph.rs @@ -0,0 +1,167 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use url::Url; + +use crate::manifest::{BuildMode, LinkMatchMode, Manifest}; + +#[derive(Debug, Clone)] +pub struct LinkPolicy { + pub match_mode: LinkMatchMode, + pub targets: BTreeSet, +} + +pub fn build_link_policies( + manifest: &Manifest, + entry_metadata: &BTreeMap, +) -> BTreeMap { + entry_metadata + .iter() + .map(|(entry_id, _metadata)| { + let entry = &manifest.entries[entry_id]; + let mode = entry + .links + .mode + .clone() + .unwrap_or(manifest.link_rules.mode.clone()); + let targets = resolve_targets(manifest, entry_id, &mode); + let match_mode = select_match_mode(manifest, entry_id, &mode); + ( + entry_id.clone(), + LinkPolicy { + match_mode, + targets, + }, + ) + }) + .collect() +} + +#[derive(Debug, Clone)] +pub struct EntryLinkMetadata { + pub source_url: Option, + pub canonical_url: Option, +} + +fn resolve_targets(manifest: &Manifest, entry_id: &str, mode: &BuildMode) -> BTreeSet { + let entry = &manifest.entries[entry_id]; + let mut targets = BTreeSet::new(); + + match mode { + BuildMode::None => return targets, + BuildMode::Auto => { + for candidate in manifest.entries.keys() { + if candidate != entry_id { + targets.insert(candidate.clone()); + } + } + } + BuildMode::Explicit => { + for rule in &manifest.link_rules.rules { + if rule.match_mode == LinkMatchMode::Disabled { + continue; + } + if selector_matches_any(&rule.from, manifest, entry_id) { + for target in expand_selectors(&rule.to, manifest) { + if target != entry_id { + targets.insert(target); + } + } + } + } + } + } + + if !entry.links.allow_to.is_empty() { + targets.retain(|candidate| entry.links.allow_to.contains(candidate)); + } + + for blocked in &entry.links.block_to { + targets.remove(blocked); + } + + targets +} + +fn select_match_mode(manifest: &Manifest, entry_id: &str, mode: &BuildMode) -> LinkMatchMode { + match mode { + BuildMode::None => LinkMatchMode::Disabled, + BuildMode::Auto => LinkMatchMode::CanonicalUrl, + BuildMode::Explicit => manifest + .link_rules + .rules + .iter() + .find(|rule| selector_matches_any(&rule.from, manifest, entry_id)) + .map(|rule| rule.match_mode.clone()) + .unwrap_or(LinkMatchMode::CanonicalUrl), + } +} + +fn selector_matches_any(selectors: &[String], manifest: &Manifest, entry_id: &str) -> bool { + selectors + .iter() + .any(|selector| selector_matches(selector, manifest, entry_id)) +} + +fn selector_matches(selector: &str, manifest: &Manifest, entry_id: &str) -> bool { + if selector == "*" { + return true; + } + if selector == entry_id { + return true; + } + if let Some(section_id) = selector.strip_prefix("section:") { + return manifest + .sections + .iter() + .find(|section| section.id == section_id) + .is_some_and(|section| section.entries.iter().any(|entry| entry == entry_id)); + } + false +} + +fn expand_selectors(selectors: &[String], manifest: &Manifest) -> BTreeSet { + let mut expanded = BTreeSet::new(); + for selector in selectors { + if selector == "*" { + expanded.extend(manifest.entries.keys().cloned()); + continue; + } + if let Some(section_id) = selector.strip_prefix("section:") { + if let Some(section) = manifest + .sections + .iter() + .find(|section| section.id == section_id) + { + expanded.extend(section.entries.iter().cloned()); + } + continue; + } + if manifest.entries.contains_key(selector) { + expanded.insert(selector.clone()); + } + } + expanded +} + +pub fn matches_target( + href: &Url, + policy: &LinkPolicy, + target_id: &str, + metadata: &EntryLinkMetadata, +) -> bool { + if !policy.targets.contains(target_id) { + return false; + } + + match policy.match_mode { + LinkMatchMode::Disabled => false, + LinkMatchMode::CanonicalUrl => metadata + .canonical_url + .as_ref() + .is_some_and(|candidate| candidate.as_str() == href.as_str()), + LinkMatchMode::SourceUrl => metadata + .source_url + .as_ref() + .is_some_and(|candidate| candidate.as_str() == href.as_str()), + } +} diff --git a/ebookm-core/src/lib.rs b/ebookm-core/src/lib.rs new file mode 100644 index 0000000..8199501 --- /dev/null +++ b/ebookm-core/src/lib.rs @@ -0,0 +1,19 @@ +mod epub; +mod error; +pub mod extract; +pub mod graph; +pub mod manifest; +pub mod normalize; +mod pipeline; +pub mod source; +mod template; + +pub use error::{EbookmError, Result}; +pub use extract::InspectResult; +pub use manifest::{ + BuildMode, EntryDefinition, EntryLinkConfig, LinkMatchMode, LinkRule, Manifest, + ProcessingDefaults, ProcessingOverrides, +}; +pub use pipeline::{ + build_epub, inspect_source, load_manifest, render_init_manifest, validate_manifest, +}; diff --git a/ebookm-core/src/manifest.rs b/ebookm-core/src/manifest.rs new file mode 100644 index 0000000..f1025fd --- /dev/null +++ b/ebookm-core/src/manifest.rs @@ -0,0 +1,207 @@ +use chrono::NaiveDate; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Manifest { + pub book: BookMetadata, + pub output: OutputConfig, + #[serde(default)] + pub defaults: DefaultsConfig, + #[serde(default)] + pub sections: Vec, + #[serde(default)] + pub entries: IndexMap, + #[serde(default)] + pub link_rules: LinkRulesConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BookMetadata { + pub title: String, + #[serde(default)] + pub author: Option, + #[serde(default = "default_language")] + pub language: String, + #[serde(default = "default_identifier")] + pub identifier: String, + #[serde(default)] + pub description: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutputConfig { + pub path: String, + #[serde(default)] + pub cover_image: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DefaultsConfig { + #[serde(default = "default_true")] + pub fetch_images: bool, + #[serde(default = "default_true")] + pub normalize_substack_embeds: bool, + #[serde(default)] + pub processing: ProcessingDefaults, + #[serde(default)] + pub metadata: MetadataOverrides, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SectionDefinition { + pub id: String, + pub title: String, + #[serde(default)] + pub entries: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntryDefinition { + pub source: SourceDefinition, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub metadata: MetadataOverrides, + #[serde(default)] + pub processing: ProcessingOverrides, + #[serde(default)] + pub toc: TocConfig, + #[serde(default)] + pub links: EntryLinkConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum SourceDefinition { + Substack { url: String }, + Html { path: String }, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct MetadataOverrides { + #[serde(default)] + pub author: Option, + #[serde(default)] + pub published: Option, + #[serde(default)] + pub subtitle: Option, + #[serde(default)] + pub summary: Option, + #[serde(default)] + pub tags: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessingDefaults { + #[serde(default = "default_true")] + pub include_author: bool, + #[serde(default = "default_true")] + pub include_date: bool, + #[serde(default = "default_true")] + pub include_source_url: bool, + #[serde(default)] + pub skip_first_paragraphs: u32, +} + +impl Default for ProcessingDefaults { + fn default() -> Self { + Self { + include_author: true, + include_date: true, + include_source_url: true, + skip_first_paragraphs: 0, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProcessingOverrides { + #[serde(default)] + pub include_author: Option, + #[serde(default)] + pub include_date: Option, + #[serde(default)] + pub include_source_url: Option, + #[serde(default)] + pub skip_first_paragraphs: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct TocConfig { + #[serde(default)] + pub title: Option, + #[serde(default)] + pub hidden: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct EntryLinkConfig { + #[serde(default)] + pub mode: Option, + #[serde(default)] + pub allow_to: Vec, + #[serde(default)] + pub block_to: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "lowercase")] +pub enum BuildMode { + #[default] + Auto, + Explicit, + None, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "kebab-case")] +pub enum LinkMatchMode { + #[default] + CanonicalUrl, + SourceUrl, + Disabled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LinkRule { + pub from: Vec, + pub to: Vec, + #[serde(default)] + pub match_mode: LinkMatchMode, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LinkRulesConfig { + #[serde(default)] + pub mode: BuildMode, + #[serde(default = "default_true")] + pub rewrite_external_substack_links: bool, + #[serde(default = "default_true")] + pub preserve_other_external_links: bool, + #[serde(default)] + pub rules: Vec, +} + +impl Default for LinkRulesConfig { + fn default() -> Self { + Self { + mode: BuildMode::Auto, + rewrite_external_substack_links: true, + preserve_other_external_links: true, + rules: Vec::new(), + } + } +} + +fn default_true() -> bool { + true +} + +fn default_language() -> String { + "en".to_string() +} + +fn default_identifier() -> String { + format!("urn:uuid:{}", uuid::Uuid::new_v4()) +} diff --git a/ebookm-core/src/normalize.rs b/ebookm-core/src/normalize.rs new file mode 100644 index 0000000..4bf8d93 --- /dev/null +++ b/ebookm-core/src/normalize.rs @@ -0,0 +1,357 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use kuchiki::traits::*; +use regex::Regex; +use sha1::{Digest, Sha1}; +use url::Url; + +use crate::error::{EbookmError, Result}; +use crate::graph::{EntryLinkMetadata, LinkPolicy, matches_target}; +use crate::manifest::{DefaultsConfig, EntryDefinition}; +use crate::source::{SourceOrigin, resolve_relative_url}; + +#[derive(Debug, Clone)] +pub struct Asset { + pub id: String, + pub href: String, + pub media_type: String, + pub bytes: Vec, +} + +#[derive(Debug, Clone)] +pub struct NormalizedDocument { + pub title: String, + pub author: Option, + pub published: Option, + pub canonical_url: Option, + pub body_xhtml: String, + pub assets: Vec, +} + +pub fn normalize_document( + entry_id: &str, + entry: &EntryDefinition, + defaults: &DefaultsConfig, + origin: &SourceOrigin, + extracted: crate::extract::ExtractedArticle, + policy: &LinkPolicy, + entry_metadata: &BTreeMap, +) -> Result { + let mut document = kuchiki::parse_html().one(format!("
      {}
      ", extracted.body_html)); + + remove_nodes(&mut document, "script,style,noscript,button,svg,source"); + if defaults.normalize_substack_embeds { + remove_nodes(&mut document, "iframe"); + } + skip_first_paragraphs( + &mut document, + entry + .processing + .skip_first_paragraphs + .unwrap_or(defaults.processing.skip_first_paragraphs), + ); + scrub_attributes(&mut document); + + let mut assets = Vec::new(); + if defaults.fetch_images { + collect_images(origin, &mut document, &mut assets)?; + } + + rewrite_links(entry_id, &mut document, origin, policy, entry_metadata); + let body_xhtml = serialize_document(&document)?; + + Ok(NormalizedDocument { + title: entry.title.clone().unwrap_or(extracted.title), + author: entry + .metadata + .author + .clone() + .or(extracted.author) + .or(defaults.metadata.author.clone()), + published: entry + .metadata + .published + .or(extracted.published) + .or(defaults.metadata.published), + canonical_url: extracted.canonical_url, + body_xhtml, + assets, + }) +} + +fn remove_nodes(document: &mut kuchiki::NodeRef, selector: &str) { + if let Ok(nodes) = document.select(selector) { + let selected: Vec<_> = nodes.collect(); + for node in selected { + node.as_node().detach(); + } + } +} + +fn collect_images( + origin: &SourceOrigin, + document: &mut kuchiki::NodeRef, + assets: &mut Vec, +) -> Result<()> { + let selected = document + .select("img") + .map(|items| items.collect::>()) + .unwrap_or_default(); + + for node in selected { + let mut attrs = node.attributes.borrow_mut(); + let src = attrs + .get("src") + .or_else(|| attrs.get("data-src")) + .map(|value| value.to_string()); + let Some(src) = src else { + continue; + }; + + if let Ok(asset) = fetch_asset(origin, &src) { + attrs.insert("src", format!("../{}", asset.href)); + assets.push(asset); + } + } + + Ok(()) +} + +fn fetch_asset(origin: &SourceOrigin, src: &str) -> Result { + match origin { + SourceOrigin::LocalFile(base_path) => fetch_local_asset(base_path, src), + SourceOrigin::Remote(base_url) => { + let resolved = base_url.join(src).map_err(|source| EbookmError::UrlParse { + value: src.to_string(), + source, + })?; + fetch_remote_asset(&resolved) + } + } +} + +fn fetch_local_asset(base_path: &Path, src: &str) -> Result { + if let Ok(url) = Url::parse(src) { + match url.scheme() { + "http" | "https" => return fetch_remote_asset(&url), + "file" => { + let path = url + .to_file_path() + .map_err(|_| EbookmError::InvalidSourcePath { + path: src.to_string(), + })?; + return build_asset_from_path(&path); + } + _ => {} + } + } + + let path = if Path::new(src).is_absolute() { + Path::new(src).to_path_buf() + } else { + base_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(src) + }; + build_asset_from_path(&path) +} + +fn fetch_remote_asset(url: &Url) -> Result { + let bytes = reqwest::blocking::get(url.clone()) + .and_then(|response| response.error_for_status()) + .map_err(|source| EbookmError::Request { + url: url.to_string(), + source, + })? + .bytes() + .map_err(|source| EbookmError::Request { + url: url.to_string(), + source, + })? + .to_vec(); + + let extension = infer_extension_from_str(url.path()); + let media_type = infer_media_type(&extension); + let digest = Sha1::digest(url.as_str().as_bytes()); + let id = format!("{:x}", digest); + Ok(Asset { + id: id.clone(), + href: format!("assets/{}.{}", id, extension), + media_type, + bytes, + }) +} + +fn build_asset_from_path(path: &Path) -> Result { + let bytes = std::fs::read(path).map_err(|source| EbookmError::Io { + path: path.display().to_string(), + source, + })?; + let extension = infer_extension_from_path(path); + let media_type = infer_media_type(&extension); + let digest = Sha1::digest(path.display().to_string().as_bytes()); + let id = format!("{:x}", digest); + Ok(Asset { + id: id.clone(), + href: format!("assets/{}.{}", id, extension), + media_type, + bytes, + }) +} + +fn rewrite_links( + entry_id: &str, + document: &mut kuchiki::NodeRef, + origin: &SourceOrigin, + policy: &LinkPolicy, + entry_metadata: &BTreeMap, +) { + let selected = document + .select("a[href]") + .map(|items| items.collect::>()) + .unwrap_or_default(); + + for node in selected { + let mut attrs = node.attributes.borrow_mut(); + let href = attrs.get("href").map(|value| value.to_string()); + let Some(href) = href else { + continue; + }; + + let Some(resolved) = resolve_relative_url(origin, &href) else { + continue; + }; + + if let Some((target_id, _)) = entry_metadata.iter().find(|(target_id, metadata)| { + *target_id != entry_id && matches_target(&resolved, policy, target_id, metadata) + }) { + attrs.insert("href", format!("../text/{}.xhtml", target_id)); + } + } +} + +fn serialize_document(document: &kuchiki::NodeRef) -> Result { + let wrapper = document + .select_first("div") + .map_err(|_| EbookmError::Epub { + message: "failed to serialize normalized document".to_string(), + })?; + + let mut bytes = Vec::new(); + for child in wrapper.as_node().children() { + child + .serialize(&mut bytes) + .map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + } + + let html = String::from_utf8(bytes).map_err(|error| EbookmError::Epub { + message: error.to_string(), + })?; + Ok(to_xhtml_fragment(&html)) +} + +fn scrub_attributes(document: &mut kuchiki::NodeRef) { + if let Ok(nodes) = document.select("*") { + let selected: Vec<_> = nodes.collect(); + for node in selected { + let mut attrs = node.attributes.borrow_mut(); + let names: Vec<_> = attrs.map.keys().cloned().collect(); + for name in names { + let local = name.local.to_string(); + let keep = match node.name.local.as_ref() { + "a" => matches!(local.as_str(), "href" | "title"), + "img" => matches!(local.as_str(), "src" | "alt"), + _ => false, + }; + if !keep { + attrs.map.remove(&name); + } + } + } + } +} + +fn skip_first_paragraphs(document: &mut kuchiki::NodeRef, count: u32) { + if count == 0 { + return; + } + let selected = document + .select("p") + .map(|items| items.take(count as usize).collect::>()) + .unwrap_or_default(); + for node in selected { + node.as_node().detach(); + } +} + +fn infer_extension_from_path(path: &Path) -> String { + path.extension() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .unwrap_or("bin") + .to_string() +} + +fn infer_extension_from_str(path: &str) -> String { + Path::new(path) + .extension() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .unwrap_or("bin") + .to_string() +} + +fn infer_media_type(extension: &str) -> String { + match extension { + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "svg" => "image/svg+xml", + "webp" => "image/webp", + _ => "application/octet-stream", + } + .to_string() +} + +fn to_xhtml_fragment(html: &str) -> String { + let img_re = Regex::new(r#"]*)>"#).expect("valid img regex"); + let hr_re = Regex::new(r#"]*)>"#).expect("valid hr regex"); + let br_re = Regex::new(r#"]*)>"#).expect("valid br regex"); + + let html = img_re.replace_all(html, "").into_owned(); + let html = hr_re.replace_all(&html, "").into_owned(); + br_re.replace_all(&html, "").into_owned() +} + +#[cfg(test)] +mod tests { + use super::to_xhtml_fragment; + use quick_xml::events::Event; + use quick_xml::Reader; + + #[test] + fn converts_void_html_tags_to_xhtml_self_closing_tags() { + let input = r#"

      Intro



      "#; + let xhtml = to_xhtml_fragment(input); + assert!(xhtml.contains(r#""#)); + assert!(xhtml.contains("
      ")); + assert!(xhtml.contains("
      ")); + + let wrapped = format!( + r#"{}"#, + xhtml + ); + let mut reader = Reader::from_str(&wrapped); + loop { + match reader.read_event() { + Ok(Event::Eof) => break, + Ok(_) => {} + Err(error) => panic!("invalid XML generated: {error}"), + } + } + } +} diff --git a/ebookm-core/src/pipeline.rs b/ebookm-core/src/pipeline.rs new file mode 100644 index 0000000..0a20df2 --- /dev/null +++ b/ebookm-core/src/pipeline.rs @@ -0,0 +1,432 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use crate::epub::write_epub; +use crate::error::{EbookmError, Result}; +use crate::extract::{InspectResult, inspect_article}; +use crate::graph::{EntryLinkMetadata, build_link_policies}; +use crate::manifest::{Manifest, SourceDefinition}; +use crate::normalize::{Asset, NormalizedDocument}; +use crate::source::{SourceSpec, load_source}; +use crate::template::INIT_MANIFEST; + +#[derive(Debug, Clone)] +pub struct BuiltChapter { + pub nav_title: String, + pub xhtml: String, +} + +#[derive(Debug, Clone, Copy)] +struct ChapterHeaderOptions { + include_author: bool, + include_date: bool, + include_source_url: bool, +} + +#[derive(Debug, Clone)] +pub struct BuiltEntry { + pub id: String, + pub hidden_from_toc: bool, + pub chapter: BuiltChapter, + pub assets: Vec, +} + +pub fn load_manifest(path: &Path) -> Result { + let contents = fs::read_to_string(path).map_err(|source| EbookmError::Io { + path: path.display().to_string(), + source, + })?; + serde_yaml::from_str(&contents).map_err(|source| EbookmError::ManifestParse { + path: path.display().to_string(), + source, + }) +} + +pub fn validate_manifest(manifest: &Manifest) -> Result> { + let mut issues = Vec::new(); + let mut warnings = Vec::new(); + + if manifest.book.title.trim().is_empty() { + issues.push("book.title must not be empty".to_string()); + } + if manifest.output.path.trim().is_empty() { + issues.push("output.path must not be empty".to_string()); + } + if manifest.sections.is_empty() { + issues.push("at least one section is required".to_string()); + } + if manifest.entries.is_empty() { + issues.push("at least one entry is required".to_string()); + } + + for section in &manifest.sections { + if section.entries.is_empty() { + warnings.push(format!("section {} has no entries", section.id)); + } + for entry_id in §ion.entries { + if !manifest.entries.contains_key(entry_id) { + issues.push(format!( + "section {} references unknown entry {}", + section.id, entry_id + )); + } + } + } + + for (entry_id, entry) in &manifest.entries { + for target in &entry.links.allow_to { + if !manifest.entries.contains_key(target) { + issues.push(format!( + "entry {entry_id} allow_to target {target} does not exist" + )); + } + } + for target in &entry.links.block_to { + if !manifest.entries.contains_key(target) { + issues.push(format!( + "entry {entry_id} block_to target {target} does not exist" + )); + } + } + } + + for rule in &manifest.link_rules.rules { + validate_selectors(manifest, &rule.from, "from", &mut issues); + validate_selectors(manifest, &rule.to, "to", &mut issues); + } + + for entry_id in manifest.entries.keys() { + if !manifest.sections.iter().any(|section| { + section + .entries + .iter() + .any(|candidate| candidate == entry_id) + }) { + warnings.push(format!("entry {entry_id} is not referenced by any section")); + } + } + + if issues.is_empty() { + Ok(warnings) + } else { + Err(EbookmError::Validation { issues }) + } +} + +pub fn inspect_source(source: &str) -> Result { + let spec = if source.starts_with("http://") || source.starts_with("https://") { + SourceSpec::from_definition( + &SourceDefinition::Substack { + url: source.to_string(), + }, + Path::new("."), + )? + } else { + SourceSpec::from_definition( + &SourceDefinition::Html { + path: source.to_string(), + }, + Path::new("."), + )? + }; + let loaded = load_source(&spec)?; + inspect_article(&loaded) +} + +pub fn build_epub(manifest: &Manifest, manifest_path: &Path) -> Result<()> { + let manifest_dir = manifest_path.parent().unwrap_or_else(|| Path::new(".")); + + let mut entry_specs = BTreeMap::new(); + let mut loaded_sources = BTreeMap::new(); + let mut extracted = BTreeMap::new(); + let mut metadata = BTreeMap::new(); + + for (entry_id, entry) in &manifest.entries { + let spec = SourceSpec::from_definition(&entry.source, manifest_dir)?; + let loaded = load_source(&spec)?; + let article = crate::extract::extract_article(&loaded)?; + let source_url = match &spec { + SourceSpec::SubstackUrl(url) => Some(url.clone()), + SourceSpec::LocalHtml(_) => None, + }; + + metadata.insert( + entry_id.clone(), + EntryLinkMetadata { + source_url, + canonical_url: article.canonical_url.clone(), + }, + ); + entry_specs.insert(entry_id.clone(), spec); + loaded_sources.insert(entry_id.clone(), loaded); + extracted.insert(entry_id.clone(), article); + } + + let policies = build_link_policies(manifest, &metadata); + let mut built_entries = Vec::new(); + + for section in &manifest.sections { + for entry_id in §ion.entries { + let entry = &manifest.entries[entry_id]; + let loaded = loaded_sources.get(entry_id).expect("entry was loaded"); + let article = extracted + .get(entry_id) + .expect("entry was extracted") + .clone(); + let policy = policies.get(entry_id).expect("policy was built"); + + let normalized = crate::normalize::normalize_document( + entry_id, + entry, + &manifest.defaults, + &loaded.origin, + article, + policy, + &metadata, + )?; + + built_entries.push(BuiltEntry { + id: entry_id.clone(), + hidden_from_toc: entry.toc.hidden, + chapter: build_chapter(entry_id, entry, &manifest.defaults, &normalized), + assets: normalized.assets, + }); + } + } + + let cover = manifest + .output + .cover_image + .as_ref() + .map(|path| load_cover(path, manifest_dir)) + .transpose()?; + let output_path = manifest_dir.join(&manifest.output.path); + write_epub(manifest, &built_entries, &output_path, cover)?; + Ok(()) +} + +pub fn render_init_manifest() -> &'static str { + INIT_MANIFEST +} + +fn build_chapter( + entry_id: &str, + entry: &crate::manifest::EntryDefinition, + defaults: &crate::manifest::DefaultsConfig, + doc: &NormalizedDocument, +) -> BuiltChapter { + let nav_title = entry.toc.title.clone().unwrap_or_else(|| doc.title.clone()); + let header = resolve_header_options(entry, defaults); + let author = doc.author.clone().unwrap_or_default(); + let published = doc + .published + .map(|date| date.to_string()) + .unwrap_or_default(); + let mut meta_lines = Vec::new(); + if header.include_author && !author.is_empty() { + meta_lines.push(format!("

      {}

      ", escape_html(&author))); + } + if header.include_date && !published.is_empty() { + meta_lines.push(format!("

      {}

      ", escape_html(&published))); + } + if header.include_source_url { + if let Some(url) = doc.canonical_url.as_ref() { + let escaped = escape_html(url.as_str()); + meta_lines.push(format!(r#"

      {0}

      "#, escaped)); + } + } + + let meta_block = if meta_lines.is_empty() { + String::new() + } else { + format!(r#"
      {}
      "#, meta_lines.join("")) + }; + + let xhtml = format!( + r#" + + + {} + + + +

      {}

      + {} + {} + +"#, + escape_html(&doc.title), + escape_html(entry_id), + escape_html(&doc.title), + meta_block, + doc.body_xhtml + ); + + BuiltChapter { nav_title, xhtml } +} + +fn validate_selectors( + manifest: &Manifest, + selectors: &[String], + field: &str, + issues: &mut Vec, +) { + for selector in selectors { + if selector == "*" { + continue; + } + if manifest.entries.contains_key(selector) { + continue; + } + if let Some(section_id) = selector.strip_prefix("section:") { + if manifest + .sections + .iter() + .any(|section| section.id == section_id) + { + continue; + } + } + issues.push(format!("unknown {field} selector {selector}")); + } +} + +fn load_cover(path: &str, manifest_dir: &Path) -> Result<(String, Vec)> { + let full_path = manifest_dir.join(path); + let bytes = fs::read(&full_path).map_err(|source| EbookmError::Io { + path: full_path.display().to_string(), + source, + })?; + let extension = full_path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("jpg"); + Ok((format!("assets/cover.{extension}"), bytes)) +} + +fn escape_html(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +fn resolve_header_options( + entry: &crate::manifest::EntryDefinition, + defaults: &crate::manifest::DefaultsConfig, +) -> ChapterHeaderOptions { + ChapterHeaderOptions { + include_author: entry + .processing + .include_author + .unwrap_or(defaults.processing.include_author), + include_date: entry + .processing + .include_date + .unwrap_or(defaults.processing.include_date), + include_source_url: entry + .processing + .include_source_url + .unwrap_or(defaults.processing.include_source_url), + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + use zip::ZipArchive; + + use super::*; + + #[test] + fn validates_and_builds_local_html_manifest() { + let temp = tempdir().expect("tempdir"); + let root = temp.path(); + + fs::write( + root.join("article.html"), + r#" + + + Local Essay + + + + +
      +

      Hello world.

      + Author +
      + +"#, + ) + .expect("write html"); + fs::write(root.join("author.jpg"), b"fake-jpeg-data").expect("write image"); + + let manifest_path = root.join("book.yaml"); + fs::write( + &manifest_path, + r#"book: + title: "Local Book" + author: "Editor" + language: "en" + identifier: "urn:uuid:test-book" +output: + path: "dist/test.epub" +defaults: + fetch_images: true + normalize_substack_embeds: true + processing: + include_author: true + include_date: false + include_source_url: false + skip_first_paragraphs: 0 +sections: + - id: "part-1" + title: "Part 1" + entries: + - "essay" +entries: + essay: + source: + kind: "html" + path: "article.html" +link_rules: + mode: "auto" +"#, + ) + .expect("write manifest"); + + let manifest = load_manifest(&manifest_path).expect("manifest"); + validate_manifest(&manifest).expect("manifest valid"); + build_epub(&manifest, &manifest_path).expect("build epub"); + + let epub_path = root.join("dist/test.epub"); + assert!(epub_path.exists()); + + let file = fs::File::open(&epub_path).expect("epub file"); + let mut archive = ZipArchive::new(file).expect("zip"); + assert!(archive.by_name("mimetype").is_ok()); + assert!(archive.by_name("OEBPS/content.opf").is_ok()); + let mut chapter = archive + .by_name("OEBPS/text/essay.xhtml") + .expect("chapter file"); + let mut chapter_contents = String::new(); + use std::io::Read; + chapter + .read_to_string(&mut chapter_contents) + .expect("read chapter"); + assert!(chapter_contents.contains("

      Local Author

      ")); + assert!(!chapter_contents.contains("

      2025-01-10

      ")); + assert!(!chapter_contents.contains("urn:uuid:test-book")); + assert!(chapter_contents.contains("../assets/")); + drop(chapter); + assert!(archive + .file_names() + .any(|name| name.starts_with("OEBPS/assets/") && name.ends_with(".jpg"))); + } +} diff --git a/ebookm-core/src/source.rs b/ebookm-core/src/source.rs new file mode 100644 index 0000000..a697c49 --- /dev/null +++ b/ebookm-core/src/source.rs @@ -0,0 +1,97 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use url::Url; + +use crate::error::{EbookmError, Result}; +use crate::manifest::SourceDefinition; + +#[derive(Debug, Clone)] +pub enum SourceSpec { + SubstackUrl(Url), + LocalHtml(PathBuf), +} + +#[derive(Debug, Clone)] +pub enum SourceOrigin { + Remote(Url), + LocalFile(PathBuf), +} + +#[derive(Debug, Clone)] +pub struct LoadedSource { + pub origin: SourceOrigin, + pub html: String, +} + +impl SourceSpec { + pub fn from_definition(definition: &SourceDefinition, manifest_dir: &Path) -> Result { + match definition { + SourceDefinition::Substack { url } => Ok(SourceSpec::SubstackUrl( + Url::parse(url).map_err(|source| EbookmError::UrlParse { + value: url.clone(), + source, + })?, + )), + SourceDefinition::Html { path } => { + let joined = manifest_dir.join(path); + Ok(SourceSpec::LocalHtml(joined)) + } + } + } +} + +pub fn load_source(spec: &SourceSpec) -> Result { + match spec { + SourceSpec::SubstackUrl(url) => { + let client = reqwest::blocking::Client::builder() + .user_agent("ebookm/0.1") + .build() + .map_err(|source| EbookmError::Request { + url: url.to_string(), + source, + })?; + let html = client + .get(url.clone()) + .send() + .and_then(|response| response.error_for_status()) + .map_err(|source| EbookmError::Request { + url: url.to_string(), + source, + })? + .text() + .map_err(|source| EbookmError::Request { + url: url.to_string(), + source, + })?; + Ok(LoadedSource { + origin: SourceOrigin::Remote(url.clone()), + html, + }) + } + SourceSpec::LocalHtml(path) => { + let html = fs::read_to_string(path).map_err(|source| EbookmError::Io { + path: path.display().to_string(), + source, + })?; + Ok(LoadedSource { + origin: SourceOrigin::LocalFile(path.clone()), + html, + }) + } + } +} + +pub fn resolve_relative_url(origin: &SourceOrigin, href: &str) -> Option { + match origin { + SourceOrigin::Remote(base) => base.join(href).ok(), + SourceOrigin::LocalFile(path) => { + if let Ok(url) = Url::parse(href) { + return Some(url); + } + let parent = path.parent()?; + let joined = parent.join(href); + Url::from_file_path(joined).ok() + } + } +} diff --git a/ebookm-core/src/template.rs b/ebookm-core/src/template.rs new file mode 100644 index 0000000..9129448 --- /dev/null +++ b/ebookm-core/src/template.rs @@ -0,0 +1,56 @@ +pub const INIT_MANIFEST: &str = r#"book: + title: "Collected Substack Essays" + author: "Author Name" + language: "en" + identifier: "urn:uuid:11111111-2222-3333-4444-555555555555" + description: "A compiled EPUB built by ebookm" + +output: + path: "dist/collection.epub" + +defaults: + fetch_images: true + normalize_substack_embeds: true + processing: + include_author: true + include_date: true + include_source_url: true + skip_first_paragraphs: 0 + metadata: + author: "Author Name" + +sections: + - id: "essays" + title: "Essays" + entries: + - "opening-post" + - "saved-html" + +entries: + opening-post: + source: + kind: "substack" + url: "https://example.substack.com/p/opening-post" + processing: + skip_first_paragraphs: 1 + toc: + title: "Opening Post" + + saved-html: + source: + kind: "html" + path: "articles/saved-post.html" + title: "Saved Local Article" + links: + mode: "explicit" + allow_to: ["opening-post"] + +link_rules: + mode: "auto" + rewrite_external_substack_links: true + preserve_other_external_links: true + rules: + - from: ["section:essays"] + to: ["section:essays"] + match_mode: "canonical-url" +"#; diff --git a/examples/articles/intro.html b/examples/articles/intro.html new file mode 100644 index 0000000..80763b9 --- /dev/null +++ b/examples/articles/intro.html @@ -0,0 +1,15 @@ + + + + Introduction + + + + + +
      +

      This is the first article in the bundled example.

      +

      It demonstrates a local HTML source entry.

      +
      + + diff --git a/examples/articles/notes.html b/examples/articles/notes.html new file mode 100644 index 0000000..ce092c6 --- /dev/null +++ b/examples/articles/notes.html @@ -0,0 +1,14 @@ + + + + Working Notes + + + + + + + diff --git a/examples/dist/example-book.epub b/examples/dist/example-book.epub new file mode 100644 index 0000000..bbc0fcc Binary files /dev/null and b/examples/dist/example-book.epub differ diff --git a/examples/example-book.yaml b/examples/example-book.yaml new file mode 100644 index 0000000..6f24249 --- /dev/null +++ b/examples/example-book.yaml @@ -0,0 +1,48 @@ +book: + title: "ebookm Example Book" + author: "ebookm" + language: "en" + identifier: "urn:uuid:ebookm-example-book" + description: "Example manifest shipped with the repository" + +output: + path: "dist/example-book.epub" + +defaults: + fetch_images: false + normalize_substack_embeds: true + metadata: + author: "ebookm" + +sections: + - id: "part-1" + title: "Examples" + entries: + - "intro" + - "notes" + +entries: + intro: + source: + kind: "html" + path: "articles/intro.html" + toc: + title: "Introduction" + + notes: + source: + kind: "html" + path: "articles/notes.html" + title: "Working Notes" + links: + mode: "explicit" + allow_to: ["intro"] + +link_rules: + mode: "explicit" + rewrite_external_substack_links: true + preserve_other_external_links: true + rules: + - from: ["notes"] + to: ["intro"] + match_mode: "canonical-url"