diff --git a/.github/workflows/actions/quarto-dev/action.yml b/.github/workflows/actions/quarto-dev/action.yml index 072c6f71e81..0b8ae669b32 100644 --- a/.github/workflows/actions/quarto-dev/action.yml +++ b/.github/workflows/actions/quarto-dev/action.yml @@ -12,17 +12,6 @@ runs: restore-keys: | ${{ runner.os }}-deno_std-2- - - name: Cache Cargo dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - package/typst-gather/target - key: ${{ runner.os }}-cargo-typst-gather-${{ hashFiles('package/typst-gather/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo-typst-gather- - - name: Configure Quarto (.sh) if: runner.os != 'Windows' shell: bash diff --git a/configure.cmd b/configure.cmd index f1eccc9c3ed..7084016d175 100644 --- a/configure.cmd +++ b/configure.cmd @@ -104,11 +104,6 @@ IF EXIST !QUARTO_BIN_PATH!\quarto.cmd ( ECHO NOTE: To use quarto please use quarto.cmd (located in this folder) or add the following path to your PATH ECHO !QUARTO_BIN_PATH! -REM Build typst-gather and install to tools directory -ECHO Building typst-gather... -cargo build --release --manifest-path package\typst-gather\Cargo.toml -COPY package\typst-gather\target\release\typst-gather.exe "!QUARTO_BIN_PATH!\tools\x86_64\" - endlocal & set QUARTO_BIN_DEV=%QUARTO_BIN_PATH% GOTO :eof diff --git a/configure.sh b/configure.sh index 64f54e790e0..859f3a3f769 100755 --- a/configure.sh +++ b/configure.sh @@ -102,8 +102,3 @@ else export QUARTO_DENO_EXTRA_OPTIONS="--reload" quarto --version fi - -# Build typst-gather and install to tools directory -echo "Building typst-gather..." -cargo build --release --manifest-path package/typst-gather/Cargo.toml -cp package/typst-gather/target/release/typst-gather "$QUARTO_BIN_PATH/tools/$DENO_ARCH_DIR/" diff --git a/package/src/common/dependencies/typst-gather.ts b/package/src/common/dependencies/typst-gather.ts index f0f192ec556..cf805e5a617 100644 --- a/package/src/common/dependencies/typst-gather.ts +++ b/package/src/common/dependencies/typst-gather.ts @@ -42,7 +42,6 @@ export function typstGather(version: string): Dependency { name: "typst-gather", bucket: "typst-gather", version, - archiveOnly: true, architectureDependencies: { "x86_64": { "windows": typstGatherRelease("typst-gather-x86_64-pc-windows-msvc.zip"), diff --git a/package/src/common/prepare-dist.ts b/package/src/common/prepare-dist.ts index b4ce12e2add..83946609d34 100755 --- a/package/src/common/prepare-dist.ts +++ b/package/src/common/prepare-dist.ts @@ -100,32 +100,6 @@ export async function prepareDist( } } - // Stage typst-gather binary (built by configure.sh) - // Only stage if the build machine architecture matches the target architecture - // (cross-compilation is not currently supported) - const buildArch = Deno.build.arch === "aarch64" ? "aarch64" : "x86_64"; - if (buildArch === config.arch) { - const typstGatherBinaryName = config.os === "windows" ? "typst-gather.exe" : "typst-gather"; - const typstGatherSrc = join( - config.directoryInfo.root, - "package/typst-gather/target/release", - typstGatherBinaryName, - ); - if (!existsSync(typstGatherSrc)) { - throw new Error( - `typst-gather binary not found at ${typstGatherSrc}\n` + - "Run ./configure.sh to build it.", - ); - } - info("\nStaging typst-gather binary"); - const typstGatherDest = join(targetDir, config.arch, typstGatherBinaryName); - ensureDirSync(join(targetDir, config.arch)); - copySync(typstGatherSrc, typstGatherDest, { overwrite: true }); - info(`Copied ${typstGatherSrc} to ${typstGatherDest}`); - } else { - info(`\nNote: Skipping typst-gather staging (build arch ${buildArch} != target arch ${config.arch})`); - } - // build quarto-preview.js info("Building Quarto Web UI"); const result = buildQuartoPreviewJs(config.directoryInfo.src, undefined, true); diff --git a/package/typst-gather/.gitignore b/package/typst-gather/.gitignore deleted file mode 100644 index ea8c4bf7f35..00000000000 --- a/package/typst-gather/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/package/typst-gather/Cargo.lock b/package/typst-gather/Cargo.lock deleted file mode 100644 index 77b2bb0277f..00000000000 --- a/package/typst-gather/Cargo.lock +++ /dev/null @@ -1,3000 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -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 = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "az" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "biblatex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d0c374feba1b9a59042a7c1cf00ce7c34b977b9134fe7c42b08e5183729f66" -dependencies = [ - "paste", - "roman-numerals-rs", - "strum", - "unic-langid", - "unicode-normalization", - "unscanny", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "cc" -version = "1.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chinese-number" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b" -dependencies = [ - "chinese-variant", - "enum-ordinalize", - "num-bigint", - "num-traits", -] - -[[package]] -name = "chinese-variant" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b52a9840ffff5d4d0058ae529fa066a75e794e3125546acfc61c23ad755e49" - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "citationberg" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6597e8bdbca37f1f56e5a80d15857b0932aead21a78d20de49e99e74933046" -dependencies = [ - "quick-xml", - "serde", -] - -[[package]] -name = "clap" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] - -[[package]] -name = "codex" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9589e1effc5cacbea347899645c654158b03b2053d24bb426fd3128ced6e423c" - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "comemo" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649d7b2d867b569729c03c0f6968db10bc95921182a1f2b2012b1b549492f39d" -dependencies = [ - "comemo-macros", - "parking_lot", - "rustc-hash", - "siphasher", - "slab", -] - -[[package]] -name = "comemo-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c87fc7e85487493ddedae1a3a34b897c77ad8825375b79265a8a162c28d535" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ecow" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" -dependencies = [ - "serde", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "env_proxy" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a5019be18538406a43b5419a5501461f0c8b49ea7dfda0cfc32f4e51fc44be1" -dependencies = [ - "log", - "url", -] - -[[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 = "euclid" -version = "0.22.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fancy-regex" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "filetime" -version = "0.2.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[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 = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "gif" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "glidesort" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" - -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "hayagriva" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb69425736f184173b3ca6e27fcba440a61492a790c786b1c6af7e06a03e575" -dependencies = [ - "biblatex", - "ciborium", - "citationberg", - "indexmap", - "paste", - "roman-numerals-rs", - "serde", - "serde_yaml", - "thiserror", - "unic-langid", - "unicode-segmentation", - "unscanny", - "url", -] - -[[package]] -name = "hayro-syntax" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9e5c7dbc0f11dc42775d1a6cc00f5f5137b90b6288dd7fe5f71d17b14d10be" -dependencies = [ - "flate2", - "kurbo 0.12.0", - "log", - "rustc-hash", - "smallvec", - "zune-jpeg 0.4.21", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "serde", - "yoke 0.7.5", - "zerofrom", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke 0.8.1", - "zerofrom", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap 0.8.1", - "tinystr 0.8.2", - "writeable 0.6.2", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap 0.7.5", - "tinystr 0.7.6", - "writeable 0.5.5", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider 1.5.0", - "tinystr 0.7.6", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections 2.1.1", - "icu_normalizer_data", - "icu_properties 2.1.2", - "icu_provider 2.1.1", - "smallvec", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections 1.5.0", - "icu_locid_transform", - "icu_properties_data 1.5.1", - "icu_provider 1.5.0", - "serde", - "tinystr 0.7.6", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections 2.1.1", - "icu_locale_core", - "icu_properties_data 2.1.2", - "icu_provider 2.1.1", - "zerotrie 0.2.3", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "postcard", - "serde", - "stable_deref_trait", - "tinystr 0.7.6", - "writeable 0.5.5", - "yoke 0.7.5", - "zerofrom", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable 0.6.2", - "yoke 0.8.1", - "zerofrom", - "zerotrie 0.2.3", - "zerovec 0.11.5", -] - -[[package]] -name = "icu_provider_blob" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c24b98d1365f55d78186c205817631a4acf08d7a45bdf5dc9dcf9c5d54dccf51" -dependencies = [ - "icu_provider 1.5.0", - "postcard", - "serde", - "writeable 0.5.5", - "zerotrie 0.1.3", - "zerovec 0.10.4", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties 2.1.2", -] - -[[package]] -name = "image" -version = "0.25.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown", - "serde", - "serde_core", -] - -[[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 = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "kamadak-exif" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" -dependencies = [ - "mutate_once", -] - -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - -[[package]] -name = "kurbo" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.7.0", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "lipsum" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" -dependencies = [ - "rand", - "rand_chacha", -] - -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" -dependencies = [ - "serde", -] - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - -[[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 = "moxcms" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "mutate_once" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "palette" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" -dependencies = [ - "approx", - "fast-srgb8", - "libm", - "palette_derive", -] - -[[package]] -name = "palette_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" -dependencies = [ - "by_address", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "parking_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 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[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.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64", - "indexmap", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.10.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec 0.11.5", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[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.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pxfm" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qcms" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edecfcd5d755a5e5d98e24cf43113e7cdaec5a070edd0f6b250c03a573da30fa" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -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 = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "roman-numerals-rs" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "rust_decimal" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" -dependencies = [ - "arrayvec", - "num-traits", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[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", -] - -[[package]] -name = "serde_json" -version = "1.0.148" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo 0.11.3", - "siphasher", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syntect" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" -dependencies = [ - "bincode", - "fancy-regex", - "flate2", - "fnv", - "once_cell", - "plist", - "regex-syntax", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "walkdir", - "yaml-rust", -] - -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thin-vec" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "serde", - "zerovec 0.10.4", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec 0.11.5", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -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 = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "two-face" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2" -dependencies = [ - "serde", - "serde_derive", - "syntect", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typst-assets" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5613cb719a6222fe9b74027c3625d107767ec187bff26b8fc931cf58942c834f" - -[[package]] -name = "typst-gather" -version = "0.1.2" -dependencies = [ - "clap", - "ecow", - "globset", - "serde", - "tempfile", - "toml", - "typst-kit", - "typst-syntax", - "walkdir", -] - -[[package]] -name = "typst-kit" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31476ec753e080ffdd543a0e74b6d319355449ff3eca3f216634f31cfd09a92a" -dependencies = [ - "dirs", - "ecow", - "env_proxy", - "fastrand", - "flate2", - "fontdb", - "native-tls", - "once_cell", - "openssl", - "serde", - "serde_json", - "tar", - "typst-library", - "typst-syntax", - "typst-timing", - "typst-utils", - "ureq", -] - -[[package]] -name = "typst-library" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e276a5de53020c43efe2111ec236252e54ea4480b5ac18063e663dfbe03d9d1b" -dependencies = [ - "az", - "bitflags 2.10.0", - "bumpalo", - "chinese-number", - "ciborium", - "codex", - "comemo", - "csv", - "ecow", - "flate2", - "fontdb", - "glidesort", - "hayagriva", - "hayro-syntax", - "icu_properties 1.5.1", - "icu_provider 1.5.0", - "icu_provider_blob", - "image", - "indexmap", - "kamadak-exif", - "kurbo 0.12.0", - "lipsum", - "memchr", - "palette", - "phf", - "png 0.17.16", - "qcms", - "rayon", - "regex", - "regex-syntax", - "roxmltree", - "rust_decimal", - "rustc-hash", - "rustybuzz", - "serde", - "serde_json", - "serde_yaml", - "siphasher", - "smallvec", - "syntect", - "time", - "toml", - "ttf-parser", - "two-face", - "typed-arena", - "typst-assets", - "typst-macros", - "typst-syntax", - "typst-timing", - "typst-utils", - "unicode-math-class", - "unicode-normalization", - "unicode-segmentation", - "unscanny", - "usvg", - "utf8_iter", - "wasmi", - "xmlwriter", -] - -[[package]] -name = "typst-macros" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "141cbd1027129fbf6bda1013f52a264df7befc7388cc8f47767d65e803fd3a59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typst-syntax" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a95d9192060e23b1e491b0b94dff676acddc92a4d672aeb8ca3890a5a734e879" -dependencies = [ - "ecow", - "rustc-hash", - "serde", - "toml", - "typst-timing", - "typst-utils", - "unicode-ident", - "unicode-math-class", - "unicode-script", - "unicode-segmentation", - "unscanny", -] - -[[package]] -name = "typst-timing" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be94f8faf19841b49574ef5c7fd7a12e2deb7c3d8deba5a596f35d2222024cd" -dependencies = [ - "parking_lot", - "serde", - "serde_json", -] - -[[package]] -name = "typst-utils" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3966c92e8fa48c7ce898130d07000d985f18206d92b250f0f939287fbccdee3" -dependencies = [ - "once_cell", - "portable-atomic", - "rayon", - "rustc-hash", - "siphasher", - "thin-vec", - "unicode-math-class", -] - -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", - "unic-langid-macros", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "serde", - "tinystr 0.8.2", -] - -[[package]] -name = "unic-langid-macros" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" -dependencies = [ - "proc-macro-hack", - "tinystr 0.8.2", - "unic-langid-impl", - "unic-langid-macros-impl", -] - -[[package]] -name = "unic-langid-macros-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" -dependencies = [ - "proc-macro-hack", - "quote", - "syn", - "unic-langid-impl", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-math-class" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "unscanny" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "flate2", - "log", - "native-tls", - "once_cell", - "serde", - "serde_json", - "url", -] - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "usvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" -dependencies = [ - "base64", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo 0.11.3", - "log", - "pico-args", - "roxmltree", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -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 = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "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.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasmi" -version = "0.51.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff" -dependencies = [ - "spin", - "wasmi_collections", - "wasmi_core", - "wasmi_ir", - "wasmparser", -] - -[[package]] -name = "wasmi_collections" -version = "0.51.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b8e98e45a2a534489f8225e765cbf1cb9a3078072605e58158910cf4749172" - -[[package]] -name = "wasmi_core" -version = "0.51.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c25f375c0cdf14810eab07f532f61f14d4966f09c747a55067fdf3196e8512e6" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmi_ir" -version = "0.51.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624e2a68a4293ecb8f564260b68394b29cf3b3edba6bce35532889a2cb33c3d9" -dependencies = [ - "wasmi_core", -] - -[[package]] -name = "wasmparser" -version = "0.228.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[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.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[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.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[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.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[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.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[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.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive 0.8.1", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f" -dependencies = [ - "displaydoc", - "litemap 0.7.5", - "serde", - "zerovec 0.10.4", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke 0.8.1", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "serde", - "yoke 0.7.5", - "zerofrom", - "zerovec-derive 0.10.3", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "yoke 0.8.1", - "zerofrom", - "zerovec-derive 0.11.2", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-core" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" - -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" -dependencies = [ - "zune-core 0.5.0", -] diff --git a/package/typst-gather/Cargo.toml b/package/typst-gather/Cargo.toml deleted file mode 100644 index 90316d4a382..00000000000 --- a/package/typst-gather/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "typst-gather" -version = "0.1.2" -edition = "2021" -rust-version = "1.81" - -[dependencies] -typst-kit = { version = "0.14.2", features = ["packages"] } -typst-syntax = "0.14.2" -ecow = "0.2" -serde = { version = "1", features = ["derive"] } -clap = { version = "4", features = ["derive"] } -toml = "0.8" -walkdir = "2" -globset = "0.4" - -[dev-dependencies] -tempfile = "3" diff --git a/package/typst-gather/README.md b/package/typst-gather/README.md deleted file mode 100644 index 8efab9c0ca7..00000000000 --- a/package/typst-gather/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# typst-gather - -Gather Typst packages locally for offline/hermetic builds. - -## Install - -```bash -cargo install --path . -``` - -## Usage - -```bash -typst-gather packages.toml -``` - -Then set `TYPST_PACKAGE_CACHE_PATH` to the destination directory when running Typst. - -## TOML format - -```toml -destination = "/path/to/packages" - -# Single path -discover = "/path/to/templates" - -# Or array of paths (files or directories) -discover = ["template.typ", "typst-show.typ", "/path/to/dir"] - -[preview] -cetz = "0.4.1" -fontawesome = "0.5.0" - -[local] -my-template = "/path/to/src" -``` - -- `destination` - Required. Directory where packages will be gathered. -- `discover` - Optional. Paths to scan for imports. Can be: - - A single string path - - An array of paths - - Each path can be a `.typ` file or a directory (scans `.typ` files non-recursively) -- `[preview]` packages are downloaded from Typst Universe (cached - skipped if already present) -- `[local]` packages are copied from the specified directory (always fresh - version read from `typst.toml`) - -## Features - -- Recursively resolves `@preview` dependencies from `#import` statements -- Uses Typst's own parser for reliable import detection -- Discover mode scans .typ files for imports -- Local packages always overwrite (clean slate) -- Preview packages skip if already cached - -## Quarto Integration - -When used with Quarto extensions, you can run: - -```bash -quarto call typst-gather -``` - -This will auto-detect `.typ` files from `_extension.yml` (template and template-partials) and gather their dependencies. diff --git a/package/typst-gather/example-packages.toml b/package/typst-gather/example-packages.toml deleted file mode 100644 index da19f2f95e4..00000000000 --- a/package/typst-gather/example-packages.toml +++ /dev/null @@ -1,8 +0,0 @@ -destination = "/path/to/cache" - -[preview] -cetz = "0.4.1" -tablex = "0.0.8" - -[local] -my-pkg = "/path/to/my-pkg" diff --git a/package/typst-gather/src/lib.rs b/package/typst-gather/src/lib.rs deleted file mode 100644 index 1370620d6ab..00000000000 --- a/package/typst-gather/src/lib.rs +++ /dev/null @@ -1,833 +0,0 @@ -//! typst-gather: Gather Typst packages locally for offline/hermetic builds. - -use std::collections::HashMap; -use std::collections::HashSet; -use std::env; -use std::path::{Path, PathBuf}; - -use ecow::EcoString; -use globset::{Glob, GlobSetBuilder}; -use serde::Deserialize; -use typst_kit::download::{Downloader, ProgressSink}; -use typst_kit::package::PackageStorage; -use typst_syntax::ast; -use typst_syntax::package::{PackageManifest, PackageSpec, PackageVersion}; -use typst_syntax::SyntaxNode; -use walkdir::WalkDir; - -/// Statistics about gathering operations. -#[derive(Debug, Default, PartialEq, Eq)] -pub struct Stats { - pub downloaded: usize, - pub copied: usize, - pub skipped: usize, - pub failed: usize, -} - -/// Result of a gather operation. -#[derive(Debug, Default)] -pub struct GatherResult { - pub stats: Stats, - /// @local imports discovered during scanning that are not configured in [local] section. - /// Each entry is (package_name, source_file_path). - pub unconfigured_local: Vec<(String, String)>, -} - -/// TOML configuration format. -/// -/// ```toml -/// destination = "/path/to/packages" -/// discover = ["/path/to/templates", "/path/to/other.typ"] -/// -/// [preview] -/// cetz = "0.4.1" -/// fletcher = "0.5.3" -/// -/// [local] -/// my-pkg = "/path/to/pkg" -/// ``` -/// Helper enum for deserializing string or array of strings -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum StringOrVec { - Single(String), - Multiple(Vec), -} - -impl Default for StringOrVec { - fn default() -> Self { - StringOrVec::Multiple(Vec::new()) - } -} - -impl From for Vec { - fn from(value: StringOrVec) -> Self { - match value { - StringOrVec::Single(s) => vec![PathBuf::from(s)], - StringOrVec::Multiple(v) => v.into_iter().map(PathBuf::from).collect(), - } - } -} - -/// Raw config for deserialization -#[derive(Debug, Deserialize, Default)] -struct RawConfig { - /// Root directory for resolving relative paths (discover, destination) - rootdir: Option, - destination: Option, - #[serde(default)] - discover: Option, - #[serde(default)] - preview: HashMap, - #[serde(default)] - local: HashMap, -} - -#[derive(Debug, Default)] -pub struct Config { - /// Root directory for resolving relative paths (discover, destination). - /// If set, discover and destination paths are resolved relative to this. - pub rootdir: Option, - /// Destination directory for gathered packages - pub destination: Option, - /// Paths to scan for imports. Can be directories (scans .typ files) or individual .typ files. - /// Accepts either a single path or an array of paths. - pub discover: Vec, - pub preview: HashMap, - pub local: HashMap, -} - -impl From for Config { - fn from(raw: RawConfig) -> Self { - Config { - rootdir: raw.rootdir, - destination: raw.destination, - discover: raw.discover.map(Into::into).unwrap_or_default(), - preview: raw.preview, - local: raw.local, - } - } -} - -/// A resolved package entry ready for gathering. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PackageEntry { - Preview { name: String, version: String }, - Local { name: String, dir: PathBuf }, -} - -impl Config { - /// Parse a TOML configuration string. - pub fn parse(content: &str) -> Result { - let raw: RawConfig = toml::from_str(content)?; - Ok(raw.into()) - } - - /// Convert config into a list of package entries. - pub fn into_entries(self) -> Vec { - let mut entries = Vec::new(); - - for (name, version) in self.preview { - entries.push(PackageEntry::Preview { name, version }); - } - - for (name, dir) in self.local { - entries.push(PackageEntry::Local { - name, - dir: PathBuf::from(dir), - }); - } - - entries - } -} - -/// Context for gathering operations, holding shared state. -struct GatherContext<'a> { - storage: PackageStorage, - dest: &'a Path, - configured_local: &'a HashSet, - processed: HashSet, - stats: Stats, - /// @local imports discovered during scanning (name -> source_file) - discovered_local: HashMap, -} - -impl<'a> GatherContext<'a> { - fn new(dest: &'a Path, configured_local: &'a HashSet) -> Self { - Self { - storage: PackageStorage::new( - Some(dest.to_path_buf()), - None, - Downloader::new("typst-gather/0.1.0"), - ), - dest, - configured_local, - processed: HashSet::new(), - stats: Stats::default(), - discovered_local: HashMap::new(), - } - } -} - -/// Gather packages to the destination directory. -pub fn gather_packages( - dest: &Path, - entries: Vec, - discover_paths: &[PathBuf], - configured_local: &HashSet, -) -> GatherResult { - let mut ctx = GatherContext::new(dest, configured_local); - - // First, process discover paths - for path in discover_paths { - discover_imports(&mut ctx, path); - } - - // Then process explicit entries - for entry in entries { - match entry { - PackageEntry::Preview { name, version } => { - cache_preview(&mut ctx, &name, &version); - } - PackageEntry::Local { name, dir } => { - gather_local(&mut ctx, &name, &dir); - } - } - } - - // Find @local imports that aren't configured - let unconfigured_local: Vec<(String, String)> = ctx.discovered_local - .into_iter() - .filter(|(name, _)| !ctx.configured_local.contains(name)) - .collect(); - - GatherResult { - stats: ctx.stats, - unconfigured_local, - } -} - -/// Scan a path for imports. If it's a directory, scans .typ files in it (non-recursive). -/// If it's a file, scans that file directly. -fn discover_imports(ctx: &mut GatherContext, path: &Path) { - if path.is_file() { - // Single file - if path.extension().is_some_and(|e| e == "typ") { - println!("Discovering imports in {}...", display_path(path)); - scan_file_for_imports(ctx, path); - } - } else if path.is_dir() { - // Directory - scan .typ files (non-recursive) - println!("Discovering imports in {}...", display_path(path)); - - let entries = match std::fs::read_dir(path) { - Ok(e) => e, - Err(e) => { - eprintln!(" Failed to read directory: {e}"); - ctx.stats.failed += 1; - return; - } - }; - - for entry in entries.flatten() { - let file_path = entry.path(); - if file_path.is_file() && file_path.extension().is_some_and(|e| e == "typ") { - scan_file_for_imports(ctx, &file_path); - } - } - } else { - eprintln!("Warning: discover path does not exist: {}", display_path(path)); - } -} - -/// Scan a single .typ file for @preview and @local imports. -/// @preview imports are cached, @local imports are tracked for later warning. -fn scan_file_for_imports(ctx: &mut GatherContext, path: &Path) { - if let Ok(content) = std::fs::read_to_string(path) { - let mut imports = Vec::new(); - collect_imports(&typst_syntax::parse(&content), &mut imports); - - let source_file = path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_else(|| path.display().to_string()); - - for spec in imports { - if spec.namespace == "preview" { - cache_preview_with_deps(ctx, &spec); - } else if spec.namespace == "local" { - // Track @local imports (only first occurrence per package name) - ctx.discovered_local.entry(spec.name.to_string()) - .or_insert(source_file.clone()); - } - } - } -} - -fn cache_preview(ctx: &mut GatherContext, name: &str, version_str: &str) { - let Ok(version): Result = version_str.parse() else { - eprintln!("Invalid version '{version_str}' for @preview/{name}"); - ctx.stats.failed += 1; - return; - }; - - let spec = PackageSpec { - namespace: EcoString::from("preview"), - name: EcoString::from(name), - version, - }; - - cache_preview_with_deps(ctx, &spec); -} - -/// Default exclude patterns for local packages (common non-package files). -const DEFAULT_EXCLUDES: &[&str] = &[ - ".git", - ".git/**", - ".github", - ".github/**", - ".gitignore", - ".gitattributes", - ".vscode", - ".vscode/**", - ".idea", - ".idea/**", - "*.bak", - "*.swp", - "*~", -]; - -fn gather_local(ctx: &mut GatherContext, name: &str, src_dir: &Path) { - // Read typst.toml to get version (and validate name) - let manifest_path = src_dir.join("typst.toml"); - let manifest: PackageManifest = match std::fs::read_to_string(&manifest_path) - .map_err(|e| e.to_string()) - .and_then(|s| toml::from_str(&s).map_err(|e| e.to_string())) - { - Ok(m) => m, - Err(e) => { - eprintln!("Error reading typst.toml for @local/{name}: {e}"); - ctx.stats.failed += 1; - return; - } - }; - - // Validate name matches - if manifest.package.name.as_str() != name { - eprintln!( - "Name mismatch for @local/{name}: typst.toml has '{}'", - manifest.package.name - ); - ctx.stats.failed += 1; - return; - } - - let version = manifest.package.version; - let dest_dir = ctx.dest.join(format!("local/{name}/{version}")); - - println!("Copying @local/{name}:{version}..."); - - // Clean slate: remove destination if exists - if dest_dir.exists() { - if let Err(e) = std::fs::remove_dir_all(&dest_dir) { - eprintln!(" Failed to remove existing dir: {e}"); - ctx.stats.failed += 1; - return; - } - } - - // Build exclude pattern matcher from defaults + manifest excludes - let mut builder = GlobSetBuilder::new(); - for pattern in DEFAULT_EXCLUDES { - if let Ok(glob) = Glob::new(pattern) { - builder.add(glob); - } - } - // Add manifest excludes if present - for pattern in &manifest.package.exclude { - if let Ok(glob) = Glob::new(pattern.as_str()) { - builder.add(glob); - } - } - let excludes = builder.build().unwrap_or_else(|_| GlobSetBuilder::new().build().unwrap()); - - // Copy files, respecting exclude patterns - if let Err(e) = copy_filtered(src_dir, &dest_dir, &excludes) { - eprintln!(" Failed to copy: {e}"); - ctx.stats.failed += 1; - return; - } - - println!(" -> {}", display_path(&dest_dir)); - ctx.stats.copied += 1; - - // Mark as processed - let spec = PackageSpec { - namespace: EcoString::from("local"), - name: EcoString::from(name), - version, - }; - ctx.processed.insert(spec.to_string()); - - // Scan for @preview dependencies - scan_deps(ctx, &dest_dir); -} - -/// Copy directory contents, excluding files that match the exclude patterns. -fn copy_filtered( - src: &Path, - dest: &Path, - excludes: &globset::GlobSet, -) -> std::io::Result<()> { - std::fs::create_dir_all(dest)?; - - for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - let relative = path.strip_prefix(src).unwrap_or(path); - - // Check if this path matches any exclude pattern - if excludes.is_match(relative) { - continue; - } - - let dest_path = dest.join(relative); - - if path.is_dir() { - std::fs::create_dir_all(&dest_path)?; - } else if path.is_file() { - if let Some(parent) = dest_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::copy(path, &dest_path)?; - } - } - - Ok(()) -} - -fn cache_preview_with_deps(ctx: &mut GatherContext, spec: &PackageSpec) { - // Skip @preview packages that are configured as @local (use local version instead) - if ctx.configured_local.contains(spec.name.as_str()) { - return; - } - - let key = spec.to_string(); - if !ctx.processed.insert(key) { - return; - } - - let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version); - let cached_path = ctx.storage.package_cache_path().map(|p| p.join(&subdir)); - - if cached_path.as_ref().is_some_and(|p| p.exists()) { - println!("Skipping {spec} (cached)"); - ctx.stats.skipped += 1; - scan_deps(ctx, cached_path.as_ref().unwrap()); - return; - } - - println!("Downloading {spec}..."); - match ctx.storage.prepare_package(spec, &mut ProgressSink) { - Ok(path) => { - println!(" -> {}", display_path(&path)); - ctx.stats.downloaded += 1; - scan_deps(ctx, &path); - } - Err(e) => { - eprintln!(" Failed: {e:?}"); - ctx.stats.failed += 1; - } - } -} - -fn scan_deps(ctx: &mut GatherContext, dir: &Path) { - for spec in find_imports(dir) { - if spec.namespace == "preview" { - cache_preview_with_deps(ctx, &spec); - } - } -} - -/// Display a path relative to the current working directory. -fn display_path(path: &Path) -> String { - if let Ok(cwd) = env::current_dir() { - if let Ok(relative) = path.strip_prefix(&cwd) { - return relative.display().to_string(); - } - } - path.display().to_string() -} - -/// Find all package imports in `.typ` files under a directory. -pub fn find_imports(dir: &Path) -> Vec { - let mut imports = Vec::new(); - for entry in WalkDir::new(dir).into_iter().flatten() { - if entry.path().extension().is_some_and(|e| e == "typ") { - if let Ok(content) = std::fs::read_to_string(entry.path()) { - collect_imports(&typst_syntax::parse(&content), &mut imports); - } - } - } - imports -} - -/// Extract package imports from a Typst syntax tree. -pub fn collect_imports(node: &SyntaxNode, imports: &mut Vec) { - if let Some(import) = node.cast::() { - if let Some(spec) = try_extract_spec(import.source()) { - imports.push(spec); - } - } - if let Some(include) = node.cast::() { - if let Some(spec) = try_extract_spec(include.source()) { - imports.push(spec); - } - } - for child in node.children() { - collect_imports(child, imports); - } -} - -/// Try to extract a PackageSpec from an expression (if it's an `@namespace/name:version` string). -pub fn try_extract_spec(expr: ast::Expr) -> Option { - if let ast::Expr::Str(s) = expr { - let val = s.get(); - if val.starts_with('@') { - return val.parse().ok(); - } - } - None -} - -#[cfg(test)] -mod tests { - use super::*; - - mod config_parsing { - use super::*; - - #[test] - fn empty_config() { - let config = Config::parse("").unwrap(); - assert!(config.destination.is_none()); - assert!(config.discover.is_empty()); - assert!(config.preview.is_empty()); - assert!(config.local.is_empty()); - } - - #[test] - fn destination_only() { - let toml = r#"destination = "/path/to/cache""#; - let config = Config::parse(toml).unwrap(); - assert_eq!(config.destination, Some(PathBuf::from("/path/to/cache"))); - assert!(config.discover.is_empty()); - assert!(config.preview.is_empty()); - assert!(config.local.is_empty()); - } - - #[test] - fn with_discover_string() { - let toml = r#" -destination = "/cache" -discover = "/path/to/templates" -"#; - let config = Config::parse(toml).unwrap(); - assert_eq!(config.destination, Some(PathBuf::from("/cache"))); - assert_eq!(config.discover, vec![PathBuf::from("/path/to/templates")]); - } - - #[test] - fn with_discover_array() { - let toml = r#" -destination = "/cache" -discover = ["/path/to/templates", "template.typ", "other.typ"] -"#; - let config = Config::parse(toml).unwrap(); - assert_eq!(config.destination, Some(PathBuf::from("/cache"))); - assert_eq!( - config.discover, - vec![ - PathBuf::from("/path/to/templates"), - PathBuf::from("template.typ"), - PathBuf::from("other.typ"), - ] - ); - } - - #[test] - fn preview_only() { - let toml = r#" -destination = "/cache" - -[preview] -cetz = "0.4.1" -fletcher = "0.5.3" -"#; - let config = Config::parse(toml).unwrap(); - assert_eq!(config.destination, Some(PathBuf::from("/cache"))); - assert_eq!(config.preview.len(), 2); - assert_eq!(config.preview.get("cetz"), Some(&"0.4.1".to_string())); - assert_eq!(config.preview.get("fletcher"), Some(&"0.5.3".to_string())); - assert!(config.local.is_empty()); - } - - #[test] - fn local_only() { - let toml = r#" -destination = "/cache" - -[local] -my-pkg = "/path/to/pkg" -other = "../relative/path" -"#; - let config = Config::parse(toml).unwrap(); - assert!(config.preview.is_empty()); - assert_eq!(config.local.len(), 2); - assert_eq!(config.local.get("my-pkg"), Some(&"/path/to/pkg".to_string())); - assert_eq!(config.local.get("other"), Some(&"../relative/path".to_string())); - } - - #[test] - fn mixed_config() { - let toml = r#" -destination = "/cache" - -[preview] -cetz = "0.4.1" - -[local] -my-pkg = "/path/to/pkg" -"#; - let config = Config::parse(toml).unwrap(); - assert_eq!(config.destination, Some(PathBuf::from("/cache"))); - assert_eq!(config.preview.len(), 1); - assert_eq!(config.local.len(), 1); - } - - #[test] - fn into_entries() { - let toml = r#" -destination = "/cache" - -[preview] -cetz = "0.4.1" - -[local] -my-pkg = "/path/to/pkg" -"#; - let config = Config::parse(toml).unwrap(); - let entries = config.into_entries(); - assert_eq!(entries.len(), 2); - - let has_preview = entries.iter().any(|e| { - matches!(e, PackageEntry::Preview { name, version } - if name == "cetz" && version == "0.4.1") - }); - let has_local = entries.iter().any(|e| { - matches!(e, PackageEntry::Local { name, dir } - if name == "my-pkg" && dir == Path::new("/path/to/pkg")) - }); - assert!(has_preview); - assert!(has_local); - } - - #[test] - fn invalid_toml() { - let result = Config::parse("not valid toml [[["); - assert!(result.is_err()); - } - - #[test] - fn extra_fields_ignored() { - let toml = r#" -destination = "/cache" - -[preview] -cetz = "0.4.1" - -[unknown_section] -foo = "bar" -"#; - // Should not error on unknown sections - let config = Config::parse(toml).unwrap(); - assert_eq!(config.preview.len(), 1); - } - } - - mod import_parsing { - use super::*; - - fn parse_imports(code: &str) -> Vec { - let mut imports = Vec::new(); - collect_imports(&typst_syntax::parse(code), &mut imports); - imports - } - - #[test] - fn simple_import() { - let imports = parse_imports(r#"#import "@preview/cetz:0.4.1""#); - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].namespace, "preview"); - assert_eq!(imports[0].name, "cetz"); - assert_eq!(imports[0].version.to_string(), "0.4.1"); - } - - #[test] - fn import_with_items() { - let imports = parse_imports(r#"#import "@preview/cetz:0.4.1": canvas, draw"#); - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].name, "cetz"); - } - - #[test] - fn multiple_imports() { - let code = r#" -#import "@preview/cetz:0.4.1" -#import "@preview/fletcher:0.5.3" -"#; - let imports = parse_imports(code); - assert_eq!(imports.len(), 2); - } - - #[test] - fn include_statement() { - let imports = parse_imports(r#"#include "@preview/template:1.0.0""#); - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].name, "template"); - } - - #[test] - fn local_import_ignored_in_extract() { - // Local imports are valid but won't be recursively fetched - let imports = parse_imports(r#"#import "@local/my-pkg:1.0.0""#); - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].namespace, "local"); - } - - #[test] - fn relative_import_ignored() { - let imports = parse_imports(r#"#import "utils.typ""#); - assert_eq!(imports.len(), 0); - } - - #[test] - fn no_imports() { - let imports = parse_imports(r#"= Hello World"#); - assert_eq!(imports.len(), 0); - } - - #[test] - fn nested_in_function() { - let code = r#" -#let setup() = { - import "@preview/cetz:0.4.1" -} -"#; - let imports = parse_imports(code); - assert_eq!(imports.len(), 1); - } - - #[test] - fn invalid_package_spec_ignored() { - // Missing version - let imports = parse_imports(r#"#import "@preview/cetz""#); - assert_eq!(imports.len(), 0); - } - - #[test] - fn complex_document() { - let code = r#" -#import "@preview/cetz:0.4.1": canvas -#import "@preview/fletcher:0.5.3": diagram, node, edge -#import "local-file.typ": helper - -= My Document - -#include "@preview/template:1.0.0" - -Some content here. - -#let f() = { - import "@preview/codly:1.2.0" -} -"#; - let imports = parse_imports(code); - assert_eq!(imports.len(), 4); - - let names: Vec<_> = imports.iter().map(|s| s.name.as_str()).collect(); - assert!(names.contains(&"cetz")); - assert!(names.contains(&"fletcher")); - assert!(names.contains(&"template")); - assert!(names.contains(&"codly")); - } - } - - mod stats { - use super::*; - - #[test] - fn default_stats() { - let stats = Stats::default(); - assert_eq!(stats.downloaded, 0); - assert_eq!(stats.copied, 0); - assert_eq!(stats.skipped, 0); - assert_eq!(stats.failed, 0); - } - } - - mod local_override { - use super::*; - - /// When a package is configured in [local], @preview imports of the same - /// package name should be skipped. This handles the case where a local - /// package contains template examples that import from @preview. - #[test] - fn configured_local_contains_check() { - let mut configured_local = HashSet::new(); - configured_local.insert("my-pkg".to_string()); - configured_local.insert("other-pkg".to_string()); - - // These should be skipped (configured as local) - assert!(configured_local.contains("my-pkg")); - assert!(configured_local.contains("other-pkg")); - - // These should NOT be skipped (not configured) - assert!(!configured_local.contains("cetz")); - assert!(!configured_local.contains("fletcher")); - } - } - - mod copy_filtering { - use super::*; - - #[test] - fn default_excludes_match_git() { - let mut builder = GlobSetBuilder::new(); - for pattern in DEFAULT_EXCLUDES { - builder.add(Glob::new(pattern).unwrap()); - } - let excludes = builder.build().unwrap(); - - // Should match .git and contents - assert!(excludes.is_match(".git")); - assert!(excludes.is_match(".git/config")); - assert!(excludes.is_match(".git/objects/pack/foo")); - - // Should match .github - assert!(excludes.is_match(".github")); - assert!(excludes.is_match(".github/workflows/ci.yml")); - - // Should match editor files - assert!(excludes.is_match(".gitignore")); - assert!(excludes.is_match("foo.bak")); - assert!(excludes.is_match("foo.swp")); - assert!(excludes.is_match("foo~")); - - // Should NOT match normal files - assert!(!excludes.is_match("lib.typ")); - assert!(!excludes.is_match("typst.toml")); - assert!(!excludes.is_match("src/main.typ")); - assert!(!excludes.is_match("template/main.typ")); - } - } -} diff --git a/package/typst-gather/src/main.rs b/package/typst-gather/src/main.rs deleted file mode 100644 index 07d951022a9..00000000000 --- a/package/typst-gather/src/main.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::collections::HashSet; -use std::path::PathBuf; -use std::process::ExitCode; - -use clap::Parser; -use typst_gather::{gather_packages, Config}; - -#[derive(Parser)] -#[command(version, about = "Gather Typst packages to a local directory")] -struct Args { - /// TOML file specifying packages to gather - spec_file: PathBuf, -} - -fn main() -> ExitCode { - let args = Args::parse(); - - let content = match std::fs::read_to_string(&args.spec_file) { - Ok(c) => c, - Err(e) => { - eprintln!("Error reading spec file: {e}"); - return ExitCode::FAILURE; - } - }; - - let config = match Config::parse(&content) { - Ok(c) => c, - Err(e) => { - eprintln!("Error parsing spec file: {e}"); - return ExitCode::FAILURE; - } - }; - - let dest = match &config.destination { - Some(d) => d.clone(), - None => { - eprintln!("Error: 'destination' field is required in spec file"); - return ExitCode::FAILURE; - } - }; - - // Resolve paths relative to rootdir if specified - let rootdir = config.rootdir.clone(); - let dest = match &rootdir { - Some(root) => root.join(&dest), - None => dest, - }; - let discover: Vec = config - .discover - .iter() - .map(|p| match &rootdir { - Some(root) => root.join(p), - None => p.clone(), - }) - .collect(); - - // Build set of configured local packages - let configured_local: HashSet = config.local.keys().cloned().collect(); - - let entries = config.into_entries(); - let result = gather_packages(&dest, entries, &discover, &configured_local); - - // Check for unconfigured @local imports FIRST (this is an error) - if !result.unconfigured_local.is_empty() { - eprintln!("\nError: Found @local imports not configured in [local] section:"); - for (name, source_file) in &result.unconfigured_local { - eprintln!(" - {name} (in {source_file})"); - } - eprintln!("\nAdd them to your config file:"); - eprintln!(" [local]"); - for (name, _) in &result.unconfigured_local { - eprintln!(" {name} = \"/path/to/{name}\""); - } - return ExitCode::FAILURE; - } - - println!( - "\nDone: {} downloaded, {} copied, {} skipped, {} failed", - result.stats.downloaded, result.stats.copied, result.stats.skipped, result.stats.failed - ); - - if result.stats.failed > 0 { - ExitCode::FAILURE - } else { - ExitCode::SUCCESS - } -} diff --git a/package/typst-gather/tests/integration.rs b/package/typst-gather/tests/integration.rs deleted file mode 100644 index 1d4c09e1198..00000000000 --- a/package/typst-gather/tests/integration.rs +++ /dev/null @@ -1,543 +0,0 @@ -//! Integration tests for typst-gather. -//! -//! These tests verify the full gathering workflow including: -//! - Local package copying -//! - Dependency scanning from .typ files -//! - Preview package caching (requires network) - -use std::collections::HashSet; -use std::fs; -use std::path::Path; - -use tempfile::TempDir; -use typst_gather::{gather_packages, find_imports, Config, PackageEntry}; - -/// Helper to create a minimal local package with typst.toml -fn create_local_package(dir: &Path, name: &str, version: &str, typ_content: Option<&str>) { - fs::create_dir_all(dir).unwrap(); - - let manifest = format!( - r#"[package] -name = "{name}" -version = "{version}" -entrypoint = "lib.typ" -"# - ); - fs::write(dir.join("typst.toml"), manifest).unwrap(); - - let content = typ_content.unwrap_or("// Empty package\n"); - fs::write(dir.join("lib.typ"), content).unwrap(); -} - -mod local_packages { - use super::*; - - #[test] - fn cache_single_local_package() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - create_local_package(src_dir.path(), "my-pkg", "1.0.0", None); - - let entries = vec![PackageEntry::Local { - name: "my-pkg".to_string(), - dir: src_dir.path().to_path_buf(), - }]; - - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 1); - assert_eq!(result.stats.failed, 0); - - // Verify package was copied to correct location - let cached = cache_dir.path().join("local/my-pkg/1.0.0"); - assert!(cached.exists()); - assert!(cached.join("typst.toml").exists()); - assert!(cached.join("lib.typ").exists()); - } - - #[test] - fn cache_local_package_overwrites_existing() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - // Create initial version - create_local_package(src_dir.path(), "my-pkg", "1.0.0", Some("// v1")); - - let entries = vec![PackageEntry::Local { - name: "my-pkg".to_string(), - dir: src_dir.path().to_path_buf(), - }]; - - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - gather_packages(cache_dir.path(), entries.clone(), &[], &configured_local); - - // Update source - fs::write(src_dir.path().join("lib.typ"), "// v2").unwrap(); - - // Cache again - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - assert_eq!(result.stats.copied, 1); - - // Verify new content - let cached_lib = cache_dir.path().join("local/my-pkg/1.0.0/lib.typ"); - let content = fs::read_to_string(cached_lib).unwrap(); - assert_eq!(content, "// v2"); - } - - #[test] - fn cache_multiple_local_packages() { - let src1 = TempDir::new().unwrap(); - let src2 = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - create_local_package(src1.path(), "pkg-one", "1.0.0", None); - create_local_package(src2.path(), "pkg-two", "2.0.0", None); - - let entries = vec![ - PackageEntry::Local { - name: "pkg-one".to_string(), - dir: src1.path().to_path_buf(), - }, - PackageEntry::Local { - name: "pkg-two".to_string(), - dir: src2.path().to_path_buf(), - }, - ]; - - let configured_local: HashSet = ["pkg-one".to_string(), "pkg-two".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 2); - assert!(cache_dir.path().join("local/pkg-one/1.0.0").exists()); - assert!(cache_dir.path().join("local/pkg-two/2.0.0").exists()); - } - - #[test] - fn fail_on_name_mismatch() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - // Create package with different name in manifest - create_local_package(src_dir.path(), "actual-name", "1.0.0", None); - - let entries = vec![PackageEntry::Local { - name: "wrong-name".to_string(), - dir: src_dir.path().to_path_buf(), - }]; - - let configured_local: HashSet = ["wrong-name".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 0); - assert_eq!(result.stats.failed, 1); - } - - #[test] - fn fail_on_missing_manifest() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - // Create directory without typst.toml - fs::create_dir_all(src_dir.path()).unwrap(); - fs::write(src_dir.path().join("lib.typ"), "// no manifest").unwrap(); - - let entries = vec![PackageEntry::Local { - name: "my-pkg".to_string(), - dir: src_dir.path().to_path_buf(), - }]; - - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 0); - assert_eq!(result.stats.failed, 1); - } - - #[test] - fn fail_on_nonexistent_directory() { - let cache_dir = TempDir::new().unwrap(); - - let entries = vec![PackageEntry::Local { - name: "my-pkg".to_string(), - dir: "/nonexistent/path/to/package".into(), - }]; - - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 0); - assert_eq!(result.stats.failed, 1); - } - - #[test] - fn preserves_subdirectories() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - create_local_package(src_dir.path(), "my-pkg", "1.0.0", None); - - // Add subdirectory with files - let sub = src_dir.path().join("src/utils"); - fs::create_dir_all(&sub).unwrap(); - fs::write(sub.join("helper.typ"), "// helper").unwrap(); - - let entries = vec![PackageEntry::Local { - name: "my-pkg".to_string(), - dir: src_dir.path().to_path_buf(), - }]; - - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 1); - - let cached_helper = cache_dir - .path() - .join("local/my-pkg/1.0.0/src/utils/helper.typ"); - assert!(cached_helper.exists()); - } -} - -mod dependency_scanning { - use super::*; - - #[test] - fn find_imports_in_single_file() { - let dir = TempDir::new().unwrap(); - - let content = r#" -#import "@preview/cetz:0.4.1": canvas -#import "@preview/fletcher:0.5.3" - -= Document -"#; - fs::write(dir.path().join("main.typ"), content).unwrap(); - - let imports = find_imports(dir.path()); - - assert_eq!(imports.len(), 2); - let names: Vec<_> = imports.iter().map(|s| s.name.as_str()).collect(); - assert!(names.contains(&"cetz")); - assert!(names.contains(&"fletcher")); - } - - #[test] - fn find_imports_in_nested_files() { - let dir = TempDir::new().unwrap(); - - fs::write( - dir.path().join("main.typ"), - r#"#import "@preview/cetz:0.4.1""#, - ) - .unwrap(); - - let sub = dir.path().join("chapters"); - fs::create_dir_all(&sub).unwrap(); - fs::write(sub.join("intro.typ"), r#"#import "@preview/fletcher:0.5.3""#).unwrap(); - - let imports = find_imports(dir.path()); - - assert_eq!(imports.len(), 2); - } - - #[test] - fn ignore_non_typ_files() { - let dir = TempDir::new().unwrap(); - - fs::write( - dir.path().join("main.typ"), - r#"#import "@preview/cetz:0.4.1""#, - ) - .unwrap(); - fs::write( - dir.path().join("notes.txt"), - r#"#import "@preview/ignored:1.0.0""#, - ) - .unwrap(); - - let imports = find_imports(dir.path()); - - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].name, "cetz"); - } - - #[test] - fn find_includes() { - let dir = TempDir::new().unwrap(); - - let content = r#"#include "@preview/template:1.0.0""#; - fs::write(dir.path().join("main.typ"), content).unwrap(); - - let imports = find_imports(dir.path()); - - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].name, "template"); - } - - #[test] - fn ignore_relative_imports() { - let dir = TempDir::new().unwrap(); - - let content = r#" -#import "@preview/cetz:0.4.1" -#import "utils.typ" -#import "../shared/common.typ" -"#; - fs::write(dir.path().join("main.typ"), content).unwrap(); - - let imports = find_imports(dir.path()); - - assert_eq!(imports.len(), 1); - assert_eq!(imports[0].name, "cetz"); - } - - #[test] - fn empty_directory() { - let dir = TempDir::new().unwrap(); - let imports = find_imports(dir.path()); - assert!(imports.is_empty()); - } -} - -mod config_integration { - use super::*; - - #[test] - fn parse_and_cache_local_from_toml() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - create_local_package(src_dir.path(), "my-pkg", "1.0.0", None); - - let toml = format!( - r#" -destination = "{}" - -[local] -my-pkg = "{}" -"#, - cache_dir.path().display(), - src_dir.path().display() - ); - - let config = Config::parse(&toml).unwrap(); - let dest = config.destination.clone().unwrap(); - let configured_local: HashSet = config.local.keys().cloned().collect(); - let entries = config.into_entries(); - let result = gather_packages(&dest, entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 1); - assert!(cache_dir.path().join("local/my-pkg/1.0.0").exists()); - } - - #[test] - fn empty_config_does_nothing() { - let cache_dir = TempDir::new().unwrap(); - - let toml = format!(r#"destination = "{}""#, cache_dir.path().display()); - let config = Config::parse(&toml).unwrap(); - let dest = config.destination.clone().unwrap(); - let configured_local: HashSet = config.local.keys().cloned().collect(); - let entries = config.into_entries(); - let result = gather_packages(&dest, entries, &[], &configured_local); - - assert_eq!(result.stats.downloaded, 0); - assert_eq!(result.stats.copied, 0); - assert_eq!(result.stats.skipped, 0); - assert_eq!(result.stats.failed, 0); - } - - #[test] - fn missing_destination_returns_none() { - let config = Config::parse("").unwrap(); - assert!(config.destination.is_none()); - } - - #[test] - fn parse_discover_field() { - let toml = r#" -destination = "/cache" -discover = "/path/to/templates" -"#; - let config = Config::parse(toml).unwrap(); - assert_eq!( - config.discover, - vec![std::path::PathBuf::from("/path/to/templates")] - ); - } - - #[test] - fn parse_discover_array() { - let toml = r#" -destination = "/cache" -discover = ["template.typ", "typst-show.typ"] -"#; - let config = Config::parse(toml).unwrap(); - assert_eq!( - config.discover, - vec![ - std::path::PathBuf::from("template.typ"), - std::path::PathBuf::from("typst-show.typ"), - ] - ); - } -} - -mod unconfigured_local { - use super::*; - - #[test] - fn detects_unconfigured_local_imports() { - let cache_dir = TempDir::new().unwrap(); - let discover_dir = TempDir::new().unwrap(); - - // Create a .typ file that imports @local/my-pkg - let content = r#"#import "@local/my-pkg:1.0.0""#; - fs::write(discover_dir.path().join("template.typ"), content).unwrap(); - - // Don't configure my-pkg in the local section - let configured_local: HashSet = HashSet::new(); - let discover = vec![discover_dir.path().to_path_buf()]; - - let result = gather_packages(cache_dir.path(), vec![], &discover, &configured_local); - - // Should have one unconfigured local - assert_eq!(result.unconfigured_local.len(), 1); - assert_eq!(result.unconfigured_local[0].0, "my-pkg"); - } - - #[test] - fn configured_local_not_reported() { - let cache_dir = TempDir::new().unwrap(); - let discover_dir = TempDir::new().unwrap(); - - // Create a .typ file that imports @local/my-pkg - let content = r#"#import "@local/my-pkg:1.0.0""#; - fs::write(discover_dir.path().join("template.typ"), content).unwrap(); - - // Configure my-pkg (even though we don't actually copy it) - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - let discover = vec![discover_dir.path().to_path_buf()]; - - let result = gather_packages(cache_dir.path(), vec![], &discover, &configured_local); - - // Should have no unconfigured local - assert!(result.unconfigured_local.is_empty()); - } -} - -/// Tests that require network access. -/// Run with: cargo test -- --ignored -mod network { - use super::*; - - #[test] - #[ignore = "requires network access"] - fn download_preview_package() { - let cache_dir = TempDir::new().unwrap(); - - let entries = vec![PackageEntry::Preview { - name: "example".to_string(), - version: "0.1.0".to_string(), - }]; - - let configured_local = HashSet::new(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.downloaded, 1); - assert_eq!(result.stats.failed, 0); - - let cached = cache_dir.path().join("preview/example/0.1.0"); - assert!(cached.exists()); - assert!(cached.join("typst.toml").exists()); - } - - #[test] - #[ignore = "requires network access"] - fn download_package_with_dependencies() { - let cache_dir = TempDir::new().unwrap(); - - // cetz has dependencies that should be auto-downloaded - let entries = vec![PackageEntry::Preview { - name: "cetz".to_string(), - version: "0.3.4".to_string(), - }]; - - let configured_local = HashSet::new(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - // Should download cetz plus its dependencies - assert!(result.stats.downloaded >= 1); - assert_eq!(result.stats.failed, 0); - } - - #[test] - #[ignore = "requires network access"] - fn skip_already_cached() { - let cache_dir = TempDir::new().unwrap(); - - let entries = vec![PackageEntry::Preview { - name: "example".to_string(), - version: "0.1.0".to_string(), - }]; - - let configured_local = HashSet::new(); - - // First download - let result1 = gather_packages(cache_dir.path(), entries.clone(), &[], &configured_local); - assert_eq!(result1.stats.downloaded, 1); - - // Second run should skip - let result2 = gather_packages(cache_dir.path(), entries, &[], &configured_local); - assert_eq!(result2.stats.downloaded, 0); - assert_eq!(result2.stats.skipped, 1); - } - - #[test] - #[ignore = "requires network access"] - fn fail_on_nonexistent_package() { - let cache_dir = TempDir::new().unwrap(); - - let entries = vec![PackageEntry::Preview { - name: "this-package-does-not-exist-12345".to_string(), - version: "0.0.0".to_string(), - }]; - - let configured_local = HashSet::new(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.downloaded, 0); - assert_eq!(result.stats.failed, 1); - } - - #[test] - #[ignore = "requires network access"] - fn local_package_triggers_preview_deps() { - let src_dir = TempDir::new().unwrap(); - let cache_dir = TempDir::new().unwrap(); - - // Create local package that imports a preview package - let content = r#" -#import "@preview/example:0.1.0" - -#let my-func() = [] -"#; - create_local_package(src_dir.path(), "my-pkg", "1.0.0", Some(content)); - - let entries = vec![PackageEntry::Local { - name: "my-pkg".to_string(), - dir: src_dir.path().to_path_buf(), - }]; - - let configured_local: HashSet = ["my-pkg".to_string()].into_iter().collect(); - let result = gather_packages(cache_dir.path(), entries, &[], &configured_local); - - assert_eq!(result.stats.copied, 1); - assert!(result.stats.downloaded >= 1); // Should have downloaded example - - assert!(cache_dir.path().join("local/my-pkg/1.0.0").exists()); - assert!(cache_dir.path().join("preview/example/0.1.0").exists()); - } -} diff --git a/src/command/call/typst-gather/cmd.ts b/src/command/call/typst-gather/cmd.ts index 280ba60089f..de88ee02b23 100644 --- a/src/command/call/typst-gather/cmd.ts +++ b/src/command/call/typst-gather/cmd.ts @@ -7,19 +7,18 @@ import { Command } from "cliffy/command/mod.ts"; import { info } from "../../../deno_ral/log.ts"; -import { architectureToolsPath } from "../../../core/resources.ts"; import { execProcess } from "../../../core/process.ts"; import { dirname, join, relative } from "../../../deno_ral/path.ts"; import { existsSync } from "../../../deno_ral/fs.ts"; -import { isWindows } from "../../../deno_ral/platform.ts"; import { expandGlobSync } from "../../../core/deno/expand-glob.ts"; import { readYaml } from "../../../core/yaml.ts"; - -// Convert path to use forward slashes for TOML compatibility -// TOML treats backslash as escape character, so Windows paths must use forward slashes -function toTomlPath(p: string): string { - return p.replace(/\\/g, "/"); -} +import { + type AnalyzeImport, + type AnalyzeResult, + runAnalyze, + toTomlPath, + typstGatherBinaryPath, +} from "../../../core/typst-gather.ts"; interface ExtensionYml { contributes?: { @@ -116,12 +115,11 @@ async function resolveConfig( const configPath = join(cwd, "typst-gather.toml"); if (existsSync(configPath)) { info(`Using config: ${configPath}`); - // Return the config file path - rust will parse it directly - // We still parse minimally to validate and show info - const content = Deno.readTextFileSync(configPath); - const config = parseSimpleToml(content); - config.configFile = configPath; - return config; + return { + configFile: configPath, + destination: "", + discover: [], + }; } // No config file - try to auto-detect from _extension.yml @@ -164,105 +162,10 @@ async function resolveConfig( }; } -function parseSimpleToml(content: string): TypstGatherConfig { - const lines = content.split("\n"); - let rootdir: string | undefined; - let destination = ""; - const discover: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - - // Parse rootdir - const rootdirMatch = trimmed.match(/^rootdir\s*=\s*"([^"]+)"/); - if (rootdirMatch) { - rootdir = rootdirMatch[1]; - continue; - } - - // Parse destination - const destMatch = trimmed.match(/^destination\s*=\s*"([^"]+)"/); - if (destMatch) { - destination = destMatch[1]; - continue; - } - - // Parse discover as string - const discoverStrMatch = trimmed.match(/^discover\s*=\s*"([^"]+)"/); - if (discoverStrMatch) { - discover.push(discoverStrMatch[1]); - continue; - } - - // Parse discover as array (simple single-line parsing) - const discoverArrMatch = trimmed.match(/^discover\s*=\s*\[([^\]]+)\]/); - if (discoverArrMatch) { - const items = discoverArrMatch[1].split(","); - for (const item of items) { - const match = item.trim().match(/"([^"]+)"/); - if (match) { - discover.push(match[1]); - } - } - } - } - - return { rootdir, destination, discover }; -} - -interface DiscoveredImport { - name: string; - version: string; - sourceFile: string; -} - -interface DiscoveryResult { - preview: DiscoveredImport[]; - local: DiscoveredImport[]; - scannedFiles: string[]; -} - -function discoverImportsFromFiles(files: string[]): DiscoveryResult { - const result: DiscoveryResult = { - preview: [], - local: [], - scannedFiles: [], - }; - - // Regex to match @namespace/name:version imports - // Note: #include is for files, not packages, so we only match #import - const importRegex = /#import\s+"@(\w+)\/([^:]+):([^"]+)"/g; - - for (const file of files) { - if (!existsSync(file)) continue; - if (!file.endsWith(".typ")) continue; - - const filename = file.split("/").pop() || file; - result.scannedFiles.push(filename); - - try { - const content = Deno.readTextFileSync(file); - let match; - while ((match = importRegex.exec(content)) !== null) { - const [, namespace, name, version] = match; - const entry = { name, version, sourceFile: filename }; - - if (namespace === "preview") { - result.preview.push(entry); - } else if (namespace === "local") { - result.local.push(entry); - } - } - } catch { - // Skip files that can't be read - } - } - - return result; -} +export type { AnalyzeImport, AnalyzeResult }; -function generateConfigContent( - discovery: DiscoveryResult, +export function generateConfigFromAnalysis( + result: AnalyzeResult, rootdir?: string, ): string { const lines: string[] = []; @@ -278,14 +181,11 @@ function generateConfigContent( lines.push(""); // Discover section - if (discovery.scannedFiles.length > 0) { - if (discovery.scannedFiles.length === 1) { - lines.push(`discover = "${toTomlPath(discovery.scannedFiles[0])}"`); - } else { - const files = discovery.scannedFiles.map((f) => `"${toTomlPath(f)}"`) - .join(", "); - lines.push(`discover = [${files}]`); - } + if (result.files.length === 1) { + lines.push(`discover = "${toTomlPath(result.files[0])}"`); + } else if (result.files.length > 1) { + const files = result.files.map((f) => `"${toTomlPath(f)}"`).join(", "); + lines.push(`discover = [${files}]`); } else { lines.push('# discover = "template.typ" # Add your .typ files here'); } @@ -293,16 +193,19 @@ function generateConfigContent( lines.push(""); // Preview section (commented out - packages will be auto-discovered) + const previewImports = result.imports.filter((i) => + i.namespace === "preview" + ); lines.push("# Preview packages are auto-discovered from imports."); lines.push("# Uncomment to pin specific versions:"); lines.push("# [preview]"); - if (discovery.preview.length > 0) { - // Deduplicate + if (previewImports.length > 0) { const seen = new Set(); - for (const { name, version } of discovery.preview) { + for (const { name, version, direct, source } of previewImports) { if (!seen.has(name)) { seen.add(name); - lines.push(`# ${name} = "${version}"`); + const suffix = direct ? "" : ` # via ${source}`; + lines.push(`# ${name} = "${version}"${suffix}`); } } } else { @@ -312,21 +215,24 @@ function generateConfigContent( lines.push(""); // Local section + const localImports = result.imports.filter( + (i) => i.namespace === "local" && i.direct, + ); lines.push( "# Local packages (@local namespace) must be configured manually.", ); - if (discovery.local.length > 0) { + if (localImports.length > 0) { lines.push("# Found @local imports:"); const seen = new Set(); - for (const { name, version, sourceFile } of discovery.local) { + for (const { name, version, source } of localImports) { if (!seen.has(name)) { seen.add(name); - lines.push(`# @local/${name}:${version} (in ${sourceFile})`); + lines.push(`# @local/${name}:${version} (in ${source})`); } } lines.push("[local]"); seen.clear(); - for (const { name } of discovery.local) { + for (const { name } of localImports) { if (!seen.has(name)) { seen.add(name); lines.push(`${name} = "/path/to/${name}" # TODO: set correct path`); @@ -373,14 +279,18 @@ async function initConfig(): Promise { info(`Found extension: ${extensionDir}`); } - // Discover imports from the files - const discovery = discoverImportsFromFiles(typFiles); + // Build analyze config with discover paths + const discoverArray = typFiles.map((f) => `"${toTomlPath(f)}"`).join(", "); + const analyzeConfig = `discover = [${discoverArray}]\n`; + + // Run typst-gather analyze to discover imports + const analysis = await runAnalyze(analyzeConfig); // Calculate relative path from cwd to extension dir for rootdir const rootdir = relative(Deno.cwd(), extensionDir); - // Generate config content - const configContent = generateConfigContent(discovery, rootdir); + // Generate config content from analysis + const configContent = generateConfigFromAnalysis(analysis, rootdir); // Write config file try { @@ -390,23 +300,30 @@ async function initConfig(): Promise { Deno.exit(1); } + const previewImports = analysis.imports.filter( + (i) => i.namespace === "preview", + ); + const localImports = analysis.imports.filter( + (i) => i.namespace === "local" && i.direct, + ); + info("Created typst-gather.toml"); - if (discovery.scannedFiles.length > 0) { - info(` Scanned: ${discovery.scannedFiles.join(", ")}`); + if (analysis.files.length > 0) { + info(` Scanned: ${analysis.files.join(", ")}`); } - if (discovery.preview.length > 0) { - info(` Found ${discovery.preview.length} @preview import(s)`); + if (previewImports.length > 0) { + info(` Found ${previewImports.length} @preview import(s)`); } - if (discovery.local.length > 0) { + if (localImports.length > 0) { info( - ` Found ${discovery.local.length} @local import(s) - configure paths in [local] section`, + ` Found ${localImports.length} @local import(s) - configure paths in [local] section`, ); } info(""); info("Next steps:"); info(" 1. Review and edit typst-gather.toml"); - if (discovery.local.length > 0) { + if (localImports.length > 0) { info(" 2. Add paths for @local packages in [local] section"); } info(" 3. Run: quarto call typst-gather"); @@ -442,37 +359,21 @@ export const typstGatherCommand = new Command() Deno.exit(1); } - if (!config.destination) { - console.error("No destination specified in configuration."); - Deno.exit(1); - } - - if (config.discover.length === 0) { - console.error("No files to discover imports from."); - Deno.exit(1); - } - - // Find typst-gather binary in standard tools location - const binaryName = isWindows ? "typst-gather.exe" : "typst-gather"; - const typstGatherBinary = architectureToolsPath(binaryName); - if (!existsSync(typstGatherBinary)) { - console.error( - `typst-gather binary not found.\n` + - `Run ./configure.sh to build and install it.`, - ); - Deno.exit(1); - } + const typstGatherBinary = typstGatherBinaryPath(); - // Determine config file to use - let configFileToUse: string; - let tempConfig: string | null = null; + info(`Running typst-gather...`); + // Run typst-gather gather + let result; if (config.configFile) { - // Use existing config file directly - rust will parse [local], [preview], etc. - configFileToUse = config.configFile; + // Existing config file — pass directly + result = await execProcess({ + cmd: typstGatherBinary, + args: ["gather", config.configFile], + cwd: Deno.cwd(), + }); } else { - // Create a temporary TOML config file for auto-detected config - tempConfig = Deno.makeTempFileSync({ suffix: ".toml" }); + // Auto-detected — pipe config on stdin const discoverArray = config.discover.map((p) => `"${toTomlPath(p)}"`) .join(", "); let tomlContent = ""; @@ -481,26 +382,15 @@ export const typstGatherCommand = new Command() } tomlContent += `destination = "${toTomlPath(config.destination)}"\n`; tomlContent += `discover = [${discoverArray}]\n`; - Deno.writeTextFileSync(tempConfig, tomlContent); - configFileToUse = tempConfig; - } - info(`Running typst-gather...`); - - // Run typst-gather - const result = await execProcess({ - cmd: typstGatherBinary, - args: [configFileToUse], - cwd: Deno.cwd(), - }); - - // Clean up temp file if we created one - if (tempConfig) { - try { - Deno.removeSync(tempConfig); - } catch { - // Ignore cleanup errors - } + result = await execProcess( + { + cmd: typstGatherBinary, + args: ["gather", "-"], + cwd: Deno.cwd(), + }, + tomlContent, + ); } if (!result.success) { diff --git a/src/command/dev-call/typst-gather/cmd.ts b/src/command/dev-call/typst-gather/cmd.ts index 8c41c4c25c4..abc51188b7d 100644 --- a/src/command/dev-call/typst-gather/cmd.ts +++ b/src/command/dev-call/typst-gather/cmd.ts @@ -37,7 +37,8 @@ export const typstGatherCommand = new Command() // Find typst-gather binary in standard tools location const binaryName = isWindows ? "typst-gather.exe" : "typst-gather"; - const typstGatherBinary = architectureToolsPath(binaryName); + const typstGatherBinary = Deno.env.get("QUARTO_TYPST_GATHER") || + architectureToolsPath(binaryName); if (!existsSync(typstGatherBinary)) { error( `typst-gather binary not found.\n` + @@ -52,7 +53,7 @@ export const typstGatherCommand = new Command() // Run typst-gather from the quarto root directory const command = new Deno.Command(typstGatherBinary, { - args: [tomlPath], + args: ["gather", tomlPath], cwd: quartoRoot, stdout: "inherit", stderr: "inherit", diff --git a/src/command/render/output-typst.ts b/src/command/render/output-typst.ts index 6c970405bad..0d0addb3ff7 100644 --- a/src/command/render/output-typst.ts +++ b/src/command/render/output-typst.ts @@ -48,70 +48,150 @@ import { TypstCompileOptions, validateRequiredTypstVersion, } from "../../core/typst.ts"; +import { runAnalyze, toTomlPath } from "../../core/typst-gather.ts"; import { asArray } from "../../core/array.ts"; import { ProjectContext } from "../../project/types.ts"; import { validatePdfStandards } from "../../core/verapdf.ts"; -// Stage typst packages to .quarto/typst-packages/ -// First stages built-in packages, then extension packages (which can override) -async function stageTypstPackages( - input: string, - projectDir?: string, -): Promise { - if (!projectDir) { - return undefined; - } +export interface NeededPackage { + namespace: string; + name: string; + version: string; +} - const packageSources: string[] = []; +// Collect all package source directories (built-in + extensions) +async function collectPackageSources( + input: string, + projectDir: string, +): Promise { + const sources: string[] = []; - // 1. Add built-in packages from quarto resources + // 1. Built-in packages const builtinPackages = resourcePath("formats/typst/packages"); if (existsSync(builtinPackages)) { - packageSources.push(builtinPackages); + sources.push(builtinPackages); } - // 2. Add packages from extensions (can override built-in) + // 2. Extension packages const extensionDirs = inputExtensionDirs(input, projectDir); const subtreePath = builtinSubtreeExtensions(); for (const extDir of extensionDirs) { - // Use readSubtreeExtensions for subtree directory, readExtensions for others const extensions = extDir === subtreePath ? await readSubtreeExtensions(extDir) : await readExtensions(extDir); for (const ext of extensions) { const packagesDir = join(ext.path, "typst/packages"); if (existsSync(packagesDir)) { - packageSources.push(packagesDir); + sources.push(packagesDir); } } } - if (packageSources.length === 0) { - return undefined; + return sources; +} + +// Build the TOML config string for typst-gather analyze +export function buildAnalyzeToml( + typstInput: string, + packageSources: string[], +): string { + const discoverPath = toTomlPath(typstInput); + const cachePaths = packageSources.map((p) => `"${toTomlPath(p)}"`).join(", "); + + return [ + `discover = ["${discoverPath}"]`, + `package-cache = [${cachePaths}]`, + ].join("\n"); +} + +// Run typst-gather analyze on the .typ file to determine needed packages +async function analyzeNeededPackages( + typstInput: string, + packageSources: string[], +): Promise { + const tomlConfig = buildAnalyzeToml(typstInput, packageSources); + + try { + const result = await runAnalyze(tomlConfig); + return result.imports.map(({ namespace, name, version }) => ({ + namespace, + name, + version, + })); + } catch { + // Fallback: if analyze fails, stage everything (current behavior) + warning("typst-gather analyze failed; staging all packages as fallback"); + return null; } +} - // Stage to .quarto/typst/packages/ - const cacheDir = projectScratchPath(projectDir, "typst/packages"); +// Stage only the needed packages from source dirs into the cache dir. +// Last write wins — extensions (listed after built-in) override built-in packages. +export function stageSelectedPackages( + sources: string[], + cacheDir: string, + needed: NeededPackage[] | null, +): void { + if (needed === null) { + stageAllPackages(sources, cacheDir); + return; + } - // Copy contents of each source directory (merging namespaces like "preview", "local") - for (const source of packageSources) { - for (const entry of Deno.readDirSync(source)) { - const srcPath = join(source, entry.name); - const destPath = join(cacheDir, entry.name); - if (!existsSync(destPath)) { - copySync(srcPath, destPath); - } else if (entry.isDirectory) { - // Merge directory contents (e.g., merge packages within "preview" namespace) - for (const subEntry of Deno.readDirSync(srcPath)) { - const subSrcPath = join(srcPath, subEntry.name); - const subDestPath = join(destPath, subEntry.name); - if (!existsSync(subDestPath)) { - copySync(subSrcPath, subDestPath); - } - } + for (const pkg of needed) { + const relPath = join(pkg.namespace, pkg.name, pkg.version); + const destPath = join(cacheDir, relPath); + + for (const source of sources) { + const srcPath = join(source, relPath); + if (existsSync(srcPath)) { + ensureDirSync(dirname(destPath)); + copySync(srcPath, destPath, { overwrite: true }); } } } +} + +// Fallback: copy all packages from all sources. Last write wins at the +// package directory level. Built-in listed first, extensions after. +export function stageAllPackages(sources: string[], cacheDir: string): void { + for (const source of sources) { + for (const nsEntry of Deno.readDirSync(source)) { + if (!nsEntry.isDirectory) continue; + const nsSrc = join(source, nsEntry.name); + const nsDest = join(cacheDir, nsEntry.name); + ensureDirSync(nsDest); + for (const pkgEntry of Deno.readDirSync(nsSrc)) { + const pkgSrc = join(nsSrc, pkgEntry.name); + const pkgDest = join(nsDest, pkgEntry.name); + copySync(pkgSrc, pkgDest, { overwrite: true }); + } + } + } +} + +// Stage typst packages to .quarto/typst-packages/ +// First stages built-in packages, then extension packages (which can override) +async function stageTypstPackages( + input: string, + typstInput: string, + projectDir?: string, +): Promise { + if (!projectDir) { + return undefined; + } + + const packageSources = await collectPackageSources(input, projectDir); + if (packageSources.length === 0) { + return undefined; + } + + const neededPackages = await analyzeNeededPackages( + typstInput, + packageSources, + ); + + const cacheDir = projectScratchPath(projectDir, "typst/packages"); + stageSelectedPackages(packageSources, cacheDir, neededPackages); return cacheDir; } @@ -166,7 +246,11 @@ export function typstPdfOutputRecipe( typstOptions.rootDir = project.dir; // Stage extension typst packages - const packagePath = await stageTypstPackages(input, project.dir); + const packagePath = await stageTypstPackages( + input, + typstInput, + project.dir, + ); if (packagePath) { typstOptions.packagePath = packagePath; } diff --git a/src/core/typst-gather.ts b/src/core/typst-gather.ts new file mode 100644 index 00000000000..8a4e9a45662 --- /dev/null +++ b/src/core/typst-gather.ts @@ -0,0 +1,68 @@ +/* + * typst-gather.ts + * + * Shared infrastructure for typst-gather binary integration. + * + * Copyright (C) 2025 Posit Software, PBC + */ + +import { existsSync } from "../deno_ral/fs.ts"; +import { isWindows } from "../deno_ral/platform.ts"; +import { architectureToolsPath } from "./resources.ts"; +import { execProcess } from "./process.ts"; + +// Convert path to use forward slashes for TOML compatibility +// TOML treats backslash as escape character, so Windows paths must use forward slashes +export function toTomlPath(p: string): string { + return p.replace(/\\/g, "/"); +} + +export interface AnalyzeImport { + namespace: string; + name: string; + version: string; + source: string; + direct: boolean; +} + +export interface AnalyzeResult { + imports: AnalyzeImport[]; + files: string[]; +} + +export function typstGatherBinaryPath(): string { + const binaryName = isWindows ? "typst-gather.exe" : "typst-gather"; + const binary = Deno.env.get("QUARTO_TYPST_GATHER") || + architectureToolsPath(binaryName); + + if (!existsSync(binary)) { + throw new Error( + `typst-gather binary not found.\n` + + `Run ./configure.sh to build and install it.`, + ); + } + + return binary; +} + +export async function runAnalyze(tomlConfig: string): Promise { + const binary = typstGatherBinaryPath(); + + const result = await execProcess( + { + cmd: binary, + args: ["analyze", "-"], + stdout: "piped", + stderr: "piped", + }, + tomlConfig, + ); + + if (!result.success) { + throw new Error( + result.stderr || "typst-gather analyze failed", + ); + } + + return JSON.parse(result.stdout!) as AnalyzeResult; +} diff --git a/tests/docs/smoke-all/typst/marginalia-only-project/.gitignore b/tests/docs/smoke-all/typst/marginalia-only-project/.gitignore new file mode 100644 index 00000000000..4adae713c4c --- /dev/null +++ b/tests/docs/smoke-all/typst/marginalia-only-project/.gitignore @@ -0,0 +1,5 @@ +/.quarto/ +**/*.quarto_ipynb +*.html +*_files/ +*.pdf diff --git a/tests/docs/smoke-all/typst/marginalia-only-project/_quarto.yml b/tests/docs/smoke-all/typst/marginalia-only-project/_quarto.yml new file mode 100644 index 00000000000..c4ffdc42e60 --- /dev/null +++ b/tests/docs/smoke-all/typst/marginalia-only-project/_quarto.yml @@ -0,0 +1,6 @@ +project: + type: default + +format: + typst: + keep-typ: true diff --git a/tests/docs/smoke-all/typst/marginalia-only-project/index.qmd b/tests/docs/smoke-all/typst/marginalia-only-project/index.qmd new file mode 100644 index 00000000000..70b6a6437d6 --- /dev/null +++ b/tests/docs/smoke-all/typst/marginalia-only-project/index.qmd @@ -0,0 +1,8 @@ +--- +title: "Marginalia Only Test" +papersize: us-letter +--- + +Main content here. + +[This note goes in the margin.]{.column-margin} diff --git a/tests/docs/smoke-all/typst/no-packages-project/_quarto.yml b/tests/docs/smoke-all/typst/no-packages-project/_quarto.yml new file mode 100644 index 00000000000..c4ffdc42e60 --- /dev/null +++ b/tests/docs/smoke-all/typst/no-packages-project/_quarto.yml @@ -0,0 +1,6 @@ +project: + type: default + +format: + typst: + keep-typ: true diff --git a/tests/docs/smoke-all/typst/no-packages-project/index.qmd b/tests/docs/smoke-all/typst/no-packages-project/index.qmd new file mode 100644 index 00000000000..d099acd9216 --- /dev/null +++ b/tests/docs/smoke-all/typst/no-packages-project/index.qmd @@ -0,0 +1,5 @@ +--- +title: "No Packages Test" +--- + +Hello world. This document uses no typst packages. diff --git a/tests/smoke/typst-gather/.gitignore b/tests/smoke/typst-gather/.gitignore index c020d281044..56757eca0ad 100644 --- a/tests/smoke/typst-gather/.gitignore +++ b/tests/smoke/typst-gather/.gitignore @@ -2,7 +2,10 @@ _extensions/test-format/typst/ with-config/_extensions/config-format/typst/ with-local/_extensions/local-format/typst/ +with-transitive-deps/_extensions/transitive-format/typst/ -# Generated config files -typst-gather.toml -with-local/typst-gather.toml.bak \ No newline at end of file +# Generated config files (only at top level and in test dirs that generate them) +/typst-gather.toml +with-local/typst-gather.toml.bak +with-transitive-deps/typst-gather.toml.bak +empty-extension/typst-gather.toml \ No newline at end of file diff --git a/tests/smoke/typst-gather/empty-extension/_extensions/empty-format/_extension.yml b/tests/smoke/typst-gather/empty-extension/_extensions/empty-format/_extension.yml new file mode 100644 index 00000000000..e0c672e4d75 --- /dev/null +++ b/tests/smoke/typst-gather/empty-extension/_extensions/empty-format/_extension.yml @@ -0,0 +1,8 @@ +title: Empty Format +author: Test +version: 1.0.0 +quarto-required: ">=1.6.0" +contributes: + formats: + html: + theme: default diff --git a/tests/smoke/typst-gather/no-extension/.gitkeep b/tests/smoke/typst-gather/no-extension/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/smoke/typst-gather/typst-gather.test.ts b/tests/smoke/typst-gather/typst-gather.test.ts index 523a38c5a3b..c5b788d17b2 100644 --- a/tests/smoke/typst-gather/typst-gather.test.ts +++ b/tests/smoke/typst-gather/typst-gather.test.ts @@ -1,6 +1,8 @@ -import { testQuartoCmd, Verify } from "../../test.ts"; +import { testQuartoCmd, unitTest, Verify } from "../../test.ts"; import { assert } from "testing/asserts"; import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { execProcess } from "../../../src/core/process.ts"; // Test 1: Auto-detection from _extension.yml const verifyPackagesCreated: Verify = { @@ -218,3 +220,310 @@ testQuartoCmd( }, "typst-gather --init-config detects @local imports", ); + +// Test 6: Rendering a project-based typst document with no package imports +// stages no packages (no preview/ or local/ dirs created) +const noPackagesProjectDir = + "docs/smoke-all/typst/no-packages-project"; + +const verifyNoPackagesStaged: Verify = { + name: "Verify no packages staged for document with no imports", + verify: async () => { + const scratchPackages = join(noPackagesProjectDir, ".quarto/typst/packages"); + const previewDir = join(scratchPackages, "preview"); + const localDir = join(scratchPackages, "local"); + assert( + !existsSync(previewDir), + `Expected no preview/ dir but found: ${previewDir}`, + ); + assert( + !existsSync(localDir), + `Expected no local/ dir but found: ${localDir}`, + ); + }, +}; + +testQuartoCmd( + "render", + [join(noPackagesProjectDir, "index.qmd"), "--to", "typst"], + [verifyNoPackagesStaged], + { + teardown: async () => { + try { + Deno.removeSync(join(noPackagesProjectDir, ".quarto"), { + recursive: true, + }); + Deno.removeSync(join(noPackagesProjectDir, "index.pdf")); + } catch { + // Ignore + } + }, + }, + "no packages staged when typst document has no imports", +); + +// Helper to run quarto as an external process and capture exit code +async function runQuarto( + args: string[], + cwd: string, + env?: Record, +): Promise<{ success: boolean; stdout: string; stderr: string }> { + const quartoPath = join( + Deno.cwd(), + "..", + "package/dist/bin/quarto", + ); + const result = await execProcess({ + cmd: quartoPath, + args, + cwd, + stdout: "piped", + stderr: "piped", + env: env ? { ...Deno.env.toObject(), ...env } : undefined, + }); + return { + success: result.success, + stdout: result.stdout || "", + stderr: result.stderr || "", + }; +} + +// Test 7: --init-config errors when typst-gather.toml already exists +unitTest( + "typst-gather --init-config errors when config already exists", + async () => { + const cwd = join(Deno.cwd(), "smoke/typst-gather"); + // Create a typst-gather.toml so --init-config should fail + const configPath = join(cwd, "typst-gather.toml"); + try { + Deno.writeTextFileSync(configPath, "# existing config\n"); + const result = await runQuarto( + ["call", "typst-gather", "--init-config"], + cwd, + ); + assert(!result.success, "Expected --init-config to fail when config exists"); + } finally { + try { + Deno.removeSync(configPath); + } catch { + // Ignore + } + } + }, +); + +// Test 8: --init-config errors when no extension directory found +unitTest( + "typst-gather --init-config errors with no extension directory", + async () => { + const cwd = join(Deno.cwd(), "smoke/typst-gather/no-extension"); + const result = await runQuarto( + ["call", "typst-gather", "--init-config"], + cwd, + ); + assert( + !result.success, + "Expected --init-config to fail with no extension", + ); + }, +); + +// Test 9: --init-config with extension that has no typst entries +// The extension has no typst template/template-partials, so extractTypstFiles +// returns empty. initConfig logs a warning but still generates a config with +// placeholder discover. This is not an error — it's a valid starting point. +unitTest( + "typst-gather --init-config warns with empty extension (no typst entries)", + async () => { + const cwd = join(Deno.cwd(), "smoke/typst-gather/empty-extension"); + const result = await runQuarto( + ["call", "typst-gather", "--init-config"], + cwd, + ); + // initConfig still generates a config file with placeholders + const configPath = join(cwd, "typst-gather.toml"); + try { + if (result.success) { + assert(existsSync(configPath), "Expected config file to be created"); + const content = Deno.readTextFileSync(configPath); + // Should have placeholder discover comment + assert( + content.includes("discover"), + "Expected discover in generated config", + ); + } else { + // If it failed, that's also acceptable — no typst files found + assert(true); + } + } finally { + try { + Deno.removeSync(configPath); + } catch { + // Ignore + } + } + }, +); + +// Test 10: --init-config detects @local imports from extension template. +// Note: --init-config only passes `discover` paths, not `[local]` config, +// so typst-gather analyze can see the direct @local import but cannot follow +// it to find transitive @preview deps (it doesn't know where the local +// package lives). The transitive resolution happens at gather time with the +// full config. +const verifyTransitiveDeps: Verify = { + name: "Verify --init-config detects @local import from template", + verify: async () => { + assert( + existsSync("typst-gather.toml"), + "Expected typst-gather.toml to be created", + ); + + const content = Deno.readTextFileSync("typst-gather.toml"); + // Should have the @local import detected + assert( + content.includes("[local]"), + "Expected [local] section", + ); + assert( + content.includes("dep-pkg"), + "Expected dep-pkg in [local] section", + ); + }, +}; + +testQuartoCmd( + "call", + ["typst-gather", "--init-config"], + [verifyTransitiveDeps], + { + cwd: () => "smoke/typst-gather/with-transitive-deps", + setup: async () => { + // Rename existing config so --init-config can run + try { + Deno.renameSync("typst-gather.toml", "typst-gather.toml.bak"); + } catch { + // Ignore if doesn't exist + } + }, + teardown: async () => { + // Restore original config and clean up generated one + try { + Deno.removeSync("typst-gather.toml"); + } catch { + // Ignore + } + try { + Deno.renameSync("typst-gather.toml.bak", "typst-gather.toml"); + } catch { + // Ignore + } + }, + }, + "typst-gather --init-config detects transitive deps from @local", +); + +// Test 11: Staging falls back to copy-all when typst-gather binary is missing +unitTest( + "staging falls back when typst-gather binary is missing", + async () => { + // Render a document that has packages, with QUARTO_TYPST_GATHER pointing + // to a nonexistent binary. The render should still succeed because + // analyzeNeededPackages catches the error and falls back to stageAll. + const projectDir = join( + Deno.cwd(), + "docs/smoke-all/typst/orange-book", + ); + const result = await runQuarto( + ["render", "index.qmd", "--to", "typst"], + projectDir, + { QUARTO_TYPST_GATHER: "/nonexistent/typst-gather-binary" }, + ); + assert( + result.success, + `Expected render to succeed with fallback staging, but failed:\n${result.stderr}`, + ); + }, +); + +// Test 12: Staging falls back when typst-gather analyze fails (non-zero exit) +unitTest( + "staging falls back when typst-gather analyze returns non-zero", + async () => { + // /usr/bin/false always exits with code 1 + const projectDir = join( + Deno.cwd(), + "docs/smoke-all/typst/orange-book", + ); + const result = await runQuarto( + ["render", "index.qmd", "--to", "typst"], + projectDir, + { QUARTO_TYPST_GATHER: "/usr/bin/false" }, + ); + assert( + result.success, + `Expected render to succeed with fallback staging, but failed:\n${result.stderr}`, + ); + }, +); + +// Test 13: Rendering a project using .column-margin stages only marginalia +const marginaliaProjectDir = + "docs/smoke-all/typst/marginalia-only-project"; + +const verifyOnlyMarginaliaStaged: Verify = { + name: "Verify only marginalia package is staged", + verify: async () => { + const scratchPackages = join(marginaliaProjectDir, ".quarto/typst/packages"); + const previewDir = join(scratchPackages, "preview"); + + // marginalia should be staged + const marginaliaDir = join(previewDir, "marginalia"); + assert( + existsSync(marginaliaDir), + `Expected marginalia package but not found: ${marginaliaDir}`, + ); + + // No other packages should be staged + const unwanted = [ + "fontawesome", + "showybox", + "theorion", + "octique", + "orange-book", + ]; + for (const pkg of unwanted) { + const pkgDir = join(previewDir, pkg); + assert( + !existsSync(pkgDir), + `Expected ${pkg} NOT to be staged but found: ${pkgDir}`, + ); + } + + // No local packages should be staged + const localDir = join(scratchPackages, "local"); + assert( + !existsSync(localDir), + `Expected no local/ dir but found: ${localDir}`, + ); + }, +}; + +testQuartoCmd( + "render", + [join(marginaliaProjectDir, "index.qmd"), "--to", "typst"], + [verifyOnlyMarginaliaStaged], + { + teardown: async () => { + try { + Deno.removeSync(join(marginaliaProjectDir, ".quarto"), { + recursive: true, + }); + Deno.removeSync(join(marginaliaProjectDir, "index.pdf")); + } catch { + // Ignore + } + }, + }, + "only marginalia staged when typst document uses column-margin", +); diff --git a/tests/smoke/typst-gather/with-transitive-deps/_extensions/transitive-format/_extension.yml b/tests/smoke/typst-gather/with-transitive-deps/_extensions/transitive-format/_extension.yml new file mode 100644 index 00000000000..df7756194e0 --- /dev/null +++ b/tests/smoke/typst-gather/with-transitive-deps/_extensions/transitive-format/_extension.yml @@ -0,0 +1,9 @@ +title: Transitive Deps Format +author: Test +version: 1.0.0 +quarto-required: ">=1.6.0" +contributes: + formats: + typst: + template-partials: + - typst-template.typ diff --git a/tests/smoke/typst-gather/with-transitive-deps/_extensions/transitive-format/typst-template.typ b/tests/smoke/typst-gather/with-transitive-deps/_extensions/transitive-format/typst-template.typ new file mode 100644 index 00000000000..63f934ca2dd --- /dev/null +++ b/tests/smoke/typst-gather/with-transitive-deps/_extensions/transitive-format/typst-template.typ @@ -0,0 +1,8 @@ +// Template that imports a local package which has transitive deps +#import "@local/dep-pkg:0.1.0": greet + +#let project(title: "", body) = { + set document(title: title) + greet() + body +} diff --git a/tests/smoke/typst-gather/with-transitive-deps/local-packages/dep-pkg/0.1.0/lib.typ b/tests/smoke/typst-gather/with-transitive-deps/local-packages/dep-pkg/0.1.0/lib.typ new file mode 100644 index 00000000000..27d0f3429fb --- /dev/null +++ b/tests/smoke/typst-gather/with-transitive-deps/local-packages/dep-pkg/0.1.0/lib.typ @@ -0,0 +1,3 @@ +// Local package that transitively depends on a preview package +#import "@preview/example:0.1.0": add +#let greet() = "Hello! 1+2=" + str(add(1, 2)) diff --git a/tests/smoke/typst-gather/with-transitive-deps/local-packages/dep-pkg/0.1.0/typst.toml b/tests/smoke/typst-gather/with-transitive-deps/local-packages/dep-pkg/0.1.0/typst.toml new file mode 100644 index 00000000000..55cd3278247 --- /dev/null +++ b/tests/smoke/typst-gather/with-transitive-deps/local-packages/dep-pkg/0.1.0/typst.toml @@ -0,0 +1,7 @@ +[package] +name = "dep-pkg" +version = "0.1.0" +entrypoint = "lib.typ" +authors = ["Test"] +license = "MIT" +description = "Local package that imports a preview package" diff --git a/tests/smoke/typst-gather/with-transitive-deps/typst-gather.toml b/tests/smoke/typst-gather/with-transitive-deps/typst-gather.toml new file mode 100644 index 00000000000..defe0b45298 --- /dev/null +++ b/tests/smoke/typst-gather/with-transitive-deps/typst-gather.toml @@ -0,0 +1,6 @@ +rootdir = "_extensions/transitive-format" +destination = "typst/packages" +discover = ["_extensions/transitive-format/typst-template.typ"] + +[local] +dep-pkg = "local-packages/dep-pkg/0.1.0" diff --git a/tests/unit/typst-gather.test.ts b/tests/unit/typst-gather.test.ts new file mode 100644 index 00000000000..180f68780cd --- /dev/null +++ b/tests/unit/typst-gather.test.ts @@ -0,0 +1,256 @@ +/* + * typst-gather.test.ts + * + * Unit tests for typst-gather config generation. + * + * Copyright (C) 2025 Posit Software, PBC + */ + +import { unitTest } from "../test.ts"; +import { assert, assertEquals } from "testing/asserts"; +import { + AnalyzeResult, + generateConfigFromAnalysis, +} from "../../src/command/call/typst-gather/cmd.ts"; + +// Helper to check a line exists in generated config +function assertContains(config: string, expected: string, msg?: string) { + assert(config.includes(expected), msg || `Expected config to contain: ${expected}\nGot:\n${config}`); +} + +function assertNotContains(config: string, unexpected: string, msg?: string) { + assert(!config.includes(unexpected), msg || `Expected config NOT to contain: ${unexpected}\nGot:\n${config}`); +} + +// --- Unit tests for generateConfigFromAnalysis --- + +unitTest( + "generateConfigFromAnalysis - empty result produces valid config", + async () => { + const result: AnalyzeResult = { imports: [], files: [] }; + const config = generateConfigFromAnalysis(result); + assertContains(config, 'destination = "typst/packages"'); + assertContains(config, '# discover = "template.typ"'); + assertContains(config, '# cetz = "0.4.1"'); + assertContains(config, "# [local]"); + }, +); + +unitTest( + "generateConfigFromAnalysis - preview only imports", + async () => { + const result: AnalyzeResult = { + imports: [ + { namespace: "preview", name: "cetz", version: "0.4.1", source: "template.typ", direct: true }, + { namespace: "preview", name: "fletcher", version: "0.5.0", source: "template.typ", direct: true }, + ], + files: ["template.typ"], + }; + const config = generateConfigFromAnalysis(result); + assertContains(config, '# cetz = "0.4.1"'); + assertContains(config, '# fletcher = "0.5.0"'); + assertContains(config, "# [local]"); + // Should NOT have an uncommented [local] section + assertNotContains(config, "\n[local]"); + }, +); + +unitTest( + "generateConfigFromAnalysis - local only imports", + async () => { + const result: AnalyzeResult = { + imports: [ + { namespace: "local", name: "my-pkg", version: "1.0.0", source: "template.typ", direct: true }, + ], + files: ["template.typ"], + }; + const config = generateConfigFromAnalysis(result); + assertContains(config, "[local]"); + assertContains(config, 'my-pkg = "/path/to/my-pkg"'); + assertContains(config, "@local/my-pkg:1.0.0 (in template.typ)"); + // Preview section should have placeholder example + assertContains(config, '# cetz = "0.4.1"'); + }, +); + +unitTest( + "generateConfigFromAnalysis - mixed preview and local imports", + async () => { + const result: AnalyzeResult = { + imports: [ + { namespace: "preview", name: "cetz", version: "0.4.1", source: "template.typ", direct: true }, + { namespace: "local", name: "my-pkg", version: "1.0.0", source: "template.typ", direct: true }, + ], + files: ["template.typ"], + }; + const config = generateConfigFromAnalysis(result); + assertContains(config, '# cetz = "0.4.1"'); + assertContains(config, "[local]"); + assertContains(config, 'my-pkg = "/path/to/my-pkg"'); + }, +); + +unitTest( + "generateConfigFromAnalysis - transitive preview from local shows source", + async () => { + const result: AnalyzeResult = { + imports: [ + { namespace: "preview", name: "cetz", version: "0.4.1", source: "template.typ", direct: true }, + { namespace: "preview", name: "oxifmt", version: "0.2.1", source: "@local/my-pkg", direct: false }, + { namespace: "local", name: "my-pkg", version: "1.0.0", source: "template.typ", direct: true }, + ], + files: ["template.typ"], + }; + const config = generateConfigFromAnalysis(result); + assertContains(config, '# cetz = "0.4.1"'); + assertContains(config, '# oxifmt = "0.2.1" # via @local/my-pkg'); + }, +); + +unitTest( + "generateConfigFromAnalysis - deduplication of preview imports", + async () => { + const result: AnalyzeResult = { + imports: [ + { namespace: "preview", name: "cetz", version: "0.4.1", source: "template.typ", direct: true }, + { namespace: "preview", name: "cetz", version: "0.4.1", source: "other.typ", direct: true }, + ], + files: ["template.typ", "other.typ"], + }; + const config = generateConfigFromAnalysis(result); + // Should only appear once + const matches = config.match(/# cetz = "0\.4\.1"/g); + assertEquals(matches?.length, 1, "Expected cetz to appear exactly once"); + }, +); + +unitTest( + "generateConfigFromAnalysis - with rootdir", + async () => { + const result: AnalyzeResult = { imports: [], files: ["template.typ"] }; + const config = generateConfigFromAnalysis(result, "_extensions/my-ext"); + assertContains(config, 'rootdir = "_extensions/my-ext"'); + assertContains(config, 'destination = "typst/packages"'); + }, +); + +unitTest( + "generateConfigFromAnalysis - no rootdir when not provided", + async () => { + const result: AnalyzeResult = { imports: [], files: ["template.typ"] }; + const config = generateConfigFromAnalysis(result); + assertNotContains(config, "rootdir"); + }, +); + +unitTest( + "generateConfigFromAnalysis - discover single file is string", + async () => { + const result: AnalyzeResult = { imports: [], files: ["template.typ"] }; + const config = generateConfigFromAnalysis(result); + assertContains(config, 'discover = "template.typ"'); + assertNotContains(config, "discover = ["); + }, +); + +unitTest( + "generateConfigFromAnalysis - discover multiple files is array", + async () => { + const result: AnalyzeResult = { + imports: [], + files: ["template.typ", "typst-show.typ"], + }; + const config = generateConfigFromAnalysis(result); + assertContains(config, 'discover = ["template.typ", "typst-show.typ"]'); + }, +); + +unitTest( + "generateConfigFromAnalysis - windows backslashes converted to forward slashes", + async () => { + const result: AnalyzeResult = { + imports: [], + files: ["subdir\\template.typ"], + }; + const config = generateConfigFromAnalysis(result, "ext\\my-ext"); + assertContains(config, 'rootdir = "ext/my-ext"'); + assertContains(config, 'discover = "subdir/template.typ"'); + assertNotContains(config, "\\"); + }, +); + +unitTest( + "generateConfigFromAnalysis - local imports only include direct", + async () => { + const result: AnalyzeResult = { + imports: [ + { namespace: "local", name: "my-pkg", version: "1.0.0", source: "template.typ", direct: true }, + { namespace: "local", name: "other-pkg", version: "2.0.0", source: "@local/my-pkg", direct: false }, + ], + files: ["template.typ"], + }; + const config = generateConfigFromAnalysis(result); + assertContains(config, "[local]"); + assertContains(config, 'my-pkg = "/path/to/my-pkg"'); + // Transitive @local should NOT appear in the [local] section + assertNotContains(config, 'other-pkg = "/path/to/other-pkg"'); + }, +); + +// --- Large import list --- + +unitTest( + "generateConfigFromAnalysis - handles large import list without truncation", + async () => { + const imports = []; + for (let i = 0; i < 60; i++) { + imports.push({ + namespace: "preview", + name: `package-${i}`, + version: `${i}.0.0`, + source: "template.typ", + direct: true, + }); + } + const result: AnalyzeResult = { imports, files: ["template.typ"] }; + const config = generateConfigFromAnalysis(result); + // All 60 packages should appear + for (let i = 0; i < 60; i++) { + assertContains(config, `# package-${i} = "${i}.0.0"`); + } + }, +); + +// --- Paths with spaces --- + +unitTest( + "generateConfigFromAnalysis - paths with spaces are quoted correctly", + async () => { + const result: AnalyzeResult = { + imports: [], + files: ["my project/my template.typ"], + }; + const config = generateConfigFromAnalysis(result, "my ext/sub dir"); + assertContains(config, 'rootdir = "my ext/sub dir"'); + assertContains(config, 'discover = "my project/my template.typ"'); + }, +); + +// --- Paths with special TOML characters --- + +unitTest( + "generateConfigFromAnalysis - paths with special chars in discover", + async () => { + const result: AnalyzeResult = { + imports: [], + files: ['dir "quoted"/template.typ'], + }; + // The current implementation doesn't escape quotes inside TOML strings. + // This test documents the current behavior — paths with quotes in them + // would produce invalid TOML. This is acceptable since filesystem paths + // with literal quotes are extremely rare. + const config = generateConfigFromAnalysis(result); + assertContains(config, "discover"); + assertContains(config, "template.typ"); + }, +); diff --git a/tests/unit/typst-staging.test.ts b/tests/unit/typst-staging.test.ts new file mode 100644 index 00000000000..0ced91a7700 --- /dev/null +++ b/tests/unit/typst-staging.test.ts @@ -0,0 +1,368 @@ +/* + * typst-staging.test.ts + * + * Unit tests for typst package staging functions. + * + * Copyright (C) 2025 Posit Software, PBC + */ + +import { unitTest } from "../test.ts"; +import { assert, assertEquals } from "testing/asserts"; +import { join } from "../../src/deno_ral/path.ts"; +import { existsSync } from "../../src/deno_ral/fs.ts"; +import { ensureDirSync } from "../../src/deno_ral/fs.ts"; +import { + buildAnalyzeToml, + type NeededPackage, + stageAllPackages, + stageSelectedPackages, +} from "../../src/command/render/output-typst.ts"; + +// Helper: create a fake package directory with a typst.toml and lib.typ +function createFakePackage( + baseDir: string, + namespace: string, + name: string, + version: string, + content = "default", +): string { + const pkgDir = join(baseDir, namespace, name, version); + ensureDirSync(pkgDir); + Deno.writeTextFileSync( + join(pkgDir, "typst.toml"), + `[package]\nname = "${name}"\nversion = "${version}"\n`, + ); + Deno.writeTextFileSync(join(pkgDir, "lib.typ"), `// ${content}\n`); + return pkgDir; +} + +// Helper: read lib.typ content to verify which source won +function readLibContent(cacheDir: string, ns: string, name: string, ver: string): string { + return Deno.readTextFileSync(join(cacheDir, ns, name, ver, "lib.typ")); +} + +// --- buildAnalyzeToml tests --- + +unitTest("buildAnalyzeToml - correct discover and package-cache fields", async () => { + const toml = buildAnalyzeToml("/path/to/doc.typ", ["/src1", "/src2"]); + assert(toml.includes('discover = ["/path/to/doc.typ"]')); + assert(toml.includes('package-cache = ["/src1", "/src2"]')); +}); + +unitTest("buildAnalyzeToml - windows backslashes converted to forward slashes", async () => { + const toml = buildAnalyzeToml( + "C:\\Users\\test\\doc.typ", + ["C:\\pkg\\source1", "D:\\pkg\\source2"], + ); + assert(toml.includes('discover = ["C:/Users/test/doc.typ"]')); + assert(toml.includes('package-cache = ["C:/pkg/source1", "D:/pkg/source2"]')); + assert(!toml.includes("\\"), "TOML should not contain backslashes"); +}); + +unitTest("buildAnalyzeToml - single source", async () => { + const toml = buildAnalyzeToml("/doc.typ", ["/only-source"]); + assert(toml.includes('package-cache = ["/only-source"]')); +}); + +unitTest("buildAnalyzeToml - paths with spaces are quoted correctly", async () => { + const toml = buildAnalyzeToml( + "/my project/doc file.typ", + ["/source one/packages", "/source two/pkgs"], + ); + assert(toml.includes('discover = ["/my project/doc file.typ"]')); + assert( + toml.includes('package-cache = ["/source one/packages", "/source two/pkgs"]'), + ); +}); + +// --- stageSelectedPackages tests --- + +unitTest("stageSelectedPackages - single package", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + createFakePackage(source, "preview", "cetz", "0.4.1"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + ]; + stageSelectedPackages([source], cache, needed); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "preview/cetz/0.4.1/lib.typ"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - multiple packages from same source", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + createFakePackage(source, "preview", "cetz", "0.4.1"); + createFakePackage(source, "preview", "fontawesome", "0.5.0"); + createFakePackage(source, "preview", "fletcher", "0.3.0"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + { namespace: "preview", name: "fontawesome", version: "0.5.0" }, + { namespace: "preview", name: "fletcher", version: "0.3.0" }, + ]; + stageSelectedPackages([source], cache, needed); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "preview/fontawesome/0.5.0/typst.toml"))); + assert(existsSync(join(cache, "preview/fletcher/0.3.0/typst.toml"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - from multiple sources", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source1 = join(tmp, "source1"); + const source2 = join(tmp, "source2"); + const cache = join(tmp, "cache"); + ensureDirSync(source1); + ensureDirSync(source2); + ensureDirSync(cache); + + createFakePackage(source1, "preview", "cetz", "0.4.1", "from-source1"); + createFakePackage(source2, "preview", "fontawesome", "0.5.0", "from-source2"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + { namespace: "preview", name: "fontawesome", version: "0.5.0" }, + ]; + stageSelectedPackages([source1, source2], cache, needed); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "preview/fontawesome/0.5.0/typst.toml"))); + assert(readLibContent(cache, "preview", "cetz", "0.4.1").includes("from-source1")); + assert(readLibContent(cache, "preview", "fontawesome", "0.5.0").includes("from-source2")); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - last source wins", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const builtin = join(tmp, "builtin"); + const extension = join(tmp, "extension"); + const cache = join(tmp, "cache"); + ensureDirSync(builtin); + ensureDirSync(extension); + ensureDirSync(cache); + + createFakePackage(builtin, "preview", "cetz", "0.4.1", "builtin-version"); + createFakePackage(extension, "preview", "cetz", "0.4.1", "extension-version"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + ]; + // builtin first, extension second — extension should win + stageSelectedPackages([builtin, extension], cache, needed); + + const content = readLibContent(cache, "preview", "cetz", "0.4.1"); + assert( + content.includes("extension-version"), + `Expected extension to win, got: ${content}`, + ); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - overwrites existing cache", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + // Pre-populate cache with old content + createFakePackage(cache, "preview", "cetz", "0.4.1", "old-cached"); + // Source has new content + createFakePackage(source, "preview", "cetz", "0.4.1", "new-source"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + ]; + stageSelectedPackages([source], cache, needed); + + const content = readLibContent(cache, "preview", "cetz", "0.4.1"); + assert( + content.includes("new-source"), + `Expected overwrite, got: ${content}`, + ); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - package not in sources is skipped", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + // Source has cetz but NOT fontawesome + createFakePackage(source, "preview", "cetz", "0.4.1"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + { namespace: "preview", name: "fontawesome", version: "0.5.0" }, + ]; + // Should not throw — missing package is silently skipped + stageSelectedPackages([source], cache, needed); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(!existsSync(join(cache, "preview/fontawesome"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - mixed namespaces", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + createFakePackage(source, "preview", "cetz", "0.4.1"); + createFakePackage(source, "local", "my-theme", "1.0.0"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + { namespace: "local", name: "my-theme", version: "1.0.0" }, + ]; + stageSelectedPackages([source], cache, needed); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "local/my-theme/1.0.0/typst.toml"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageSelectedPackages - preserves package contents", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + // Create package with subdirectory + const pkgDir = join(source, "preview/cetz/0.4.1"); + ensureDirSync(pkgDir); + Deno.writeTextFileSync(join(pkgDir, "typst.toml"), '[package]\nname = "cetz"\n'); + Deno.writeTextFileSync(join(pkgDir, "lib.typ"), "// main\n"); + ensureDirSync(join(pkgDir, "src")); + Deno.writeTextFileSync(join(pkgDir, "src/draw.typ"), "// draw\n"); + Deno.writeTextFileSync(join(pkgDir, "src/canvas.typ"), "// canvas\n"); + + const needed: NeededPackage[] = [ + { namespace: "preview", name: "cetz", version: "0.4.1" }, + ]; + stageSelectedPackages([source], cache, needed); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "preview/cetz/0.4.1/lib.typ"))); + assert(existsSync(join(cache, "preview/cetz/0.4.1/src/draw.typ"))); + assert(existsSync(join(cache, "preview/cetz/0.4.1/src/canvas.typ"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +// --- stageAllPackages (fallback) tests --- + +unitTest("stageSelectedPackages - null needed triggers stageAll fallback", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + createFakePackage(source, "preview", "cetz", "0.4.1"); + createFakePackage(source, "preview", "fontawesome", "0.5.0"); + + // null = fallback, should copy everything + stageSelectedPackages([source], cache, null); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "preview/fontawesome/0.5.0/typst.toml"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageAllPackages - merges packages across sources", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source1 = join(tmp, "source1"); + const source2 = join(tmp, "source2"); + const cache = join(tmp, "cache"); + ensureDirSync(source1); + ensureDirSync(source2); + ensureDirSync(cache); + + // source1 has cetz and fontawesome + createFakePackage(source1, "preview", "cetz", "0.4.1", "builtin-cetz"); + createFakePackage(source1, "preview", "fontawesome", "0.5.0", "builtin-fa"); + // source2 has only a custom cetz (extension override) + createFakePackage(source2, "preview", "cetz", "0.4.1", "ext-cetz"); + + stageAllPackages([source1, source2], cache); + + // fontawesome from source1 should be preserved + assert(existsSync(join(cache, "preview/fontawesome/0.5.0/typst.toml"))); + assert(readLibContent(cache, "preview", "fontawesome", "0.5.0").includes("builtin-fa")); + + // cetz should be overwritten by source2 (last write wins) + const cetzContent = readLibContent(cache, "preview", "cetz", "0.4.1"); + assert( + cetzContent.includes("ext-cetz"), + `Expected extension override, got: ${cetzContent}`, + ); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +}); + +unitTest("stageAllPackages - handles mixed namespaces", async () => { + const tmp = Deno.makeTempDirSync({ prefix: "quarto-stage-test-" }); + try { + const source = join(tmp, "source"); + const cache = join(tmp, "cache"); + ensureDirSync(source); + ensureDirSync(cache); + + createFakePackage(source, "preview", "cetz", "0.4.1"); + createFakePackage(source, "local", "my-theme", "1.0.0"); + + stageAllPackages([source], cache); + + assert(existsSync(join(cache, "preview/cetz/0.4.1/typst.toml"))); + assert(existsSync(join(cache, "local/my-theme/1.0.0/typst.toml"))); + } finally { + Deno.removeSync(tmp, { recursive: true }); + } +});