From 2322e49549f477a351308969a15f8e6c622dc1d7 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 4 Mar 2026 20:22:34 +0000 Subject: [PATCH] feat: external artifacts builder (#13485) This PR reverts the built-in artifact render while a decision is being reached. No impact expected on any features --- MODULE.bazel.lock | 44 +- codex-rs/Cargo.lock | 698 +--- codex-rs/Cargo.toml | 14 +- codex-rs/artifact-presentation/BUILD.bazel | 6 - codex-rs/artifact-presentation/Cargo.toml | 28 - codex-rs/artifact-presentation/src/lib.rs | 6 - .../src/presentation_artifact/api.rs | 249 -- .../src/presentation_artifact/args.rs | 729 ---- .../src/presentation_artifact/inspect.rs | 871 ---- .../src/presentation_artifact/manager.rs | 3203 --------------- .../src/presentation_artifact/mod.rs | 10 - .../src/presentation_artifact/model.rs | 1934 --------- .../src/presentation_artifact/parsing.rs | 1273 ------ .../src/presentation_artifact/pptx.rs | 951 ----- .../src/presentation_artifact/proto.rs | 614 --- .../src/presentation_artifact/response.rs | 138 - .../src/presentation_artifact/snapshot.rs | 339 -- codex-rs/artifact-presentation/src/tests.rs | 3512 ----------------- codex-rs/artifact-spreadsheet/BUILD.bazel | 6 - codex-rs/artifact-spreadsheet/Cargo.toml | 25 - codex-rs/artifact-spreadsheet/src/address.rs | 245 -- codex-rs/artifact-spreadsheet/src/chart.rs | 357 -- .../artifact-spreadsheet/src/conditional.rs | 308 -- codex-rs/artifact-spreadsheet/src/error.rs | 39 - codex-rs/artifact-spreadsheet/src/formula.rs | 535 --- codex-rs/artifact-spreadsheet/src/lib.rs | 26 - codex-rs/artifact-spreadsheet/src/manager.rs | 3346 ---------------- codex-rs/artifact-spreadsheet/src/model.rs | 1787 --------- codex-rs/artifact-spreadsheet/src/pivot.rs | 177 - codex-rs/artifact-spreadsheet/src/render.rs | 373 -- codex-rs/artifact-spreadsheet/src/style.rs | 580 --- codex-rs/artifact-spreadsheet/src/table.rs | 630 --- codex-rs/artifact-spreadsheet/src/tests.rs | 1228 ------ codex-rs/artifact-spreadsheet/src/xlsx.rs | 860 ---- codex-rs/artifacts/BUILD.bazel | 6 + codex-rs/artifacts/Cargo.toml | 25 + codex-rs/artifacts/src/client.rs | 446 +++ codex-rs/artifacts/src/lib.rs | 23 + codex-rs/artifacts/src/runtime.rs | 522 +++ codex-rs/core/Cargo.toml | 3 +- codex-rs/core/src/codex.rs | 24 - codex-rs/core/src/state/session.rs | 10 - codex-rs/core/src/tools/handlers/artifacts.rs | 412 ++ codex-rs/core/src/tools/handlers/mod.rs | 6 +- .../tools/handlers/presentation_artifact.rs | 250 -- .../tools/handlers/spreadsheet_artifact.rs | 235 -- codex-rs/core/src/tools/spec.rs | 132 +- codex-rs/deny.toml | 1 - codex-rs/package-manager/BUILD.bazel | 6 + codex-rs/package-manager/Cargo.toml | 26 + codex-rs/package-manager/src/lib.rs | 793 ++++ 51 files changed, 2425 insertions(+), 25636 deletions(-) delete mode 100644 codex-rs/artifact-presentation/BUILD.bazel delete mode 100644 codex-rs/artifact-presentation/Cargo.toml delete mode 100644 codex-rs/artifact-presentation/src/lib.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/api.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/args.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/inspect.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/manager.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/mod.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/model.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/parsing.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/pptx.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/proto.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/response.rs delete mode 100644 codex-rs/artifact-presentation/src/presentation_artifact/snapshot.rs delete mode 100644 codex-rs/artifact-presentation/src/tests.rs delete mode 100644 codex-rs/artifact-spreadsheet/BUILD.bazel delete mode 100644 codex-rs/artifact-spreadsheet/Cargo.toml delete mode 100644 codex-rs/artifact-spreadsheet/src/address.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/chart.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/conditional.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/error.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/formula.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/lib.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/manager.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/model.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/pivot.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/render.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/style.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/table.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/tests.rs delete mode 100644 codex-rs/artifact-spreadsheet/src/xlsx.rs create mode 100644 codex-rs/artifacts/BUILD.bazel create mode 100644 codex-rs/artifacts/Cargo.toml create mode 100644 codex-rs/artifacts/src/client.rs create mode 100644 codex-rs/artifacts/src/lib.rs create mode 100644 codex-rs/artifacts/src/runtime.rs create mode 100644 codex-rs/core/src/tools/handlers/artifacts.rs delete mode 100644 codex-rs/core/src/tools/handlers/presentation_artifact.rs delete mode 100644 codex-rs/core/src/tools/handlers/spreadsheet_artifact.rs create mode 100644 codex-rs/package-manager/BUILD.bazel create mode 100644 codex-rs/package-manager/Cargo.toml create mode 100644 codex-rs/package-manager/src/lib.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 46220ab3c..a25099cc9 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -684,7 +684,6 @@ "bytes_1.11.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"extra-platforms\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.60\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "bytestring_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{\"serde\":[\"dep:serde_core\"]}}", "bzip2-sys_0.1.13+1.0.8": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"}],\"features\":{\"__disabled\":[],\"static\":[]}}", - "bzip2_0.4.4": "{\"dependencies\":[{\"name\":\"bzip2-sys\",\"req\":\"^0.1.11\"},{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"features\":[\"quickcheck\"],\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck6\",\"package\":\"quickcheck\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"static\":[\"bzip2-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}", "bzip2_0.5.2": "{\"dependencies\":[{\"name\":\"bzip2-sys\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"features\":[\"rust-allocator\",\"semver-prefix\"],\"name\":\"libbz2-rs-sys\",\"optional\":true,\"req\":\"^0.1.3\"},{\"features\":[\"quickcheck1\"],\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5.4\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"dep:bzip2-sys\"],\"libbz2-rs-sys\":[\"dep:libbz2-rs-sys\",\"bzip2-sys?/__disabled\"],\"static\":[\"bzip2-sys?/static\"]}}", "cached_0.56.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.6\"},{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"cached_proc_macro\",\"optional\":true,\"req\":\"^0.25.0\"},{\"name\":\"cached_proc_macro_types\",\"optional\":true,\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"copy_dir\",\"req\":\"^0.1.3\"},{\"name\":\"directories\",\"optional\":true,\"req\":\"^6.0\"},{\"default_features\":false,\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"googletest\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"features\":[\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"r2d2\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"r2d2\"],\"name\":\"redis\",\"optional\":true,\"req\":\"^0.32\"},{\"name\":\"rmp-serde\",\"optional\":true,\"req\":\"^1.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"name\":\"sled\",\"optional\":true,\"req\":\"^0.34\"},{\"kind\":\"dev\",\"name\":\"smartstring\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"macros\",\"time\",\"sync\",\"parking_lot\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"web-time\",\"req\":\"^1.1.0\"}],\"features\":{\"ahash\":[\"dep:ahash\",\"hashbrown/default\"],\"async\":[\"futures\",\"tokio\",\"async-trait\"],\"async_tokio_rt_multi_thread\":[\"async\",\"tokio/rt-multi-thread\"],\"default\":[\"proc_macro\",\"ahash\"],\"disk_store\":[\"sled\",\"serde\",\"rmp-serde\",\"directories\"],\"proc_macro\":[\"cached_proc_macro\",\"cached_proc_macro_types\"],\"redis_ahash\":[\"redis_store\",\"redis/ahash\"],\"redis_async_std\":[\"redis_store\",\"async\",\"redis/aio\",\"redis/async-std-comp\",\"redis/tls\",\"redis/async-std-tls-comp\"],\"redis_connection_manager\":[\"redis_store\",\"redis/connection-manager\"],\"redis_store\":[\"redis\",\"r2d2\",\"serde\",\"serde_json\"],\"redis_tokio\":[\"redis_store\",\"async\",\"redis/aio\",\"redis/tokio-comp\",\"redis/tls\",\"redis/tokio-native-tls-comp\"],\"wasm\":[]}}", "cached_proc_macro_0.25.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.20.8\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.49\"},{\"name\":\"quote\",\"req\":\"^1.0.6\"},{\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", @@ -725,7 +724,6 @@ "const-oid_0.9.6": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"}],\"features\":{\"db\":[],\"std\":[]}}", "const_format_0.2.35": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"arrayvec\",\"req\":\"^0.7.0\"},{\"name\":\"const_format_proc_macros\",\"req\":\"=0.2.34\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^1.3.5\"},{\"default_features\":false,\"name\":\"konst\",\"optional\":true,\"req\":\"^0.2.13\"}],\"features\":{\"__debug\":[\"const_format_proc_macros/debug\"],\"__docsrs\":[],\"__inline_const_pat_tests\":[\"__test\",\"fmt\"],\"__only_new_tests\":[\"__test\"],\"__test\":[],\"all\":[\"fmt\",\"derive\",\"rust_1_64\",\"assert\"],\"assert\":[\"assertc\"],\"assertc\":[\"fmt\",\"assertcp\"],\"assertcp\":[\"rust_1_51\"],\"const_generics\":[\"rust_1_51\"],\"constant_time_as_str\":[\"fmt\"],\"default\":[],\"derive\":[\"fmt\",\"const_format_proc_macros/derive\"],\"fmt\":[\"rust_1_83\"],\"more_str_macros\":[\"rust_1_64\"],\"nightly_const_generics\":[\"const_generics\"],\"rust_1_51\":[],\"rust_1_64\":[\"rust_1_51\",\"konst\",\"konst/rust_1_64\"],\"rust_1_83\":[\"rust_1_64\"]}}", "const_format_proc_macros_0.2.34": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^1.3.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.19\"},{\"name\":\"quote\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^1.0.38\"},{\"name\":\"unicode-xid\",\"req\":\"^0.2\"}],\"features\":{\"all\":[\"derive\"],\"debug\":[\"syn/extra-traits\"],\"default\":[],\"derive\":[\"syn\",\"syn/derive\",\"syn/printing\"]}}", - "constant_time_eq_0.1.5": "{\"dependencies\":[],\"features\":{}}", "constant_time_eq_0.3.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"count_instructions\",\"req\":\"^0.1.3\"},{\"features\":[\"cargo_bench_support\",\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"}],\"features\":{\"count_instructions_test\":[]}}", "convert_case_0.10.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{}}", "convert_case_0.6.0": "{\"dependencies\":[{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.18.0\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{\"random\":[\"rand\"]}}", @@ -749,8 +747,6 @@ "crossterm_winapi_0.9.1": "{\"dependencies\":[{\"features\":[\"winbase\",\"consoleapi\",\"processenv\",\"handleapi\",\"synchapi\",\"impl-default\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "crunchy_0.2.4": "{\"dependencies\":[],\"features\":{\"default\":[\"limit_128\"],\"limit_1024\":[],\"limit_128\":[],\"limit_2048\":[],\"limit_256\":[],\"limit_512\":[],\"limit_64\":[],\"std\":[]}}", "crypto-common_0.1.7": "{\"dependencies\":[{\"features\":[\"more_lengths\"],\"name\":\"generic-array\",\"req\":\"=0.14.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"typenum\",\"req\":\"^1.14\"}],\"features\":{\"getrandom\":[\"rand_core/getrandom\"],\"std\":[]}}", - "cssparser-macros_0.6.1": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", - "cssparser_0.31.2": "{\"dependencies\":[{\"name\":\"cssparser-macros\",\"req\":\"^0.6.1\"},{\"kind\":\"dev\",\"name\":\"difference\",\"req\":\"^2.0\"},{\"name\":\"dtoa-short\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"encoding_rs\",\"req\":\"^0.8\"},{\"name\":\"itoa\",\"req\":\"^1.0\"},{\"features\":[\"macros\"],\"name\":\"phf\",\"req\":\">=0.8, <=0.11\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"smallvec\",\"req\":\"^1.0\"}],\"features\":{\"bench\":[],\"dummy_match_byte\":[]}}", "csv-core_0.1.13": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"arrayvec\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2\"}],\"features\":{\"default\":[],\"libc\":[\"memchr/libc\"]}}", "csv_1.4.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"serde\"],\"kind\":\"dev\",\"name\":\"bstr\",\"req\":\"^1.7.0\"},{\"name\":\"csv-core\",\"req\":\"^0.1.11\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"ryu\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"}],\"features\":{}}", "ctor-proc-macro_0.0.7": "{\"dependencies\":[],\"features\":{\"default\":[]}}", @@ -783,7 +779,6 @@ "derive_arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"derive\",\"parsing\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "derive_more-impl_1.0.0": "{\"dependencies\":[{\"name\":\"convert_case\",\"optional\":true,\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.13.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"syn\",\"req\":\"^2.0.45\"},{\"name\":\"unicode-xid\",\"optional\":true,\"req\":\"^0.2.2\"}],\"features\":{\"add\":[],\"add_assign\":[],\"as_ref\":[\"syn/extra-traits\",\"syn/visit\"],\"constructor\":[],\"debug\":[\"syn/extra-traits\",\"dep:unicode-xid\"],\"default\":[],\"deref\":[],\"deref_mut\":[],\"display\":[\"syn/extra-traits\",\"dep:unicode-xid\"],\"error\":[\"syn/extra-traits\"],\"from\":[\"syn/extra-traits\"],\"from_str\":[],\"full\":[\"add\",\"add_assign\",\"as_ref\",\"constructor\",\"debug\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\",\"from_str\",\"index\",\"index_mut\",\"into\",\"into_iterator\",\"is_variant\",\"mul\",\"mul_assign\",\"not\",\"sum\",\"try_from\",\"try_into\",\"try_unwrap\",\"unwrap\"],\"index\":[],\"index_mut\":[],\"into\":[\"syn/extra-traits\"],\"into_iterator\":[],\"is_variant\":[\"dep:convert_case\"],\"mul\":[\"syn/extra-traits\"],\"mul_assign\":[\"syn/extra-traits\"],\"not\":[\"syn/extra-traits\"],\"sum\":[],\"testing-helpers\":[\"dep:rustc_version\"],\"try_from\":[],\"try_into\":[\"syn/extra-traits\"],\"try_unwrap\":[\"dep:convert_case\"],\"unwrap\":[\"dep:convert_case\"]}}", "derive_more-impl_2.1.1": "{\"dependencies\":[{\"name\":\"convert_case\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4\"},{\"name\":\"syn\",\"req\":\"^2.0.45\"},{\"name\":\"unicode-xid\",\"optional\":true,\"req\":\"^0.2.2\"}],\"features\":{\"add\":[\"syn/extra-traits\",\"syn/visit\"],\"add_assign\":[\"syn/extra-traits\",\"syn/visit\"],\"as_ref\":[\"syn/extra-traits\",\"syn/visit\"],\"constructor\":[],\"debug\":[\"syn/extra-traits\",\"dep:unicode-xid\"],\"default\":[],\"deref\":[],\"deref_mut\":[],\"display\":[\"syn/extra-traits\",\"dep:unicode-xid\",\"dep:convert_case\"],\"eq\":[\"syn/extra-traits\",\"syn/visit\"],\"error\":[\"syn/extra-traits\"],\"from\":[\"syn/extra-traits\"],\"from_str\":[\"syn/full\",\"syn/visit\",\"dep:convert_case\"],\"full\":[\"add\",\"add_assign\",\"as_ref\",\"constructor\",\"debug\",\"deref\",\"deref_mut\",\"display\",\"eq\",\"error\",\"from\",\"from_str\",\"index\",\"index_mut\",\"into\",\"into_iterator\",\"is_variant\",\"mul\",\"mul_assign\",\"not\",\"sum\",\"try_from\",\"try_into\",\"try_unwrap\",\"unwrap\"],\"index\":[],\"index_mut\":[],\"into\":[\"syn/extra-traits\",\"syn/visit-mut\"],\"into_iterator\":[],\"is_variant\":[\"dep:convert_case\"],\"mul\":[\"syn/extra-traits\",\"syn/visit\"],\"mul_assign\":[\"syn/extra-traits\",\"syn/visit\"],\"not\":[\"syn/extra-traits\"],\"sum\":[],\"testing-helpers\":[\"syn/full\"],\"try_from\":[],\"try_into\":[\"syn/extra-traits\",\"syn/full\",\"syn/visit-mut\"],\"try_unwrap\":[\"dep:convert_case\"],\"unwrap\":[\"dep:convert_case\"]}}", - "derive_more_0.99.20": "{\"dependencies\":[{\"name\":\"convert_case\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"build\",\"name\":\"peg\",\"optional\":true,\"req\":\"^0.5\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"add\":[],\"add_assign\":[],\"as_mut\":[],\"as_ref\":[],\"constructor\":[],\"default\":[\"add_assign\",\"add\",\"as_mut\",\"as_ref\",\"constructor\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\",\"from_str\",\"index\",\"index_mut\",\"into\",\"into_iterator\",\"iterator\",\"mul_assign\",\"mul\",\"not\",\"sum\",\"try_into\",\"is_variant\",\"unwrap\"],\"deref\":[],\"deref_mut\":[],\"display\":[\"syn/extra-traits\"],\"error\":[\"syn/extra-traits\"],\"from\":[\"syn/extra-traits\"],\"from_str\":[],\"generate-parsing-rs\":[\"peg\"],\"index\":[],\"index_mut\":[],\"into\":[\"syn/extra-traits\"],\"into_iterator\":[],\"is_variant\":[\"convert_case\"],\"iterator\":[],\"mul\":[\"syn/extra-traits\"],\"mul_assign\":[\"syn/extra-traits\"],\"nightly\":[],\"not\":[\"syn/extra-traits\"],\"sum\":[],\"testing-helpers\":[\"rustc_version\"],\"track-caller\":[],\"try_into\":[\"syn/extra-traits\"],\"unwrap\":[\"convert_case\",\"rustc_version\"]}}", "derive_more_1.0.0": "{\"dependencies\":[{\"name\":\"derive_more-impl\",\"req\":\"=1.0.0\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.56\"}],\"features\":{\"add\":[\"derive_more-impl/add\"],\"add_assign\":[\"derive_more-impl/add_assign\"],\"as_ref\":[\"derive_more-impl/as_ref\"],\"constructor\":[\"derive_more-impl/constructor\"],\"debug\":[\"derive_more-impl/debug\"],\"default\":[\"std\"],\"deref\":[\"derive_more-impl/deref\"],\"deref_mut\":[\"derive_more-impl/deref_mut\"],\"display\":[\"derive_more-impl/display\"],\"error\":[\"derive_more-impl/error\"],\"from\":[\"derive_more-impl/from\"],\"from_str\":[\"derive_more-impl/from_str\"],\"full\":[\"add\",\"add_assign\",\"as_ref\",\"constructor\",\"debug\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\",\"from_str\",\"index\",\"index_mut\",\"into\",\"into_iterator\",\"is_variant\",\"mul\",\"mul_assign\",\"not\",\"sum\",\"try_from\",\"try_into\",\"try_unwrap\",\"unwrap\"],\"index\":[\"derive_more-impl/index\"],\"index_mut\":[\"derive_more-impl/index_mut\"],\"into\":[\"derive_more-impl/into\"],\"into_iterator\":[\"derive_more-impl/into_iterator\"],\"is_variant\":[\"derive_more-impl/is_variant\"],\"mul\":[\"derive_more-impl/mul\"],\"mul_assign\":[\"derive_more-impl/mul_assign\"],\"not\":[\"derive_more-impl/not\"],\"std\":[],\"sum\":[\"derive_more-impl/sum\"],\"testing-helpers\":[\"derive_more-impl/testing-helpers\",\"dep:rustc_version\"],\"try_from\":[\"derive_more-impl/try_from\"],\"try_into\":[\"derive_more-impl/try_into\"],\"try_unwrap\":[\"derive_more-impl/try_unwrap\"],\"unwrap\":[\"derive_more-impl/unwrap\"]}}", "derive_more_2.1.1": "{\"dependencies\":[{\"name\":\"derive_more-impl\",\"req\":\"=2.1.1\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.56\"}],\"features\":{\"add\":[\"derive_more-impl/add\"],\"add_assign\":[\"derive_more-impl/add_assign\"],\"as_ref\":[\"derive_more-impl/as_ref\"],\"constructor\":[\"derive_more-impl/constructor\"],\"debug\":[\"derive_more-impl/debug\"],\"default\":[\"std\"],\"deref\":[\"derive_more-impl/deref\"],\"deref_mut\":[\"derive_more-impl/deref_mut\"],\"display\":[\"derive_more-impl/display\"],\"eq\":[\"derive_more-impl/eq\"],\"error\":[\"derive_more-impl/error\"],\"from\":[\"derive_more-impl/from\"],\"from_str\":[\"derive_more-impl/from_str\"],\"full\":[\"add\",\"add_assign\",\"as_ref\",\"constructor\",\"debug\",\"deref\",\"deref_mut\",\"display\",\"eq\",\"error\",\"from\",\"from_str\",\"index\",\"index_mut\",\"into\",\"into_iterator\",\"is_variant\",\"mul\",\"mul_assign\",\"not\",\"sum\",\"try_from\",\"try_into\",\"try_unwrap\",\"unwrap\"],\"index\":[\"derive_more-impl/index\"],\"index_mut\":[\"derive_more-impl/index_mut\"],\"into\":[\"derive_more-impl/into\"],\"into_iterator\":[\"derive_more-impl/into_iterator\"],\"is_variant\":[\"derive_more-impl/is_variant\"],\"mul\":[\"derive_more-impl/mul\"],\"mul_assign\":[\"derive_more-impl/mul_assign\"],\"not\":[\"derive_more-impl/not\"],\"std\":[],\"sum\":[\"derive_more-impl/sum\"],\"testing-helpers\":[\"derive_more-impl/testing-helpers\",\"dep:rustc_version\"],\"try_from\":[\"derive_more-impl/try_from\"],\"try_into\":[\"derive_more-impl/try_into\"],\"try_unwrap\":[\"derive_more-impl/try_unwrap\"],\"unwrap\":[\"derive_more-impl/unwrap\"]}}", "deunicode_1.6.2": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"alloc\"]}}", @@ -800,15 +795,12 @@ "displaydoc_0.2.5": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.24\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "dotenvy_0.15.7": "{\"dependencies\":[{\"name\":\"clap\",\"optional\":true,\"req\":\"^3.2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"}],\"features\":{\"cli\":[\"clap\"]}}", "downcast-rs_1.2.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", - "dtoa-short_0.3.5": "{\"dependencies\":[{\"name\":\"dtoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"float-cmp\",\"req\":\"^0.4\"}],\"features\":{}}", - "dtoa_1.0.11": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{}}", "dtor-proc-macro_0.0.6": "{\"dependencies\":[],\"features\":{\"default\":[]}}", "dtor_0.1.1": "{\"dependencies\":[{\"name\":\"dtor-proc-macro\",\"optional\":true,\"req\":\"=0.0.6\"},{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"}],\"features\":{\"__no_warn_on_missing_unsafe\":[],\"default\":[\"proc_macro\",\"__no_warn_on_missing_unsafe\"],\"proc_macro\":[\"dep:dtor-proc-macro\"],\"used_linker\":[]}}", "dunce_1.0.5": "{\"dependencies\":[],\"features\":{}}", "dupe_0.9.1": "{\"dependencies\":[{\"name\":\"dupe_derive\",\"req\":\"=0.9.1\"}],\"features\":{}}", "dupe_derive_0.9.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0.3\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "dyn-clone_1.0.20": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{}}", - "ego-tree_0.6.3": "{\"dependencies\":[],\"features\":{}}", "either_1.15.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[],\"use_std\":[\"std\"]}}", "ena_0.14.3": "{\"dependencies\":[{\"name\":\"dogged\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"log\",\"req\":\"^0.4\"}],\"features\":{\"bench\":[],\"persistent\":[\"dogged\"]}}", "encode_unicode_1.0.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"features\":[\"https-native\"],\"kind\":\"dev\",\"name\":\"minreq\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -839,6 +831,7 @@ "fdeflate_0.3.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.7.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"simd-adler32\",\"req\":\"^0.3.4\"}],\"features\":{}}", "fiat-crypto_0.2.9": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "filedescriptor_0.8.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"winuser\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"processthreadsapi\",\"winsock2\",\"processenv\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", + "filetime_0.2.27": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.27\",\"target\":\"cfg(unix)\"},{\"name\":\"libredox\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{}}", "find-crate_0.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^0.11\"},{\"name\":\"toml\",\"req\":\"^0.5.2\"}],\"features\":{}}", "find-msvc-tools_0.1.9": "{\"dependencies\":[],\"features\":{}}", "findshlibs_0.10.2": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.67\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"name\":\"libc\",\"req\":\"^0.2.104\"},{\"features\":[\"psapi\",\"memoryapi\",\"libloaderapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{}}", @@ -861,7 +854,6 @@ "form_urlencoded_1.2.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"}],\"features\":{\"alloc\":[\"percent-encoding/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"percent-encoding/std\"]}}", "fs_extra_1.3.0": "{\"dependencies\":[],\"features\":{}}", "fsevent-sys_4.1.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.68\"}],\"features\":{}}", - "futf_0.1.5": "{\"dependencies\":[{\"name\":\"mac\",\"req\":\"^0.1.0\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1.0.2\"}],\"features\":{}}", "futures-channel_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", "futures-core_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-executor_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"name\":\"num_cpus\",\"optional\":true,\"req\":\"^1.8.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\",\"num_cpus\"]}}", @@ -891,7 +883,6 @@ "git+https://github.com/openai-oss-forks/tungstenite-rs?rev=9200079d3b54a1ff51072e24d81fd354f085156f#9200079d3b54a1ff51072e24d81fd354f085156f_tungstenite": "{\"dependencies\":[{\"name\":\"bytes\"},{\"default_features\":true,\"features\":[],\"name\":\"data-encoding\",\"optional\":true},{\"default_features\":false,\"features\":[\"zlib\"],\"name\":\"flate2\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"headers\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"http\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"httparse\",\"optional\":true},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"name\":\"rand\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"sha1\",\"optional\":true},{\"name\":\"thiserror\"},{\"default_features\":true,\"features\":[],\"name\":\"url\",\"optional\":true},{\"name\":\"utf-8\"},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\"],\"default\":[\"handshake\"],\"deflate\":[\"headers\",\"flate2\"],\"handshake\":[\"data-encoding\",\"headers\",\"httparse\",\"sha1\"],\"headers\":[\"http\",\"dep:headers\"],\"native-tls\":[\"native-tls-crate\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"proxy\":[\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"url\":[\"dep:url\"]},\"strip_prefix\":\"\"}", "glob_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3\"}],\"features\":{}}", "globset_0.4.18": "{\"dependencies\":[{\"name\":\"aho-corasick\",\"req\":\"^1.1.1\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"features\":[\"std\",\"perf\",\"syntax\",\"meta\",\"nfa\",\"hybrid\"],\"name\":\"regex-automata\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-syntax\",\"req\":\"^0.8.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.188\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.107\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"log\"],\"serde1\":[\"serde\"],\"simd-accel\":[]}}", - "h2_0.3.27": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^0.2\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.24\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^0.25\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", "h2_0.4.13": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", "half_2.7.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.4.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"crunchy\",\"req\":\"^0.2.2\",\"target\":\"cfg(target_arch = \\\"spirv\\\")\"},{\"kind\":\"dev\",\"name\":\"crunchy\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.16\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_distr\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"simd\"],\"name\":\"zerocopy\",\"req\":\"^0.8.26\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"nightly\":[],\"rand_distr\":[\"dep:rand\",\"dep:rand_distr\"],\"std\":[\"alloc\"],\"use-intrinsics\":[],\"zerocopy\":[]}}", "hashbrown_0.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"bumpalo\",\"optional\":true,\"req\":\"^3.5.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"ahash-compile-time-rng\":[\"ahash/compile-time-rng\"],\"default\":[\"ahash\",\"inline-more\"],\"inline-more\":[],\"nightly\":[],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", @@ -911,9 +902,7 @@ "home_0.5.12": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "hostname_0.4.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(unix, target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.6.1\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.65\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"default\":[],\"set\":[]}}", "hound_3.5.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cpal\",\"req\":\"^0.2.12\"}],\"features\":{}}", - "html5ever_0.26.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\",\"target\":\"cfg(bench)\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"mac\",\"req\":\"^0.1\"},{\"name\":\"markup5ever\",\"req\":\"^0.11\"},{\"kind\":\"build\",\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\",\"full\",\"fold\"],\"kind\":\"build\",\"name\":\"syn\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"typed-arena\",\"req\":\"^1.3.0\"}],\"features\":{}}", "http-body-util_0.1.3": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\",\"sync\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{\"channel\":[\"dep:tokio\"],\"default\":[],\"full\":[\"channel\"]}}", - "http-body_0.4.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^0.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{}}", "http-body_1.0.1": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"}],\"features\":{}}", "http-range-header_0.4.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.8.3\"}],\"features\":{}}", "http_0.2.12": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"indexmap\",\"req\":\"<=1.8\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"seahash\",\"req\":\"^3.0.5\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}", @@ -922,10 +911,8 @@ "httpdate_1.0.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"}],\"features\":{}}", "hyper-rustls_0.27.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"hyper\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"server-auto\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"tls12\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"io-std\",\"macros\",\"net\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"rustls/aws_lc_rs\"],\"default\":[\"native-tokio\",\"http1\",\"tls12\",\"logging\",\"aws-lc-rs\"],\"fips\":[\"aws-lc-rs\",\"rustls/fips\"],\"http1\":[\"hyper-util/http1\"],\"http2\":[\"hyper-util/http2\"],\"logging\":[\"log\",\"tokio-rustls/logging\",\"rustls/logging\"],\"native-tokio\":[\"rustls-native-certs\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"tokio-rustls/tls12\",\"rustls/tls12\"],\"webpki-tokio\":[\"webpki-roots\"]}}", "hyper-timeout_0.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"hyper\",\"req\":\"^1.1\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"hyper-tls\",\"req\":\"^0.6\"},{\"features\":[\"client-legacy\",\"http1\"],\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"features\":[\"client-legacy\",\"http1\",\"server\",\"server-graceful\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.35\"},{\"features\":[\"io-std\",\"io-util\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.35\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{}}", - "hyper-tls_0.5.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"tcp\",\"client\"],\"name\":\"hyper\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14.2\"},{\"name\":\"native-tls\",\"req\":\"^0.2.1\"},{\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io-std\",\"macros\",\"io-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"name\":\"tokio-native-tls\",\"req\":\"^0.3\"}],\"features\":{\"vendored\":[\"native-tls/vendored\"]}}", "hyper-tls_0.6.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"name\":\"native-tls\",\"req\":\"^0.2.1\"},{\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io-std\",\"macros\",\"io-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"name\":\"tokio-native-tls\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{\"alpn\":[\"native-tls/alpn\"],\"vendored\":[\"native-tls/vendored\"]}}", "hyper-util_0.1.19": "{\"dependencies\":[{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"req\":\"^1.7.1\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.16\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.16\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1.8.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.4.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.9\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.4\"},{\"kind\":\"dev\",\"name\":\"pnet_datalink\",\"req\":\"^0.35.0\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"macos\\\"))\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\">=0.5.9, <0.7\"},{\"name\":\"system-configuration\",\"optional\":true,\"req\":\">=0.5, <0.7\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"macros\",\"test-util\",\"signal\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tower-layer\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tower-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"windows-registry\",\"optional\":true,\"req\":\">=0.3, <0.7\",\"target\":\"cfg(windows)\"}],\"features\":{\"__internal_happy_eyeballs_tests\":[],\"client\":[\"hyper/client\",\"tokio/net\",\"dep:tracing\",\"dep:futures-channel\",\"dep:tower-service\"],\"client-legacy\":[\"client\",\"dep:socket2\",\"tokio/sync\",\"dep:libc\",\"dep:futures-util\"],\"client-pool\":[\"client\",\"dep:futures-util\",\"dep:tower-layer\"],\"client-proxy\":[\"client\",\"dep:base64\",\"dep:ipnet\",\"dep:percent-encoding\"],\"client-proxy-system\":[\"dep:system-configuration\",\"dep:windows-registry\"],\"default\":[],\"full\":[\"client\",\"client-legacy\",\"client-pool\",\"client-proxy\",\"client-proxy-system\",\"server\",\"server-auto\",\"server-graceful\",\"service\",\"http1\",\"http2\",\"tokio\",\"tracing\"],\"http1\":[\"hyper/http1\"],\"http2\":[\"hyper/http2\"],\"server\":[\"hyper/server\"],\"server-auto\":[\"server\",\"http1\",\"http2\"],\"server-graceful\":[\"server\",\"tokio/sync\"],\"service\":[\"dep:tower-service\"],\"tokio\":[\"dep:tokio\",\"tokio/rt\",\"tokio/time\"],\"tracing\":[\"dep:tracing\"]}}", - "hyper_0.14.32": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.3.24\"},{\"name\":\"http\",\"req\":\"^0.2\"},{\"name\":\"http-body\",\"req\":\"^0.4\"},{\"name\":\"httparse\",\"req\":\"^1.8\"},{\"name\":\"httpdate\",\"req\":\"^1.0\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.4\"},{\"kind\":\"dev\",\"name\":\"pnet_datalink\",\"req\":\"^0.27.2\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"macos\\\"))\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\">=0.4.7, <0.6.0\"},{\"kind\":\"dev\",\"name\":\"spmc\",\"req\":\"^0.3\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.27\"},{\"features\":[\"fs\",\"macros\",\"io-std\",\"io-util\",\"rt\",\"rt-multi-thread\",\"sync\",\"time\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.27\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"codec\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2\"},{\"name\":\"want\",\"req\":\"^0.3\"}],\"features\":{\"__internal_happy_eyeballs_tests\":[],\"backports\":[],\"client\":[],\"default\":[],\"deprecated\":[],\"ffi\":[\"libc\"],\"full\":[\"client\",\"http1\",\"http2\",\"server\",\"stream\",\"runtime\"],\"http1\":[],\"http2\":[\"h2\"],\"nightly\":[],\"runtime\":[\"tcp\",\"tokio/rt\",\"tokio/time\"],\"server\":[],\"stream\":[],\"tcp\":[\"socket2\",\"tokio/net\",\"tokio/rt\",\"tokio/time\"]}}", "hyper_1.8.1": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"optional\":true,\"req\":\"^1.1.2\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"form_urlencoded\",\"req\":\"^1\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"sink\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4.2\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"httparse\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"itoa\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2.4\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.4\"},{\"name\":\"pin-utils\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.12\"},{\"kind\":\"dev\",\"name\":\"spmc\",\"req\":\"^0.3\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"fs\",\"macros\",\"net\",\"io-std\",\"io-util\",\"rt\",\"rt-multi-thread\",\"sync\",\"time\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7.10\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"want\",\"optional\":true,\"req\":\"^0.3\"}],\"features\":{\"capi\":[],\"client\":[\"dep:want\",\"dep:pin-project-lite\",\"dep:smallvec\"],\"default\":[],\"ffi\":[\"dep:http-body-util\",\"dep:futures-util\"],\"full\":[\"client\",\"http1\",\"http2\",\"server\"],\"http1\":[\"dep:atomic-waker\",\"dep:futures-channel\",\"dep:futures-core\",\"dep:httparse\",\"dep:itoa\",\"dep:pin-utils\"],\"http2\":[\"dep:futures-channel\",\"dep:futures-core\",\"dep:h2\"],\"nightly\":[],\"server\":[\"dep:httpdate\",\"dep:pin-project-lite\",\"dep:smallvec\"],\"tracing\":[\"dep:tracing\"]}}", "i18n-config_0.4.8": "{\"dependencies\":[{\"name\":\"basic-toml\",\"req\":\"^0.1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"serde\"],\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{}}", "i18n-embed-fl_0.9.4": "{\"dependencies\":[{\"name\":\"dashmap\",\"optional\":true,\"req\":\"^6.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"find-crate\",\"req\":\"^0.6\"},{\"name\":\"fluent\",\"req\":\"^0.16\"},{\"name\":\"fluent-syntax\",\"req\":\"^0.11\"},{\"name\":\"i18n-config\",\"req\":\"^0.4.7\"},{\"features\":[\"fluent-system\",\"filesystem-assets\"],\"name\":\"i18n-embed\",\"req\":\"^0.15.4\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"name\":\"proc-macro-error2\",\"req\":\"^2.0.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rust-embed\",\"req\":\"^8.0\"},{\"name\":\"strsim\",\"req\":\"^0.11\"},{\"features\":[\"derive\",\"proc-macro\",\"parsing\",\"printing\",\"extra-traits\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{\"dashmap\":[\"dep:dashmap\"]}}", @@ -1014,10 +1001,8 @@ "lsp-types_0.94.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.0.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.34\"},{\"name\":\"serde_json\",\"req\":\"^1.0.50\"},{\"name\":\"serde_repr\",\"req\":\"^0.1\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.0.0\"}],\"features\":{\"default\":[],\"proposed\":[]}}", "lzma-rs_0.3.0": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"crc\",\"req\":\"^3.0.0\"},{\"name\":\"env_logger\",\"optional\":true,\"req\":\"^0.9.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"rust-lzma\",\"req\":\"^0.5\"}],\"features\":{\"enable_logging\":[\"env_logger\",\"log\"],\"raw_decoder\":[],\"stream\":[]}}", "lzma-sys_0.1.20": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.34\"},{\"name\":\"libc\",\"req\":\"^0.2.51\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.14\"}],\"features\":{\"static\":[]}}", - "mac_0.1.1": "{\"dependencies\":[],\"features\":{}}", "mach2_0.4.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"}],\"features\":{\"default\":[],\"unstable\":[]}}", "maplit_1.0.2": "{\"dependencies\":[],\"features\":{}}", - "markup5ever_0.11.0": "{\"dependencies\":[{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"phf\",\"req\":\"^0.10\"},{\"kind\":\"build\",\"name\":\"phf_codegen\",\"req\":\"^0.10\"},{\"name\":\"string_cache\",\"req\":\"^0.8\"},{\"kind\":\"build\",\"name\":\"string_cache_codegen\",\"req\":\"^0.5.1\"},{\"name\":\"tendril\",\"req\":\"^0.4\"}],\"features\":{}}", "matchers_0.2.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"syntax\",\"dfa-build\",\"dfa-search\"],\"name\":\"regex-automata\",\"req\":\"^0.4\"}],\"features\":{\"unicode\":[\"regex-automata/unicode\"]}}", "matchit_0.8.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "matchit_0.9.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"wayfind\",\"req\":\"^0.8\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", @@ -1109,26 +1094,17 @@ "parking_2.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{}}", "parking_lot_0.12.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"name\":\"lock_api\",\"req\":\"^0.4.14\"},{\"name\":\"parking_lot_core\",\"req\":\"^0.9.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"}],\"features\":{\"arc_lock\":[\"lock_api/arc_lock\"],\"deadlock_detection\":[\"parking_lot_core/deadlock_detection\"],\"default\":[],\"hardware-lock-elision\":[],\"nightly\":[\"parking_lot_core/nightly\",\"lock_api/nightly\"],\"owning_ref\":[\"lock_api/owning_ref\"],\"send_guard\":[],\"serde\":[\"lock_api/serde\"]}}", "parking_lot_core_0.9.12": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.60\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.95\",\"target\":\"cfg(unix)\"},{\"name\":\"petgraph\",\"optional\":true,\"req\":\"^0.6.0\"},{\"name\":\"redox_syscall\",\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"windows-link\",\"req\":\"^0.2.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"deadlock_detection\":[\"petgraph\",\"backtrace\"],\"nightly\":[]}}", - "password-hash_0.4.2": "{\"dependencies\":[{\"name\":\"base64ct\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"}],\"features\":{\"alloc\":[\"base64ct/alloc\"],\"default\":[\"rand_core\"],\"std\":[\"alloc\",\"base64ct/std\",\"rand_core/std\"]}}", "paste_1.0.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"paste-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}", "pastey_0.2.1": "{\"dependencies\":[],\"features\":{}}", "path-absolutize_3.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"path-dedot\",\"req\":\"^3.1.1\"},{\"kind\":\"dev\",\"name\":\"slash-formatter\",\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"lazy_static_cache\":[\"path-dedot/lazy_static_cache\"],\"once_cell_cache\":[\"path-dedot/once_cell_cache\"],\"unsafe_cache\":[\"path-dedot/unsafe_cache\"],\"use_unix_paths_on_wasm\":[\"path-dedot/use_unix_paths_on_wasm\"]}}", "path-dedot_3.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"lazy_static\",\"optional\":true,\"req\":\"^1.4\"},{\"name\":\"once_cell\",\"req\":\"^1.4\"}],\"features\":{\"lazy_static_cache\":[\"lazy_static\"],\"once_cell_cache\":[],\"unsafe_cache\":[],\"use_unix_paths_on_wasm\":[]}}", "pathdiff_0.2.3": "{\"dependencies\":[{\"name\":\"camino\",\"optional\":true,\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1.0.0\"}],\"features\":{}}", - "pbkdf2_0.11.0": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"hmac\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"sha1\",\"optional\":true,\"package\":\"sha-1\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"package\":\"sha-1\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"simple\"],\"parallel\":[\"rayon\",\"std\"],\"simple\":[\"hmac\",\"password-hash\",\"sha2\"],\"std\":[\"password-hash/std\"]}}", "pbkdf2_0.12.2": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"hmac\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.5\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.7\"},{\"default_features\":false,\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"hmac\"],\"parallel\":[\"rayon\",\"std\"],\"simple\":[\"hmac\",\"password-hash\",\"sha2\"],\"std\":[\"password-hash/std\"]}}", "pem-rfc7468_0.7.0": "{\"dependencies\":[{\"name\":\"base64ct\",\"req\":\"^1.4\"}],\"features\":{\"alloc\":[\"base64ct/alloc\"],\"std\":[\"alloc\",\"base64ct/std\"]}}", "pem_3.0.6": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde_core\"],\"std\":[\"base64/std\",\"serde_core?/std\"]}}", "percent-encoding_2.3.2": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "petgraph_0.6.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.7.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"kind\":\"dev\",\"name\":\"defmac\",\"req\":\"^0.2.1\"},{\"default_features\":false,\"name\":\"fixedbitset\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2.1\"},{\"name\":\"indexmap\",\"req\":\"^2.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.12.1\"},{\"kind\":\"dev\",\"name\":\"odds\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.5.5\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.5.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"all\":[\"unstable\",\"quickcheck\",\"matrix_graph\",\"stable_graph\",\"graphmap\",\"rayon\"],\"default\":[\"graphmap\",\"stable_graph\",\"matrix_graph\"],\"generate\":[],\"graphmap\":[],\"matrix_graph\":[],\"rayon\":[\"dep:rayon\",\"indexmap/rayon\"],\"serde-1\":[\"serde\",\"serde_derive\"],\"stable_graph\":[],\"unstable\":[\"generate\"]}}", "petgraph_0.8.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.7.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"kind\":\"dev\",\"name\":\"defmac\",\"req\":\"^0.2.1\"},{\"name\":\"dot-parser\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"dot-parser-macros\",\"optional\":true,\"req\":\"^0.5.1\"},{\"default_features\":false,\"name\":\"fixedbitset\",\"req\":\"^0.5.7\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2.1\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15.0\"},{\"default_features\":false,\"name\":\"indexmap\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.12.1\"},{\"kind\":\"dev\",\"name\":\"odds\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.5.5\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.5.3\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"all\":[\"unstable\",\"quickcheck\",\"matrix_graph\",\"stable_graph\",\"graphmap\",\"rayon\",\"dot_parser\"],\"default\":[\"std\",\"graphmap\",\"stable_graph\",\"matrix_graph\"],\"dot_parser\":[\"std\",\"dep:dot-parser\",\"dep:dot-parser-macros\"],\"generate\":[],\"graphmap\":[],\"matrix_graph\":[],\"quickcheck\":[\"std\",\"dep:quickcheck\",\"graphmap\",\"stable_graph\"],\"rayon\":[\"std\",\"dep:rayon\",\"indexmap/rayon\",\"hashbrown/rayon\"],\"serde-1\":[\"serde\",\"serde_derive\"],\"stable_graph\":[\"serde?/alloc\"],\"std\":[\"indexmap/std\"],\"unstable\":[\"generate\"]}}", - "phf_0.10.1": "{\"dependencies\":[{\"name\":\"phf_macros\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"phf_shared\",\"req\":\"^0.10.0\"},{\"name\":\"proc-macro-hack\",\"optional\":true,\"req\":\"^0.5.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"macros\":[\"phf_macros\",\"proc-macro-hack\"],\"std\":[\"phf_shared/std\"],\"uncased\":[\"phf_shared/uncased\"],\"unicase\":[\"phf_shared/unicase\"]}}", - "phf_0.11.3": "{\"dependencies\":[{\"name\":\"phf_macros\",\"optional\":true,\"req\":\"^0.11.3\"},{\"default_features\":false,\"name\":\"phf_shared\",\"req\":\"^0.11.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"macros\":[\"phf_macros\"],\"std\":[\"phf_shared/std\"],\"uncased\":[\"phf_shared/uncased\"],\"unicase\":[\"phf_macros?/unicase\",\"phf_shared/unicase\"]}}", - "phf_codegen_0.10.0": "{\"dependencies\":[{\"name\":\"phf_generator\",\"req\":\"^0.10.0\"},{\"name\":\"phf_shared\",\"req\":\"^0.10.0\"}],\"features\":{}}", - "phf_generator_0.10.0": "{\"dependencies\":[{\"name\":\"criterion\",\"optional\":true,\"req\":\"=0.3.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"=0.3.4\"},{\"default_features\":false,\"name\":\"phf_shared\",\"req\":\"^0.10.0\"},{\"features\":[\"small_rng\"],\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{}}", - "phf_generator_0.11.3": "{\"dependencies\":[{\"name\":\"criterion\",\"optional\":true,\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"name\":\"phf_shared\",\"req\":\"^0.11.2\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{}}", - "phf_macros_0.11.3": "{\"dependencies\":[{\"name\":\"phf_generator\",\"req\":\"^0.11.1\"},{\"default_features\":false,\"name\":\"phf_shared\",\"req\":\"^0.11.2\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"},{\"name\":\"unicase_\",\"optional\":true,\"package\":\"unicase\",\"req\":\"^2.4.0\"}],\"features\":{\"unicase\":[\"unicase_\",\"phf_shared/unicase\"]}}", - "phf_shared_0.10.0": "{\"dependencies\":[{\"name\":\"siphasher\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"uncased\",\"optional\":true,\"req\":\"^0.9.6\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.4.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "phf_shared_0.11.3": "{\"dependencies\":[{\"name\":\"siphasher\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"uncased\",\"optional\":true,\"req\":\"^0.9.9\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.4.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "pin-project-internal_1.1.10": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0.25\"},{\"default_features\":false,\"features\":[\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\",\"full\",\"visit-mut\"],\"name\":\"syn\",\"req\":\"^2.0.1\"}],\"features\":{}}", "pin-project-lite_0.2.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", @@ -1147,7 +1123,6 @@ "portable-pty_0.9.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"bitflags\",\"req\":\"^1.3\",\"target\":\"cfg(windows)\"},{\"name\":\"downcast-rs\",\"req\":\"^1.0\"},{\"name\":\"filedescriptor\",\"req\":\"^0.8.3\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"term\",\"fs\"],\"name\":\"nix\",\"req\":\"^0.28\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serial2\",\"req\":\"^0.2\"},{\"name\":\"shared_library\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"shell-words\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"smol\",\"req\":\"^2.0\"},{\"features\":[\"winuser\",\"consoleapi\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"synchapi\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"},{\"name\":\"winreg\",\"req\":\"^0.10\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"serde_support\":[\"serde\",\"serde_derive\"]}}", "potential_utf_0.1.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"writeable\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"writeable/alloc\",\"zerovec?/alloc\"],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\"],\"writeable\":[\"dep:writeable\"],\"zerovec\":[\"dep:zerovec\"]}}", "powerfmt_0.2.0": "{\"dependencies\":[{\"name\":\"powerfmt-macros\",\"optional\":true,\"req\":\"=0.1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"macros\"],\"macros\":[\"dep:powerfmt-macros\"],\"std\":[\"alloc\"]}}", - "ppt-rs_0.2.6": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"clock\",\"std\"],\"name\":\"chrono\",\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"name\":\"clap\",\"req\":\"^4.5\"},{\"name\":\"pulldown-cmark\",\"req\":\"^0.10\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"features\":[\"blocking\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.11\"},{\"name\":\"scraper\",\"optional\":true,\"req\":\"^0.18\"},{\"name\":\"syntect\",\"req\":\"^5.2\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.5\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"req\":\"^1.0\"},{\"name\":\"xml-rs\",\"req\":\"^0.8\"},{\"name\":\"zip\",\"req\":\"^0.6\"}],\"features\":{\"default\":[\"web2ppt\"],\"web2ppt\":[\"reqwest\",\"scraper\",\"url\"]}}", "ppv-lite86_0.2.21": "{\"dependencies\":[{\"features\":[\"simd\"],\"name\":\"zerocopy\",\"req\":\"^0.8.23\"}],\"features\":{\"default\":[\"std\"],\"no_simd\":[],\"simd\":[],\"std\":[]}}", "precomputed-hash_0.1.1": "{\"dependencies\":[],\"features\":{}}", "predicates-core_1.0.9": "{\"dependencies\":[],\"features\":{}}", @@ -1214,7 +1189,6 @@ "regex-syntax_0.6.29": "{\"dependencies\":[],\"features\":{\"default\":[\"unicode\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", "regex-syntax_0.8.8": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\",\"unicode\"],\"std\":[],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", "regex_1.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aho-corasick\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"atty\",\"humantime\",\"termcolor\"],\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.6.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"alloc\",\"syntax\",\"meta\",\"nfa-pikevm\"],\"name\":\"regex-automata\",\"req\":\"^0.4.12\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"default\":[\"std\",\"perf\",\"unicode\",\"regex-syntax/default\"],\"logging\":[\"aho-corasick?/logging\",\"memchr?/logging\",\"regex-automata/logging\"],\"pattern\":[],\"perf\":[\"perf-cache\",\"perf-dfa\",\"perf-onepass\",\"perf-backtrack\",\"perf-inline\",\"perf-literal\"],\"perf-backtrack\":[\"regex-automata/nfa-backtrack\"],\"perf-cache\":[],\"perf-dfa\":[\"regex-automata/hybrid\"],\"perf-dfa-full\":[\"regex-automata/dfa-build\",\"regex-automata/dfa-search\"],\"perf-inline\":[\"regex-automata/perf-inline\"],\"perf-literal\":[\"dep:aho-corasick\",\"dep:memchr\",\"regex-automata/perf-literal\"],\"perf-onepass\":[\"regex-automata/dfa-onepass\"],\"std\":[\"aho-corasick?/std\",\"memchr?/std\",\"regex-automata/std\",\"regex-syntax/std\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"regex-automata/unicode\",\"regex-syntax/unicode\"],\"unicode-age\":[\"regex-automata/unicode-age\",\"regex-syntax/unicode-age\"],\"unicode-bool\":[\"regex-automata/unicode-bool\",\"regex-syntax/unicode-bool\"],\"unicode-case\":[\"regex-automata/unicode-case\",\"regex-syntax/unicode-case\"],\"unicode-gencat\":[\"regex-automata/unicode-gencat\",\"regex-syntax/unicode-gencat\"],\"unicode-perl\":[\"regex-automata/unicode-perl\",\"regex-automata/unicode-word-boundary\",\"regex-syntax/unicode-perl\"],\"unicode-script\":[\"regex-automata/unicode-script\",\"regex-syntax/unicode-script\"],\"unicode-segment\":[\"regex-automata/unicode-segment\",\"regex-syntax/unicode-segment\"],\"unstable\":[\"pattern\"],\"use_std\":[\"std\"]}}", - "reqwest_0.11.27": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"tokio\"],\"name\":\"async-compression\",\"optional\":true,\"req\":\"^0.4.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"base64\",\"req\":\"^0.21\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^3.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.17.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.20.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"req\":\"^0.3.14\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio-runtime\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.24\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^0.2\"},{\"name\":\"http-body\",\"req\":\"^0.4.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tcp\",\"http1\",\"http2\",\"client\",\"runtime\"],\"name\":\"hyper\",\"req\":\"^0.14.21\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tcp\",\"stream\",\"http1\",\"http2\",\"client\",\"server\",\"runtime\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.24.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"ipnet\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.45\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libflate\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"once_cell\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls-rustls\",\"ring\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"dangerous_configuration\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.21.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-pemfile\",\"optional\":true,\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"name\":\"sync_wrapper\",\"req\":\"^0.1.2\"},{\"name\":\"system-configuration\",\"req\":\"^0.5.1\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.24\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-socks\",\"optional\":true,\"req\":\"^0.5.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"url\",\"req\":\"^2.2\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.68\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.68\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\"],\"name\":\"web-sys\",\"req\":\"^0.3.25\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"winreg\",\"req\":\"^0.50.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"__internal_proxy_sys_no_cache\":[],\"__rustls\":[\"hyper-rustls\",\"tokio-rustls\",\"rustls\",\"__tls\"],\"__tls\":[\"dep:rustls-pemfile\"],\"blocking\":[\"futures-util/io\",\"tokio/sync\"],\"brotli\":[\"async-compression\",\"async-compression/brotli\",\"tokio-util\"],\"cookies\":[\"cookie_crate\",\"cookie_store\"],\"default\":[\"default-tls\"],\"default-tls\":[\"hyper-tls\",\"native-tls-crate\",\"__tls\",\"tokio-native-tls\"],\"deflate\":[\"async-compression\",\"async-compression/zlib\",\"tokio-util\"],\"gzip\":[\"async-compression\",\"async-compression/gzip\",\"tokio-util\"],\"hickory-dns\":[\"hickory-resolver\"],\"http3\":[\"rustls-tls-manual-roots\",\"h3\",\"h3-quinn\",\"quinn\",\"futures-channel\"],\"json\":[\"serde_json\"],\"multipart\":[\"mime_guess\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-native-certs\",\"__rustls\"],\"rustls-tls-webpki-roots\":[\"webpki-roots\",\"__rustls\"],\"socks\":[\"tokio-socks\"],\"stream\":[\"tokio/fs\",\"tokio-util\",\"wasm-streams\"],\"trust-dns\":[\"hickory-dns\"]}}", "reqwest_0.12.28": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.28\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"rustls\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"features\":[\"futures\"],\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\",\"RequestCache\"],\"name\":\"web-sys\",\"req\":\"^0.3.28\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"}],\"features\":{\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-ring\":[\"hyper-rustls?/ring\",\"tokio-rustls?/ring\",\"rustls?/ring\",\"quinn?/ring\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls-tls-manual-roots\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde_json\"],\"macos-system-configuration\":[\"system-proxy\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate?/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"rustls-tls-manual-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-manual-roots-no-provider\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-tls-native-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-native-roots-no-provider\":[\"dep:rustls-native-certs\",\"hyper-rustls?/native-tokio\",\"__rustls\"],\"rustls-tls-no-provider\":[\"rustls-tls-manual-roots-no-provider\"],\"rustls-tls-webpki-roots\":[\"rustls-tls-webpki-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-webpki-roots-no-provider\":[\"dep:webpki-roots\",\"hyper-rustls?/webpki-tokio\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"trust-dns\":[],\"zstd\":[\"tower-http/decompression-zstd\"]}}", "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", @@ -1233,7 +1207,6 @@ "rustix_0.38.44": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"itoa\",\"optional\":true,\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.161\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.161\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.161\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.5.2\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"))\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_NetworkManagement_IpHelper\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"procfs\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"cc\":[],\"default\":[\"std\",\"use-libc-auxv\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"linux-raw-sys/io_uring\"],\"libc-extra-traits\":[\"libc?/extra_traits\"],\"linux_4_11\":[],\"linux_latest\":[\"linux_4_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[\"fs\"],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"procfs\":[\"once_cell\",\"itoa\",\"fs\"],\"pty\":[\"itoa\",\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"compiler_builtins\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\",\"compiler_builtins?/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\",\"libc-extra-traits\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\",\"libc-extra-traits\"],\"use-libc-auxv\":[]}}", "rustix_1.1.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}", "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", - "rustls-pemfile_1.0.4": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.21\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", "rustls-webpki_0.103.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", "rustls_0.23.36": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", @@ -1254,7 +1227,6 @@ "schemars_derive_1.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.2.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"serde_derive_internals\",\"req\":\"^0.29.1\"},{\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"extra-traits\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "scoped-tls_1.0.1": "{\"dependencies\":[],\"features\":{}}", "scopeguard_1.2.0": "{\"dependencies\":[],\"features\":{\"default\":[\"use_std\"],\"use_std\":[]}}", - "scraper_0.18.1": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"cssparser\",\"req\":\"^0.31.0\"},{\"name\":\"ego-tree\",\"req\":\"^0.6.2\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2.21\"},{\"name\":\"html5ever\",\"req\":\"^0.26\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.0\"},{\"name\":\"selectors\",\"req\":\"^0.25.0\"},{\"name\":\"tendril\",\"req\":\"^0.4.3\"}],\"features\":{\"atomic\":[],\"default\":[\"main\",\"errors\"],\"deterministic\":[\"indexmap\"],\"errors\":[],\"main\":[\"getopts\"]}}", "scrypt_0.11.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"password-hash\",\"req\":\"^0.5\"},{\"name\":\"pbkdf2\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"salsa20\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"simple\",\"std\"],\"simple\":[\"password-hash\"],\"std\":[\"password-hash/std\"]}}", "sdd_3.0.10": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.6\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{}}", "seccompiler_0.5.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.153\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.27\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.9\"}],\"features\":{\"json\":[\"serde\",\"serde_json\"]}}", @@ -1263,7 +1235,6 @@ "security-framework-sys_2.15.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"name\":\"libc\",\"req\":\"^0.2.150\"}],\"features\":{\"OSX_10_10\":[\"OSX_10_9\"],\"OSX_10_11\":[\"OSX_10_10\"],\"OSX_10_12\":[\"OSX_10_11\"],\"OSX_10_13\":[\"OSX_10_12\"],\"OSX_10_14\":[\"OSX_10_13\"],\"OSX_10_15\":[\"OSX_10_14\"],\"OSX_10_9\":[],\"default\":[\"OSX_10_12\"]}}", "security-framework_2.11.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.6\"},{\"name\":\"core-foundation\",\"req\":\"^0.9.4\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"libc\",\"req\":\"^0.2.139\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4.6\"},{\"default_features\":false,\"name\":\"security-framework-sys\",\"req\":\"^2.11.1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.17\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.16\"}],\"features\":{\"OSX_10_10\":[\"OSX_10_9\",\"security-framework-sys/OSX_10_10\"],\"OSX_10_11\":[\"OSX_10_10\",\"security-framework-sys/OSX_10_11\"],\"OSX_10_12\":[\"OSX_10_11\",\"security-framework-sys/OSX_10_12\"],\"OSX_10_13\":[\"OSX_10_12\",\"security-framework-sys/OSX_10_13\",\"alpn\",\"session-tickets\",\"serial-number-bigint\"],\"OSX_10_14\":[\"OSX_10_13\",\"security-framework-sys/OSX_10_14\"],\"OSX_10_15\":[\"OSX_10_14\",\"security-framework-sys/OSX_10_15\"],\"OSX_10_9\":[\"security-framework-sys/OSX_10_9\"],\"alpn\":[],\"default\":[\"OSX_10_12\"],\"job-bless\":[],\"nightly\":[],\"serial-number-bigint\":[\"dep:num-bigint\"],\"session-tickets\":[]}}", "security-framework_3.5.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.6\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"libc\",\"req\":\"^0.2.139\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"name\":\"security-framework-sys\",\"req\":\"^2.15\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.12.0\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.23\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.16\"}],\"features\":{\"OSX_10_12\":[\"security-framework-sys/OSX_10_12\"],\"OSX_10_13\":[\"OSX_10_12\",\"security-framework-sys/OSX_10_13\",\"alpn\",\"session-tickets\"],\"OSX_10_14\":[\"OSX_10_13\",\"security-framework-sys/OSX_10_14\"],\"OSX_10_15\":[\"OSX_10_14\",\"security-framework-sys/OSX_10_15\"],\"alpn\":[],\"default\":[\"OSX_10_12\"],\"job-bless\":[],\"nightly\":[],\"session-tickets\":[],\"sync-keychain\":[\"OSX_10_13\"]}}", - "selectors_0.25.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"cssparser\",\"req\":\"^0.31\"},{\"default_features\":false,\"features\":[\"add\",\"add_assign\"],\"name\":\"derive_more\",\"req\":\"^0.99\"},{\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1\"},{\"name\":\"phf\",\"req\":\"^0.10\"},{\"kind\":\"build\",\"name\":\"phf_codegen\",\"req\":\"^0.10\"},{\"name\":\"precomputed-hash\",\"req\":\"^0.1\"},{\"name\":\"servo_arc\",\"req\":\"^0.3\"},{\"name\":\"smallvec\",\"req\":\"^1.0\"}],\"features\":{\"bench\":[]}}", "self_cell_0.10.3": "{\"dependencies\":[{\"name\":\"new_self_cell\",\"package\":\"self_cell\",\"req\":\"^1\"}],\"features\":{\"old_rust\":[\"new_self_cell/old_rust\"]}}", "self_cell_1.2.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"=1.1.0\"},{\"name\":\"rustversion\",\"optional\":true,\"req\":\">=1\"}],\"features\":{\"old_rust\":[\"rustversion\"]}}", "semver_1.0.27": "{\"dependencies\":[{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"package\":\"serde_core\",\"req\":\"^1.0.220\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\",\"target\":\"cfg(any())\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde\"],\"std\":[]}}", @@ -1292,7 +1263,6 @@ "serial2_0.2.33": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert2\",\"req\":\"^0.3.11\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\",\"target\":\"cfg(unix)\"},{\"name\":\"libc\",\"req\":\"^0.2.109\",\"target\":\"cfg(unix)\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.108\"},{\"features\":[\"commapi\",\"fileapi\",\"handleapi\",\"ioapiset\",\"std\",\"synchapi\",\"winbase\",\"winerror\",\"winreg\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{\"doc\":[],\"doc-cfg\":[],\"rs4xx\":[],\"serde\":[\"dep:serde\"],\"unix\":[],\"windows\":[]}}", "serial_test_3.3.1": "{\"dependencies\":[{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"env_logger\",\"optional\":true,\"req\":\">=0.6.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"fslock\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"use_std\"],\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\">=0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\">=0.4.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"default_features\":false,\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"scc\",\"req\":\"^2\"},{\"name\":\"serial_test_derive\",\"req\":\"~3.3.1\"}],\"features\":{\"async\":[\"dep:futures-executor\",\"dep:futures-util\",\"serial_test_derive/async\"],\"default\":[\"logging\",\"async\"],\"docsrs\":[\"dep:document-features\"],\"file_locks\":[\"dep:fslock\"],\"logging\":[\"dep:log\"],\"test_logging\":[\"logging\",\"dep:env_logger\",\"serial_test_derive/test_logging\"]}}", "serial_test_derive_3.3.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\">=0.6.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"full\",\"printing\",\"parsing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"async\":[],\"default\":[],\"file_locks\":[],\"test_logging\":[]}}", - "servo_arc_0.3.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"stable_deref_trait\",\"req\":\"^1.0.0\"}],\"features\":{\"gecko_refcount_logging\":[],\"servo\":[\"serde\"]}}", "sha1_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha1-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"sha1-asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", "sha1_smol_1.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "sha2_0.10.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha2-asm\",\"optional\":true,\"req\":\"^0.6.1\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"}],\"features\":{\"asm\":[\"sha2-asm\"],\"asm-aarch64\":[\"asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"force-soft-compact\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", @@ -1307,7 +1277,6 @@ "simd-adler32_0.3.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adler\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"adler32\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"const-generics\":[],\"default\":[\"std\",\"const-generics\"],\"nightly\":[],\"std\":[]}}", "simdutf8_0.1.5": "{\"dependencies\":[],\"features\":{\"aarch64_neon\":[],\"aarch64_neon_prefetch\":[],\"default\":[\"std\"],\"hints\":[],\"public_imp\":[],\"std\":[]}}", "similar_2.7.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"console\",\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.10.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.68\"},{\"name\":\"unicode-segmentation\",\"optional\":true,\"req\":\"^1.7.1\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1.1\"}],\"features\":{\"bytes\":[\"bstr\",\"text\"],\"default\":[\"text\"],\"inline\":[\"text\"],\"text\":[],\"unicode\":[\"text\",\"unicode-segmentation\",\"bstr?/unicode\",\"bstr?/std\"],\"wasm32_web_time\":[\"web-time\"]}}", - "siphasher_0.3.11": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde_no_std\":[\"serde/alloc\"],\"serde_std\":[\"std\",\"serde/std\"],\"std\":[]}}", "siphasher_1.0.2": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde_no_std\":[\"serde/alloc\"],\"serde_std\":[\"std\",\"serde/std\"],\"std\":[]}}", "slab_0.4.12": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "smallvec_1.15.1": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"bincode\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"bincode1\",\"package\":\"bincode\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1.0\"},{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"unty\",\"optional\":true,\"req\":\"^0.0.4\"}],\"features\":{\"const_generics\":[],\"const_new\":[\"const_generics\"],\"debugger_visualizer\":[],\"drain_filter\":[],\"drain_keep_rest\":[\"drain_filter\"],\"impl_bincode\":[\"bincode\",\"unty\"],\"may_dangle\":[],\"specialization\":[],\"union\":[],\"write\":[]}}", @@ -1334,7 +1303,6 @@ "stop-words_0.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"human_regex\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"constructed\":[],\"default\":[\"iso\"],\"iso\":[],\"nltk\":[],\"unimplemented\":[]}}", "streaming-iterator_0.1.9": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "string_cache_0.8.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1.0.2\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"precomputed-hash\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"serde_support\"],\"serde_support\":[\"serde\"]}}", - "string_cache_codegen_0.5.4": "{\"dependencies\":[{\"name\":\"phf_generator\",\"req\":\"^0.11\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"}],\"features\":{}}", "stringprep_0.1.5": "{\"dependencies\":[{\"name\":\"unicode-bidi\",\"req\":\"^0.3\"},{\"name\":\"unicode-normalization\",\"req\":\"^0.1\"},{\"name\":\"unicode-properties\",\"req\":\"^0.1.1\"}],\"features\":{}}", "strsim_0.10.0": "{\"dependencies\":[],\"features\":{}}", "strsim_0.11.1": "{\"dependencies\":[],\"features\":{}}", @@ -1347,18 +1315,15 @@ "supports-color_3.0.2": "{\"dependencies\":[{\"name\":\"is_ci\",\"req\":\"^1.2.0\"}],\"features\":{}}", "syn_1.0.109": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.46\"},{\"default_features\":false,\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"features\":[\"blocking\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"syn-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4.16\"},{\"kind\":\"dev\",\"name\":\"termcolor\",\"req\":\"^1.0\"},{\"name\":\"unicode-ident\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.1\"}],\"features\":{\"clone-impls\":[],\"default\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\"],\"derive\":[],\"extra-traits\":[],\"fold\":[],\"full\":[],\"parsing\":[],\"printing\":[\"quote\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"quote/proc-macro\"],\"test\":[\"syn-test-suite/all-features\"],\"visit\":[],\"visit-mut\":[]}}", "syn_2.0.114": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1\"},{\"features\":[\"blocking\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.13\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"syn-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4.16\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"termcolor\",\"req\":\"^1\"},{\"name\":\"unicode-ident\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\",\"target\":\"cfg(not(miri))\"}],\"features\":{\"clone-impls\":[],\"default\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\"],\"derive\":[],\"extra-traits\":[],\"fold\":[],\"full\":[],\"parsing\":[],\"printing\":[\"dep:quote\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"quote?/proc-macro\"],\"test\":[\"syn-test-suite/all-features\"],\"visit\":[],\"visit-mut\":[]}}", - "sync_wrapper_0.1.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"}],\"features\":{\"futures\":[\"futures-core\"]}}", "sync_wrapper_1.0.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"}],\"features\":{\"futures\":[\"futures-core\"]}}", "synstructure_0.13.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"visit\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"synstructure_test_traits\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"proc-macro\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"syn/proc-macro\",\"quote/proc-macro\"]}}", "syntect_5.3.0": "{\"dependencies\":[{\"name\":\"bincode\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"fancy-regex\",\"optional\":true,\"req\":\"^0.16.2\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"fnv\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"getopts\",\"req\":\"^0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"default_features\":false,\"name\":\"onig\",\"optional\":true,\"req\":\"^6.5.1\"},{\"name\":\"plist\",\"optional\":true,\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"public-api\",\"req\":\"^0.50.1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustdoc-json\",\"req\":\"^0.9.7\"},{\"kind\":\"dev\",\"name\":\"rustup-toolchain\",\"req\":\"^0.1.5\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"name\":\"walkdir\",\"req\":\"^2.0\"},{\"name\":\"yaml-rust\",\"optional\":true,\"req\":\"^0.4.5\"}],\"features\":{\"default\":[\"default-onig\"],\"default-fancy\":[\"parsing\",\"default-syntaxes\",\"default-themes\",\"html\",\"plist-load\",\"yaml-load\",\"dump-load\",\"dump-create\",\"regex-fancy\"],\"default-onig\":[\"parsing\",\"default-syntaxes\",\"default-themes\",\"html\",\"plist-load\",\"yaml-load\",\"dump-load\",\"dump-create\",\"regex-onig\"],\"default-syntaxes\":[\"parsing\",\"dump-load\"],\"default-themes\":[\"dump-load\"],\"dump-create\":[\"flate2\",\"bincode\"],\"dump-load\":[\"flate2\",\"bincode\"],\"html\":[\"parsing\"],\"metadata\":[\"parsing\",\"plist-load\",\"dep:serde_json\"],\"parsing\":[\"regex-syntax\",\"fnv\",\"dump-create\",\"dump-load\"],\"plist-load\":[\"plist\",\"dep:serde_json\"],\"regex-fancy\":[\"fancy-regex\"],\"regex-onig\":[\"onig\"],\"yaml-load\":[\"yaml-rust\",\"parsing\"]}}", "sys-locale_0.3.2": "{\"dependencies\":[{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"features\":[\"Window\",\"WorkerGlobalScope\",\"Navigator\",\"WorkerNavigator\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"}],\"features\":{\"js\":[\"js-sys\",\"wasm-bindgen\",\"web-sys\"]}}", - "system-configuration-sys_0.5.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.49\"}],\"features\":{}}", "system-configuration-sys_0.6.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.149\"}],\"features\":{}}", - "system-configuration_0.5.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.5\"}],\"features\":{}}", "system-configuration_0.6.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}", "tagptr_0.2.0": "{\"dependencies\":[],\"features\":{}}", + "tar_0.4.44": "{\"dependencies\":[{\"name\":\"filetime\",\"req\":\"^0.2.8\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"xattr\",\"optional\":true,\"req\":\"^1.1.3\",\"target\":\"cfg(unix)\"}],\"features\":{\"default\":[\"xattr\"]}}", "tempfile_3.24.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fastrand\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.1.3\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Storage_FileSystem\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"getrandom\"],\"nightly\":[]}}", - "tendril_0.4.3": "{\"dependencies\":[{\"name\":\"encoding\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8.12\"},{\"name\":\"futf\",\"req\":\"^0.1.5\"},{\"name\":\"mac\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.4\"},{\"name\":\"utf-8\",\"req\":\"^0.7\"}],\"features\":{\"bench\":[]}}", "term_0.7.0": "{\"dependencies\":[{\"name\":\"dirs-next\",\"req\":\"^2\"},{\"name\":\"rustversion\",\"req\":\"^1\",\"target\":\"cfg(windows)\"},{\"features\":[\"consoleapi\",\"wincon\",\"handleapi\",\"fileapi\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[]}}", "termcolor_1.4.1": "{\"dependencies\":[{\"name\":\"winapi-util\",\"req\":\"^0.1.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "terminal_size_0.4.3": "{\"dependencies\":[{\"features\":[\"termios\"],\"name\":\"rustix\",\"req\":\"^1.0.1\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\"],\"name\":\"windows-sys\",\"req\":\"^0.60.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -1568,8 +1533,8 @@ "x11rb_0.13.2": "{\"dependencies\":[{\"name\":\"as-raw-xcb-connection\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"gethostname\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.19\"},{\"kind\":\"dev\",\"name\":\"polling\",\"req\":\"^3.4\"},{\"name\":\"raw-window-handle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"std\",\"event\",\"fs\",\"net\",\"system\"],\"name\":\"rustix\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"x11rb-protocol\",\"req\":\"^0.13.2\"},{\"name\":\"xcursor\",\"optional\":true,\"req\":\"^0.3.7\"}],\"features\":{\"all-extensions\":[\"x11rb-protocol/all-extensions\",\"composite\",\"damage\",\"dbe\",\"dpms\",\"dri2\",\"dri3\",\"glx\",\"present\",\"randr\",\"record\",\"render\",\"res\",\"screensaver\",\"shape\",\"shm\",\"sync\",\"xevie\",\"xf86dri\",\"xf86vidmode\",\"xfixes\",\"xinerama\",\"xinput\",\"xkb\",\"xprint\",\"xselinux\",\"xtest\",\"xv\",\"xvmc\"],\"allow-unsafe-code\":[\"libc\",\"as-raw-xcb-connection\"],\"composite\":[\"x11rb-protocol/composite\",\"xfixes\"],\"cursor\":[\"render\",\"resource_manager\",\"xcursor\"],\"damage\":[\"x11rb-protocol/damage\",\"xfixes\"],\"dbe\":[\"x11rb-protocol/dbe\"],\"dl-libxcb\":[\"allow-unsafe-code\",\"libloading\",\"once_cell\"],\"dpms\":[\"x11rb-protocol/dpms\"],\"dri2\":[\"x11rb-protocol/dri2\"],\"dri3\":[\"x11rb-protocol/dri3\"],\"extra-traits\":[\"x11rb-protocol/extra-traits\"],\"glx\":[\"x11rb-protocol/glx\"],\"image\":[],\"present\":[\"x11rb-protocol/present\",\"randr\",\"xfixes\",\"sync\"],\"randr\":[\"x11rb-protocol/randr\",\"render\"],\"record\":[\"x11rb-protocol/record\"],\"render\":[\"x11rb-protocol/render\"],\"request-parsing\":[\"x11rb-protocol/request-parsing\"],\"res\":[\"x11rb-protocol/res\"],\"resource_manager\":[\"x11rb-protocol/resource_manager\"],\"screensaver\":[\"x11rb-protocol/screensaver\"],\"shape\":[\"x11rb-protocol/shape\"],\"shm\":[\"x11rb-protocol/shm\"],\"sync\":[\"x11rb-protocol/sync\"],\"xevie\":[\"x11rb-protocol/xevie\"],\"xf86dri\":[\"x11rb-protocol/xf86dri\"],\"xf86vidmode\":[\"x11rb-protocol/xf86vidmode\"],\"xfixes\":[\"x11rb-protocol/xfixes\",\"render\",\"shape\"],\"xinerama\":[\"x11rb-protocol/xinerama\"],\"xinput\":[\"x11rb-protocol/xinput\",\"xfixes\"],\"xkb\":[\"x11rb-protocol/xkb\"],\"xprint\":[\"x11rb-protocol/xprint\"],\"xselinux\":[\"x11rb-protocol/xselinux\"],\"xtest\":[\"x11rb-protocol/xtest\"],\"xv\":[\"x11rb-protocol/xv\",\"shm\"],\"xvmc\":[\"x11rb-protocol/xvmc\",\"xv\"]}}", "x25519-dalek_2.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"serde?/alloc\",\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"getrandom\":[\"rand_core/getrandom\"],\"precomputed-tables\":[\"curve25519-dalek/precomputed-tables\"],\"reusable_secrets\":[],\"serde\":[\"dep:serde\",\"curve25519-dalek/serde\"],\"static_secrets\":[],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}", "x509-parser_0.18.1": "{\"dependencies\":[{\"features\":[\"datetime\"],\"name\":\"asn1-rs\",\"req\":\"^0.7.0\"},{\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"data-encoding\",\"req\":\"^2.2.1\"},{\"features\":[\"bigint\"],\"name\":\"der-parser\",\"req\":\"^10.0\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"features\":[\"crypto\",\"x509\",\"x962\"],\"name\":\"oid-registry\",\"req\":\"^0.8.1\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.12\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.35\"}],\"features\":{\"default\":[],\"validate\":[],\"verify\":[\"ring\"],\"verify-aws\":[\"aws-lc-rs\"]}}", + "xattr_1.6.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.150\",\"target\":\"cfg(any(target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"hurd\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"default\":[\"unsupported\"],\"unsupported\":[]}}", "xdg-home_1.3.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}", - "xml-rs_0.8.28": "{\"dependencies\":[],\"features\":{}}", "xz2_0.1.7": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1.26\"},{\"name\":\"lzma-sys\",\"req\":\"^0.1.18\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1.17\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.12\"}],\"features\":{\"static\":[\"lzma-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}", "yaml-rust_0.4.5": "{\"dependencies\":[{\"name\":\"linked-hash-map\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"}],\"features\":{}}", "yansi_1.0.1": "{\"dependencies\":[{\"name\":\"is-terminal\",\"optional\":true,\"req\":\"^0.4.11\"}],\"features\":{\"_nightly\":[],\"alloc\":[],\"default\":[\"std\"],\"detect-env\":[\"std\"],\"detect-tty\":[\"is-terminal\",\"std\"],\"hyperlink\":[\"std\"],\"std\":[\"alloc\"]}}", @@ -1588,14 +1553,11 @@ "zerotrie_0.2.3": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"litemap\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"zerofrom\",\"optional\":true,\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[],\"databake\":[\"dep:databake\",\"zerovec?/databake\"],\"default\":[],\"litemap\":[\"dep:litemap\",\"alloc\"],\"serde\":[\"dep:serde_core\",\"dep:litemap\",\"alloc\",\"litemap/serde\",\"zerovec?/serde\"],\"yoke\":[\"dep:yoke\"],\"zerofrom\":[\"dep:zerofrom\"]}}", "zerovec-derive_0.11.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.61\"},{\"name\":\"quote\",\"req\":\"^1.0.28\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0.21\"}],\"features\":{}}", "zerovec_0.11.5": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"xxhash64\"],\"name\":\"twox-hash\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerovec-derive\",\"optional\":true,\"req\":\"^0.11.1\"}],\"features\":{\"alloc\":[\"serde?/alloc\"],\"databake\":[\"dep:databake\"],\"derive\":[\"dep:zerovec-derive\"],\"hashmap\":[\"dep:twox-hash\",\"alloc\"],\"serde\":[\"dep:serde\"],\"std\":[],\"yoke\":[\"dep:yoke\"]}}", - "zip_0.6.6": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8.2\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"bzip2\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"constant_time_eq\",\"optional\":true,\"req\":\"^0.1.5\"},{\"name\":\"crc32fast\",\"req\":\"^1.3.2\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.8\",\"target\":\"cfg(any(all(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\"), target_arch = \\\"mips\\\", target_arch = \\\"powerpc\\\"))\"},{\"default_features\":false,\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0.23\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.5\"},{\"features\":[\"reset\"],\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12.1\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.11.0\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.7\"},{\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.7\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.11.2\"}],\"features\":{\"aes-crypto\":[\"aes\",\"constant_time_eq\",\"hmac\",\"pbkdf2\",\"sha1\"],\"default\":[\"aes-crypto\",\"bzip2\",\"deflate\",\"time\",\"zstd\"],\"deflate\":[\"flate2/rust_backend\"],\"deflate-miniz\":[\"flate2/default\"],\"deflate-zlib\":[\"flate2/zlib\"],\"unreserved\":[]}}", "zip_2.4.2": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.95\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"req\":\"^1.4.1\",\"target\":\"cfg(fuzzing)\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bzip2\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"=4.4.18\"},{\"name\":\"constant_time_eq\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"crc32fast\",\"req\":\"^1.4\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.21\",\"target\":\"cfg(any(all(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\"), target_arch = \\\"mips\\\", target_arch = \\\"powerpc\\\"))\"},{\"name\":\"deflate64\",\"optional\":true,\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"wasm_js\",\"std\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.1\"},{\"features\":[\"wasm_js\",\"std\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3.1\"},{\"features\":[\"reset\"],\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"lzma-rs\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"memchr\",\"req\":\"^2.7\"},{\"default_features\":false,\"name\":\"nt-time\",\"optional\":true,\"req\":\"^0.10.6\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.15\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.37\"},{\"default_features\":false,\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.37\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5\"},{\"name\":\"xz2\",\"optional\":true,\"req\":\"^0.1.7\"},{\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"},{\"name\":\"zopfli\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"_all-features\":[],\"_deflate-any\":[],\"aes-crypto\":[\"aes\",\"constant_time_eq\",\"hmac\",\"pbkdf2\",\"sha1\",\"getrandom\",\"zeroize\"],\"chrono\":[\"chrono/default\"],\"default\":[\"aes-crypto\",\"bzip2\",\"deflate64\",\"deflate\",\"lzma\",\"time\",\"zstd\",\"xz\"],\"deflate\":[\"flate2/rust_backend\",\"deflate-zopfli\",\"deflate-flate2\"],\"deflate-flate2\":[\"_deflate-any\"],\"deflate-miniz\":[\"deflate\",\"deflate-flate2\"],\"deflate-zlib\":[\"flate2/zlib\",\"deflate-flate2\"],\"deflate-zlib-ng\":[\"flate2/zlib-ng\",\"deflate-flate2\"],\"deflate-zopfli\":[\"zopfli\",\"_deflate-any\"],\"lzma\":[\"lzma-rs/stream\"],\"nt-time\":[\"dep:nt-time\"],\"unreserved\":[],\"xz\":[\"dep:xz2\"]}}", "zmij_1.0.19": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"opt-level\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"ryu\",\"req\":\"^1\"}],\"features\":{}}", "zopfli_0.8.3": "{\"dependencies\":[{\"name\":\"bumpalo\",\"req\":\"^3.19.0\"},{\"default_features\":false,\"name\":\"crc32fast\",\"optional\":true,\"req\":\"^1.5.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.28\"},{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.8.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"proptest-derive\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"simd-adler32\",\"optional\":true,\"req\":\"^0.3.7\"}],\"features\":{\"default\":[\"gzip\",\"std\",\"zlib\"],\"gzip\":[\"dep:crc32fast\"],\"nightly\":[\"crc32fast?/nightly\"],\"std\":[\"crc32fast?/std\",\"dep:log\",\"simd-adler32?/std\"],\"zlib\":[\"dep:simd-adler32\"]}}", - "zstd-safe_5.0.2+zstd.1.5.2": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.21\"},{\"default_features\":false,\"name\":\"zstd-sys\",\"req\":\"^2.0.1\"}],\"features\":{\"arrays\":[],\"bindgen\":[\"zstd-sys/bindgen\"],\"debug\":[\"zstd-sys/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-sys/experimental\"],\"legacy\":[\"zstd-sys/legacy\"],\"no_asm\":[\"zstd-sys/no_asm\"],\"pkg-config\":[\"zstd-sys/pkg-config\"],\"std\":[\"zstd-sys/std\"],\"thin\":[\"zstd-sys/thin\"],\"zdict_builder\":[\"zstd-sys/zdict_builder\"],\"zstdmt\":[\"zstd-sys/zstdmt\"]}}", "zstd-safe_7.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"zstd-sys\",\"req\":\"^2.0.15\"}],\"features\":{\"arrays\":[],\"bindgen\":[\"zstd-sys/bindgen\"],\"debug\":[\"zstd-sys/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-sys/experimental\"],\"fat-lto\":[\"zstd-sys/fat-lto\"],\"legacy\":[\"zstd-sys/legacy\"],\"no_asm\":[\"zstd-sys/no_asm\"],\"pkg-config\":[\"zstd-sys/pkg-config\"],\"seekable\":[\"zstd-sys/seekable\"],\"std\":[\"zstd-sys/std\"],\"thin\":[\"zstd-sys/thin\"],\"thin-lto\":[\"zstd-sys/thin-lto\"],\"zdict_builder\":[\"zstd-sys/zdict_builder\"],\"zstdmt\":[\"zstd-sys/zstdmt\"]}}", "zstd-sys_2.0.16+zstd.1.5.7": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.45\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.28\"}],\"features\":{\"debug\":[],\"default\":[\"legacy\",\"zdict_builder\",\"bindgen\"],\"experimental\":[],\"fat-lto\":[],\"legacy\":[],\"no_asm\":[],\"no_wasm_shim\":[],\"non-cargo\":[],\"pkg-config\":[],\"seekable\":[],\"std\":[],\"thin\":[],\"thin-lto\":[],\"zdict_builder\":[],\"zstdmt\":[]}}", - "zstd_0.11.2+zstd.1.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"humansize\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"zstd-safe\",\"req\":\"^5.0.1\"}],\"features\":{\"arrays\":[\"zstd-safe/arrays\"],\"bindgen\":[\"zstd-safe/bindgen\"],\"debug\":[\"zstd-safe/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-safe/experimental\"],\"legacy\":[\"zstd-safe/legacy\"],\"no_asm\":[\"zstd-safe/no_asm\"],\"pkg-config\":[\"zstd-safe/pkg-config\"],\"thin\":[\"zstd-safe/thin\"],\"wasm\":[],\"zdict_builder\":[\"zstd-safe/zdict_builder\"],\"zstdmt\":[\"zstd-safe/zstdmt\"]}}", "zstd_0.13.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"humansize\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"zstd-safe\",\"req\":\"^7.1.0\"}],\"features\":{\"arrays\":[\"zstd-safe/arrays\"],\"bindgen\":[\"zstd-safe/bindgen\"],\"debug\":[\"zstd-safe/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-safe/experimental\"],\"fat-lto\":[\"zstd-safe/fat-lto\"],\"legacy\":[\"zstd-safe/legacy\"],\"no_asm\":[\"zstd-safe/no_asm\"],\"pkg-config\":[\"zstd-safe/pkg-config\"],\"thin\":[\"zstd-safe/thin\"],\"thin-lto\":[\"zstd-safe/thin-lto\"],\"wasm\":[],\"zdict_builder\":[\"zstd-safe/zdict_builder\"],\"zstdmt\":[\"zstd-safe/zstdmt\"]}}", "zune-core_0.4.12": "{\"dependencies\":[{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.52\"}],\"features\":{\"std\":[]}}", "zune-core_0.5.1": "{\"dependencies\":[{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"std\":[]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1974883dd..ba3156dad 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -831,9 +831,9 @@ dependencies = [ "form_urlencoded", "futures-util", "http 1.4.0", - "http-body 1.0.1", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit 0.8.4", @@ -845,7 +845,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower", "tower-layer", @@ -862,11 +862,11 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body 1.0.1", + "http-body", "http-body-util", "mime", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -1096,16 +1096,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - [[package]] name = "bzip2" version = "0.5.2" @@ -1403,7 +1393,7 @@ dependencies = [ "http 1.4.0", "pretty_assertions", "regex-lite", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "thiserror 2.0.18", @@ -1539,36 +1529,21 @@ dependencies = [ ] [[package]] -name = "codex-artifact-presentation" +name = "codex-artifacts" version = "0.0.0" dependencies = [ - "base64 0.22.1", - "image", - "ppt-rs", + "codex-package-manager", "pretty_assertions", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", + "sha2", "tempfile", "thiserror 2.0.18", - "tiny_http", - "uuid", - "zip 2.4.2", -] - -[[package]] -name = "codex-artifact-spreadsheet" -version = "0.0.0" -dependencies = [ - "base64 0.22.1", - "pretty_assertions", - "regex", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "uuid", - "zip 2.4.2", + "tokio", + "url", + "wiremock", + "zip", ] [[package]] @@ -1590,7 +1565,7 @@ dependencies = [ "codex-core", "codex-protocol", "pretty_assertions", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", ] @@ -1678,7 +1653,7 @@ dependencies = [ "opentelemetry", "opentelemetry_sdk", "rand 0.9.2", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "thiserror 2.0.18", @@ -1686,7 +1661,7 @@ dependencies = [ "tracing", "tracing-opentelemetry", "tracing-subscriber", - "zstd 0.13.3", + "zstd", ] [[package]] @@ -1730,7 +1705,7 @@ dependencies = [ "owo-colors", "pretty_assertions", "ratatui", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "supports-color 3.0.2", @@ -1799,8 +1774,7 @@ dependencies = [ "codex-app-server-protocol", "codex-apply-patch", "codex-arg0", - "codex-artifact-presentation", - "codex-artifact-spreadsheet", + "codex-artifacts", "codex-async-utils", "codex-client", "codex-config", @@ -1858,7 +1832,7 @@ dependencies = [ "pretty_assertions", "rand 0.9.2", "regex-lite", - "reqwest 0.12.28", + "reqwest", "rmcp", "schemars 0.8.22", "seccompiler", @@ -1891,8 +1865,8 @@ dependencies = [ "wildmatch", "windows-sys 0.52.0", "wiremock", - "zip 2.4.2", - "zstd 0.13.3", + "zip", + "zstd", ] [[package]] @@ -2089,7 +2063,7 @@ name = "codex-lmstudio" version = "0.0.0" dependencies = [ "codex-core", - "reqwest 0.12.28", + "reqwest", "serde_json", "tokio", "tracing", @@ -2108,7 +2082,7 @@ dependencies = [ "codex-core", "core_test_support", "rand 0.9.2", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "sha2", @@ -2189,7 +2163,7 @@ dependencies = [ "codex-core", "futures", "pretty_assertions", - "reqwest 0.12.28", + "reqwest", "semver", "serde_json", "tokio", @@ -2216,7 +2190,7 @@ dependencies = [ "opentelemetry_sdk", "os_info", "pretty_assertions", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "strum_macros 0.27.2", @@ -2228,6 +2202,25 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "codex-package-manager" +version = "0.0.0" +dependencies = [ + "flate2", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "sha2", + "tar", + "tempfile", + "thiserror 2.0.18", + "tokio", + "url", + "wiremock", + "zip", +] + [[package]] name = "codex-process-hardening" version = "0.0.0" @@ -2272,7 +2265,7 @@ dependencies = [ "codex-process-hardening", "ctor 0.6.3", "libc", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "tiny_http", @@ -2294,7 +2287,7 @@ dependencies = [ "keyring", "oauth2", "pretty_assertions", - "reqwest 0.12.28", + "reqwest", "rmcp", "schemars 0.8.22", "serde", @@ -2480,7 +2473,7 @@ dependencies = [ "ratatui", "ratatui-macros", "regex-lite", - "reqwest 0.12.28", + "reqwest", "rmcp", "serde", "serde_json", @@ -2822,12 +2815,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "constant_time_eq" version = "0.3.1" @@ -2903,7 +2890,7 @@ dependencies = [ "notify", "pretty_assertions", "regex-lite", - "reqwest 0.12.28", + "reqwest", "serde_json", "shlex", "tempfile", @@ -2911,7 +2898,7 @@ dependencies = [ "tokio-tungstenite", "walkdir", "wiremock", - "zstd 0.13.3", + "zstd", ] [[package]] @@ -3080,29 +3067,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cssparser" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.11.3", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.114", -] - [[package]] name = "csv" version = "1.4.0" @@ -3423,17 +3387,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "derive_more" version = "1.0.0" @@ -3603,21 +3556,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - [[package]] name = "dtor" version = "0.1.1" @@ -3665,12 +3603,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ego-tree" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a0bb14ac04a9fcf170d0bbbef949b44cc492f4452bd20c095636956f653642" - [[package]] name = "either" version = "1.15.0" @@ -3940,6 +3872,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-crate" version = "0.6.3" @@ -4134,16 +4077,6 @@ dependencies = [ "libc", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.31" @@ -4372,25 +4305,6 @@ dependencies = [ "regex-syntax 0.8.8", ] -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.13" @@ -4600,20 +4514,6 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" -[[package]] -name = "html5ever" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "http" version = "0.2.12" @@ -4635,17 +4535,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -4665,7 +4554,7 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body 1.0.1", + "http-body", "pin-project-lite", ] @@ -4687,30 +4576,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -4721,9 +4586,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", + "h2", "http 1.4.0", - "http-body 1.0.1", + "http-body", "httparse", "httpdate", "itoa", @@ -4741,7 +4606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper", "hyper-util", "rustls", "rustls-native-certs", @@ -4758,26 +4623,13 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.1", + "hyper", "hyper-util", "pin-project-lite", "tokio", "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -4786,7 +4638,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -4806,14 +4658,14 @@ dependencies = [ "futures-core", "futures-util", "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2 0.6.2", - "system-configuration 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", @@ -5721,12 +5573,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "mach2" version = "0.4.3" @@ -5742,20 +5588,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" -[[package]] -name = "markup5ever" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" -dependencies = [ - "log", - "phf 0.10.1", - "phf_codegen", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "matchers" version = "0.2.0" @@ -6253,7 +6085,7 @@ dependencies = [ "getrandom 0.2.17", "http 1.4.0", "rand 0.8.5", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "serde_path_to_error", @@ -6614,7 +6446,7 @@ dependencies = [ "bytes", "http 1.4.0", "opentelemetry", - "reqwest 0.12.28", + "reqwest", ] [[package]] @@ -6629,7 +6461,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest 0.12.28", + "reqwest", "serde_json", "thiserror 2.0.18", "tokio", @@ -6758,17 +6590,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -6805,18 +6626,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - [[package]] name = "pbkdf2" version = "0.12.2" @@ -6873,84 +6682,13 @@ dependencies = [ "indexmap 2.13.0", ] -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -7127,26 +6865,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppt-rs" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2135454fb8669ee8eef79c87a7e9ad6f5d563d13fb1041746e2d3e3e4d716749" -dependencies = [ - "chrono", - "clap", - "pulldown-cmark", - "regex", - "reqwest 0.11.27", - "scraper", - "syntect", - "thiserror 1.0.69", - "url", - "uuid", - "xml-rs", - "zip 0.6.6", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -7527,7 +7245,7 @@ version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3ff6a3c8ae690be8167e43777ba0bf6b0c8c2f6de165c538666affe2a32fd81" dependencies = [ - "h2 0.4.13", + "h2", "pin-project-lite", "rama-core", "rama-http", @@ -7599,7 +7317,7 @@ dependencies = [ "const_format", "fnv", "http 1.4.0", - "http-body 1.0.1", + "http-body", "http-body-util", "itoa", "memchr", @@ -7614,7 +7332,7 @@ dependencies = [ "rand 0.9.2", "serde", "serde_json", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", ] @@ -7984,46 +7702,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.50.0", -] - [[package]] name = "reqwest" version = "0.12.28" @@ -8036,13 +7714,13 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.13", + "h2", "http 1.4.0", - "http-body 1.0.1", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-rustls", - "hyper-tls 0.6.0", + "hyper-tls", "hyper-util", "js-sys", "log", @@ -8058,7 +7736,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls", @@ -8107,14 +7785,14 @@ dependencies = [ "chrono", "futures", "http 1.4.0", - "http-body 1.0.1", + "http-body", "http-body-util", "oauth2", "pastey", "pin-project-lite", "process-wrap", "rand 0.9.2", - "reqwest 0.12.28", + "reqwest", "rmcp-macros", "schemars 1.2.1", "serde", @@ -8302,15 +7980,6 @@ dependencies = [ "security-framework 3.5.1", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -8519,29 +8188,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scraper" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" -dependencies = [ - "ahash", - "cssparser", - "ego-tree", - "getopts", - "html5ever", - "once_cell", - "selectors", - "tendril", -] - [[package]] name = "scrypt" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "pbkdf2 0.12.2", + "pbkdf2", "salsa20", "sha2", ] @@ -8625,25 +8278,6 @@ dependencies = [ "libc", ] -[[package]] -name = "selectors" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" -dependencies = [ - "bitflags 2.10.0", - "cssparser", - "derive_more 0.99.20", - "fxhash", - "log", - "new_debug_unreachable", - "phf 0.10.1", - "phf_codegen", - "precomputed-hash", - "servo_arc", - "smallvec", -] - [[package]] name = "self_cell" version = "0.10.3" @@ -8673,7 +8307,7 @@ checksum = "2f925d575b468e88b079faf590a8dd0c9c99e2ec29e9bab663ceb8b45056312f" dependencies = [ "httpdate", "native-tls", - "reqwest 0.12.28", + "reqwest", "sentry-actix", "sentry-backtrace", "sentry-contexts", @@ -8978,15 +8612,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "servo_arc" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "sha1" version = "0.10.6" @@ -9105,12 +8730,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.2" @@ -9397,7 +9016,7 @@ checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" dependencies = [ "bytes", "futures-util", - "http-body 1.0.1", + "http-body", "http-body-util", "pin-project-lite", ] @@ -9527,21 +9146,8 @@ checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", ] [[package]] @@ -9654,12 +9260,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -9710,17 +9310,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -9729,17 +9318,7 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -9758,6 +9337,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[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" @@ -9771,17 +9361,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "term" version = "0.7.0" @@ -10233,15 +9812,15 @@ dependencies = [ "base64 0.22.1", "bytes", "http 1.4.0", - "http-body 1.0.1", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", "rustls-native-certs", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", @@ -10273,7 +9852,7 @@ dependencies = [ "indexmap 2.13.0", "pin-project-lite", "slab", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-util", "tower-layer", @@ -10291,7 +9870,7 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "http-body 1.0.1", + "http-body", "iri-string", "pin-project-lite", "tower", @@ -11732,7 +11311,7 @@ dependencies = [ "futures", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "log", "once_cell", @@ -11821,6 +11400,16 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + [[package]] name = "xdg-home" version = "1.3.0" @@ -11831,12 +11420,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - [[package]] name = "xz2" version = "0.1.7" @@ -12050,26 +11633,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "aes", - "byteorder", - "bzip2 0.4.4", - "constant_time_eq 0.1.5", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2 0.11.0", - "sha1", - "time", - "zstd 0.11.2+zstd.1.5.2", -] - [[package]] name = "zip" version = "2.4.2" @@ -12078,8 +11641,8 @@ checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" dependencies = [ "aes", "arbitrary", - "bzip2 0.5.2", - "constant_time_eq 0.3.1", + "bzip2", + "constant_time_eq", "crc32fast", "crossbeam-utils", "deflate64", @@ -12090,14 +11653,14 @@ dependencies = [ "indexmap 2.13.0", "lzma-rs", "memchr", - "pbkdf2 0.12.2", + "pbkdf2", "sha1", "thiserror 2.0.18", "time", "xz2", "zeroize", "zopfli", - "zstd 0.13.3", + "zstd", ] [[package]] @@ -12118,32 +11681,13 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe 5.0.2+zstd.1.5.2", -] - [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe 7.2.4", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", + "zstd-safe", ] [[package]] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 9ee05e21b..21cbad4b1 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -33,8 +33,6 @@ members = [ "mcp-server", "network-proxy", "ollama", - "artifact-presentation", - "artifact-spreadsheet", "process-hardening", "protocol", "rmcp-client", @@ -66,6 +64,8 @@ members = [ "state", "codex-experimental-api-macros", "test-macros", + "package-manager", + "artifacts", ] resolver = "2" @@ -83,6 +83,8 @@ license = "Apache-2.0" app_test_support = { path = "app-server/tests/common" } codex-ansi-escape = { path = "ansi-escape" } codex-api = { path = "codex-api" } +codex-artifacts = { path = "artifacts" } +codex-package-manager = { path = "package-manager" } codex-app-server = { path = "app-server" } codex-app-server-protocol = { path = "app-server-protocol" } codex-app-server-test-client = { path = "app-server-test-client" } @@ -111,8 +113,6 @@ codex-mcp-server = { path = "mcp-server" } codex-network-proxy = { path = "network-proxy" } codex-ollama = { path = "ollama" } codex-otel = { path = "otel" } -codex-artifact-presentation = { path = "artifact-presentation" } -codex-artifact-spreadsheet = { path = "artifact-spreadsheet" } codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-responses-api-proxy = { path = "responses-api-proxy" } @@ -181,6 +181,7 @@ encoding_rs = "0.8.35" env-flags = "0.1.1" env_logger = "0.11.9" eventsource-stream = "0.2.3" +flate2 = "1.1.4" futures = { version = "0.3", default-features = false } gethostname = "1.1.0" globset = "0.4" @@ -219,7 +220,6 @@ owo-colors = "4.3.0" path-absolutize = "3.1.1" pathdiff = "0.2" portable-pty = "0.9.0" -ppt-rs = "0.2.6" predicates = "3" pretty_assertions = "1.4.1" pulldown-cmark = "0.10" @@ -267,6 +267,7 @@ supports-color = "3.0.2" syntect = "5" sys-locale = "0.3.2" tempfile = "3.23.0" +tar = "0.4.44" test-log = "0.2.19" textwrap = "0.16.2" thiserror = "2.0.17" @@ -353,8 +354,7 @@ ignored = [ "icu_provider", "openssl-sys", "codex-utils-readiness", - "codex-secrets", - "codex-artifact-spreadsheet" + "codex-secrets" ] [profile.release] diff --git a/codex-rs/artifact-presentation/BUILD.bazel b/codex-rs/artifact-presentation/BUILD.bazel deleted file mode 100644 index d89e395b3..000000000 --- a/codex-rs/artifact-presentation/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "artifact-presentation", - crate_name = "codex_artifact_presentation", -) diff --git a/codex-rs/artifact-presentation/Cargo.toml b/codex-rs/artifact-presentation/Cargo.toml deleted file mode 100644 index 898beb40f..000000000 --- a/codex-rs/artifact-presentation/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "codex-artifact-presentation" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -name = "codex_artifact_presentation" -path = "src/lib.rs" - -[lints] -workspace = true - -[dependencies] -base64 = { workspace = true } -image = { workspace = true, features = ["jpeg", "png"] } -ppt-rs = { workspace = true } -reqwest = { workspace = true, features = ["blocking"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -uuid = { workspace = true, features = ["v4"] } -zip = { workspace = true } - -[dev-dependencies] -pretty_assertions = { workspace = true } -tempfile = { workspace = true } -tiny_http = { workspace = true } diff --git a/codex-rs/artifact-presentation/src/lib.rs b/codex-rs/artifact-presentation/src/lib.rs deleted file mode 100644 index 34bca50a9..000000000 --- a/codex-rs/artifact-presentation/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod presentation_artifact; - -#[cfg(test)] -mod tests; - -pub use presentation_artifact::*; diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/api.rs b/codex-rs/artifact-presentation/src/presentation_artifact/api.rs deleted file mode 100644 index 5680bf97f..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/api.rs +++ /dev/null @@ -1,249 +0,0 @@ -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use image::GenericImageView; -use image::ImageFormat; -use image::codecs::jpeg::JpegEncoder; -use image::imageops::FilterType; -use ppt_rs::Chart; -use ppt_rs::ChartSeries; -use ppt_rs::ChartType; -use ppt_rs::Hyperlink as PptHyperlink; -use ppt_rs::HyperlinkAction as PptHyperlinkAction; -use ppt_rs::Image; -use ppt_rs::Presentation; -use ppt_rs::Shape; -use ppt_rs::ShapeFill; -use ppt_rs::ShapeLine; -use ppt_rs::ShapeType; -use ppt_rs::SlideContent; -use ppt_rs::SlideLayout; -use ppt_rs::TableBuilder; -use ppt_rs::TableCell; -use ppt_rs::TableRow; -use ppt_rs::generator::ArrowSize; -use ppt_rs::generator::ArrowType; -use ppt_rs::generator::CellAlign; -use ppt_rs::generator::Connector; -use ppt_rs::generator::ConnectorLine; -use ppt_rs::generator::ConnectorType; -use ppt_rs::generator::LineDash; -use ppt_rs::generator::generate_image_content_type; -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value; -use std::collections::BTreeSet; -use std::collections::HashMap; -use std::collections::HashSet; -use std::io::Cursor; -use std::io::Read; -use std::io::Seek; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; -use thiserror::Error; -use uuid::Uuid; -use zip::ZipArchive; -use zip::ZipWriter; -use zip::write::SimpleFileOptions; - -const POINT_TO_EMU: u32 = 12_700; -const DEFAULT_SLIDE_WIDTH_POINTS: u32 = 720; -const DEFAULT_SLIDE_HEIGHT_POINTS: u32 = 540; -const DEFAULT_IMPORTED_TITLE_LEFT: u32 = 36; -const DEFAULT_IMPORTED_TITLE_TOP: u32 = 24; -const DEFAULT_IMPORTED_TITLE_WIDTH: u32 = 648; -const DEFAULT_IMPORTED_TITLE_HEIGHT: u32 = 48; -const DEFAULT_IMPORTED_CONTENT_LEFT: u32 = 48; -const DEFAULT_IMPORTED_CONTENT_TOP: u32 = 96; -const DEFAULT_IMPORTED_CONTENT_WIDTH: u32 = 624; -const DEFAULT_IMPORTED_CONTENT_HEIGHT: u32 = 324; - -#[derive(Debug, Error)] -pub enum PresentationArtifactError { - #[error("missing `artifact_id` for action `{action}`")] - MissingArtifactId { action: String }, - #[error("unknown artifact id `{artifact_id}` for action `{action}`")] - UnknownArtifactId { action: String, artifact_id: String }, - #[error("unknown action `{0}`")] - UnknownAction(String), - #[error("invalid args for action `{action}`: {message}")] - InvalidArgs { action: String, message: String }, - #[error("unsupported feature for action `{action}`: {message}")] - UnsupportedFeature { action: String, message: String }, - #[error("failed to import PPTX `{path}`: {message}")] - ImportFailed { path: PathBuf, message: String }, - #[error("failed to export PPTX `{path}`: {message}")] - ExportFailed { path: PathBuf, message: String }, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct PresentationArtifactRequest { - pub artifact_id: Option, - pub action: String, - #[serde(default)] - pub args: Value, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct PresentationArtifactToolRequest { - pub artifact_id: Option, - pub actions: Vec, -} - -#[derive(Debug, Clone)] -pub struct PresentationArtifactExecutionRequest { - pub artifact_id: Option, - pub requests: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct PresentationArtifactToolAction { - pub action: String, - #[serde(default)] - pub args: Value, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PathAccessKind { - Read, - Write, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PathAccessRequirement { - pub action: String, - pub kind: PathAccessKind, - pub path: PathBuf, -} - -impl PresentationArtifactRequest { - pub fn is_mutating(&self) -> bool { - !is_read_only_action(&self.action) - } - - pub fn required_path_accesses( - &self, - cwd: &Path, - ) -> Result, PresentationArtifactError> { - let access = match self.action.as_str() { - "import_pptx" => { - let args: ImportPptxArgs = parse_args(&self.action, &self.args)?; - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Read, - path: resolve_path(cwd, &args.path), - }] - } - "export_pptx" => { - let args: ExportPptxArgs = parse_args(&self.action, &self.args)?; - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Write, - path: resolve_path(cwd, &args.path), - }] - } - "export_preview" => { - let args: ExportPreviewArgs = parse_args(&self.action, &self.args)?; - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Write, - path: resolve_path(cwd, &args.path), - }] - } - "add_image" => { - let args: AddImageArgs = parse_args(&self.action, &self.args)?; - match args.image_source()? { - ImageInputSource::Path(path) => vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Read, - path: resolve_path(cwd, &path), - }], - ImageInputSource::DataUrl(_) - | ImageInputSource::Blob(_) - | ImageInputSource::Uri(_) - | ImageInputSource::Placeholder => Vec::new(), - } - } - "replace_image" => { - let args: ReplaceImageArgs = parse_args(&self.action, &self.args)?; - match ( - &args.path, - &args.data_url, - &args.blob, - &args.uri, - &args.prompt, - ) { - (Some(path), None, None, None, None) => vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Read, - path: resolve_path(cwd, path), - }], - (None, Some(_), None, None, None) - | (None, None, Some(_), None, None) - | (None, None, None, Some(_), None) - | (None, None, None, None, Some(_)) => Vec::new(), - _ => { - return Err(PresentationArtifactError::InvalidArgs { - action: self.action.clone(), - message: - "provide exactly one of `path`, `data_url`, `blob`, or `uri`, or provide `prompt` for a placeholder image" - .to_string(), - }); - } - } - } - _ => Vec::new(), - }; - Ok(access) - } -} - -impl PresentationArtifactToolRequest { - pub fn is_mutating(&self) -> Result { - Ok(self.actions.iter().any(|request| !is_read_only_action(&request.action))) - } - - pub fn into_execution_request( - self, - ) -> Result { - if self.actions.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: "presentation_artifact".to_string(), - message: "`actions` must contain at least one item".to_string(), - }); - } - Ok(PresentationArtifactExecutionRequest { - artifact_id: self.artifact_id, - requests: self - .actions - .into_iter() - .map(|request| PresentationArtifactRequest { - artifact_id: None, - action: request.action, - args: request.args, - }) - .collect(), - }) - } - - pub fn required_path_accesses( - &self, - cwd: &Path, - ) -> Result, PresentationArtifactError> { - let mut accesses = Vec::new(); - for request in &self.actions { - accesses.extend( - PresentationArtifactRequest { - artifact_id: None, - action: request.action.clone(), - args: request.args.clone(), - } - .required_path_accesses(cwd)?, - ); - } - Ok(accesses) - } -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/args.rs b/codex-rs/artifact-presentation/src/presentation_artifact/args.rs deleted file mode 100644 index a139dcb05..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/args.rs +++ /dev/null @@ -1,729 +0,0 @@ -#[derive(Debug, Deserialize)] -struct CreateArgs { - name: Option, - slide_size: Option, - theme: Option, -} - -#[derive(Debug, Deserialize)] -struct ImportPptxArgs { - path: PathBuf, -} - -#[derive(Debug, Deserialize)] -struct ExportPptxArgs { - path: PathBuf, -} - -#[derive(Debug, Deserialize)] -struct ExportPreviewArgs { - path: PathBuf, - slide_index: Option, - format: Option, - scale: Option, - quality: Option, -} - -#[derive(Debug, Default, Deserialize)] -struct AddSlideArgs { - layout: Option, - notes: Option, - background_fill: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateLayoutArgs { - name: String, - kind: Option, - parent_layout_id: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum PreviewOutputFormat { - Png, - Jpeg, - Svg, -} - -impl PreviewOutputFormat { - fn extension(self) -> &'static str { - match self { - Self::Png => "png", - Self::Jpeg => "jpg", - Self::Svg => "svg", - } - } -} - -#[derive(Debug, Deserialize)] -struct AddLayoutPlaceholderArgs { - layout_id: String, - name: String, - placeholder_type: String, - index: Option, - text: Option, - geometry: Option, - position: Option, -} - -#[derive(Debug, Deserialize)] -struct LayoutIdArgs { - layout_id: String, -} - -#[derive(Debug, Deserialize)] -struct SetSlideLayoutArgs { - slide_index: u32, - layout_id: String, -} - -#[derive(Debug, Deserialize)] -struct UpdatePlaceholderTextArgs { - slide_index: u32, - name: String, - text: String, -} - -#[derive(Debug, Deserialize)] -struct NotesArgs { - slide_index: u32, - text: Option, -} - -#[derive(Debug, Deserialize)] -struct NotesVisibilityArgs { - slide_index: u32, - visible: bool, -} - -#[derive(Debug, Deserialize)] -struct ThemeArgs { - color_scheme: HashMap, - major_font: Option, - minor_font: Option, -} - -#[derive(Debug, Deserialize)] -struct StyleNameArgs { - name: String, -} - -#[derive(Debug, Deserialize)] -struct AddStyleArgs { - name: String, - #[serde(flatten)] - styling: TextStylingArgs, -} - -#[derive(Debug, Deserialize)] -struct InspectArgs { - kind: Option, - include: Option, - exclude: Option, - search: Option, - target_id: Option, - target: Option, - max_chars: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct InspectTargetArgs { - id: String, - before_lines: Option, - after_lines: Option, -} - -#[derive(Debug, Deserialize)] -struct ResolveArgs { - id: String, -} - -#[derive(Debug, Clone, Deserialize)] -struct PatchOperationInput { - artifact_id: Option, - action: String, - #[serde(default)] - args: Value, -} - -#[derive(Debug, Deserialize)] -struct RecordPatchArgs { - operations: Vec, -} - -#[derive(Debug, Deserialize)] -struct ApplyPatchArgs { - operations: Option>, - patch: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PresentationPatch { - version: u32, - artifact_id: String, - operations: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PatchOperation { - action: String, - #[serde(default)] - args: Value, -} - -#[derive(Debug, Default, Deserialize)] -struct InsertSlideArgs { - index: Option, - after_slide_index: Option, - layout: Option, - notes: Option, - background_fill: Option, -} - -#[derive(Debug, Deserialize)] -struct SlideIndexArgs { - slide_index: u32, -} - -#[derive(Debug, Deserialize)] -struct MoveSlideArgs { - from_index: u32, - to_index: u32, -} - -#[derive(Debug, Deserialize)] -struct SetActiveSlideArgs { - slide_index: u32, -} - -#[derive(Debug, Deserialize)] -struct SetSlideBackgroundArgs { - slide_index: u32, - fill: String, -} - -#[derive(Debug, Clone, Deserialize)] -struct PositionArgs { - left: u32, - top: u32, - width: u32, - height: u32, - rotation: Option, - flip_horizontal: Option, - flip_vertical: Option, -} - -#[derive(Debug, Clone, Default, Deserialize)] -struct PartialPositionArgs { - left: Option, - top: Option, - width: Option, - height: Option, - rotation: Option, - flip_horizontal: Option, - flip_vertical: Option, -} - -#[derive(Debug, Clone, Default, Deserialize)] -struct TextStylingArgs { - style: Option, - font_size: Option, - font_family: Option, - color: Option, - fill: Option, - alignment: Option, - bold: Option, - italic: Option, - underline: Option, -} - -#[derive(Debug, Clone, Default, Deserialize)] -struct TextLayoutArgs { - insets: Option, - wrap: Option, - auto_fit: Option, - vertical_alignment: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct TextInsetsArgs { - left: u32, - right: u32, - top: u32, - bottom: u32, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -enum RichTextInput { - Plain(String), - Paragraphs(Vec), -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -enum RichParagraphInput { - Plain(String), - Runs(Vec), -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -enum RichRunInput { - Plain(String), - Styled(RichRunObjectInput), -} - -#[derive(Debug, Clone, Deserialize)] -struct RichRunObjectInput { - run: String, - #[serde(default)] - text_style: TextStylingArgs, - link: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct RichTextLinkInput { - uri: Option, - is_external: Option, -} - -#[derive(Debug, Deserialize)] -struct AddTextShapeArgs { - slide_index: u32, - text: String, - position: PositionArgs, - #[serde(flatten)] - styling: TextStylingArgs, - #[serde(default)] - text_layout: TextLayoutArgs, -} - -#[derive(Debug, Clone, Default, Deserialize)] -struct StrokeArgs { - color: String, - width: u32, - style: Option, -} - -#[derive(Debug, Deserialize)] -struct AddShapeArgs { - slide_index: u32, - geometry: String, - position: PositionArgs, - fill: Option, - stroke: Option, - text: Option, - rotation: Option, - flip_horizontal: Option, - flip_vertical: Option, - #[serde(default)] - text_style: TextStylingArgs, - #[serde(default)] - text_layout: TextLayoutArgs, -} - -#[derive(Debug, Clone, Default, Deserialize)] -struct ConnectorLineArgs { - color: Option, - width: Option, - style: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PointArgs { - left: u32, - top: u32, -} - -#[derive(Debug, Deserialize)] -struct AddConnectorArgs { - slide_index: u32, - connector_type: String, - start: PointArgs, - end: PointArgs, - line: Option, - start_arrow: Option, - end_arrow: Option, - arrow_size: Option, - label: Option, -} - -#[derive(Debug, Deserialize)] -struct AddImageArgs { - slide_index: u32, - path: Option, - data_url: Option, - blob: Option, - uri: Option, - position: PositionArgs, - fit: Option, - crop: Option, - rotation: Option, - flip_horizontal: Option, - flip_vertical: Option, - lock_aspect_ratio: Option, - alt: Option, - prompt: Option, -} - -impl AddImageArgs { - fn image_source(&self) -> Result { - match (&self.path, &self.data_url, &self.blob, &self.uri) { - (Some(path), None, None, None) => Ok(ImageInputSource::Path(path.clone())), - (None, Some(data_url), None, None) => Ok(ImageInputSource::DataUrl(data_url.clone())), - (None, None, Some(blob), None) => Ok(ImageInputSource::Blob(blob.clone())), - (None, None, None, Some(uri)) => Ok(ImageInputSource::Uri(uri.clone())), - (None, None, None, None) if self.prompt.is_some() => Ok(ImageInputSource::Placeholder), - _ => Err(PresentationArtifactError::InvalidArgs { - action: "add_image".to_string(), - message: - "provide exactly one of `path`, `data_url`, `blob`, or `uri`, or provide `prompt` for a placeholder image" - .to_string(), - }), - } - } -} - -enum ImageInputSource { - Path(PathBuf), - DataUrl(String), - Blob(String), - Uri(String), - Placeholder, -} - -#[derive(Debug, Clone, Deserialize)] -struct ImageCropArgs { - left: f64, - top: f64, - right: f64, - bottom: f64, -} - -#[derive(Debug, Deserialize)] -struct AddTableArgs { - slide_index: u32, - position: PositionArgs, - rows: Vec>, - column_widths: Option>, - row_heights: Option>, - style: Option, - style_options: Option, - borders: Option, - right_to_left: Option, -} - -#[derive(Debug, Deserialize)] -struct AddChartArgs { - slide_index: u32, - position: PositionArgs, - chart_type: String, - categories: Vec, - series: Vec, - title: Option, - style_index: Option, - has_legend: Option, - legend_position: Option, - #[serde(default)] - legend_text_style: TextStylingArgs, - x_axis_title: Option, - y_axis_title: Option, - data_labels: Option, - chart_fill: Option, - plot_area_fill: Option, -} - -#[derive(Debug, Deserialize)] -struct ChartSeriesArgs { - name: String, - values: Vec, - categories: Option>, - x_values: Option>, - fill: Option, - stroke: Option, - marker: Option, - data_label_overrides: Option>, -} - -#[derive(Debug, Clone, Deserialize)] -struct ChartMarkerArgs { - symbol: Option, - size: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct ChartDataLabelsArgs { - show_value: Option, - show_category_name: Option, - show_leader_lines: Option, - position: Option, - #[serde(default)] - text_style: TextStylingArgs, -} - -#[derive(Debug, Clone, Deserialize)] -struct ChartDataLabelOverrideArgs { - idx: u32, - text: Option, - position: Option, - #[serde(default)] - text_style: TextStylingArgs, - fill: Option, - stroke: Option, -} - -#[derive(Debug, Deserialize)] -struct UpdateTextArgs { - element_id: String, - text: String, - #[serde(default)] - styling: TextStylingArgs, - #[serde(default)] - text_layout: TextLayoutArgs, -} - -#[derive(Debug, Deserialize)] -struct SetRichTextArgs { - element_id: Option, - slide_index: Option, - row: Option, - column: Option, - notes: Option, - text: RichTextInput, - #[serde(default)] - styling: TextStylingArgs, - #[serde(default)] - text_layout: TextLayoutArgs, -} - -#[derive(Debug, Deserialize)] -struct FormatTextRangeArgs { - element_id: Option, - slide_index: Option, - row: Option, - column: Option, - notes: Option, - query: Option, - occurrence: Option, - start_cp: Option, - length: Option, - #[serde(default)] - styling: TextStylingArgs, - #[serde(default)] - text_layout: TextLayoutArgs, - link: Option, - spacing_before: Option, - spacing_after: Option, - line_spacing: Option, -} - -#[derive(Debug, Deserialize)] -struct ReplaceTextArgs { - element_id: String, - search: String, - replace: String, -} - -#[derive(Debug, Deserialize)] -struct InsertTextAfterArgs { - element_id: String, - after: String, - insert: String, -} - -#[derive(Debug, Deserialize)] -struct SetHyperlinkArgs { - element_id: String, - link_type: Option, - url: Option, - slide_index: Option, - address: Option, - subject: Option, - path: Option, - tooltip: Option, - highlight_click: Option, - clear: Option, -} - -#[derive(Debug, Deserialize)] -struct UpdateShapeStyleArgs { - element_id: String, - position: Option, - fill: Option, - stroke: Option, - rotation: Option, - flip_horizontal: Option, - flip_vertical: Option, - fit: Option, - crop: Option, - lock_aspect_ratio: Option, - z_order: Option, - #[serde(default)] - text_layout: TextLayoutArgs, -} - -#[derive(Debug, Deserialize)] -struct ElementIdArgs { - element_id: String, -} - -#[derive(Debug, Deserialize)] -struct ReplaceImageArgs { - element_id: String, - path: Option, - data_url: Option, - blob: Option, - uri: Option, - fit: Option, - crop: Option, - rotation: Option, - flip_horizontal: Option, - flip_vertical: Option, - lock_aspect_ratio: Option, - alt: Option, - prompt: Option, -} - -#[derive(Debug, Deserialize)] -struct UpdateTableCellArgs { - element_id: String, - row: u32, - column: u32, - value: Value, - #[serde(default)] - styling: TextStylingArgs, - background_fill: Option, - alignment: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct TableStyleOptionsArgs { - header_row: Option, - banded_rows: Option, - banded_columns: Option, - first_column: Option, - last_column: Option, - total_row: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct TableBorderArgs { - color: String, - width: u32, -} - -#[derive(Debug, Clone, Deserialize)] -struct TableBordersArgs { - outside: Option, - inside: Option, - top: Option, - bottom: Option, - left: Option, - right: Option, -} - -#[derive(Debug, Deserialize)] -struct UpdateTableStyleArgs { - element_id: String, - style: Option, - style_options: Option, - borders: Option, - right_to_left: Option, -} - -#[derive(Debug, Deserialize)] -struct StyleTableBlockArgs { - element_id: String, - row: u32, - column: u32, - row_count: u32, - column_count: u32, - #[serde(default)] - styling: TextStylingArgs, - background_fill: Option, - alignment: Option, - borders: Option, -} - -#[derive(Debug, Deserialize)] -struct MergeTableCellsArgs { - element_id: String, - start_row: u32, - end_row: u32, - start_column: u32, - end_column: u32, -} - -#[derive(Debug, Deserialize)] -struct UpdateChartArgs { - element_id: String, - title: Option, - categories: Option>, - style_index: Option, - has_legend: Option, - legend_position: Option, - #[serde(default)] - legend_text_style: TextStylingArgs, - x_axis_title: Option, - y_axis_title: Option, - data_labels: Option, - chart_fill: Option, - plot_area_fill: Option, -} - -#[derive(Debug, Deserialize)] -struct AddChartSeriesArgs { - element_id: String, - name: String, - values: Vec, - categories: Option>, - x_values: Option>, - fill: Option, - stroke: Option, - marker: Option, -} - -#[derive(Debug, Deserialize)] -struct SetCommentAuthorArgs { - display_name: String, - initials: String, - email: Option, -} - -#[derive(Debug, Clone, Deserialize)] -struct CommentPositionArgs { - x: u32, - y: u32, -} - -#[derive(Debug, Deserialize)] -struct AddCommentThreadArgs { - slide_index: Option, - element_id: Option, - query: Option, - occurrence: Option, - start_cp: Option, - length: Option, - text: String, - position: Option, -} - -#[derive(Debug, Deserialize)] -struct AddCommentReplyArgs { - thread_id: String, - text: String, -} - -#[derive(Debug, Deserialize)] -struct ToggleCommentReactionArgs { - thread_id: String, - message_id: Option, - emoji: String, -} - -#[derive(Debug, Deserialize)] -struct CommentThreadIdArgs { - thread_id: String, -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/inspect.rs b/codex-rs/artifact-presentation/src/presentation_artifact/inspect.rs deleted file mode 100644 index 608e0734e..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/inspect.rs +++ /dev/null @@ -1,871 +0,0 @@ -fn inspect_document(document: &PresentationDocument, args: &InspectArgs) -> String { - let include_kinds = args - .include - .as_deref() - .or(args.kind.as_deref()) - .unwrap_or( - "deck,slide,textbox,shape,connector,table,chart,image,notes,layoutList,textRange,comment", - ); - let included_kinds = include_kinds - .split(',') - .map(str::trim) - .filter(|entry| !entry.is_empty()) - .collect::>(); - let excluded_kinds = args - .exclude - .as_deref() - .unwrap_or_default() - .split(',') - .map(str::trim) - .filter(|entry| !entry.is_empty()) - .collect::>(); - let include = |name: &str| included_kinds.contains(name) && !excluded_kinds.contains(name); - let mut records: Vec<(Value, Option)> = Vec::new(); - if include("deck") { - records.push(( - serde_json::json!({ - "kind": "deck", - "id": format!("pr/{}", document.artifact_id), - "name": document.name, - "slides": document.slides.len(), - "styleIds": document - .named_text_styles() - .iter() - .map(|style| format!("st/{}", style.name)) - .collect::>(), - "activeSlideIndex": document.active_slide_index, - "activeSlideId": document.active_slide_index.and_then(|index| document.slides.get(index)).map(|slide| format!("sl/{}", slide.slide_id)), - "commentThreadIds": document - .comment_threads - .iter() - .map(|thread| format!("th/{}", thread.thread_id)) - .collect::>(), - }), - None, - )); - } - if include("styleList") { - for style in document.named_text_styles() { - records.push((named_text_style_to_json(&style, "st"), None)); - } - } - if include("layoutList") { - for layout in &document.layouts { - let placeholders = resolved_layout_placeholders(document, &layout.layout_id, "inspect") - .unwrap_or_default() - .into_iter() - .map(|placeholder| { - serde_json::json!({ - "name": placeholder.definition.name, - "type": placeholder.definition.placeholder_type, - "sourceLayoutId": placeholder.source_layout_id, - "textPreview": placeholder.definition.text, - }) - }) - .collect::>(); - records.push(( - serde_json::json!({ - "kind": "layout", - "id": format!("ly/{}", layout.layout_id), - "layoutId": layout.layout_id, - "name": layout.name, - "type": match layout.kind { LayoutKind::Layout => "layout", LayoutKind::Master => "master" }, - "parentLayoutId": layout.parent_layout_id, - "placeholders": placeholders, - }), - None, - )); - } - } - for (index, slide) in document.slides.iter().enumerate() { - let slide_id = format!("sl/{}", slide.slide_id); - if include("slide") { - records.push(( - serde_json::json!({ - "kind": "slide", - "id": slide_id, - "slide": index + 1, - "slideIndex": index, - "isActive": document.active_slide_index == Some(index), - "layoutId": slide.layout_id, - "elements": slide.elements.len(), - }), - Some(slide_id.clone()), - )); - } - if include("notes") && !slide.notes.text.is_empty() { - records.push(( - serde_json::json!({ - "kind": "notes", - "id": format!("nt/{}", slide.slide_id), - "slide": index + 1, - "visible": slide.notes.visible, - "text": slide.notes.text, - "textPreview": slide.notes.text.replace('\n', " | "), - "textChars": slide.notes.text.chars().count(), - "textLines": slide.notes.text.lines().count(), - "richText": rich_text_to_proto(&slide.notes.text, &slide.notes.rich_text), - }), - Some(slide_id.clone()), - )); - } - if include("textRange") { - records.extend( - slide - .notes - .rich_text - .ranges - .iter() - .map(|range| { - let mut record = text_range_to_proto(&slide.notes.text, range); - record["kind"] = Value::String("textRange".to_string()); - record["slide"] = Value::from(index + 1); - record["slideIndex"] = Value::from(index); - record["hostAnchor"] = Value::String(format!("nt/{}", slide.slide_id)); - record["hostKind"] = Value::String("notes".to_string()); - (record, Some(slide_id.clone())) - }), - ); - } - for element in &slide.elements { - let mut record = match element { - PresentationElement::Text(text) => { - if !include("textbox") { - continue; - } - serde_json::json!({ - "kind": "textbox", - "id": format!("sh/{}", text.element_id), - "slide": index + 1, - "text": text.text, - "textStyle": text_style_to_proto(&text.style), - "textPreview": text.text.replace('\n', " | "), - "textChars": text.text.chars().count(), - "textLines": text.text.lines().count(), - "richText": rich_text_to_proto(&text.text, &text.rich_text), - "bbox": [text.frame.left, text.frame.top, text.frame.width, text.frame.height], - "bboxUnit": "points", - }) - } - PresentationElement::Shape(shape) => { - if !(include("shape") || include("textbox") && shape.text.is_some()) { - continue; - } - let kind = if shape.text.is_some() && include("textbox") { - "textbox" - } else { - "shape" - }; - let mut record = serde_json::json!({ - "kind": kind, - "id": format!("sh/{}", shape.element_id), - "slide": index + 1, - "geometry": format!("{:?}", shape.geometry), - "text": shape.text, - "textStyle": text_style_to_proto(&shape.text_style), - "richText": shape - .text - .as_ref() - .zip(shape.rich_text.as_ref()) - .map(|(text, rich_text)| rich_text_to_proto(text, rich_text)) - .unwrap_or(Value::Null), - "rotation": shape.rotation_degrees, - "flipHorizontal": shape.flip_horizontal, - "flipVertical": shape.flip_vertical, - "bbox": [shape.frame.left, shape.frame.top, shape.frame.width, shape.frame.height], - "bboxUnit": "points", - }); - if let Some(text) = &shape.text { - record["textPreview"] = Value::String(text.replace('\n', " | ")); - record["textChars"] = Value::from(text.chars().count()); - record["textLines"] = Value::from(text.lines().count()); - } - record - } - PresentationElement::Connector(connector) => { - if !include("shape") && !include("connector") { - continue; - } - serde_json::json!({ - "kind": "connector", - "id": format!("cn/{}", connector.element_id), - "slide": index + 1, - "connectorType": format!("{:?}", connector.connector_type), - "start": [connector.start.left, connector.start.top], - "end": [connector.end.left, connector.end.top], - "lineStyle": format!("{:?}", connector.line_style), - "label": connector.label, - }) - } - PresentationElement::Table(table) => { - if !include("table") { - continue; - } - serde_json::json!({ - "kind": "table", - "id": format!("tb/{}", table.element_id), - "slide": index + 1, - "rows": table.rows.len(), - "cols": table.rows.iter().map(std::vec::Vec::len).max().unwrap_or(0), - "columnWidths": table.column_widths, - "rowHeights": table.row_heights, - "preview": table.rows.first().map(|row| row.iter().map(|cell| cell.text.clone()).collect::>().join(" | ")), - "style": table.style, - "styleOptions": table_style_options_to_proto(&table.style_options), - "borders": table.borders.as_ref().map(table_borders_to_proto), - "rightToLeft": table.right_to_left, - "cellTextStyles": table - .rows - .iter() - .map(|row| row.iter().map(|cell| text_style_to_proto(&cell.text_style)).collect::>()) - .collect::>(), - "rowsData": table - .rows - .iter() - .map(|row| row.iter().map(table_cell_to_proto).collect::>()) - .collect::>(), - "bbox": [table.frame.left, table.frame.top, table.frame.width, table.frame.height], - "bboxUnit": "points", - }) - } - PresentationElement::Chart(chart) => { - if !include("chart") { - continue; - } - serde_json::json!({ - "kind": "chart", - "id": format!("ch/{}", chart.element_id), - "slide": index + 1, - "chartType": format!("{:?}", chart.chart_type), - "title": chart.title, - "styleIndex": chart.style_index, - "hasLegend": chart.has_legend, - "legend": chart.legend.as_ref().map(chart_legend_to_proto), - "xAxis": chart.x_axis.as_ref().map(chart_axis_to_proto), - "yAxis": chart.y_axis.as_ref().map(chart_axis_to_proto), - "dataLabels": chart.data_labels.as_ref().map(chart_data_labels_to_proto), - "chartFill": chart.chart_fill, - "plotAreaFill": chart.plot_area_fill, - "series": chart - .series - .iter() - .map(|series| serde_json::json!({ - "name": series.name, - "values": series.values, - "categories": series.categories, - "xValues": series.x_values, - "fill": series.fill, - "stroke": series.stroke.as_ref().map(stroke_to_proto), - "marker": series.marker.as_ref().map(chart_marker_to_proto), - "dataLabelOverrides": series - .data_label_overrides - .iter() - .map(chart_data_label_override_to_proto) - .collect::>(), - })) - .collect::>(), - "bbox": [chart.frame.left, chart.frame.top, chart.frame.width, chart.frame.height], - "bboxUnit": "points", - }) - } - PresentationElement::Image(image) => { - if !include("image") { - continue; - } - serde_json::json!({ - "kind": "image", - "id": format!("im/{}", image.element_id), - "slide": index + 1, - "alt": image.alt_text, - "prompt": image.prompt, - "fit": format!("{:?}", image.fit_mode), - "rotation": image.rotation_degrees, - "flipHorizontal": image.flip_horizontal, - "flipVertical": image.flip_vertical, - "crop": image.crop.map(|(left, top, right, bottom)| serde_json::json!({ - "left": left, - "top": top, - "right": right, - "bottom": bottom, - })), - "isPlaceholder": image.is_placeholder, - "lockAspectRatio": image.lock_aspect_ratio, - "bbox": [image.frame.left, image.frame.top, image.frame.width, image.frame.height], - "bboxUnit": "points", - }) - } - }; - if let Some(placeholder) = match element { - PresentationElement::Text(text) => text.placeholder.as_ref(), - PresentationElement::Shape(shape) => shape.placeholder.as_ref(), - PresentationElement::Connector(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => None, - PresentationElement::Image(image) => image.placeholder.as_ref(), - } { - record["placeholder"] = Value::String(placeholder.placeholder_type.clone()); - record["placeholderName"] = Value::String(placeholder.name.clone()); - record["placeholderIndex"] = - placeholder.index.map(Value::from).unwrap_or(Value::Null); - } - if let PresentationElement::Shape(shape) = element - && let Some(stroke) = &shape.stroke - { - record["stroke"] = serde_json::json!({ - "color": stroke.color, - "width": stroke.width, - "style": stroke.style.as_api_str(), - }); - } - if let Some(hyperlink) = match element { - PresentationElement::Text(text) => text.hyperlink.as_ref(), - PresentationElement::Shape(shape) => shape.hyperlink.as_ref(), - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => None, - } { - record["hyperlink"] = hyperlink.to_json(); - } - records.push((record, Some(slide_id.clone()))); - if include("textRange") { - match element { - PresentationElement::Text(text) => { - records.extend(text.rich_text.ranges.iter().map(|range| { - let mut record = text_range_to_proto(&text.text, range); - record["kind"] = Value::String("textRange".to_string()); - record["slide"] = Value::from(index + 1); - record["slideIndex"] = Value::from(index); - record["hostAnchor"] = Value::String(format!("sh/{}", text.element_id)); - record["hostKind"] = Value::String("textbox".to_string()); - (record, Some(slide_id.clone())) - })); - } - PresentationElement::Shape(shape) => { - if let Some((text, rich_text)) = shape.text.as_ref().zip(shape.rich_text.as_ref()) { - records.extend(rich_text.ranges.iter().map(|range| { - let mut record = text_range_to_proto(text, range); - record["kind"] = Value::String("textRange".to_string()); - record["slide"] = Value::from(index + 1); - record["slideIndex"] = Value::from(index); - record["hostAnchor"] = Value::String(format!("sh/{}", shape.element_id)); - record["hostKind"] = Value::String("textbox".to_string()); - (record, Some(slide_id.clone())) - })); - } - } - PresentationElement::Table(table) => { - for (row_index, row) in table.rows.iter().enumerate() { - for (column_index, cell) in row.iter().enumerate() { - records.extend(cell.rich_text.ranges.iter().map(|range| { - let mut record = text_range_to_proto(&cell.text, range); - record["kind"] = Value::String("textRange".to_string()); - record["slide"] = Value::from(index + 1); - record["slideIndex"] = Value::from(index); - record["hostAnchor"] = Value::String(format!( - "tb/{}#cell/{row_index}/{column_index}", - table.element_id - )); - record["hostKind"] = Value::String("tableCell".to_string()); - (record, Some(slide_id.clone())) - })); - } - } - } - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Chart(_) => {} - } - } - } - } - if include("comment") { - records.extend(document.comment_threads.iter().map(|thread| { - let mut record = comment_thread_to_proto(thread); - record["id"] = Value::String(format!("th/{}", thread.thread_id)); - (record, None) - })); - } - - if let Some(target_id) = args.target_id.as_deref() { - records.retain(|(record, slide_id)| { - legacy_target_matches(target_id, record, slide_id.as_deref()) - }); - if records.is_empty() { - records.push(( - serde_json::json!({ - "kind": "notice", - "noticeType": "targetNotFound", - "target": { "id": target_id }, - "message": format!("No inspect records matched target `{target_id}`."), - }), - None, - )); - } - } - - if let Some(search) = args.search.as_deref() { - let search_lowercase = search.to_ascii_lowercase(); - records.retain(|(record, _)| { - record - .to_string() - .to_ascii_lowercase() - .contains(&search_lowercase) - }); - if records.is_empty() { - records.push(( - serde_json::json!({ - "kind": "notice", - "noticeType": "noMatches", - "search": search, - "message": format!("No inspect records matched search `{search}`."), - }), - None, - )); - } - } - - if let Some(target) = args.target.as_ref() { - if let Some(target_index) = records.iter().position(|(record, _)| { - record.get("id").and_then(Value::as_str) == Some(target.id.as_str()) - }) { - let start = target_index.saturating_sub(target.before_lines.unwrap_or(0)); - let end = (target_index + target.after_lines.unwrap_or(0) + 1).min(records.len()); - records = records.into_iter().skip(start).take(end - start).collect(); - } else { - records = vec![( - serde_json::json!({ - "kind": "notice", - "noticeType": "targetNotFound", - "target": { - "id": target.id, - "beforeLines": target.before_lines, - "afterLines": target.after_lines, - }, - "message": format!("No inspect records matched target `{}`.", target.id), - }), - None, - )]; - } - } - - let mut lines = Vec::new(); - let mut omitted_lines = 0usize; - let mut omitted_chars = 0usize; - for line in records.into_iter().map(|(record, _)| record.to_string()) { - let separator_len = usize::from(!lines.is_empty()); - if let Some(max_chars) = args.max_chars - && lines.iter().map(String::len).sum::() + separator_len + line.len() > max_chars - { - omitted_lines += 1; - omitted_chars += line.len(); - continue; - } - lines.push(line); - } - if omitted_lines > 0 { - lines.push( - serde_json::json!({ - "kind": "notice", - "noticeType": "truncation", - "maxChars": args.max_chars, - "omittedLines": omitted_lines, - "omittedChars": omitted_chars, - "message": format!( - "Truncated inspect output by omitting {omitted_lines} lines. Increase maxChars or narrow the filter." - ), - }) - .to_string(), - ); - } - lines.join("\n") -} - -fn legacy_target_matches(target_id: &str, record: &Value, slide_id: Option<&str>) -> bool { - record.get("id").and_then(Value::as_str) == Some(target_id) || slide_id == Some(target_id) -} - -fn add_text_metadata(record: &mut Value, text: &str) { - record["textPreview"] = Value::String(text.replace('\n', " | ")); - record["textChars"] = Value::from(text.chars().count()); - record["textLines"] = Value::from(text.lines().count()); -} - -fn normalize_element_lookup_id(element_id: &str) -> &str { - element_id - .split_once('/') - .map(|(_, normalized)| normalized) - .unwrap_or(element_id) -} - -fn resolve_anchor( - document: &PresentationDocument, - id: &str, - action: &str, -) -> Result { - if id == format!("pr/{}", document.artifact_id) { - return Ok(serde_json::json!({ - "kind": "deck", - "id": id, - "artifactId": document.artifact_id, - "name": document.name, - "slideCount": document.slides.len(), - "styleIds": document - .named_text_styles() - .iter() - .map(|style| format!("st/{}", style.name)) - .collect::>(), - "activeSlideIndex": document.active_slide_index, - "activeSlideId": document.active_slide_index.and_then(|index| document.slides.get(index)).map(|slide| format!("sl/{}", slide.slide_id)), - })); - } - if let Some(style_name) = id.strip_prefix("st/") { - let named_style = document - .named_text_styles() - .into_iter() - .find(|style| style.name == style_name) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown style id `{id}`"), - })?; - return Ok(named_text_style_to_json(&named_style, "st")); - } - - for (slide_index, slide) in document.slides.iter().enumerate() { - let slide_id = format!("sl/{}", slide.slide_id); - if id == slide_id { - return Ok(serde_json::json!({ - "kind": "slide", - "id": slide_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "isActive": document.active_slide_index == Some(slide_index), - "layoutId": slide.layout_id, - "notesId": (!slide.notes.text.is_empty()).then(|| format!("nt/{}", slide.slide_id)), - "elementIds": slide.elements.iter().map(|element| { - let prefix = match element { - PresentationElement::Text(_) | PresentationElement::Shape(_) => "sh", - PresentationElement::Connector(_) => "cn", - PresentationElement::Image(_) => "im", - PresentationElement::Table(_) => "tb", - PresentationElement::Chart(_) => "ch", - }; - format!("{prefix}/{}", element.element_id()) - }).collect::>(), - })); - } - let notes_id = format!("nt/{}", slide.slide_id); - if id == notes_id { - let mut record = serde_json::json!({ - "kind": "notes", - "id": notes_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "visible": slide.notes.visible, - "text": slide.notes.text, - }); - add_text_metadata(&mut record, &slide.notes.text); - record["richText"] = rich_text_to_proto(&slide.notes.text, &slide.notes.rich_text); - return Ok(record); - } - if let Some(range_id) = id.strip_prefix("tr/") - && let Some(record) = slide - .notes - .rich_text - .ranges - .iter() - .find(|range| range.range_id == range_id) - .map(|range| { - let mut record = text_range_to_proto(&slide.notes.text, range); - record["kind"] = Value::String("textRange".to_string()); - record["id"] = Value::String(id.to_string()); - record["slide"] = Value::from(slide_index + 1); - record["slideIndex"] = Value::from(slide_index); - record["hostAnchor"] = Value::String(notes_id.clone()); - record["hostKind"] = Value::String("notes".to_string()); - record - }) - { - return Ok(record); - } - for element in &slide.elements { - let mut record = match element { - PresentationElement::Text(text) => { - let mut record = serde_json::json!({ - "kind": "textbox", - "id": format!("sh/{}", text.element_id), - "elementId": text.element_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "text": text.text, - "textStyle": text_style_to_proto(&text.style), - "richText": rich_text_to_proto(&text.text, &text.rich_text), - "bbox": [text.frame.left, text.frame.top, text.frame.width, text.frame.height], - "bboxUnit": "points", - }); - add_text_metadata(&mut record, &text.text); - record - } - PresentationElement::Shape(shape) => { - let mut record = serde_json::json!({ - "kind": if shape.text.is_some() { "textbox" } else { "shape" }, - "id": format!("sh/{}", shape.element_id), - "elementId": shape.element_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "geometry": format!("{:?}", shape.geometry), - "text": shape.text, - "textStyle": text_style_to_proto(&shape.text_style), - "richText": shape - .text - .as_ref() - .zip(shape.rich_text.as_ref()) - .map(|(text, rich_text)| rich_text_to_proto(text, rich_text)) - .unwrap_or(Value::Null), - "rotation": shape.rotation_degrees, - "flipHorizontal": shape.flip_horizontal, - "flipVertical": shape.flip_vertical, - "bbox": [shape.frame.left, shape.frame.top, shape.frame.width, shape.frame.height], - "bboxUnit": "points", - }); - if let Some(text) = &shape.text { - add_text_metadata(&mut record, text); - } - record - } - PresentationElement::Connector(connector) => serde_json::json!({ - "kind": "connector", - "id": format!("cn/{}", connector.element_id), - "elementId": connector.element_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "connectorType": format!("{:?}", connector.connector_type), - "start": [connector.start.left, connector.start.top], - "end": [connector.end.left, connector.end.top], - "lineStyle": format!("{:?}", connector.line_style), - "label": connector.label, - }), - PresentationElement::Image(image) => serde_json::json!({ - "kind": "image", - "id": format!("im/{}", image.element_id), - "elementId": image.element_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "alt": image.alt_text, - "prompt": image.prompt, - "fit": format!("{:?}", image.fit_mode), - "rotation": image.rotation_degrees, - "flipHorizontal": image.flip_horizontal, - "flipVertical": image.flip_vertical, - "crop": image.crop.map(|(left, top, right, bottom)| serde_json::json!({ - "left": left, - "top": top, - "right": right, - "bottom": bottom, - })), - "isPlaceholder": image.is_placeholder, - "lockAspectRatio": image.lock_aspect_ratio, - "bbox": [image.frame.left, image.frame.top, image.frame.width, image.frame.height], - "bboxUnit": "points", - }), - PresentationElement::Table(table) => serde_json::json!({ - "kind": "table", - "id": format!("tb/{}", table.element_id), - "elementId": table.element_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "rows": table.rows.len(), - "cols": table.rows.iter().map(std::vec::Vec::len).max().unwrap_or(0), - "columnWidths": table.column_widths, - "rowHeights": table.row_heights, - "style": table.style, - "styleOptions": table_style_options_to_proto(&table.style_options), - "borders": table.borders.as_ref().map(table_borders_to_proto), - "rightToLeft": table.right_to_left, - "cellTextStyles": table - .rows - .iter() - .map(|row| row.iter().map(|cell| text_style_to_proto(&cell.text_style)).collect::>()) - .collect::>(), - "rowsData": table - .rows - .iter() - .map(|row| row.iter().map(table_cell_to_proto).collect::>()) - .collect::>(), - "bbox": [table.frame.left, table.frame.top, table.frame.width, table.frame.height], - "bboxUnit": "points", - }), - PresentationElement::Chart(chart) => serde_json::json!({ - "kind": "chart", - "id": format!("ch/{}", chart.element_id), - "elementId": chart.element_id, - "slide": slide_index + 1, - "slideIndex": slide_index, - "chartType": format!("{:?}", chart.chart_type), - "title": chart.title, - "styleIndex": chart.style_index, - "hasLegend": chart.has_legend, - "legend": chart.legend.as_ref().map(chart_legend_to_proto), - "xAxis": chart.x_axis.as_ref().map(chart_axis_to_proto), - "yAxis": chart.y_axis.as_ref().map(chart_axis_to_proto), - "dataLabels": chart.data_labels.as_ref().map(chart_data_labels_to_proto), - "chartFill": chart.chart_fill, - "plotAreaFill": chart.plot_area_fill, - "series": chart - .series - .iter() - .map(|series| serde_json::json!({ - "name": series.name, - "values": series.values, - "categories": series.categories, - "xValues": series.x_values, - "fill": series.fill, - "stroke": series.stroke.as_ref().map(stroke_to_proto), - "marker": series.marker.as_ref().map(chart_marker_to_proto), - "dataLabelOverrides": series - .data_label_overrides - .iter() - .map(chart_data_label_override_to_proto) - .collect::>(), - })) - .collect::>(), - "bbox": [chart.frame.left, chart.frame.top, chart.frame.width, chart.frame.height], - "bboxUnit": "points", - }), - }; - if let Some(hyperlink) = match element { - PresentationElement::Text(text) => text.hyperlink.as_ref(), - PresentationElement::Shape(shape) => shape.hyperlink.as_ref(), - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => None, - } { - record["hyperlink"] = hyperlink.to_json(); - } - if let PresentationElement::Shape(shape) = element - && let Some(stroke) = &shape.stroke - { - record["stroke"] = serde_json::json!({ - "color": stroke.color, - "width": stroke.width, - "style": stroke.style.as_api_str(), - }); - } - if let Some(placeholder) = match element { - PresentationElement::Text(text) => text.placeholder.as_ref(), - PresentationElement::Shape(shape) => shape.placeholder.as_ref(), - PresentationElement::Image(image) => image.placeholder.as_ref(), - PresentationElement::Connector(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => None, - } { - record["placeholder"] = Value::String(placeholder.placeholder_type.clone()); - record["placeholderName"] = Value::String(placeholder.name.clone()); - record["placeholderIndex"] = - placeholder.index.map(Value::from).unwrap_or(Value::Null); - } - if record.get("id").and_then(Value::as_str) == Some(id) { - return Ok(record); - } - if let Some(range_id) = id.strip_prefix("tr/") { - match element { - PresentationElement::Text(text) => { - if let Some(range) = - text.rich_text.ranges.iter().find(|range| range.range_id == range_id) - { - let mut range_record = text_range_to_proto(&text.text, range); - range_record["kind"] = Value::String("textRange".to_string()); - range_record["id"] = Value::String(id.to_string()); - range_record["slide"] = Value::from(slide_index + 1); - range_record["slideIndex"] = Value::from(slide_index); - range_record["hostAnchor"] = - Value::String(format!("sh/{}", text.element_id)); - range_record["hostKind"] = Value::String("textbox".to_string()); - return Ok(range_record); - } - } - PresentationElement::Shape(shape) => { - if let Some((text, rich_text)) = - shape.text.as_ref().zip(shape.rich_text.as_ref()) - && let Some(range) = - rich_text.ranges.iter().find(|range| range.range_id == range_id) - { - let mut range_record = text_range_to_proto(text, range); - range_record["kind"] = Value::String("textRange".to_string()); - range_record["id"] = Value::String(id.to_string()); - range_record["slide"] = Value::from(slide_index + 1); - range_record["slideIndex"] = Value::from(slide_index); - range_record["hostAnchor"] = - Value::String(format!("sh/{}", shape.element_id)); - range_record["hostKind"] = Value::String("textbox".to_string()); - return Ok(range_record); - } - } - PresentationElement::Table(table) => { - for (row_index, row) in table.rows.iter().enumerate() { - for (column_index, cell) in row.iter().enumerate() { - if let Some(range) = cell - .rich_text - .ranges - .iter() - .find(|range| range.range_id == range_id) - { - let mut range_record = text_range_to_proto(&cell.text, range); - range_record["kind"] = Value::String("textRange".to_string()); - range_record["id"] = Value::String(id.to_string()); - range_record["slide"] = Value::from(slide_index + 1); - range_record["slideIndex"] = Value::from(slide_index); - range_record["hostAnchor"] = Value::String(format!( - "tb/{}#cell/{row_index}/{column_index}", - table.element_id - )); - range_record["hostKind"] = - Value::String("tableCell".to_string()); - return Ok(range_record); - } - } - } - } - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Chart(_) => {} - } - } - } - } - - if let Some(thread_id) = id.strip_prefix("th/") - && let Some(thread) = document - .comment_threads - .iter() - .find(|thread| thread.thread_id == thread_id) - { - let mut record = comment_thread_to_proto(thread); - record["id"] = Value::String(id.to_string()); - return Ok(record); - } - - for layout in &document.layouts { - let layout_id = format!("ly/{}", layout.layout_id); - if id == layout_id { - return Ok(serde_json::json!({ - "kind": "layout", - "id": layout_id, - "layoutId": layout.layout_id, - "name": layout.name, - "type": match layout.kind { - LayoutKind::Layout => "layout", - LayoutKind::Master => "master", - }, - "parentLayoutId": layout.parent_layout_id, - "placeholders": layout_placeholder_list(document, &layout.layout_id, action)?, - })); - } - } - - Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown resolve id `{id}`"), - }) -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/manager.rs b/codex-rs/artifact-presentation/src/presentation_artifact/manager.rs deleted file mode 100644 index 8539b62c7..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/manager.rs +++ /dev/null @@ -1,3203 +0,0 @@ -#[derive(Debug, Default)] -pub struct PresentationArtifactManager { - documents: HashMap, - undo_stack: Vec, - redo_stack: Vec, -} - -#[derive(Debug, Clone)] -struct HistoryEntry { - artifact_id: String, - action: String, - before: Option, - after: Option, -} - -impl PresentationArtifactManager { - pub fn execute_requests( - &mut self, - request: PresentationArtifactExecutionRequest, - cwd: &Path, - ) -> Result { - let PresentationArtifactExecutionRequest { - artifact_id, - requests, - } = request; - let request_count = requests.len(); - let mut current_artifact_id = artifact_id; - let mut executed_actions = Vec::with_capacity(request_count); - let mut exported_paths = Vec::new(); - let mut last_response = None; - - for mut request in requests { - if request.artifact_id.is_none() { - request.artifact_id = current_artifact_id.clone(); - } - let response = self.execute(request, cwd)?; - current_artifact_id = Some(response.artifact_id.clone()); - exported_paths.extend(response.exported_paths.iter().cloned()); - executed_actions.push(response.action.clone()); - last_response = Some(response); - } - - let mut response = last_response.ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: "presentation_artifact".to_string(), - message: "request sequence must contain at least one action".to_string(), - })?; - if request_count > 1 { - let final_summary = response.summary.clone(); - response.action = "batch".to_string(); - response.summary = - format!("Executed {request_count} actions sequentially. {final_summary}"); - response.executed_actions = Some(executed_actions); - response.exported_paths = exported_paths; - } - Ok(response) - } - - pub fn execute( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - match request.action.as_str() { - "undo" => return self.undo(request), - "redo" => return self.redo(request), - "record_patch" => return self.record_patch(request), - "apply_patch" => return self.apply_patch(request, cwd), - _ => {} - } - - let action = request.action.clone(); - let before = if tracks_history(&action) { - request - .artifact_id - .as_ref() - .and_then(|artifact_id| self.documents.get(artifact_id).cloned()) - } else { - None - }; - let response = self.execute_action(request, cwd)?; - if tracks_history(&action) { - let after = self.documents.get(&response.artifact_id).cloned(); - self.push_history_entry(HistoryEntry { - artifact_id: response.artifact_id.clone(), - action, - before, - after, - }); - } - Ok(response) - } - - fn execute_action( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - match request.action.as_str() { - "create" => self.create(request), - "import_pptx" => self.import_pptx(request, cwd), - "export_pptx" => self.export_pptx(request, cwd), - "export_preview" => self.export_preview(request, cwd), - "get_summary" => self.get_summary(request), - "list_slides" => self.list_slides(request), - "list_layouts" => self.list_layouts(request), - "list_layout_placeholders" => self.list_layout_placeholders(request), - "list_slide_placeholders" => self.list_slide_placeholders(request), - "inspect" => self.inspect(request), - "resolve" => self.resolve(request), - "to_proto" => self.proto_snapshot(request), - "add_slide" => self.add_slide(request), - "insert_slide" => self.insert_slide(request), - "duplicate_slide" => self.duplicate_slide(request), - "move_slide" => self.move_slide(request), - "delete_slide" => self.delete_slide(request), - "create_layout" => self.create_layout(request), - "add_layout_placeholder" => self.add_layout_placeholder(request), - "set_slide_layout" => self.set_slide_layout(request), - "update_placeholder_text" => self.update_placeholder_text(request), - "set_theme" => self.set_theme(request), - "add_style" => self.add_style(request), - "get_style" => self.get_style(request), - "describe_styles" => self.describe_styles(request), - "set_notes" => self.set_notes(request), - "set_notes_rich_text" => self.set_notes_rich_text(request), - "append_notes" => self.append_notes(request), - "clear_notes" => self.clear_notes(request), - "set_notes_visibility" => self.set_notes_visibility(request), - "set_active_slide" => self.set_active_slide(request), - "set_slide_background" => self.set_slide_background(request), - "add_text_shape" => self.add_text_shape(request), - "add_shape" => self.add_shape(request), - "add_connector" => self.add_connector(request), - "add_image" => self.add_image(request, cwd), - "replace_image" => self.replace_image(request, cwd), - "add_table" => self.add_table(request), - "update_table_style" => self.update_table_style(request), - "style_table_block" => self.style_table_block(request), - "update_table_cell" => self.update_table_cell(request), - "merge_table_cells" => self.merge_table_cells(request), - "add_chart" => self.add_chart(request), - "update_chart" => self.update_chart(request), - "add_chart_series" => self.add_chart_series(request), - "update_text" => self.update_text(request), - "set_rich_text" => self.set_rich_text(request), - "format_text_range" => self.format_text_range(request), - "replace_text" => self.replace_text(request), - "insert_text_after" => self.insert_text_after(request), - "set_hyperlink" => self.set_hyperlink(request), - "set_comment_author" => self.set_comment_author(request), - "add_comment_thread" => self.add_comment_thread(request), - "add_comment_reply" => self.add_comment_reply(request), - "toggle_comment_reaction" => self.toggle_comment_reaction(request), - "resolve_comment_thread" => self.resolve_comment_thread(request), - "reopen_comment_thread" => self.reopen_comment_thread(request), - "update_shape_style" => self.update_shape_style(request), - "bring_to_front" => self.bring_to_front(request), - "send_to_back" => self.send_to_back(request), - "delete_element" => self.delete_element(request), - "delete_artifact" => self.delete_artifact(request), - other => Err(PresentationArtifactError::UnknownAction(other.to_string())), - } - } - - fn push_history_entry(&mut self, entry: HistoryEntry) { - self.undo_stack.push(entry); - self.redo_stack.clear(); - } - - fn create( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: CreateArgs = parse_args(&request.action, &request.args)?; - let mut document = PresentationDocument::new(args.name); - if let Some(slide_size) = args.slide_size { - document.slide_size = parse_slide_size(&slide_size, &request.action)?; - } - if let Some(theme) = args.theme { - document.theme = normalize_theme(theme, &request.action)?; - } - let artifact_id = document.artifact_id.clone(); - let summary = format!( - "Created presentation artifact `{artifact_id}` with {} slides", - document.slides.len() - ); - let snapshot = snapshot_for_document(&document); - let mut response = - PresentationArtifactResponse::new(artifact_id, request.action, summary, snapshot); - response.theme = Some(document.theme_snapshot()); - self.documents - .insert(response.artifact_id.clone(), document); - Ok(response) - } - - fn import_pptx( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - let args: ImportPptxArgs = parse_args(&request.action, &request.args)?; - let path = resolve_path(cwd, &args.path); - let document = if let Some(document) = import_codex_metadata_document(&path) - .map_err(|message| PresentationArtifactError::ImportFailed { - path: path.clone(), - message, - })? - { - document - } else { - let imported = Presentation::from_path(&path).map_err(|error| { - PresentationArtifactError::ImportFailed { - path: path.clone(), - message: error.to_string(), - } - })?; - let mut document = PresentationDocument::from_ppt_rs(imported); - import_pptx_images(&path, &mut document, &request.action)?; - document - }; - let artifact_id = document.artifact_id.clone(); - let slide_count = document.slides.len(); - let snapshot = snapshot_for_document(&document); - self.documents.insert(artifact_id.clone(), document); - let summary = format!( - "Imported `{}` as artifact `{artifact_id}` with {slide_count} slides", - path.display() - ); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - summary, - snapshot, - )) - } - - fn export_pptx( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - let args: ExportPptxArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let path = resolve_path(cwd, &args.path); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: path.clone(), - message: error.to_string(), - } - })?; - } - - let bytes = build_pptx_bytes(document, &request.action).map_err(|message| { - PresentationArtifactError::ExportFailed { - path: path.clone(), - message, - } - })?; - std::fs::write(&path, bytes).map_err(|error| PresentationArtifactError::ExportFailed { - path: path.clone(), - message: error.to_string(), - })?; - - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Exported presentation to `{}`", path.display()), - snapshot_for_document(document), - ); - response.exported_paths.push(path); - Ok(response) - } - - fn export_preview( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - let args: ExportPreviewArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let output_path = resolve_path(cwd, &args.path); - let preview_format = - parse_preview_output_format(args.format.as_deref(), &output_path, &request.action)?; - let scale = normalize_preview_scale(args.scale, &request.action)?; - let quality = normalize_preview_quality(args.quality, &request.action)?; - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: output_path.clone(), - message: error.to_string(), - } - })?; - } - let temp_dir = - std::env::temp_dir().join(format!("presentation_preview_{}", Uuid::new_v4().simple())); - std::fs::create_dir_all(&temp_dir).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: output_path.clone(), - message: error.to_string(), - } - })?; - let preview_document = if let Some(slide_index) = args.slide_index { - let slide = document - .slides - .get(slide_index as usize) - .cloned() - .ok_or_else(|| { - index_out_of_range(&request.action, slide_index as usize, document.slides.len()) - })?; - let slide_id = slide.slide_id.clone(); - PresentationDocument { - artifact_id: document.artifact_id.clone(), - name: document.name.clone(), - slide_size: document.slide_size, - theme: document.theme.clone(), - custom_text_styles: document.custom_text_styles.clone(), - layouts: Vec::new(), - slides: vec![slide], - active_slide_index: Some(0), - comment_self: document.comment_self.clone(), - comment_threads: document - .comment_threads - .iter() - .filter(|thread| match &thread.target { - CommentTarget::Slide { slide_id: target_slide_id } - | CommentTarget::Element { slide_id: target_slide_id, .. } - | CommentTarget::TextRange { slide_id: target_slide_id, .. } => { - target_slide_id == &slide_id - } - }) - .cloned() - .collect(), - next_slide_seq: 1, - next_element_seq: 1, - next_layout_seq: 1, - next_text_range_seq: document.next_text_range_seq, - next_comment_thread_seq: document.next_comment_thread_seq, - next_comment_message_seq: document.next_comment_message_seq, - } - } else { - document.clone() - }; - write_preview_images(&preview_document, &temp_dir, &request.action)?; - let mut exported_paths = collect_pngs(&temp_dir)?; - if args.slide_index.is_some() { - let rendered = - exported_paths - .pop() - .ok_or_else(|| PresentationArtifactError::ExportFailed { - path: output_path.clone(), - message: "preview renderer produced no images".to_string(), - })?; - write_preview_image( - &rendered, - &output_path, - preview_format, - scale, - quality, - &request.action, - )?; - exported_paths = vec![output_path]; - } else { - std::fs::create_dir_all(&output_path).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: output_path.clone(), - message: error.to_string(), - } - })?; - let mut relocated = Vec::new(); - for rendered in exported_paths { - let filename = rendered.file_name().ok_or_else(|| { - PresentationArtifactError::ExportFailed { - path: output_path.clone(), - message: "rendered preview had no filename".to_string(), - } - })?; - let stem = Path::new(filename) - .file_stem() - .and_then(|value| value.to_str()) - .unwrap_or("preview"); - let target = output_path.join(format!("{stem}.{}", preview_format.extension())); - write_preview_image( - &rendered, - &target, - preview_format, - scale, - quality, - &request.action, - )?; - relocated.push(target); - } - exported_paths = relocated; - } - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - "Exported slide preview".to_string(), - snapshot_for_document(document), - ); - response.exported_paths = exported_paths; - Ok(response) - } - - fn get_summary( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Presentation `{}` has {} slides, {} elements, {} layouts, and active slide {}", - document.name.as_deref().unwrap_or("Untitled"), - document.slides.len(), - document.total_element_count(), - document.layouts.len(), - document - .active_slide_index - .map(|index| index.to_string()) - .unwrap_or_else(|| "none".to_string()) - ), - snapshot_for_document(document), - ); - response.slide_list = Some(slide_list(document)); - response.layout_list = Some(layout_list(document)); - response.theme = Some(document.theme_snapshot()); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn list_slides( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Listed {} slides", document.slides.len()), - snapshot_for_document(document), - ); - response.slide_list = Some(slide_list(document)); - response.theme = Some(document.theme_snapshot()); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn list_layouts( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Listed {} layouts", document.layouts.len()), - snapshot_for_document(document), - ); - response.layout_list = Some(layout_list(document)); - response.theme = Some(document.theme_snapshot()); - Ok(response) - } - - fn list_layout_placeholders( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: LayoutIdArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let placeholders = layout_placeholder_list(document, &args.layout_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Listed {} placeholders for layout `{}`", - placeholders.len(), - args.layout_id - ), - snapshot_for_document(document), - ); - response.placeholder_list = Some(placeholders); - response.layout_list = Some(layout_list(document)); - Ok(response) - } - - fn list_slide_placeholders( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SlideIndexArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let slide_index = args.slide_index as usize; - let slide = document.slides.get(slide_index).ok_or_else(|| { - index_out_of_range(&request.action, slide_index, document.slides.len()) - })?; - let placeholders = slide_placeholder_list(slide, slide_index); - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Listed {} placeholders for slide {}", - placeholders.len(), - args.slide_index - ), - snapshot_for_document(document), - ); - response.placeholder_list = Some(placeholders); - response.slide_list = Some(slide_list(document)); - Ok(response) - } - - fn inspect( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: InspectArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let inspect_ndjson = inspect_document(document, &args); - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - "Generated inspection snapshot".to_string(), - snapshot_for_document(document), - ); - response.inspect_ndjson = Some(inspect_ndjson); - response.theme = Some(document.theme_snapshot()); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn resolve( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ResolveArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let resolved_record = resolve_anchor(document, &args.id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Resolved `{}`", args.id), - snapshot_for_document(document), - ); - response.resolved_record = Some(resolved_record); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn proto_snapshot( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - "Generated proto snapshot".to_string(), - snapshot_for_document(document), - ); - response.proto_json = Some(document_to_proto(document, "to_proto")?); - response.theme = Some(document.theme_snapshot()); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn record_patch( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: RecordPatchArgs = parse_args(&request.action, &request.args)?; - let patch = self.normalize_patch( - request.artifact_id.as_deref(), - args.operations, - &request.action, - )?; - let document = self.get_document(&patch.artifact_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - patch.artifact_id.clone(), - request.action, - format!("Recorded patch with {} operations", patch.operations.len()), - snapshot_for_document(document), - ); - response.patch = Some(serde_json::to_value(&patch).map_err(|error| { - PresentationArtifactError::InvalidArgs { - action: "record_patch".to_string(), - message: format!("failed to serialize patch: {error}"), - } - })?); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn apply_patch( - &mut self, - request: PresentationArtifactRequest, - _cwd: &Path, - ) -> Result { - let args: ApplyPatchArgs = parse_args(&request.action, &request.args)?; - let patch = if let Some(patch) = args.patch { - self.normalize_serialized_patch(request.artifact_id.as_deref(), patch, &request.action)? - } else { - let operations = - args.operations - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "provide either `patch` or `operations`".to_string(), - })?; - self.normalize_patch(request.artifact_id.as_deref(), operations, &request.action)? - }; - let before = self.documents.get(&patch.artifact_id).cloned(); - let Some(before_document) = before else { - return Err(PresentationArtifactError::UnknownArtifactId { - action: request.action, - artifact_id: patch.artifact_id, - }); - }; - for operation in &patch.operations { - let nested_request = PresentationArtifactRequest { - artifact_id: Some(patch.artifact_id.clone()), - action: operation.action.clone(), - args: operation.args.clone(), - }; - if let Err(error) = self.execute_action(nested_request, Path::new(".")) { - self.documents - .insert(patch.artifact_id.clone(), before_document); - return Err(error); - } - } - let document = self - .get_document(&patch.artifact_id, &request.action)? - .clone(); - self.push_history_entry(HistoryEntry { - artifact_id: patch.artifact_id.clone(), - action: request.action.clone(), - before: Some(before_document), - after: Some(document.clone()), - }); - let mut response = PresentationArtifactResponse::new( - patch.artifact_id.clone(), - request.action, - format!("Applied patch with {} operations", patch.operations.len()), - snapshot_for_document(&document), - ); - response.patch = Some(serde_json::to_value(&patch).map_err(|error| { - PresentationArtifactError::InvalidArgs { - action: "apply_patch".to_string(), - message: format!("failed to serialize patch: {error}"), - } - })?); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn undo( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let position = self - .undo_stack - .iter() - .rposition(|entry| { - request - .artifact_id - .as_deref() - .is_none_or(|artifact_id| artifact_id == entry.artifact_id) - }) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: request.action.clone(), - message: "nothing to undo".to_string(), - })?; - let entry = self.undo_stack.remove(position); - match &entry.before { - Some(document) => { - self.documents - .insert(entry.artifact_id.clone(), document.clone()); - } - None => { - self.documents.remove(&entry.artifact_id); - } - } - self.redo_stack.push(entry.clone()); - Ok(response_for_document_state( - entry.artifact_id, - request.action, - format!("Undid `{}`", entry.action), - entry.before.as_ref(), - )) - } - - fn redo( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let position = self - .redo_stack - .iter() - .rposition(|entry| { - request - .artifact_id - .as_deref() - .is_none_or(|artifact_id| artifact_id == entry.artifact_id) - }) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: request.action.clone(), - message: "nothing to redo".to_string(), - })?; - let entry = self.redo_stack.remove(position); - match &entry.after { - Some(document) => { - self.documents - .insert(entry.artifact_id.clone(), document.clone()); - } - None => { - self.documents.remove(&entry.artifact_id); - } - } - self.undo_stack.push(entry.clone()); - Ok(response_for_document_state( - entry.artifact_id, - request.action, - format!("Redid `{}`", entry.action), - entry.after.as_ref(), - )) - } - - fn create_layout( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: CreateLayoutArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let layout_id = document.next_layout_id(); - let kind = match args.kind.as_deref() { - Some("master") => LayoutKind::Master, - Some("layout") | None => LayoutKind::Layout, - Some(other) => { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!("unsupported layout kind `{other}`"), - }); - } - }; - let parent_layout_id = args - .parent_layout_id - .map(|parent_layout_ref| { - document - .get_layout(&parent_layout_ref, &request.action) - .map(|layout| layout.layout_id.clone()) - }) - .transpose()?; - document.layouts.push(LayoutDocument { - layout_id: layout_id.clone(), - name: args.name, - kind, - parent_layout_id, - placeholders: Vec::new(), - }); - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Created layout `{layout_id}`"), - snapshot_for_document(document), - ); - response.layout_list = Some(layout_list(document)); - Ok(response) - } - - fn add_layout_placeholder( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddLayoutPlaceholderArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let geometry = args - .geometry - .as_deref() - .map(|value| parse_shape_geometry(value, &request.action)) - .transpose()? - .unwrap_or(ShapeGeometry::Rectangle); - let frame = args.position.unwrap_or(PositionArgs { - left: 48, - top: 72, - width: 624, - height: 96, - rotation: None, - flip_horizontal: None, - flip_vertical: None, - }); - let layout_id = document - .get_layout(&args.layout_id, &request.action)? - .layout_id - .clone(); - let layout = document - .layouts - .iter_mut() - .find(|layout| layout.layout_id == layout_id) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: request.action.clone(), - message: format!("unknown layout id `{}`", args.layout_id), - })?; - layout.placeholders.push(PlaceholderDefinition { - name: args.name, - placeholder_type: args.placeholder_type, - index: args.index, - text: args.text, - geometry, - frame: frame.into(), - }); - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Added placeholder to layout `{}`", layout.layout_id), - snapshot_for_document(document), - ); - response.layout_list = Some(layout_list(document)); - Ok(response) - } - - fn set_slide_layout( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetSlideLayoutArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let layout = document - .get_layout(&args.layout_id, &request.action)? - .clone(); - let placeholders = - resolved_layout_placeholders(document, &layout.layout_id, &request.action)? - .into_iter() - .map(|resolved| resolved.definition) - .collect::>(); - let mut placeholder_elements = Vec::new(); - for placeholder in placeholders { - placeholder_elements.push(materialize_placeholder_element( - document.next_element_id(), - placeholder, - placeholder_elements.len(), - )); - } - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide.elements.retain(|element| match element { - PresentationElement::Text(text) => text.placeholder.is_none(), - PresentationElement::Shape(shape) => shape.placeholder.is_none(), - PresentationElement::Image(image) => image.placeholder.is_none(), - _ => true, - }); - slide.layout_id = Some(layout.layout_id.clone()); - slide.elements.extend(placeholder_elements); - resequence_z_order(slide); - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Applied layout `{}` to slide {}", - layout.layout_id, args.slide_index - ), - snapshot_for_document(document), - ); - response.slide_list = Some(slide_list(document)); - response.layout_list = Some(layout_list(document)); - Ok(response) - } - - fn update_placeholder_text( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: UpdatePlaceholderTextArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - let target_name = args.name.to_ascii_lowercase(); - let element = slide - .elements - .iter_mut() - .find(|element| match element { - PresentationElement::Text(text) => text - .placeholder - .as_ref() - .map(|placeholder| placeholder.name.eq_ignore_ascii_case(&target_name)) - .unwrap_or(false), - PresentationElement::Shape(shape) => shape - .placeholder - .as_ref() - .map(|placeholder| placeholder.name.eq_ignore_ascii_case(&target_name)) - .unwrap_or(false), - _ => false, - }) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: request.action.clone(), - message: format!( - "placeholder `{}` was not found on slide {}", - args.name, args.slide_index - ), - })?; - match element { - PresentationElement::Text(text) => text.text = args.text, - PresentationElement::Shape(shape) => shape.text = Some(args.text), - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => {} - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Updated placeholder `{}` on slide {}", - args.name, args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn set_theme( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ThemeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - document.theme = normalize_theme(args, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - "Updated theme".to_string(), - snapshot_for_document(document), - ); - response.theme = Some(document.theme_snapshot()); - Ok(response) - } - - fn add_style( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let style_name = normalize_style_name(&args.name, &request.action)?; - let mut style = - normalize_text_style_with_document(document, &args.styling, &request.action)?; - style.style_name = Some(style_name.clone()); - let style_record = NamedTextStyle { - name: style_name.clone(), - style: style.clone(), - built_in: false, - }; - document - .custom_text_styles - .insert(style_name.clone(), style); - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Added text style `{style_name}`"), - snapshot_for_document(document), - ); - response.resolved_record = Some(named_text_style_to_json(&style_record, "st")); - Ok(response) - } - - fn get_style( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: StyleNameArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let normalized_style_name = normalize_style_name(&args.name, &request.action)?; - let named_style = document - .named_text_styles() - .into_iter() - .find(|style| style.name == normalized_style_name) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: request.action.clone(), - message: format!("unknown text style `{}`", args.name), - })?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Resolved text style `{}`", args.name), - snapshot_for_document(document), - ); - response.resolved_record = Some(named_text_style_to_json(&named_style, "st")); - Ok(response) - } - - fn describe_styles( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document(&artifact_id, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - "Described text styles".to_string(), - snapshot_for_document(document), - ); - response.resolved_record = Some(serde_json::json!({ - "kind": "styleList", - "styles": document - .named_text_styles() - .iter() - .map(|style| named_text_style_to_json(style, "st")) - .collect::>(), - })); - Ok(response) - } - - fn set_notes( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: NotesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide.notes.text = args.text.unwrap_or_default(); - slide.notes.rich_text = RichTextState::default(); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated notes for slide {}", args.slide_index), - snapshot_for_document(document), - )) - } - - fn set_notes_rich_text( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetRichTextArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let slide_index = args.slide_index.ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "`slide_index` is required for notes rich text".to_string(), - })?; - let (text, mut rich_text) = - normalize_rich_text_input(document, args.text, &request.action)?; - rich_text.layout = normalize_text_layout(&args.text_layout, &request.action)?; - let slide = document.get_slide_mut(slide_index, &request.action)?; - slide.notes.text = text; - slide.notes.rich_text = rich_text; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated rich notes for slide {slide_index}"), - snapshot_for_document(document), - )) - } - - fn append_notes( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: NotesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - let text = args.text.unwrap_or_default(); - if slide.notes.text.is_empty() { - slide.notes.text = text; - } else { - slide.notes.text = format!("{}\n{text}", slide.notes.text); - } - slide.notes.rich_text = RichTextState::default(); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Appended notes for slide {}", args.slide_index), - snapshot_for_document(document), - )) - } - - fn clear_notes( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: NotesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide.notes.text.clear(); - slide.notes.rich_text = RichTextState::default(); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Cleared notes for slide {}", args.slide_index), - snapshot_for_document(document), - )) - } - - fn set_notes_visibility( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: NotesVisibilityArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide.notes.visible = args.visible; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated notes visibility for slide {}", args.slide_index), - snapshot_for_document(document), - )) - } - - fn set_active_slide( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetActiveSlideArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - document.set_active_slide_index(args.slide_index as usize, &request.action)?; - let mut response = PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Set active slide to {}", args.slide_index), - snapshot_for_document(document), - ); - response.slide_list = Some(slide_list(document)); - response.active_slide_index = document.active_slide_index; - Ok(response) - } - - fn add_slide( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddSlideArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let mut slide = document.new_slide(args.notes, args.background_fill, &request.action)?; - if let Some(layout_id) = args.layout { - apply_layout_to_slide(document, &mut slide, &layout_id, &request.action)?; - } - let index = document.append_slide(slide); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Added slide at index {index}"), - snapshot_for_document(document), - )) - } - - fn insert_slide( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: InsertSlideArgs = parse_args(&request.action, &request.args)?; - if args.index.is_some() && args.after_slide_index.is_some() { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: "provide at most one of `index` or `after_slide_index`".to_string(), - }); - } - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let index = if let Some(index) = args.index { - to_index(index)? - } else if let Some(after_slide_index) = args.after_slide_index { - after_slide_index as usize + 1 - } else { - document - .active_slide_index - .map(|active_slide_index| active_slide_index + 1) - .unwrap_or(document.slides.len()) - }; - if index > document.slides.len() { - return Err(index_out_of_range( - &request.action, - index, - document.slides.len(), - )); - } - let mut slide = document.new_slide(args.notes, args.background_fill, &request.action)?; - if let Some(layout_id) = args.layout { - apply_layout_to_slide(document, &mut slide, &layout_id, &request.action)?; - } - document.adjust_active_slide_for_insert(index); - document.slides.insert(index, slide); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Inserted slide at index {index}"), - snapshot_for_document(document), - )) - } - - fn duplicate_slide( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SlideIndexArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let source = document - .slides - .get(args.slide_index as usize) - .cloned() - .ok_or_else(|| { - index_out_of_range( - &request.action, - args.slide_index as usize, - document.slides.len(), - ) - })?; - let duplicated = document.clone_slide(source); - let insert_at = args.slide_index as usize + 1; - document.adjust_active_slide_for_insert(insert_at); - document.slides.insert(insert_at, duplicated); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Duplicated slide {} to index {insert_at}", args.slide_index), - snapshot_for_document(document), - )) - } - - fn move_slide( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: MoveSlideArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let from = args.from_index as usize; - let to = args.to_index as usize; - if from >= document.slides.len() { - return Err(index_out_of_range( - &request.action, - from, - document.slides.len(), - )); - } - if to >= document.slides.len() { - return Err(index_out_of_range( - &request.action, - to, - document.slides.len(), - )); - } - let slide = document.slides.remove(from); - document.slides.insert(to, slide); - document.adjust_active_slide_for_move(from, to); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Moved slide from index {from} to {to}"), - snapshot_for_document(document), - )) - } - - fn delete_slide( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SlideIndexArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let index = args.slide_index as usize; - if index >= document.slides.len() { - return Err(index_out_of_range( - &request.action, - index, - document.slides.len(), - )); - } - document.slides.remove(index); - document.adjust_active_slide_for_delete(index); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Deleted slide at index {index}"), - snapshot_for_document(document), - )) - } - - fn set_slide_background( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetSlideBackgroundArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let fill = normalize_color_with_document(document, &args.fill, &request.action, "fill")?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide.background_fill = Some(fill); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated background for slide {}", args.slide_index), - snapshot_for_document(document), - )) - } - - fn add_text_shape( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddTextShapeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let style = normalize_text_style_with_document(document, &args.styling, &request.action)?; - let fill = args - .styling - .fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, &request.action, "fill")) - .transpose()?; - let text_layout = normalize_text_layout(&args.text_layout, &request.action)?; - let element_id = document.next_element_id(); - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide.elements.push(PresentationElement::Text(TextElement { - element_id: element_id.clone(), - text: args.text, - frame: args.position.into(), - fill, - style, - hyperlink: None, - rich_text: RichTextState { - ranges: Vec::new(), - layout: text_layout, - }, - placeholder: None, - z_order: slide.elements.len(), - })); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Added text element `{element_id}` to slide {}", - args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn add_shape( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddShapeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let text_style = - normalize_text_style_with_document(document, &args.text_style, &request.action)?; - let fill = args - .fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, &request.action, "fill")) - .transpose()?; - let stroke = parse_stroke(document, args.stroke, &request.action)?; - let text_layout = normalize_text_layout(&args.text_layout, &request.action)?; - let rich_text = args.text.as_ref().map(|_| RichTextState { - ranges: Vec::new(), - layout: text_layout, - }); - let element_id = document.next_element_id(); - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide - .elements - .push(PresentationElement::Shape(ShapeElement { - element_id: element_id.clone(), - geometry: parse_shape_geometry(&args.geometry, &request.action)?, - frame: args.position.clone().into(), - fill, - stroke, - text: args.text, - text_style, - hyperlink: None, - rich_text, - placeholder: None, - rotation_degrees: args.rotation.or(args.position.rotation), - flip_horizontal: args - .flip_horizontal - .or(args.position.flip_horizontal) - .unwrap_or(false), - flip_vertical: args - .flip_vertical - .or(args.position.flip_vertical) - .unwrap_or(false), - z_order: slide.elements.len(), - })); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Added shape element `{element_id}` to slide {}", - args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn add_connector( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddConnectorArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let element_id = document.next_element_id(); - let line = parse_connector_line(document, args.line, &request.action)?; - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide - .elements - .push(PresentationElement::Connector(ConnectorElement { - element_id: element_id.clone(), - connector_type: parse_connector_kind(&args.connector_type, &request.action)?, - start: args.start, - end: args.end, - line: StrokeStyle { - color: line.color, - width: line.width, - style: LineStyle::Solid, - }, - line_style: line.style, - start_arrow: args - .start_arrow - .as_deref() - .map(|value| parse_connector_arrow(value, &request.action)) - .transpose()? - .unwrap_or(ConnectorArrowKind::None), - end_arrow: args - .end_arrow - .as_deref() - .map(|value| parse_connector_arrow(value, &request.action)) - .transpose()? - .unwrap_or(ConnectorArrowKind::None), - arrow_size: args - .arrow_size - .as_deref() - .map(|value| parse_connector_arrow_size(value, &request.action)) - .transpose()? - .unwrap_or(ConnectorArrowScale::Medium), - label: args.label, - z_order: slide.elements.len(), - })); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Added connector element `{element_id}` to slide {}", - args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn add_image( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - let args: AddImageArgs = parse_args(&request.action, &request.args)?; - let image_source = args.image_source()?; - let is_placeholder = matches!(image_source, ImageInputSource::Placeholder); - let image_payload = match image_source { - ImageInputSource::Path(path) => Some(load_image_payload_from_path( - &resolve_path(cwd, &path), - &request.action, - )?), - ImageInputSource::DataUrl(data_url) => Some(load_image_payload_from_data_url( - &data_url, - &request.action, - )?), - ImageInputSource::Blob(blob) => { - Some(load_image_payload_from_blob(&blob, &request.action)?) - } - ImageInputSource::Uri(uri) => Some(load_image_payload_from_uri(&uri, &request.action)?), - ImageInputSource::Placeholder => None, - }; - let fit_mode = args.fit.unwrap_or(ImageFitMode::Stretch); - let lock_aspect_ratio = args - .lock_aspect_ratio - .unwrap_or(fit_mode != ImageFitMode::Stretch); - let crop = args - .crop - .map(|crop| normalize_image_crop(crop, &request.action)) - .transpose()?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let element_id = document.next_element_id(); - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide - .elements - .push(PresentationElement::Image(ImageElement { - element_id: element_id.clone(), - frame: args.position.clone().into(), - payload: image_payload, - fit_mode, - crop, - rotation_degrees: args.rotation.or(args.position.rotation), - flip_horizontal: args - .flip_horizontal - .or(args.position.flip_horizontal) - .unwrap_or(false), - flip_vertical: args - .flip_vertical - .or(args.position.flip_vertical) - .unwrap_or(false), - lock_aspect_ratio, - alt_text: args.alt, - prompt: args.prompt, - is_placeholder, - placeholder: None, - z_order: slide.elements.len(), - })); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Added image element `{element_id}` to slide {}", - args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn replace_image( - &mut self, - request: PresentationArtifactRequest, - cwd: &Path, - ) -> Result { - let args: ReplaceImageArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let image_source = match ( - &args.path, - &args.data_url, - &args.blob, - &args.uri, - &args.prompt, - ) { - (Some(path), None, None, None, None) => ImageInputSource::Path(path.clone()), - (None, Some(data_url), None, None, None) => ImageInputSource::DataUrl(data_url.clone()), - (None, None, Some(blob), None, None) => ImageInputSource::Blob(blob.clone()), - (None, None, None, Some(uri), None) => ImageInputSource::Uri(uri.clone()), - (None, None, None, None, Some(_)) => ImageInputSource::Placeholder, - _ => { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: - "provide exactly one of `path`, `data_url`, `blob`, or `uri`, or provide `prompt` for a placeholder image" - .to_string(), - }); - } - }; - let is_placeholder = matches!(image_source, ImageInputSource::Placeholder); - let image_payload = match image_source { - ImageInputSource::Path(path) => Some(load_image_payload_from_path( - &resolve_path(cwd, &path), - "replace_image", - )?), - ImageInputSource::DataUrl(data_url) => Some(load_image_payload_from_data_url( - &data_url, - "replace_image", - )?), - ImageInputSource::Blob(blob) => { - Some(load_image_payload_from_blob(&blob, "replace_image")?) - } - ImageInputSource::Uri(uri) => Some(load_image_payload_from_uri(&uri, "replace_image")?), - ImageInputSource::Placeholder => None, - }; - let fit_mode = args.fit.unwrap_or(ImageFitMode::Stretch); - let lock_aspect_ratio = args - .lock_aspect_ratio - .unwrap_or(fit_mode != ImageFitMode::Stretch); - let crop = args - .crop - .map(|crop| normalize_image_crop(crop, &request.action)) - .transpose()?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Image(image) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not an image", args.element_id), - }); - }; - image.payload = image_payload; - image.fit_mode = fit_mode; - image.crop = crop; - if let Some(rotation) = args.rotation { - image.rotation_degrees = Some(rotation); - } - if let Some(flip_horizontal) = args.flip_horizontal { - image.flip_horizontal = flip_horizontal; - } - if let Some(flip_vertical) = args.flip_vertical { - image.flip_vertical = flip_vertical; - } - image.lock_aspect_ratio = lock_aspect_ratio; - image.alt_text = args.alt; - image.prompt = args.prompt; - image.is_placeholder = is_placeholder; - Ok(PresentationArtifactResponse::new( - artifact_id, - "replace_image".to_string(), - format!("Replaced image `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn add_table( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddTableArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let rows = coerce_table_rows(args.rows, &request.action)?; - let borders = parse_table_borders(document, args.borders, &request.action)?; - let style_options = parse_table_style_options(args.style_options); - let mut frame: Rect = args.position.into(); - let (column_widths, row_heights) = normalize_table_dimensions( - &rows, - frame, - args.column_widths, - args.row_heights, - &request.action, - )?; - frame.width = column_widths.iter().sum(); - frame.height = row_heights.iter().sum(); - let element_id = document.next_element_id(); - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide - .elements - .push(PresentationElement::Table(TableElement { - element_id: element_id.clone(), - frame, - rows, - column_widths, - row_heights, - style: args.style, - style_options, - borders, - right_to_left: args.right_to_left.unwrap_or(false), - merges: Vec::new(), - z_order: slide.elements.len(), - })); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Added table element `{element_id}` to slide {}", - args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn update_table_cell( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: UpdateTableCellArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let text_style = - normalize_text_style_with_document(document, &args.styling, &request.action)?; - let background_fill = args - .background_fill - .as_deref() - .map(|fill| { - normalize_color_with_document(document, fill, &request.action, "background_fill") - }) - .transpose()?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Table(table) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not a table", args.element_id), - }); - }; - let row = args.row as usize; - let column = args.column as usize; - if row >= table.rows.len() || column >= table.rows[row].len() { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!("cell ({row}, {column}) is out of bounds"), - }); - } - let cell = &mut table.rows[row][column]; - cell.text = cell_value_to_string(args.value); - cell.text_style = text_style; - cell.background_fill = background_fill; - cell.rich_text = RichTextState::default(); - cell.alignment = args - .alignment - .as_deref() - .map(|value| parse_alignment(value, &request.action)) - .transpose()?; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated table cell ({row}, {column})"), - snapshot_for_document(document), - )) - } - - fn update_table_style( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: UpdateTableStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let borders = parse_table_borders(document, args.borders, &request.action)?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Table(table) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not a table", args.element_id), - }); - }; - table.style = args.style; - if let Some(borders) = borders { - if let Some(existing) = table.borders.as_mut() { - if borders.outside.is_some() { - existing.outside = borders.outside; - } - if borders.inside.is_some() { - existing.inside = borders.inside; - } - if borders.top.is_some() { - existing.top = borders.top; - } - if borders.bottom.is_some() { - existing.bottom = borders.bottom; - } - if borders.left.is_some() { - existing.left = borders.left; - } - if borders.right.is_some() { - existing.right = borders.right; - } - } else { - table.borders = Some(borders); - } - } - if let Some(style_options) = args.style_options { - if let Some(value) = style_options.header_row { - table.style_options.header_row = value; - } - if let Some(value) = style_options.banded_rows { - table.style_options.banded_rows = value; - } - if let Some(value) = style_options.banded_columns { - table.style_options.banded_columns = value; - } - if let Some(value) = style_options.first_column { - table.style_options.first_column = value; - } - if let Some(value) = style_options.last_column { - table.style_options.last_column = value; - } - if let Some(value) = style_options.total_row { - table.style_options.total_row = value; - } - } - if let Some(right_to_left) = args.right_to_left { - table.right_to_left = right_to_left; - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated table style for `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn style_table_block( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: StyleTableBlockArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let text_style = - normalize_text_style_with_document(document, &args.styling, &request.action)?; - let background_fill = args - .background_fill - .as_deref() - .map(|fill| { - normalize_color_with_document(document, fill, &request.action, "background_fill") - }) - .transpose()?; - let borders = parse_table_borders(document, args.borders, &request.action)?; - let alignment = args - .alignment - .as_deref() - .map(|value| parse_alignment(value, &request.action)) - .transpose()?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Table(table) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not a table", args.element_id), - }); - }; - let end_row = (args.row + args.row_count) as usize; - let end_column = (args.column + args.column_count) as usize; - for row_index in args.row as usize..end_row { - if row_index >= table.rows.len() { - break; - } - for column_index in args.column as usize..end_column { - if column_index >= table.rows[row_index].len() { - break; - } - let cell = &mut table.rows[row_index][column_index]; - cell.text_style = text_style.clone(); - if let Some(fill) = background_fill.clone() { - cell.background_fill = Some(fill); - } - if let Some(alignment) = alignment { - cell.alignment = Some(alignment); - } - if let Some(borders) = borders.clone() { - cell.borders = Some(borders); - } - } - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Styled table block for `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn merge_table_cells( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: MergeTableCellsArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Table(table) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not a table", args.element_id), - }); - }; - let region = TableMergeRegion { - start_row: args.start_row as usize, - end_row: args.end_row as usize, - start_column: args.start_column as usize, - end_column: args.end_column as usize, - }; - table.merges.push(region); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Merged table cells in `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn add_chart( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddChartArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let chart_type = parse_chart_type(&args.chart_type, &request.action)?; - let series = parse_chart_series(document, args.series, &request.action)?; - let legend_text_style = - normalize_text_style_with_document(document, &args.legend_text_style, &request.action)?; - let data_labels = parse_chart_data_labels(document, args.data_labels, &request.action)?; - let chart_fill = args - .chart_fill - .as_deref() - .map(|value| { - normalize_color_with_document(document, value, &request.action, "chart_fill") - }) - .transpose()?; - let plot_area_fill = args - .plot_area_fill - .as_deref() - .map(|value| { - normalize_color_with_document(document, value, &request.action, "plot_area_fill") - }) - .transpose()?; - let element_id = document.next_element_id(); - let slide = document.get_slide_mut(args.slide_index, &request.action)?; - slide - .elements - .push(PresentationElement::Chart(ChartElement { - element_id: element_id.clone(), - frame: args.position.into(), - chart_type, - categories: args.categories, - series, - title: args.title, - style_index: args.style_index, - has_legend: args.has_legend.unwrap_or(false), - legend: Some(ChartLegend { - position: args.legend_position, - text_style: legend_text_style, - }), - x_axis: Some(ChartAxisSpec { - title: args.x_axis_title, - }), - y_axis: Some(ChartAxisSpec { - title: args.y_axis_title, - }), - data_labels, - chart_fill, - plot_area_fill, - z_order: slide.elements.len(), - })); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!( - "Added chart element `{element_id}` to slide {}", - args.slide_index - ), - snapshot_for_document(document), - )) - } - - fn update_chart( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: UpdateChartArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let legend_text_style = - normalize_text_style_with_document(document, &args.legend_text_style, &request.action)?; - let data_labels = parse_chart_data_labels(document, args.data_labels, &request.action)?; - let chart_fill = args - .chart_fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, &request.action, "chart_fill")) - .transpose()?; - let plot_area_fill = args - .plot_area_fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, &request.action, "plot_area_fill")) - .transpose()?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Chart(chart) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not a chart", args.element_id), - }); - }; - if let Some(title) = args.title { - chart.title = Some(title); - } - if let Some(categories) = args.categories { - chart.categories = categories; - } - if let Some(style_index) = args.style_index { - chart.style_index = Some(style_index); - } - if let Some(has_legend) = args.has_legend { - chart.has_legend = has_legend; - } - if args.legend_position.is_some() || !text_style_is_empty(&legend_text_style) { - chart.legend = Some(ChartLegend { - position: args.legend_position, - text_style: legend_text_style, - }); - } - if args.x_axis_title.is_some() { - chart.x_axis = Some(ChartAxisSpec { - title: args.x_axis_title, - }); - } - if args.y_axis_title.is_some() { - chart.y_axis = Some(ChartAxisSpec { - title: args.y_axis_title, - }); - } - if data_labels.is_some() { - chart.data_labels = data_labels; - } - if chart_fill.is_some() { - chart.chart_fill = chart_fill; - } - if plot_area_fill.is_some() { - chart.plot_area_fill = plot_area_fill; - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated chart `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn add_chart_series( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddChartSeriesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let series = parse_chart_series( - document, - vec![ChartSeriesArgs { - name: args.name, - values: args.values, - categories: args.categories, - x_values: args.x_values, - fill: args.fill, - stroke: args.stroke, - marker: args.marker, - data_label_overrides: None, - }], - &request.action, - )?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - let PresentationElement::Chart(chart) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{}` is not a chart", args.element_id), - }); - }; - chart.series.extend(series); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Added chart series to `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn update_text( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: UpdateTextArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let style = normalize_text_style_with_document(document, &args.styling, &request.action)?; - let fill = args - .styling - .fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, &request.action, "fill")) - .transpose()?; - let text_layout = normalize_text_layout(&args.text_layout, &request.action)?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - match element { - PresentationElement::Text(text) => { - text.text = args.text; - if let Some(fill) = fill.clone() { - text.fill = Some(fill); - } - text.style = style; - text.rich_text = RichTextState { - ranges: Vec::new(), - layout: text_layout, - }; - } - PresentationElement::Shape(shape) => { - if shape.text.is_none() { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` does not contain editable text", - args.element_id - ), - }); - } - shape.text = Some(args.text); - if let Some(fill) = fill { - shape.fill = Some(fill); - } - shape.text_style = style; - shape.rich_text = Some(RichTextState { - ranges: Vec::new(), - layout: text_layout, - }); - } - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` is `{}`; only text-bearing elements support `update_text`", - args.element_id, - other.kind() - ), - }); - } - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated text for element `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn set_rich_text( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetRichTextArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let (text, mut rich_text) = - normalize_rich_text_input(document, args.text, &request.action)?; - rich_text.layout = normalize_text_layout(&args.text_layout, &request.action)?; - let style = normalize_text_style_with_document(document, &args.styling, &request.action)?; - if args.notes.unwrap_or(false) { - let slide_index = args.slide_index.ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "`slide_index` is required for notes rich text".to_string(), - })?; - let slide = document.get_slide_mut(slide_index, &request.action)?; - slide.notes.text = text; - slide.notes.rich_text = rich_text; - return Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated rich text for notes on slide {slide_index}"), - snapshot_for_document(document), - )); - } - if let Some(element_id) = args.element_id { - if let (Some(row), Some(column)) = (args.row, args.column) { - let element = document.find_element_mut(&element_id, &request.action)?; - let PresentationElement::Table(table) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{element_id}` is not a table"), - }); - }; - let row = row as usize; - let column = column as usize; - if row >= table.rows.len() || column >= table.rows[row].len() { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!("cell ({row}, {column}) is out of bounds"), - }); - } - let cell = &mut table.rows[row][column]; - cell.text = text; - cell.text_style = style; - cell.rich_text = rich_text; - return Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated rich text for table cell ({row}, {column})"), - snapshot_for_document(document), - )); - } - let element = document.find_element_mut(&element_id, &request.action)?; - match element { - PresentationElement::Text(text_element) => { - text_element.text = text; - text_element.style = style; - text_element.rich_text = rich_text; - } - PresentationElement::Shape(shape) => { - if shape.text.is_none() { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{element_id}` does not contain editable text" - ), - }); - } - shape.text = Some(text); - shape.text_style = style; - shape.rich_text = Some(rich_text); - } - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{element_id}` is `{}`; only text-bearing elements support `set_rich_text`", - other.kind() - ), - }); - } - } - return Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated rich text for element `{element_id}`"), - snapshot_for_document(document), - )); - } - Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: "provide `element_id` or `slide_index` with `notes: true`".to_string(), - }) - } - - fn format_text_range( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: FormatTextRangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let style = normalize_text_style_with_document(document, &args.styling, &request.action)?; - let hyperlink = args - .link - .as_ref() - .map(|link| parse_rich_text_link(link, &request.action)) - .transpose()?; - let layout = normalize_text_layout(&args.text_layout, &request.action)?; - let apply_annotation = - |range_id: String, - text: &str, - rich_text: &mut RichTextState| - -> Result<(), PresentationArtifactError> { - let (start_cp, length, _) = resolve_text_range_selector( - text, - args.query.as_deref(), - args.occurrence, - args.start_cp, - args.length, - &request.action, - )?; - rich_text.ranges.push(TextRangeAnnotation { - range_id, - start_cp, - length, - style: style.clone(), - hyperlink: hyperlink.clone(), - spacing_before: args.spacing_before.map(|value| value * 100), - spacing_after: args.spacing_after.map(|value| value * 100), - line_spacing: args.line_spacing, - }); - if layout.insets.is_some() - || layout.wrap.is_some() - || layout.auto_fit.is_some() - || layout.vertical_alignment.is_some() - { - rich_text.layout = layout.clone(); - } - Ok(()) - }; - if args.notes.unwrap_or(false) { - let slide_index = args.slide_index.ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "`slide_index` is required for notes text ranges".to_string(), - })?; - let range_id = document.next_text_range_id(); - let slide = document.get_slide_mut(slide_index, &request.action)?; - apply_annotation(range_id, &slide.notes.text, &mut slide.notes.rich_text)?; - return Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Formatted notes text range on slide {slide_index}"), - snapshot_for_document(document), - )); - } - let element_id = args.element_id.clone().ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "`element_id` is required unless formatting notes".to_string(), - })?; - if let (Some(row), Some(column)) = (args.row, args.column) { - let range_id = document.next_text_range_id(); - let element = document.find_element_mut(&element_id, &request.action)?; - let PresentationElement::Table(table) = element else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!("element `{element_id}` is not a table"), - }); - }; - let row = row as usize; - let column = column as usize; - if row >= table.rows.len() || column >= table.rows[row].len() { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!("cell ({row}, {column}) is out of bounds"), - }); - } - let cell = &mut table.rows[row][column]; - apply_annotation(range_id, &cell.text, &mut cell.rich_text)?; - return Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Formatted table cell text range ({row}, {column})"), - snapshot_for_document(document), - )); - } - let range_id = document.next_text_range_id(); - let element = document.find_element_mut(&element_id, &request.action)?; - match element { - PresentationElement::Text(text) => { - apply_annotation(range_id, &text.text, &mut text.rich_text)?; - } - PresentationElement::Shape(shape) => { - let text_value = shape.text.as_ref().ok_or_else(|| { - PresentationArtifactError::UnsupportedFeature { - action: request.action.clone(), - message: format!("element `{element_id}` does not contain editable text"), - } - })?; - let rich_text = shape.rich_text.get_or_insert_with(RichTextState::default); - apply_annotation(range_id, text_value, rich_text)?; - } - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{element_id}` is `{}`; only text-bearing elements support `format_text_range`", - other.kind() - ), - }); - } - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Formatted text range on element `{element_id}`"), - snapshot_for_document(document), - )) - } - - fn replace_text( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ReplaceTextArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - match element { - PresentationElement::Text(text) => { - if !text.text.contains(&args.search) { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!( - "text `{}` was not found in element `{}`", - args.search, args.element_id - ), - }); - } - text.text = text.text.replace(&args.search, &args.replace); - } - PresentationElement::Shape(shape) => { - let Some(text) = &mut shape.text else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` does not contain editable text", - args.element_id - ), - }); - }; - if !text.contains(&args.search) { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!( - "text `{}` was not found in element `{}`", - args.search, args.element_id - ), - }); - } - *text = text.replace(&args.search, &args.replace); - } - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` is `{}`; only text-bearing elements support `replace_text`", - args.element_id, - other.kind() - ), - }); - } - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Replaced text in element `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn insert_text_after( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: InsertTextAfterArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let element = document.find_element_mut(&args.element_id, &request.action)?; - match element { - PresentationElement::Text(text) => { - let Some(index) = text.text.find(&args.after) else { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!( - "text `{}` was not found in element `{}`", - args.after, args.element_id - ), - }); - }; - let insert_at = index + args.after.len(); - text.text.insert_str(insert_at, &args.insert); - } - PresentationElement::Shape(shape) => { - let Some(text) = &mut shape.text else { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` does not contain editable text", - args.element_id - ), - }); - }; - let Some(index) = text.find(&args.after) else { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: format!( - "text `{}` was not found in element `{}`", - args.after, args.element_id - ), - }); - }; - let insert_at = index + args.after.len(); - text.insert_str(insert_at, &args.insert); - } - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` is `{}`; only text-bearing elements support `insert_text_after`", - args.element_id, - other.kind() - ), - }); - } - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Inserted text in element `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn set_hyperlink( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetHyperlinkArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let clear = args.clear.unwrap_or(false); - let hyperlink = if clear { - None - } else { - Some(parse_hyperlink_state(document, &args, &request.action)?) - }; - let element = document.find_element_mut(&args.element_id, &request.action)?; - match element { - PresentationElement::Text(text) => text.hyperlink = hyperlink, - PresentationElement::Shape(shape) => shape.hyperlink = hyperlink, - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: format!( - "element `{}` is `{}`; only text boxes and shapes support `set_hyperlink`", - args.element_id, - other.kind() - ), - }); - } - } - Ok(PresentationArtifactResponse::new( - artifact_id, - "set_hyperlink".to_string(), - if clear { - format!("Cleared hyperlink for element `{}`", args.element_id) - } else { - format!("Updated hyperlink for element `{}`", args.element_id) - }, - snapshot_for_document(document), - )) - } - - fn set_comment_author( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: SetCommentAuthorArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - document.comment_self = Some(CommentAuthorProfile { - display_name: args.display_name, - initials: args.initials, - email: args.email, - }); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - "Updated comment author".to_string(), - snapshot_for_document(document), - )) - } - - fn add_comment_thread( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddCommentThreadArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let author = document.comment_self.clone().ok_or_else(|| { - PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "set a comment author first with `set_comment_author`".to_string(), - } - })?; - let target = if let Some(slide_index) = args.slide_index { - let slide = document.get_slide_mut(slide_index, &request.action)?; - if let Some(element_id) = args.element_id { - if args.query.is_some() || args.start_cp.is_some() { - let (text, _rich_text) = lookup_text_target( - slide, - &element_id, - None, - None, - &request.action, - )?; - let (start_cp, length, context) = resolve_text_range_selector( - text, - args.query.as_deref(), - args.occurrence, - args.start_cp, - args.length, - &request.action, - )?; - CommentTarget::TextRange { - slide_id: slide.slide_id.clone(), - element_id: normalize_element_lookup_id(&element_id).to_string(), - start_cp, - length, - context, - } - } else { - CommentTarget::Element { - slide_id: slide.slide_id.clone(), - element_id: normalize_element_lookup_id(&element_id).to_string(), - } - } - } else { - CommentTarget::Slide { - slide_id: slide.slide_id.clone(), - } - } - } else { - return Err(PresentationArtifactError::InvalidArgs { - action: request.action, - message: "`slide_index` is required for comment threads".to_string(), - }); - }; - let thread_id = document.next_comment_thread_id(); - let message_id = document.next_comment_message_id(); - document.comment_threads.push(CommentThread { - thread_id: thread_id.clone(), - target, - position: args.position.map(|position| CommentPosition { - x: position.x, - y: position.y, - }), - status: CommentThreadStatus::Active, - messages: vec![CommentMessage { - message_id, - author, - text: args.text, - created_at: "2026-03-03T00:00:00Z".to_string(), - reactions: Vec::new(), - }], - }); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Added comment thread `{thread_id}`"), - snapshot_for_document(document), - )) - } - - fn add_comment_reply( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: AddCommentReplyArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let author = document.comment_self.clone().ok_or_else(|| { - PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: "set a comment author first with `set_comment_author`".to_string(), - } - })?; - let message_id = document.next_comment_message_id(); - let thread = document - .comment_threads - .iter_mut() - .find(|thread| thread.thread_id == args.thread_id) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("unknown comment thread `{}`", args.thread_id), - })?; - thread.messages.push(CommentMessage { - message_id, - author, - text: args.text, - created_at: "2026-03-03T00:00:00Z".to_string(), - reactions: Vec::new(), - }); - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Added reply to `{}`", args.thread_id), - snapshot_for_document(document), - )) - } - - fn toggle_comment_reaction( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ToggleCommentReactionArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let thread = document - .comment_threads - .iter_mut() - .find(|thread| thread.thread_id == args.thread_id) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("unknown comment thread `{}`", args.thread_id), - })?; - let target_message_id = args - .message_id - .clone() - .or_else(|| thread.messages.last().map(|message| message.message_id.clone())) - .unwrap_or_default(); - let message = thread - .messages - .iter_mut() - .find(|message| message.message_id == target_message_id) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("unknown comment message `{target_message_id}`"), - })?; - if let Some(index) = message.reactions.iter().position(|emoji| emoji == &args.emoji) { - message.reactions.remove(index); - } else { - message.reactions.push(args.emoji.clone()); - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Toggled reaction on `{}`", args.thread_id), - snapshot_for_document(document), - )) - } - - fn resolve_comment_thread( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: CommentThreadIdArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let thread = document - .comment_threads - .iter_mut() - .find(|thread| thread.thread_id == args.thread_id) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("unknown comment thread `{}`", args.thread_id), - })?; - thread.status = CommentThreadStatus::Resolved; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Resolved `{}`", args.thread_id), - snapshot_for_document(document), - )) - } - - fn reopen_comment_thread( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: CommentThreadIdArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let thread = document - .comment_threads - .iter_mut() - .find(|thread| thread.thread_id == args.thread_id) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("unknown comment thread `{}`", args.thread_id), - })?; - thread.status = CommentThreadStatus::Active; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Reopened `{}`", args.thread_id), - snapshot_for_document(document), - )) - } - - fn update_shape_style( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: UpdateShapeStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let fill = args - .fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, &request.action, "fill")) - .transpose()?; - let stroke = args - .stroke - .clone() - .map(|value| parse_required_stroke(document, value, &request.action)) - .transpose()?; - let text_layout = normalize_text_layout(&args.text_layout, &request.action)?; - let has_text_layout = text_layout.insets.is_some() - || text_layout.wrap.is_some() - || text_layout.auto_fit.is_some() - || text_layout.vertical_alignment.is_some(); - let element = document.find_element_mut(&args.element_id, &request.action)?; - match element { - PresentationElement::Text(text) => { - if let Some(position) = args.position { - text.frame = apply_partial_position(text.frame, position); - } - if let Some(fill) = fill.clone() { - text.fill = Some(fill); - } - if has_text_layout { - text.rich_text.layout = text_layout; - } - if args.stroke.is_some() - || args.rotation.is_some() - || args.flip_horizontal.is_some() - || args.flip_vertical.is_some() - { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: - "text elements support only `position`, `z_order`, and `fill` updates" - .to_string(), - }); - } - } - PresentationElement::Shape(shape) => { - let position_rotation = args - .position - .as_ref() - .and_then(|position| position.rotation); - let position_flip_horizontal = args - .position - .as_ref() - .and_then(|position| position.flip_horizontal); - let position_flip_vertical = args - .position - .as_ref() - .and_then(|position| position.flip_vertical); - if let Some(position) = args.position { - shape.frame = apply_partial_position(shape.frame, position); - } - if let Some(fill) = fill { - shape.fill = Some(fill); - } - if let Some(stroke) = stroke { - shape.stroke = Some(stroke); - } - if let Some(rotation) = args.rotation.or(position_rotation) { - shape.rotation_degrees = Some(rotation); - } - if let Some(flip_horizontal) = args.flip_horizontal.or(position_flip_horizontal) { - shape.flip_horizontal = flip_horizontal; - } - if let Some(flip_vertical) = args.flip_vertical.or(position_flip_vertical) { - shape.flip_vertical = flip_vertical; - } - if shape.text.is_some() && has_text_layout { - shape.rich_text = Some(RichTextState { - ranges: shape.rich_text.take().unwrap_or_default().ranges, - layout: text_layout, - }); - } - } - PresentationElement::Connector(connector) => { - if args.fill.is_some() - || args.rotation.is_some() - || args.flip_horizontal.is_some() - || args.flip_vertical.is_some() - || args.fit.is_some() - || args.crop.is_some() - || args.lock_aspect_ratio.is_some() - { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: - "connector elements support only `position`, `stroke`, and `z_order` updates" - .to_string(), - }); - } - if let Some(position) = args.position { - let updated = apply_partial_position( - Rect { - left: connector.start.left, - top: connector.start.top, - width: connector.end.left.abs_diff(connector.start.left), - height: connector.end.top.abs_diff(connector.start.top), - }, - position, - ); - connector.start = PointArgs { - left: updated.left, - top: updated.top, - }; - connector.end = PointArgs { - left: updated.left.saturating_add(updated.width), - top: updated.top.saturating_add(updated.height), - }; - } - if let Some(stroke) = stroke { - connector.line = stroke; - } - } - PresentationElement::Image(image) => { - let position_rotation = args - .position - .as_ref() - .and_then(|position| position.rotation); - let position_flip_horizontal = args - .position - .as_ref() - .and_then(|position| position.flip_horizontal); - let position_flip_vertical = args - .position - .as_ref() - .and_then(|position| position.flip_vertical); - if args.fill.is_some() || args.stroke.is_some() { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: - "image elements support only `position`, `fit`, `crop`, `rotation`, `flip_horizontal`, `flip_vertical`, `lock_aspect_ratio`, and `z_order` updates" - .to_string(), - }); - } - if let Some(position) = args.position { - image.frame = apply_partial_position_to_image(image, position); - } - if let Some(fit) = args.fit { - image.fit_mode = fit; - if !matches!(fit, ImageFitMode::Stretch) && args.lock_aspect_ratio.is_none() { - image.lock_aspect_ratio = true; - } - } - if let Some(crop) = args.crop { - image.crop = Some(normalize_image_crop(crop, &request.action)?); - } - if let Some(rotation) = args.rotation.or(position_rotation) { - image.rotation_degrees = Some(rotation); - } - if let Some(flip_horizontal) = args.flip_horizontal.or(position_flip_horizontal) { - image.flip_horizontal = flip_horizontal; - } - if let Some(flip_vertical) = args.flip_vertical.or(position_flip_vertical) { - image.flip_vertical = flip_vertical; - } - if let Some(lock_aspect_ratio) = args.lock_aspect_ratio { - image.lock_aspect_ratio = lock_aspect_ratio; - } - } - PresentationElement::Table(table) => { - if args.fill.is_some() - || args.stroke.is_some() - || args.rotation.is_some() - || args.flip_horizontal.is_some() - || args.flip_vertical.is_some() - { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: "table elements support only `position` and `z_order` updates" - .to_string(), - }); - } - if let Some(position) = args.position { - table.frame = apply_partial_position(table.frame, position); - } - } - PresentationElement::Chart(chart) => { - if args.fill.is_some() - || args.stroke.is_some() - || args.rotation.is_some() - || args.flip_horizontal.is_some() - || args.flip_vertical.is_some() - { - return Err(PresentationArtifactError::UnsupportedFeature { - action: request.action, - message: "chart elements support only `position` and `z_order` updates" - .to_string(), - }); - } - if let Some(position) = args.position { - chart.frame = apply_partial_position(chart.frame, position); - } - } - } - if let Some(z_order) = args.z_order { - document.set_z_order(&args.element_id, z_order as usize, &request.action)?; - } - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Updated style for element `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn delete_element( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ElementIdArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - document.remove_element(&args.element_id, &request.action)?; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Deleted element `{}`", args.element_id), - snapshot_for_document(document), - )) - } - - fn bring_to_front( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ElementIdArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - let target_index = document.total_element_count(); - document.set_z_order(&args.element_id, target_index, &request.action)?; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Brought `{}` to front", args.element_id), - snapshot_for_document(document), - )) - } - - fn send_to_back( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let args: ElementIdArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let document = self.get_document_mut(&artifact_id, &request.action)?; - document.set_z_order(&args.element_id, 0, &request.action)?; - Ok(PresentationArtifactResponse::new( - artifact_id, - request.action, - format!("Sent `{}` to back", args.element_id), - snapshot_for_document(document), - )) - } - - fn delete_artifact( - &mut self, - request: PresentationArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let removed = self.documents.remove(&artifact_id).ok_or_else(|| { - PresentationArtifactError::UnknownArtifactId { - action: request.action.clone(), - artifact_id: artifact_id.clone(), - } - })?; - Ok(PresentationArtifactResponse { - artifact_id, - action: request.action, - summary: format!( - "Deleted in-memory artifact `{}` with {} slides", - removed.artifact_id, - removed.slides.len() - ), - executed_actions: None, - exported_paths: Vec::new(), - artifact_snapshot: None, - slide_list: None, - layout_list: None, - placeholder_list: None, - theme: None, - inspect_ndjson: None, - resolved_record: None, - proto_json: None, - patch: None, - active_slide_index: None, - }) - } - - fn get_document( - &self, - artifact_id: &str, - action: &str, - ) -> Result<&PresentationDocument, PresentationArtifactError> { - self.documents.get(artifact_id).ok_or_else(|| { - PresentationArtifactError::UnknownArtifactId { - action: action.to_string(), - artifact_id: artifact_id.to_string(), - } - }) - } - - fn get_document_mut( - &mut self, - artifact_id: &str, - action: &str, - ) -> Result<&mut PresentationDocument, PresentationArtifactError> { - self.documents.get_mut(artifact_id).ok_or_else(|| { - PresentationArtifactError::UnknownArtifactId { - action: action.to_string(), - artifact_id: artifact_id.to_string(), - } - }) - } - - fn normalize_patch( - &self, - request_artifact_id: Option<&str>, - operations: Vec, - action: &str, - ) -> Result { - if operations.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`operations` must contain at least one entry".to_string(), - }); - } - let mut patch_artifact_id = request_artifact_id.map(str::to_owned); - let mut normalized_operations = Vec::with_capacity(operations.len()); - for operation in operations { - let operation_artifact_id = operation - .artifact_id - .or_else(|| request_artifact_id.map(str::to_owned)) - .ok_or_else(|| PresentationArtifactError::MissingArtifactId { - action: action.to_string(), - })?; - if let Some(existing_artifact_id) = &patch_artifact_id { - if existing_artifact_id != &operation_artifact_id { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "patch operations must target a single artifact, found both `{existing_artifact_id}` and `{operation_artifact_id}`" - ), - }); - } - } else { - patch_artifact_id = Some(operation_artifact_id.clone()); - } - if !patch_operation_supported(&operation.action) { - return Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!( - "patch operations do not support nested action `{}`", - operation.action - ), - }); - } - normalized_operations.push(PatchOperation { - action: operation.action, - args: operation.args, - }); - } - let artifact_id = - patch_artifact_id.ok_or_else(|| PresentationArtifactError::MissingArtifactId { - action: action.to_string(), - })?; - self.get_document(&artifact_id, action)?; - Ok(PresentationPatch { - version: 1, - artifact_id, - operations: normalized_operations, - }) - } - - fn normalize_serialized_patch( - &self, - request_artifact_id: Option<&str>, - patch: PresentationPatch, - action: &str, - ) -> Result { - if patch.version != 1 { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("unsupported patch version `{}`", patch.version), - }); - } - if patch.operations.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`patch.operations` must contain at least one entry".to_string(), - }); - } - if let Some(request_artifact_id) = request_artifact_id - && request_artifact_id != patch.artifact_id - { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "request artifact `{request_artifact_id}` does not match patch artifact `{}`", - patch.artifact_id - ), - }); - } - for operation in &patch.operations { - if !patch_operation_supported(&operation.action) { - return Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!( - "patch operations do not support nested action `{}`", - operation.action - ), - }); - } - } - self.get_document(&patch.artifact_id, action)?; - Ok(patch) - } -} - -fn lookup_text_target<'a>( - slide: &'a PresentationSlide, - element_id: &str, - _row: Option, - _column: Option, - action: &str, -) -> Result<(&'a str, Option<&'a RichTextState>), PresentationArtifactError> { - let normalized_element_id = normalize_element_lookup_id(element_id); - let element = slide - .elements - .iter() - .find(|element| element.element_id() == normalized_element_id) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown element `{element_id}` on slide `{}`", slide.slide_id), - })?; - match element { - PresentationElement::Text(text) => Ok((&text.text, Some(&text.rich_text))), - PresentationElement::Shape(shape) => shape - .text - .as_deref() - .map(|text| (text, shape.rich_text.as_ref())) - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("element `{element_id}` does not contain editable text"), - }), - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!( - "element `{element_id}` does not support text-range comments" - ), - }), - } -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/mod.rs b/codex-rs/artifact-presentation/src/presentation_artifact/mod.rs deleted file mode 100644 index 8497062a9..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -include!("api.rs"); -include!("manager.rs"); -include!("response.rs"); -include!("model.rs"); -include!("args.rs"); -include!("parsing.rs"); -include!("proto.rs"); -include!("inspect.rs"); -include!("pptx.rs"); -include!("snapshot.rs"); diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/model.rs b/codex-rs/artifact-presentation/src/presentation_artifact/model.rs deleted file mode 100644 index b5cd5d00a..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/model.rs +++ /dev/null @@ -1,1934 +0,0 @@ -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct ThemeState { - color_scheme: HashMap, - major_font: Option, - minor_font: Option, -} - -impl ThemeState { - fn resolve_color(&self, color: &str) -> Option { - let key = color.trim().to_ascii_lowercase(); - let alias = match key.as_str() { - "background1" => "bg1", - "background2" => "bg2", - "text1" => "tx1", - "text2" => "tx2", - "dark1" => "dk1", - "dark2" => "dk2", - "light1" => "lt1", - "light2" => "lt2", - other => other, - }; - self.color_scheme - .get(alias) - .or_else(|| self.color_scheme.get(&key)) - .cloned() - .map(|value| value.trim_start_matches('#').to_uppercase()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum LayoutKind { - Layout, - Master, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct LayoutDocument { - layout_id: String, - name: String, - kind: LayoutKind, - parent_layout_id: Option, - placeholders: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PlaceholderDefinition { - name: String, - placeholder_type: String, - index: Option, - text: Option, - geometry: ShapeGeometry, - frame: Rect, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ResolvedPlaceholder { - source_layout_id: String, - definition: PlaceholderDefinition, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct NotesState { - text: String, - visible: bool, - #[serde(default)] - rich_text: RichTextState, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct TextStyle { - style_name: Option, - font_size: Option, - font_family: Option, - color: Option, - alignment: Option, - bold: bool, - italic: bool, - underline: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct NamedTextStyle { - name: String, - style: TextStyle, - built_in: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct HyperlinkState { - target: HyperlinkTarget, - tooltip: Option, - highlight_click: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -enum HyperlinkTarget { - Url(String), - Slide(u32), - FirstSlide, - LastSlide, - NextSlide, - PreviousSlide, - EndShow, - Email { - address: String, - subject: Option, - }, - File(String), -} - -impl HyperlinkTarget { - fn relationship_target(&self) -> String { - match self { - Self::Url(url) => url.clone(), - Self::Slide(slide_index) => format!("slide{}.xml", slide_index + 1), - Self::FirstSlide => "ppaction://hlinkshowjump?jump=firstslide".to_string(), - Self::LastSlide => "ppaction://hlinkshowjump?jump=lastslide".to_string(), - Self::NextSlide => "ppaction://hlinkshowjump?jump=nextslide".to_string(), - Self::PreviousSlide => "ppaction://hlinkshowjump?jump=previousslide".to_string(), - Self::EndShow => "ppaction://hlinkshowjump?jump=endshow".to_string(), - Self::Email { address, subject } => { - let mut mailto = format!("mailto:{address}"); - if let Some(subject) = subject { - mailto.push_str(&format!("?subject={subject}")); - } - mailto - } - Self::File(path) => format!("file:///{}", path.replace('\\', "/")), - } - } - - fn is_external(&self) -> bool { - matches!(self, Self::Url(_) | Self::Email { .. } | Self::File(_)) - } -} - -impl HyperlinkState { - fn to_ppt_rs(&self, relationship_id: &str) -> PptHyperlink { - let hyperlink = match &self.target { - HyperlinkTarget::Url(url) => PptHyperlink::new(PptHyperlinkAction::url(url)), - HyperlinkTarget::Slide(slide_index) => { - PptHyperlink::new(PptHyperlinkAction::slide(slide_index + 1)) - } - HyperlinkTarget::FirstSlide => PptHyperlink::new(PptHyperlinkAction::FirstSlide), - HyperlinkTarget::LastSlide => PptHyperlink::new(PptHyperlinkAction::LastSlide), - HyperlinkTarget::NextSlide => PptHyperlink::new(PptHyperlinkAction::NextSlide), - HyperlinkTarget::PreviousSlide => PptHyperlink::new(PptHyperlinkAction::PreviousSlide), - HyperlinkTarget::EndShow => PptHyperlink::new(PptHyperlinkAction::EndShow), - HyperlinkTarget::Email { address, subject } => PptHyperlink::new(match subject { - Some(subject) => PptHyperlinkAction::email_with_subject(address, subject), - None => PptHyperlinkAction::email(address), - }), - HyperlinkTarget::File(path) => PptHyperlink::new(PptHyperlinkAction::file(path)), - }; - let hyperlink = if let Some(tooltip) = &self.tooltip { - hyperlink.with_tooltip(tooltip) - } else { - hyperlink - }; - hyperlink - .with_highlight_click(self.highlight_click) - .with_r_id(relationship_id) - } - - fn to_json(&self) -> Value { - let mut record = match &self.target { - HyperlinkTarget::Url(url) => serde_json::json!({ - "type": "url", - "url": url, - }), - HyperlinkTarget::Slide(slide_index) => serde_json::json!({ - "type": "slide", - "slideIndex": slide_index, - }), - HyperlinkTarget::FirstSlide => serde_json::json!({ - "type": "firstSlide", - }), - HyperlinkTarget::LastSlide => serde_json::json!({ - "type": "lastSlide", - }), - HyperlinkTarget::NextSlide => serde_json::json!({ - "type": "nextSlide", - }), - HyperlinkTarget::PreviousSlide => serde_json::json!({ - "type": "previousSlide", - }), - HyperlinkTarget::EndShow => serde_json::json!({ - "type": "endShow", - }), - HyperlinkTarget::Email { address, subject } => serde_json::json!({ - "type": "email", - "address": address, - "subject": subject, - }), - HyperlinkTarget::File(path) => serde_json::json!({ - "type": "file", - "path": path, - }), - }; - record["tooltip"] = self - .tooltip - .as_ref() - .map(|tooltip| Value::String(tooltip.clone())) - .unwrap_or(Value::Null); - record["highlightClick"] = Value::Bool(self.highlight_click); - record - } - - fn relationship_xml(&self, relationship_id: &str) -> String { - let target_mode = if self.target.is_external() { - r#" TargetMode="External""# - } else { - "" - }; - format!( - r#""#, - ppt_rs::escape_xml(&self.target.relationship_target()), - ) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -enum TextAlignment { - Left, - Center, - Right, - Justify, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct RichTextState { - #[serde(default)] - ranges: Vec, - #[serde(default)] - layout: TextLayoutState, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TextRangeAnnotation { - range_id: String, - start_cp: usize, - length: usize, - style: TextStyle, - hyperlink: Option, - spacing_before: Option, - spacing_after: Option, - line_spacing: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct TextLayoutState { - insets: Option, - wrap: Option, - auto_fit: Option, - vertical_alignment: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -struct TextInsets { - left: u32, - right: u32, - top: u32, - bottom: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum TextWrapMode { - Square, - None, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum TextAutoFitMode { - None, - ShrinkText, - ResizeShapeToFitText, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum TextVerticalAlignment { - Top, - Middle, - Bottom, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CommentAuthorProfile { - display_name: String, - initials: String, - email: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CommentPosition { - x: u32, - y: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum CommentThreadStatus { - Active, - Resolved, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CommentMessage { - message_id: String, - author: CommentAuthorProfile, - text: String, - created_at: String, - #[serde(default)] - reactions: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -enum CommentTarget { - Slide { - slide_id: String, - }, - Element { - slide_id: String, - element_id: String, - }, - TextRange { - slide_id: String, - element_id: String, - start_cp: usize, - length: usize, - context: Option, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CommentThread { - thread_id: String, - target: CommentTarget, - position: Option, - status: CommentThreadStatus, - messages: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TableBorder { - color: String, - width: u32, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct TableBorders { - outside: Option, - inside: Option, - top: Option, - bottom: Option, - left: Option, - right: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct TableStyleOptions { - header_row: bool, - banded_rows: bool, - banded_columns: bool, - first_column: bool, - last_column: bool, - total_row: bool, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct ChartMarkerStyle { - symbol: Option, - size: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct ChartDataLabels { - show_value: bool, - show_category_name: bool, - show_leader_lines: bool, - position: Option, - text_style: TextStyle, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct ChartLegend { - position: Option, - text_style: TextStyle, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct ChartAxisSpec { - title: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct ChartDataLabelOverride { - idx: usize, - text: Option, - position: Option, - text_style: TextStyle, - fill: Option, - stroke: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct PlaceholderRef { - name: String, - placeholder_type: String, - index: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TableMergeRegion { - start_row: usize, - end_row: usize, - start_column: usize, - end_column: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TableCellSpec { - text: String, - text_style: TextStyle, - background_fill: Option, - alignment: Option, - #[serde(default)] - rich_text: RichTextState, - borders: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PresentationDocument { - artifact_id: String, - name: Option, - slide_size: Rect, - theme: ThemeState, - custom_text_styles: HashMap, - layouts: Vec, - slides: Vec, - active_slide_index: Option, - #[serde(default)] - comment_self: Option, - #[serde(default)] - comment_threads: Vec, - next_slide_seq: u32, - next_element_seq: u32, - next_layout_seq: u32, - next_text_range_seq: u32, - next_comment_thread_seq: u32, - next_comment_message_seq: u32, -} - -impl PresentationDocument { - fn new(name: Option) -> Self { - Self { - artifact_id: format!("presentation_{}", Uuid::new_v4().simple()), - name, - slide_size: Rect { - left: 0, - top: 0, - width: DEFAULT_SLIDE_WIDTH_POINTS, - height: DEFAULT_SLIDE_HEIGHT_POINTS, - }, - theme: ThemeState::default(), - custom_text_styles: HashMap::new(), - layouts: Vec::new(), - slides: Vec::new(), - active_slide_index: None, - comment_self: None, - comment_threads: Vec::new(), - next_slide_seq: 1, - next_element_seq: 1, - next_layout_seq: 1, - next_text_range_seq: 1, - next_comment_thread_seq: 1, - next_comment_message_seq: 1, - } - } - - fn from_ppt_rs(presentation: Presentation) -> Self { - let mut document = Self::new( - (!presentation.get_title().is_empty()).then(|| presentation.get_title().to_string()), - ); - for imported_slide in presentation.slides() { - let mut slide = PresentationSlide { - slide_id: format!("slide_{}", document.next_slide_seq), - notes: NotesState { - text: imported_slide.notes.clone().unwrap_or_default(), - visible: true, - rich_text: RichTextState::default(), - }, - background_fill: None, - layout_id: None, - elements: Vec::new(), - }; - document.next_slide_seq += 1; - - if !imported_slide.title.is_empty() { - slide.elements.push(PresentationElement::Text(TextElement { - element_id: document.next_element_id(), - text: imported_slide.title.clone(), - frame: Rect { - left: DEFAULT_IMPORTED_TITLE_LEFT, - top: DEFAULT_IMPORTED_TITLE_TOP, - width: DEFAULT_IMPORTED_TITLE_WIDTH, - height: DEFAULT_IMPORTED_TITLE_HEIGHT, - }, - fill: None, - style: TextStyle::default(), - hyperlink: None, - rich_text: RichTextState::default(), - placeholder: None, - z_order: slide.elements.len(), - })); - } - - if !imported_slide.content.is_empty() { - slide.elements.push(PresentationElement::Text(TextElement { - element_id: document.next_element_id(), - text: imported_slide.content.join("\n"), - frame: Rect { - left: DEFAULT_IMPORTED_CONTENT_LEFT, - top: DEFAULT_IMPORTED_CONTENT_TOP, - width: DEFAULT_IMPORTED_CONTENT_WIDTH, - height: DEFAULT_IMPORTED_CONTENT_HEIGHT, - }, - fill: None, - style: TextStyle::default(), - hyperlink: None, - rich_text: RichTextState::default(), - placeholder: None, - z_order: slide.elements.len(), - })); - } - - for imported_shape in &imported_slide.shapes { - slide - .elements - .push(PresentationElement::Shape(ShapeElement { - element_id: document.next_element_id(), - geometry: ShapeGeometry::from_shape_type(imported_shape.shape_type), - frame: Rect::from_emu( - imported_shape.x, - imported_shape.y, - imported_shape.width, - imported_shape.height, - ), - fill: imported_shape.fill.as_ref().map(|fill| fill.color.clone()), - stroke: imported_shape.line.as_ref().map(|line| StrokeStyle { - color: line.color.clone(), - width: emu_to_points(line.width), - style: LineStyle::Solid, - }), - text: imported_shape.text.clone(), - text_style: TextStyle::default(), - hyperlink: None, - rich_text: imported_shape - .text - .as_ref() - .map(|_| RichTextState::default()), - placeholder: None, - rotation_degrees: imported_shape.rotation, - flip_horizontal: false, - flip_vertical: false, - z_order: slide.elements.len(), - })); - } - - if let Some(imported_table) = &imported_slide.table { - slide - .elements - .push(PresentationElement::Table(TableElement { - element_id: document.next_element_id(), - frame: Rect::from_emu( - imported_table.x, - imported_table.y, - imported_table.width(), - imported_table.height(), - ), - rows: imported_table - .rows - .iter() - .map(|row| { - row.cells - .iter() - .map(|text| TableCellSpec { - text: text.text.clone(), - text_style: TextStyle::default(), - background_fill: None, - alignment: None, - rich_text: RichTextState::default(), - borders: None, - }) - .collect() - }) - .collect(), - column_widths: imported_table - .column_widths - .iter() - .copied() - .map(emu_to_points) - .collect(), - row_heights: imported_table - .rows - .iter() - .map(|row| row.height.map_or(400_000, |height| height)) - .map(emu_to_points) - .collect(), - style: None, - style_options: TableStyleOptions::default(), - borders: None, - right_to_left: false, - merges: Vec::new(), - z_order: slide.elements.len(), - })); - } - - document.slides.push(slide); - } - document.active_slide_index = (!document.slides.is_empty()).then_some(0); - document - } - - fn new_slide( - &mut self, - notes: Option, - background_fill: Option, - action: &str, - ) -> Result { - let normalized_fill = background_fill - .map(|value| { - normalize_color_with_palette(Some(&self.theme), &value, action, "background_fill") - }) - .transpose()?; - let slide = PresentationSlide { - slide_id: format!("slide_{}", self.next_slide_seq), - notes: NotesState { - text: notes.unwrap_or_default(), - visible: true, - rich_text: RichTextState::default(), - }, - background_fill: normalized_fill, - layout_id: None, - elements: Vec::new(), - }; - self.next_slide_seq += 1; - Ok(slide) - } - - fn append_slide(&mut self, slide: PresentationSlide) -> usize { - let index = self.slides.len(); - self.slides.push(slide); - if self.active_slide_index.is_none() { - self.active_slide_index = Some(index); - } - index - } - - fn clone_slide(&mut self, slide: PresentationSlide) -> PresentationSlide { - let mut clone = slide; - clone.slide_id = format!("slide_{}", self.next_slide_seq); - self.next_slide_seq += 1; - for element in &mut clone.elements { - element.set_element_id(self.next_element_id()); - } - clone - } - - fn next_element_id(&mut self) -> String { - let element_id = format!("element_{}", self.next_element_seq); - self.next_element_seq += 1; - element_id - } - - fn next_text_range_id(&mut self) -> String { - let range_id = format!("range_{}", self.next_text_range_seq); - self.next_text_range_seq += 1; - range_id - } - - fn next_comment_thread_id(&mut self) -> String { - let thread_id = format!("thread_{}", self.next_comment_thread_seq); - self.next_comment_thread_seq += 1; - thread_id - } - - fn next_comment_message_id(&mut self) -> String { - let message_id = format!("message_{}", self.next_comment_message_seq); - self.next_comment_message_seq += 1; - message_id - } - - fn total_element_count(&self) -> usize { - self.slides.iter().map(|slide| slide.elements.len()).sum() - } - - fn set_active_slide_index( - &mut self, - slide_index: usize, - action: &str, - ) -> Result<(), PresentationArtifactError> { - if slide_index >= self.slides.len() { - return Err(index_out_of_range(action, slide_index, self.slides.len())); - } - self.active_slide_index = Some(slide_index); - Ok(()) - } - - fn adjust_active_slide_for_insert(&mut self, inserted_index: usize) { - match self.active_slide_index { - None => self.active_slide_index = Some(inserted_index), - Some(active_index) if inserted_index <= active_index => { - self.active_slide_index = Some(active_index + 1); - } - Some(_) => {} - } - } - - fn adjust_active_slide_for_move(&mut self, from_index: usize, to_index: usize) { - if let Some(active_index) = self.active_slide_index { - self.active_slide_index = Some(if active_index == from_index { - to_index - } else if from_index < active_index && active_index <= to_index { - active_index - 1 - } else if to_index <= active_index && active_index < from_index { - active_index + 1 - } else { - active_index - }); - } - } - - fn adjust_active_slide_for_delete(&mut self, deleted_index: usize) { - self.active_slide_index = match self.active_slide_index { - None => None, - Some(_) if self.slides.is_empty() => None, - Some(active_index) if active_index == deleted_index => { - Some(deleted_index.min(self.slides.len() - 1)) - } - Some(active_index) if deleted_index < active_index => Some(active_index - 1), - Some(active_index) => Some(active_index), - }; - } - - fn next_layout_id(&mut self) -> String { - let layout_id = format!("layout_{}", self.next_layout_seq); - self.next_layout_seq += 1; - layout_id - } - - fn get_layout( - &self, - layout_ref: &str, - action: &str, - ) -> Result<&LayoutDocument, PresentationArtifactError> { - if let Some(layout) = self - .layouts - .iter() - .find(|layout| layout.layout_id == layout_ref) - { - return Ok(layout); - } - - let exact_name_matches = self - .layouts - .iter() - .filter(|layout| layout.name == layout_ref) - .collect::>(); - if exact_name_matches.len() > 1 { - return Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("layout name `{layout_ref}` is ambiguous"), - }); - } - if let Some(layout) = exact_name_matches.into_iter().next() { - return Ok(layout); - } - - let case_insensitive_name_matches = self - .layouts - .iter() - .filter(|layout| layout.name.eq_ignore_ascii_case(layout_ref)) - .collect::>(); - if case_insensitive_name_matches.len() > 1 { - return Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("layout name `{layout_ref}` is ambiguous"), - }); - } - case_insensitive_name_matches - .into_iter() - .next() - .ok_or_else(|| PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown layout id or name `{layout_ref}`"), - }) - } - - fn theme_snapshot(&self) -> ThemeSnapshot { - ThemeSnapshot { - color_scheme: self.theme.color_scheme.clone(), - hex_color_map: self.theme.color_scheme.clone(), - major_font: self.theme.major_font.clone(), - minor_font: self.theme.minor_font.clone(), - } - } - - fn named_text_styles(&self) -> Vec { - let mut styles = built_in_text_styles(&self.theme) - .into_iter() - .map(|(name, style)| NamedTextStyle { - name, - style, - built_in: true, - }) - .collect::>(); - styles.extend( - self.custom_text_styles - .iter() - .map(|(name, style)| NamedTextStyle { - name: name.clone(), - style: style.clone(), - built_in: false, - }), - ); - styles.sort_by_cached_key(|style| style.name.to_ascii_lowercase()); - styles - } - - fn resolve_named_text_style( - &self, - style_name: &str, - action: &str, - ) -> Result { - let normalized_style_name = style_name.trim().to_ascii_lowercase(); - if let Some(style) = self.custom_text_styles.get(&normalized_style_name) { - return Ok(style.clone()); - } - built_in_text_style(&self.theme, &normalized_style_name).ok_or_else(|| { - PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown text style `{style_name}`"), - } - }) - } - - fn find_element_mut( - &mut self, - element_id: &str, - action: &str, - ) -> Result<&mut PresentationElement, PresentationArtifactError> { - let element_id = normalize_element_lookup_id(element_id); - for slide in &mut self.slides { - if let Some(element) = slide - .elements - .iter_mut() - .find(|element| element.element_id() == element_id) - { - return Ok(element); - } - } - Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown element id `{element_id}`"), - }) - } - - fn get_slide_mut( - &mut self, - slide_index: u32, - action: &str, - ) -> Result<&mut PresentationSlide, PresentationArtifactError> { - let index = slide_index as usize; - if index >= self.slides.len() { - return Err(index_out_of_range(action, index, self.slides.len())); - } - Ok(&mut self.slides[index]) - } - - fn remove_element( - &mut self, - element_id: &str, - action: &str, - ) -> Result<(), PresentationArtifactError> { - let element_id = normalize_element_lookup_id(element_id); - for slide in &mut self.slides { - if let Some(index) = slide - .elements - .iter() - .position(|element| element.element_id() == element_id) - { - slide.elements.remove(index); - resequence_z_order(slide); - return Ok(()); - } - } - Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown element id `{element_id}`"), - }) - } - - fn set_z_order( - &mut self, - element_id: &str, - target_index: usize, - action: &str, - ) -> Result<(), PresentationArtifactError> { - let element_id = normalize_element_lookup_id(element_id); - for slide in &mut self.slides { - if let Some(current_index) = slide - .elements - .iter() - .position(|element| element.element_id() == element_id) - { - let destination = target_index.min(slide.elements.len().saturating_sub(1)); - let element = slide.elements.remove(current_index); - slide.elements.insert(destination, element); - resequence_z_order(slide); - return Ok(()); - } - } - Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("unknown element id `{element_id}`"), - }) - } - - fn to_ppt_rs(&self) -> Presentation { - let mut presentation = self - .name - .as_deref() - .map(Presentation::with_title) - .unwrap_or_default(); - for slide in &self.slides { - presentation = presentation.add_slide(slide.to_ppt_rs(self.slide_size)); - } - presentation - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PresentationSlide { - slide_id: String, - notes: NotesState, - background_fill: Option, - layout_id: Option, - elements: Vec, -} - -struct ImportedPicture { - relationship_id: String, - frame: Rect, - crop: Option, - alt_text: Option, - rotation_degrees: Option, - flip_horizontal: bool, - flip_vertical: bool, - lock_aspect_ratio: bool, -} - -fn import_pptx_images( - path: &Path, - document: &mut PresentationDocument, - action: &str, -) -> Result<(), PresentationArtifactError> { - let file = - std::fs::File::open(path).map_err(|error| PresentationArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - let mut archive = - ZipArchive::new(file).map_err(|error| PresentationArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - for slide_index in 0..document.slides.len() { - let slide_number = slide_index + 1; - let slide_xml_path = format!("ppt/slides/slide{slide_number}.xml"); - let Some(slide_xml) = - zip_entry_string_if_exists(&mut archive, &slide_xml_path).map_err(|message| { - PresentationArtifactError::ImportFailed { - path: path.to_path_buf(), - message, - } - })? - else { - continue; - }; - let pictures = parse_imported_pictures(&slide_xml); - if pictures.is_empty() { - continue; - } - let relationships = zip_entry_string_if_exists( - &mut archive, - &format!("ppt/slides/_rels/slide{slide_number}.xml.rels"), - ) - .map_err(|message| PresentationArtifactError::ImportFailed { - path: path.to_path_buf(), - message, - })? - .map(|xml| parse_slide_image_relationship_targets(&xml)) - .unwrap_or_default(); - let mut imported_images = Vec::new(); - for picture in pictures { - let Some(target) = relationships.get(&picture.relationship_id) else { - continue; - }; - let media_path = resolve_zip_relative_path(&slide_xml_path, target); - let Some(bytes) = - zip_entry_bytes_if_exists(&mut archive, &media_path).map_err(|message| { - PresentationArtifactError::ImportFailed { - path: path.to_path_buf(), - message, - } - })? - else { - continue; - }; - let Some(filename) = Path::new(&media_path) - .file_name() - .and_then(|name| name.to_str()) - .map(str::to_owned) - else { - continue; - }; - let Ok(payload) = build_image_payload(bytes, filename, action) else { - continue; - }; - imported_images.push(ImageElement { - element_id: document.next_element_id(), - frame: picture.frame, - payload: Some(payload), - fit_mode: ImageFitMode::Stretch, - crop: picture.crop, - rotation_degrees: picture.rotation_degrees, - flip_horizontal: picture.flip_horizontal, - flip_vertical: picture.flip_vertical, - lock_aspect_ratio: picture.lock_aspect_ratio, - alt_text: picture.alt_text, - prompt: None, - is_placeholder: false, - placeholder: None, - z_order: 0, - }); - } - let slide = &mut document.slides[slide_index]; - for mut image in imported_images { - image.z_order = slide.elements.len(); - slide.elements.push(PresentationElement::Image(image)); - } - } - Ok(()) -} - -fn zip_entry_string_if_exists( - archive: &mut ZipArchive, - path: &str, -) -> Result, String> { - let Some(bytes) = zip_entry_bytes_if_exists(archive, path)? else { - return Ok(None); - }; - String::from_utf8(bytes) - .map(Some) - .map_err(|error| format!("zip entry `{path}` is not valid UTF-8: {error}")) -} - -fn zip_entry_bytes_if_exists( - archive: &mut ZipArchive, - path: &str, -) -> Result>, String> { - match archive.by_name(path) { - Ok(mut entry) => { - let mut bytes = Vec::new(); - entry - .read_to_end(&mut bytes) - .map_err(|error| format!("failed to read zip entry `{path}`: {error}"))?; - Ok(Some(bytes)) - } - Err(zip::result::ZipError::FileNotFound) => Ok(None), - Err(error) => Err(format!("failed to open zip entry `{path}`: {error}")), - } -} - -fn parse_imported_pictures(slide_xml: &str) -> Vec { - let mut pictures = Vec::new(); - let mut remaining = slide_xml; - while let Some(start) = remaining.find("") { - let block_start = start; - let Some(block_end_offset) = remaining[block_start..].find("") else { - break; - }; - let block_end = block_start + block_end_offset + "".len(); - let block = &remaining[block_start..block_end]; - remaining = &remaining[block_end..]; - - let Some(relationship_id) = xml_tag_attribute(block, "().unwrap_or(0.0) / 100_000.0, - xml_tag_attribute(block, "().ok()) - .unwrap_or(0.0) - / 100_000.0, - xml_tag_attribute(block, "().ok()) - .unwrap_or(0.0) - / 100_000.0, - xml_tag_attribute(block, "().ok()) - .unwrap_or(0.0) - / 100_000.0, - ) - }), - alt_text: xml_tag_attribute(block, "().ok()) - .map(|rotation| (rotation as f64 / 60_000.0).round() as i32), - flip_horizontal: xml_tag_attribute(block, " HashMap { - let mut relationships = HashMap::new(); - let mut remaining = rels_xml; - while let Some(start) = remaining.find("") else { - break; - }; - let tag_end = tag_start + tag_end_offset + 2; - let tag = &remaining[tag_start..tag_end]; - remaining = &remaining[tag_end..]; - if xml_attribute(tag, "Type").as_deref() - != Some("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") - { - continue; - } - let (Some(id), Some(target)) = (xml_attribute(tag, "Id"), xml_attribute(tag, "Target")) - else { - continue; - }; - relationships.insert(id, target); - } - relationships -} - -fn resolve_zip_relative_path(base_path: &str, target: &str) -> String { - let mut components = Path::new(base_path) - .parent() - .into_iter() - .flat_map(Path::components) - .filter_map(|component| match component { - std::path::Component::Normal(value) => Some(value.to_string_lossy().to_string()), - std::path::Component::CurDir => None, - std::path::Component::ParentDir => None, - std::path::Component::RootDir | std::path::Component::Prefix(_) => None, - }) - .collect::>(); - for component in Path::new(target).components() { - match component { - std::path::Component::Normal(value) => { - components.push(value.to_string_lossy().to_string()) - } - std::path::Component::ParentDir => { - components.pop(); - } - std::path::Component::CurDir => {} - std::path::Component::RootDir | std::path::Component::Prefix(_) => { - components.clear(); - } - } - } - components.join("/") -} - -fn xml_tag_attribute(xml: &str, tag_start: &str, attribute: &str) -> Option { - let start = xml.find(tag_start)?; - let tag = &xml[start..start + xml[start..].find('>')?]; - xml_attribute(tag, attribute) -} - -fn xml_attribute(tag: &str, attribute: &str) -> Option { - let pattern = format!(r#"{attribute}=""#); - let start = tag.find(&pattern)? + pattern.len(); - let end = start + tag[start..].find('"')?; - Some( - tag[start..end] - .replace(""", "\"") - .replace("'", "'") - .replace("<", "<") - .replace(">", ">") - .replace("&", "&"), - ) -} - -impl PresentationSlide { - fn to_ppt_rs(&self, slide_size: Rect) -> SlideContent { - let mut content = SlideContent::new("").layout(SlideLayout::Blank); - if self.notes.visible && !self.notes.text.is_empty() { - content = content.notes(&self.notes.text); - } - - if let Some(background_fill) = &self.background_fill { - content = content.add_shape( - Shape::new( - ShapeType::Rectangle, - 0, - 0, - points_to_emu(slide_size.width), - points_to_emu(slide_size.height), - ) - .with_fill(ShapeFill::new(background_fill)), - ); - } - - let mut ordered = self.elements.clone(); - ordered.sort_by_key(PresentationElement::z_order); - let mut hyperlink_seq = 1_u32; - for element in ordered { - match element { - PresentationElement::Text(text) => { - let mut shape = Shape::new( - ShapeType::Rectangle, - points_to_emu(text.frame.left), - points_to_emu(text.frame.top), - points_to_emu(text.frame.width), - points_to_emu(text.frame.height), - ) - .with_text(&text.text); - if let Some(fill) = text.fill { - shape = shape.with_fill(ShapeFill::new(&fill)); - } - if let Some(hyperlink) = &text.hyperlink { - let relationship_id = format!("rIdHyperlink{hyperlink_seq}"); - hyperlink_seq += 1; - shape = shape.with_hyperlink(hyperlink.to_ppt_rs(&relationship_id)); - } - content = content.add_shape(shape); - } - PresentationElement::Shape(shape) => { - let mut ppt_shape = Shape::new( - shape.geometry.to_ppt_rs(), - points_to_emu(shape.frame.left), - points_to_emu(shape.frame.top), - points_to_emu(shape.frame.width), - points_to_emu(shape.frame.height), - ); - if let Some(text) = shape.text { - ppt_shape = ppt_shape.with_text(&text); - } - if let Some(fill) = shape.fill { - ppt_shape = ppt_shape.with_fill(ShapeFill::new(&fill)); - } - if let Some(stroke) = shape.stroke { - ppt_shape = ppt_shape - .with_line(ShapeLine::new(&stroke.color, points_to_emu(stroke.width))); - } - if let Some(rotation) = shape.rotation_degrees { - ppt_shape = ppt_shape.with_rotation(rotation); - } - if let Some(hyperlink) = &shape.hyperlink { - let relationship_id = format!("rIdHyperlink{hyperlink_seq}"); - hyperlink_seq += 1; - ppt_shape = ppt_shape.with_hyperlink(hyperlink.to_ppt_rs(&relationship_id)); - } - content = content.add_shape(ppt_shape); - } - PresentationElement::Connector(connector) => { - let mut ppt_connector = Connector::new( - connector.connector_type.to_ppt_rs(), - points_to_emu(connector.start.left), - points_to_emu(connector.start.top), - points_to_emu(connector.end.left), - points_to_emu(connector.end.top), - ) - .with_line( - ConnectorLine::new( - &connector.line.color, - points_to_emu(connector.line.width), - ) - .with_dash(connector.line_style.to_ppt_rs()), - ) - .with_arrow_size(connector.arrow_size.to_ppt_rs()) - .with_start_arrow(connector.start_arrow.to_ppt_rs()) - .with_end_arrow(connector.end_arrow.to_ppt_rs()); - if let Some(label) = connector.label { - ppt_connector = ppt_connector.with_label(&label); - } - content = content.add_connector(ppt_connector); - } - PresentationElement::Image(image) => { - if let Some(ref payload) = image.payload { - let mut ppt_image = Image::from_bytes( - payload.bytes.clone(), - points_to_emu(image.frame.width), - points_to_emu(image.frame.height), - &payload.format, - ) - .position( - points_to_emu(image.frame.left), - points_to_emu(image.frame.top), - ); - if image.fit_mode != ImageFitMode::Stretch { - let (x, y, width, height, crop) = fit_image(&image); - ppt_image = Image::from_bytes( - payload.bytes.clone(), - points_to_emu(width), - points_to_emu(height), - &payload.format, - ) - .position(points_to_emu(x), points_to_emu(y)); - if let Some((left, top, right, bottom)) = crop { - ppt_image = ppt_image.with_crop(left, top, right, bottom); - } - } - if let Some((left, top, right, bottom)) = image.crop { - ppt_image = ppt_image.with_crop(left, top, right, bottom); - } - content = content.add_image(ppt_image); - } else { - let mut placeholder = Shape::new( - ShapeType::Rectangle, - points_to_emu(image.frame.left), - points_to_emu(image.frame.top), - points_to_emu(image.frame.width), - points_to_emu(image.frame.height), - ) - .with_text(image.prompt.as_deref().unwrap_or("Image placeholder")); - if let Some(rotation) = image.rotation_degrees { - placeholder = placeholder.with_rotation(rotation); - } - content = content.add_shape(placeholder); - } - } - PresentationElement::Table(table) => { - let mut builder = TableBuilder::new( - table - .column_widths - .iter() - .copied() - .map(points_to_emu) - .collect(), - ) - .position( - points_to_emu(table.frame.left), - points_to_emu(table.frame.top), - ); - for (row_index, row) in table.rows.into_iter().enumerate() { - let cells = row - .into_iter() - .enumerate() - .map(|(column_index, cell)| { - build_table_cell(cell, &table.merges, row_index, column_index) - }) - .collect::>(); - let mut table_row = TableRow::new(cells); - if let Some(height) = table.row_heights.get(row_index) { - table_row = table_row.with_height(points_to_emu(*height)); - } - builder = builder.add_row(table_row); - } - content = content.table(builder.build()); - } - PresentationElement::Chart(chart) => { - let mut ppt_chart = Chart::new( - chart.title.as_deref().unwrap_or("Chart"), - chart.chart_type.to_ppt_rs(), - chart.categories, - points_to_emu(chart.frame.left), - points_to_emu(chart.frame.top), - points_to_emu(chart.frame.width), - points_to_emu(chart.frame.height), - ); - for series in chart.series { - ppt_chart = - ppt_chart.add_series(ChartSeries::new(&series.name, series.values)); - } - content = content.add_chart(ppt_chart); - } - } - } - content - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -enum PresentationElement { - Text(TextElement), - Shape(ShapeElement), - Connector(ConnectorElement), - Image(ImageElement), - Table(TableElement), - Chart(ChartElement), -} - -impl PresentationElement { - fn element_id(&self) -> &str { - match self { - Self::Text(element) => &element.element_id, - Self::Shape(element) => &element.element_id, - Self::Connector(element) => &element.element_id, - Self::Image(element) => &element.element_id, - Self::Table(element) => &element.element_id, - Self::Chart(element) => &element.element_id, - } - } - - fn set_element_id(&mut self, new_id: String) { - match self { - Self::Text(element) => element.element_id = new_id, - Self::Shape(element) => element.element_id = new_id, - Self::Connector(element) => element.element_id = new_id, - Self::Image(element) => element.element_id = new_id, - Self::Table(element) => element.element_id = new_id, - Self::Chart(element) => element.element_id = new_id, - } - } - - fn kind(&self) -> &'static str { - match self { - Self::Text(_) => "text", - Self::Shape(_) => "shape", - Self::Connector(_) => "connector", - Self::Image(_) => "image", - Self::Table(_) => "table", - Self::Chart(_) => "chart", - } - } - - fn z_order(&self) -> usize { - match self { - Self::Text(element) => element.z_order, - Self::Shape(element) => element.z_order, - Self::Connector(element) => element.z_order, - Self::Image(element) => element.z_order, - Self::Table(element) => element.z_order, - Self::Chart(element) => element.z_order, - } - } - - fn set_z_order(&mut self, z_order: usize) { - match self { - Self::Text(element) => element.z_order = z_order, - Self::Shape(element) => element.z_order = z_order, - Self::Connector(element) => element.z_order = z_order, - Self::Image(element) => element.z_order = z_order, - Self::Table(element) => element.z_order = z_order, - Self::Chart(element) => element.z_order = z_order, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TextElement { - element_id: String, - text: String, - frame: Rect, - fill: Option, - style: TextStyle, - hyperlink: Option, - #[serde(default)] - rich_text: RichTextState, - placeholder: Option, - z_order: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ShapeElement { - element_id: String, - geometry: ShapeGeometry, - frame: Rect, - fill: Option, - stroke: Option, - text: Option, - text_style: TextStyle, - hyperlink: Option, - rich_text: Option, - placeholder: Option, - rotation_degrees: Option, - flip_horizontal: bool, - flip_vertical: bool, - z_order: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ConnectorElement { - element_id: String, - connector_type: ConnectorKind, - start: PointArgs, - end: PointArgs, - line: StrokeStyle, - line_style: LineStyle, - start_arrow: ConnectorArrowKind, - end_arrow: ConnectorArrowKind, - arrow_size: ConnectorArrowScale, - label: Option, - z_order: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct ImageElement { - pub(crate) element_id: String, - pub(crate) frame: Rect, - pub(crate) payload: Option, - pub(crate) fit_mode: ImageFitMode, - pub(crate) crop: Option, - pub(crate) rotation_degrees: Option, - pub(crate) flip_horizontal: bool, - pub(crate) flip_vertical: bool, - pub(crate) lock_aspect_ratio: bool, - pub(crate) alt_text: Option, - pub(crate) prompt: Option, - pub(crate) is_placeholder: bool, - pub(crate) placeholder: Option, - pub(crate) z_order: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TableElement { - element_id: String, - frame: Rect, - rows: Vec>, - column_widths: Vec, - row_heights: Vec, - style: Option, - style_options: TableStyleOptions, - borders: Option, - right_to_left: bool, - merges: Vec, - z_order: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ChartElement { - element_id: String, - frame: Rect, - chart_type: ChartTypeSpec, - categories: Vec, - series: Vec, - title: Option, - style_index: Option, - has_legend: bool, - legend: Option, - x_axis: Option, - y_axis: Option, - data_labels: Option, - chart_fill: Option, - plot_area_fill: Option, - z_order: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct ImagePayload { - pub(crate) bytes: Vec, - pub(crate) format: String, - pub(crate) width_px: u32, - pub(crate) height_px: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ChartSeriesSpec { - name: String, - values: Vec, - categories: Option>, - x_values: Option>, - fill: Option, - stroke: Option, - marker: Option, - #[serde(default)] - data_label_overrides: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum ShapeGeometry { - Rectangle, - RoundedRectangle, - Ellipse, - Triangle, - RightTriangle, - Diamond, - Pentagon, - Hexagon, - Octagon, - Star4, - Star5, - Star6, - Star8, - RightArrow, - LeftArrow, - UpArrow, - DownArrow, - LeftRightArrow, - UpDownArrow, - Chevron, - Heart, - Cloud, - Wave, - FlowChartProcess, - FlowChartDecision, - FlowChartConnector, - Parallelogram, - Trapezoid, -} - -impl ShapeGeometry { - fn from_shape_type(shape_type: ShapeType) -> Self { - match shape_type { - ShapeType::RoundedRectangle => Self::RoundedRectangle, - ShapeType::Ellipse | ShapeType::Circle => Self::Ellipse, - ShapeType::Triangle => Self::Triangle, - ShapeType::RightTriangle => Self::RightTriangle, - ShapeType::Diamond => Self::Diamond, - ShapeType::Pentagon => Self::Pentagon, - ShapeType::Hexagon => Self::Hexagon, - ShapeType::Octagon => Self::Octagon, - ShapeType::Star4 => Self::Star4, - ShapeType::Star5 => Self::Star5, - ShapeType::Star6 => Self::Star6, - ShapeType::Star8 => Self::Star8, - ShapeType::RightArrow => Self::RightArrow, - ShapeType::LeftArrow => Self::LeftArrow, - ShapeType::UpArrow => Self::UpArrow, - ShapeType::DownArrow => Self::DownArrow, - ShapeType::LeftRightArrow => Self::LeftRightArrow, - ShapeType::UpDownArrow => Self::UpDownArrow, - ShapeType::ChevronArrow => Self::Chevron, - ShapeType::Heart => Self::Heart, - ShapeType::Cloud => Self::Cloud, - ShapeType::Wave => Self::Wave, - ShapeType::FlowChartProcess => Self::FlowChartProcess, - ShapeType::FlowChartDecision => Self::FlowChartDecision, - ShapeType::FlowChartConnector => Self::FlowChartConnector, - ShapeType::Parallelogram => Self::Parallelogram, - ShapeType::Trapezoid => Self::Trapezoid, - _ => Self::Rectangle, - } - } - - fn to_ppt_rs(self) -> ShapeType { - match self { - Self::Rectangle => ShapeType::Rectangle, - Self::RoundedRectangle => ShapeType::RoundedRectangle, - Self::Ellipse => ShapeType::Ellipse, - Self::Triangle => ShapeType::Triangle, - Self::RightTriangle => ShapeType::RightTriangle, - Self::Diamond => ShapeType::Diamond, - Self::Pentagon => ShapeType::Pentagon, - Self::Hexagon => ShapeType::Hexagon, - Self::Octagon => ShapeType::Octagon, - Self::Star4 => ShapeType::Star4, - Self::Star5 => ShapeType::Star5, - Self::Star6 => ShapeType::Star6, - Self::Star8 => ShapeType::Star8, - Self::RightArrow => ShapeType::RightArrow, - Self::LeftArrow => ShapeType::LeftArrow, - Self::UpArrow => ShapeType::UpArrow, - Self::DownArrow => ShapeType::DownArrow, - Self::LeftRightArrow => ShapeType::LeftRightArrow, - Self::UpDownArrow => ShapeType::UpDownArrow, - Self::Chevron => ShapeType::ChevronArrow, - Self::Heart => ShapeType::Heart, - Self::Cloud => ShapeType::Cloud, - Self::Wave => ShapeType::Wave, - Self::FlowChartProcess => ShapeType::FlowChartProcess, - Self::FlowChartDecision => ShapeType::FlowChartDecision, - Self::FlowChartConnector => ShapeType::FlowChartConnector, - Self::Parallelogram => ShapeType::Parallelogram, - Self::Trapezoid => ShapeType::Trapezoid, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum ChartTypeSpec { - Bar, - BarHorizontal, - BarStacked, - BarStacked100, - Line, - LineMarkers, - LineStacked, - Pie, - Doughnut, - Area, - AreaStacked, - AreaStacked100, - Scatter, - ScatterLines, - ScatterSmooth, - Bubble, - Radar, - RadarFilled, - StockHlc, - StockOhlc, - Combo, -} - -impl ChartTypeSpec { - fn to_ppt_rs(self) -> ChartType { - match self { - Self::Bar => ChartType::Bar, - Self::BarHorizontal => ChartType::BarHorizontal, - Self::BarStacked => ChartType::BarStacked, - Self::BarStacked100 => ChartType::BarStacked100, - Self::Line => ChartType::Line, - Self::LineMarkers => ChartType::LineMarkers, - Self::LineStacked => ChartType::LineStacked, - Self::Pie => ChartType::Pie, - Self::Doughnut => ChartType::Doughnut, - Self::Area => ChartType::Area, - Self::AreaStacked => ChartType::AreaStacked, - Self::AreaStacked100 => ChartType::AreaStacked100, - Self::Scatter => ChartType::Scatter, - Self::ScatterLines => ChartType::ScatterLines, - Self::ScatterSmooth => ChartType::ScatterSmooth, - Self::Bubble => ChartType::Bubble, - Self::Radar => ChartType::Radar, - Self::RadarFilled => ChartType::RadarFilled, - Self::StockHlc => ChartType::StockHLC, - Self::StockOhlc => ChartType::StockOHLC, - Self::Combo => ChartType::Combo, - } - } -} - -impl ConnectorKind { - fn to_ppt_rs(self) -> ConnectorType { - match self { - Self::Straight => ConnectorType::Straight, - Self::Elbow => ConnectorType::Elbow, - Self::Curved => ConnectorType::Curved, - } - } -} - -impl ConnectorArrowKind { - fn to_ppt_rs(self) -> ArrowType { - match self { - Self::None => ArrowType::None, - Self::Triangle => ArrowType::Triangle, - Self::Stealth => ArrowType::Stealth, - Self::Diamond => ArrowType::Diamond, - Self::Oval => ArrowType::Oval, - Self::Open => ArrowType::Open, - } - } -} - -impl ConnectorArrowScale { - fn to_ppt_rs(self) -> ArrowSize { - match self { - Self::Small => ArrowSize::Small, - Self::Medium => ArrowSize::Medium, - Self::Large => ArrowSize::Large, - } - } -} - -impl LineStyle { - fn to_ppt_rs(self) -> LineDash { - match self { - Self::Solid => LineDash::Solid, - Self::Dashed => LineDash::Dash, - Self::Dotted => LineDash::Dot, - Self::DashDot => LineDash::DashDot, - Self::DashDotDot => LineDash::DashDotDot, - Self::LongDash => LineDash::LongDash, - Self::LongDashDot => LineDash::LongDashDot, - } - } - - fn to_ppt_xml(self) -> &'static str { - match self { - Self::Solid => "solid", - Self::Dashed => "dash", - Self::Dotted => "dot", - Self::DashDot => "dashDot", - Self::DashDotDot => "dashDotDot", - Self::LongDash => "lgDash", - Self::LongDashDot => "lgDashDot", - } - } - - fn as_api_str(self) -> &'static str { - match self { - Self::Solid => "solid", - Self::Dashed => "dashed", - Self::Dotted => "dotted", - Self::DashDot => "dash-dot", - Self::DashDotDot => "dash-dot-dot", - Self::LongDash => "long-dash", - Self::LongDashDot => "long-dash-dot", - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub(crate) enum ImageFitMode { - Stretch, - Contain, - Cover, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct StrokeStyle { - color: String, - width: u32, - style: LineStyle, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum ConnectorKind { - Straight, - Elbow, - Curved, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum ConnectorArrowKind { - None, - Triangle, - Stealth, - Diamond, - Oval, - Open, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum ConnectorArrowScale { - Small, - Medium, - Large, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -enum LineStyle { - Solid, - Dashed, - Dotted, - DashDot, - DashDotDot, - LongDash, - LongDashDot, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub(crate) struct Rect { - pub(crate) left: u32, - pub(crate) top: u32, - pub(crate) width: u32, - pub(crate) height: u32, -} - -impl Rect { - fn from_emu(left: u32, top: u32, width: u32, height: u32) -> Self { - Self { - left: emu_to_points(left), - top: emu_to_points(top), - width: emu_to_points(width), - height: emu_to_points(height), - } - } -} - -impl From for Rect { - fn from(value: PositionArgs) -> Self { - Self { - left: value.left, - top: value.top, - width: value.width, - height: value.height, - } - } -} - -fn apply_partial_position(rect: Rect, position: PartialPositionArgs) -> Rect { - Rect { - left: position.left.unwrap_or(rect.left), - top: position.top.unwrap_or(rect.top), - width: position.width.unwrap_or(rect.width), - height: position.height.unwrap_or(rect.height), - } -} - -fn apply_partial_position_to_image(image: &ImageElement, position: PartialPositionArgs) -> Rect { - let mut frame = apply_partial_position(image.frame, position.clone()); - if image.lock_aspect_ratio { - let base_ratio = image - .payload - .as_ref() - .map(|payload| payload.width_px as f64 / payload.height_px as f64) - .unwrap_or_else(|| image.frame.width as f64 / image.frame.height as f64); - if let Some(width) = position.width - && position.height.is_none() - { - frame.height = (width as f64 / base_ratio).round() as u32; - } else if let Some(height) = position.height - && position.width.is_none() - { - frame.width = (height as f64 * base_ratio).round() as u32; - } - } - frame -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/parsing.rs b/codex-rs/artifact-presentation/src/presentation_artifact/parsing.rs deleted file mode 100644 index efca31a99..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/parsing.rs +++ /dev/null @@ -1,1273 +0,0 @@ -fn parse_args(action: &str, value: &Value) -> Result -where - T: for<'de> Deserialize<'de>, -{ - serde_json::from_value(value.clone()).map_err(|error| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: error.to_string(), - }) -} - -fn required_artifact_id( - request: &PresentationArtifactRequest, -) -> Result { - request - .artifact_id - .clone() - .ok_or_else(|| PresentationArtifactError::MissingArtifactId { - action: request.action.clone(), - }) -} - -fn is_read_only_action(action: &str) -> bool { - matches!( - action, - "get_summary" - | "list_slides" - | "list_layouts" - | "list_layout_placeholders" - | "list_slide_placeholders" - | "inspect" - | "resolve" - | "to_proto" - | "get_style" - | "describe_styles" - | "record_patch" - ) -} - -fn tracks_history(action: &str) -> bool { - !is_read_only_action(action) - && !matches!( - action, - "export_pptx" | "export_preview" | "undo" | "redo" | "apply_patch" - ) -} - -fn patch_operation_supported(action: &str) -> bool { - tracks_history(action) && !matches!(action, "create" | "import_pptx" | "delete_artifact") -} - -fn resolve_path(cwd: &Path, path: &Path) -> PathBuf { - if path.is_absolute() { - path.to_path_buf() - } else { - cwd.join(path) - } -} - -fn normalize_color( - color: &str, - action: &str, - field: &str, -) -> Result { - normalize_color_with_palette(None, color, action, field) -} - -fn normalize_color_with_document( - document: &PresentationDocument, - color: &str, - action: &str, - field: &str, -) -> Result { - normalize_color_with_palette(Some(&document.theme), color, action, field) -} - -fn normalize_color_with_palette( - theme: Option<&ThemeState>, - color: &str, - action: &str, - field: &str, -) -> Result { - let trimmed = color.trim(); - let normalized = theme - .and_then(|palette| palette.resolve_color(trimmed)) - .unwrap_or_else(|| trimmed.trim_start_matches('#').to_uppercase()); - if normalized.len() != 6 - || !normalized - .chars() - .all(|character| character.is_ascii_hexdigit()) - { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("field `{field}` must be a 6-digit RGB hex color"), - }); - } - Ok(normalized) -} - -fn parse_shape_geometry( - geometry: &str, - action: &str, -) -> Result { - match geometry { - "rectangle" | "rect" => Ok(ShapeGeometry::Rectangle), - "rounded_rectangle" | "roundedRect" => Ok(ShapeGeometry::RoundedRectangle), - "ellipse" | "circle" => Ok(ShapeGeometry::Ellipse), - "triangle" => Ok(ShapeGeometry::Triangle), - "right_triangle" => Ok(ShapeGeometry::RightTriangle), - "diamond" => Ok(ShapeGeometry::Diamond), - "pentagon" => Ok(ShapeGeometry::Pentagon), - "hexagon" => Ok(ShapeGeometry::Hexagon), - "octagon" => Ok(ShapeGeometry::Octagon), - "star4" => Ok(ShapeGeometry::Star4), - "star" | "star5" => Ok(ShapeGeometry::Star5), - "star6" => Ok(ShapeGeometry::Star6), - "star8" => Ok(ShapeGeometry::Star8), - "right_arrow" => Ok(ShapeGeometry::RightArrow), - "left_arrow" => Ok(ShapeGeometry::LeftArrow), - "up_arrow" => Ok(ShapeGeometry::UpArrow), - "down_arrow" => Ok(ShapeGeometry::DownArrow), - "left_right_arrow" | "leftRightArrow" => Ok(ShapeGeometry::LeftRightArrow), - "up_down_arrow" | "upDownArrow" => Ok(ShapeGeometry::UpDownArrow), - "chevron" => Ok(ShapeGeometry::Chevron), - "heart" => Ok(ShapeGeometry::Heart), - "cloud" => Ok(ShapeGeometry::Cloud), - "wave" => Ok(ShapeGeometry::Wave), - "flowChartProcess" | "flow_chart_process" => Ok(ShapeGeometry::FlowChartProcess), - "flowChartDecision" | "flow_chart_decision" => Ok(ShapeGeometry::FlowChartDecision), - "flowChartConnector" | "flow_chart_connector" => Ok(ShapeGeometry::FlowChartConnector), - "parallelogram" => Ok(ShapeGeometry::Parallelogram), - "trapezoid" => Ok(ShapeGeometry::Trapezoid), - _ => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("geometry `{geometry}` is not supported"), - }), - } -} - -fn parse_chart_type( - chart_type: &str, - action: &str, -) -> Result { - match chart_type { - "bar" => Ok(ChartTypeSpec::Bar), - "bar_horizontal" => Ok(ChartTypeSpec::BarHorizontal), - "bar_stacked" => Ok(ChartTypeSpec::BarStacked), - "bar_stacked_100" => Ok(ChartTypeSpec::BarStacked100), - "line" => Ok(ChartTypeSpec::Line), - "line_markers" => Ok(ChartTypeSpec::LineMarkers), - "line_stacked" => Ok(ChartTypeSpec::LineStacked), - "pie" => Ok(ChartTypeSpec::Pie), - "doughnut" => Ok(ChartTypeSpec::Doughnut), - "area" => Ok(ChartTypeSpec::Area), - "area_stacked" => Ok(ChartTypeSpec::AreaStacked), - "area_stacked_100" => Ok(ChartTypeSpec::AreaStacked100), - "scatter" => Ok(ChartTypeSpec::Scatter), - "scatter_lines" => Ok(ChartTypeSpec::ScatterLines), - "scatter_smooth" => Ok(ChartTypeSpec::ScatterSmooth), - "bubble" => Ok(ChartTypeSpec::Bubble), - "radar" => Ok(ChartTypeSpec::Radar), - "radar_filled" => Ok(ChartTypeSpec::RadarFilled), - "stock_hlc" => Ok(ChartTypeSpec::StockHlc), - "stock_ohlc" => Ok(ChartTypeSpec::StockOhlc), - "combo" => Ok(ChartTypeSpec::Combo), - _ => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("chart_type `{chart_type}` is not supported"), - }), - } -} - -fn parse_chart_marker(marker: Option) -> ChartMarkerStyle { - marker - .map(|marker| ChartMarkerStyle { - symbol: marker.symbol, - size: marker.size, - }) - .unwrap_or_default() -} - -fn parse_chart_data_labels( - document: &PresentationDocument, - data_labels: Option, - action: &str, -) -> Result, PresentationArtifactError> { - data_labels - .map(|data_labels| { - Ok(ChartDataLabels { - show_value: data_labels.show_value.unwrap_or(false), - show_category_name: data_labels.show_category_name.unwrap_or(false), - show_leader_lines: data_labels.show_leader_lines.unwrap_or(false), - position: data_labels.position, - text_style: normalize_text_style_with_document( - document, - &data_labels.text_style, - action, - )?, - }) - }) - .transpose() -} - -fn parse_chart_series( - document: &PresentationDocument, - series: Vec, - action: &str, -) -> Result, PresentationArtifactError> { - series - .into_iter() - .map(|entry| { - if entry.values.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("series `{}` must contain at least one value", entry.name), - }); - } - let fill = entry - .fill - .as_deref() - .map(|value| normalize_color_with_document(document, value, action, "series.fill")) - .transpose()?; - let stroke = entry - .stroke - .map(|stroke| { - Ok(StrokeStyle { - color: normalize_color_with_document( - document, - &stroke.color, - action, - "series.stroke.color", - )?, - width: stroke.width, - style: stroke - .style - .as_deref() - .map(|value| parse_line_style(value, action)) - .transpose()? - .unwrap_or(LineStyle::Solid), - }) - }) - .transpose()?; - let data_label_overrides = entry - .data_label_overrides - .unwrap_or_default() - .into_iter() - .map(|override_args| { - Ok(ChartDataLabelOverride { - idx: override_args.idx as usize, - text: override_args.text, - position: override_args.position, - text_style: normalize_text_style_with_document( - document, - &override_args.text_style, - action, - )?, - fill: override_args - .fill - .as_deref() - .map(|value| { - normalize_color_with_document(document, value, action, "fill") - }) - .transpose()?, - stroke: override_args - .stroke - .map(|stroke| { - Ok(StrokeStyle { - color: normalize_color_with_document( - document, - &stroke.color, - action, - "stroke.color", - )?, - width: stroke.width, - style: stroke - .style - .as_deref() - .map(|value| parse_line_style(value, action)) - .transpose()? - .unwrap_or(LineStyle::Solid), - }) - }) - .transpose()?, - }) - }) - .collect::, _>>()?; - Ok(ChartSeriesSpec { - name: entry.name, - values: entry.values, - categories: entry.categories, - x_values: entry.x_values, - fill, - stroke, - marker: Some(parse_chart_marker(entry.marker)), - data_label_overrides, - }) - }) - .collect() -} - -fn parse_stroke( - document: &PresentationDocument, - stroke: Option, - action: &str, -) -> Result, PresentationArtifactError> { - stroke - .map(|value| parse_required_stroke(document, value, action)) - .transpose() -} - -fn parse_required_stroke( - document: &PresentationDocument, - stroke: StrokeArgs, - action: &str, -) -> Result { - Ok(StrokeStyle { - color: normalize_color_with_document(document, &stroke.color, action, "stroke.color")?, - width: stroke.width, - style: stroke - .style - .as_deref() - .map(|style| parse_line_style(style, action)) - .transpose()? - .unwrap_or(LineStyle::Solid), - }) -} - -fn parse_connector_kind( - connector_type: &str, - action: &str, -) -> Result { - match connector_type { - "straight" => Ok(ConnectorKind::Straight), - "elbow" => Ok(ConnectorKind::Elbow), - "curved" => Ok(ConnectorKind::Curved), - _ => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("connector_type `{connector_type}` is not supported"), - }), - } -} - -fn parse_connector_arrow( - value: &str, - action: &str, -) -> Result { - match value { - "none" => Ok(ConnectorArrowKind::None), - "triangle" => Ok(ConnectorArrowKind::Triangle), - "stealth" => Ok(ConnectorArrowKind::Stealth), - "diamond" => Ok(ConnectorArrowKind::Diamond), - "oval" => Ok(ConnectorArrowKind::Oval), - "open" => Ok(ConnectorArrowKind::Open), - _ => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("connector arrow `{value}` is not supported"), - }), - } -} - -fn parse_connector_arrow_size( - value: &str, - action: &str, -) -> Result { - match value { - "small" => Ok(ConnectorArrowScale::Small), - "medium" => Ok(ConnectorArrowScale::Medium), - "large" => Ok(ConnectorArrowScale::Large), - _ => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("connector arrow_size `{value}` is not supported"), - }), - } -} - -fn parse_line_style(value: &str, action: &str) -> Result { - match value { - "solid" => Ok(LineStyle::Solid), - "dashed" => Ok(LineStyle::Dashed), - "dotted" => Ok(LineStyle::Dotted), - "dash-dot" | "dash_dot" => Ok(LineStyle::DashDot), - "dash-dot-dot" | "dash_dot_dot" => Ok(LineStyle::DashDotDot), - "long-dash" | "long_dash" => Ok(LineStyle::LongDash), - "long-dash-dot" | "long_dash_dot" => Ok(LineStyle::LongDashDot), - _ => Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("line style `{value}` is not supported"), - }), - } -} - -fn parse_connector_line( - document: &PresentationDocument, - line: Option, - action: &str, -) -> Result { - let line = line.unwrap_or_default(); - Ok(ParsedConnectorLine { - color: line - .color - .as_deref() - .map(|value| normalize_color_with_document(document, value, action, "line.color")) - .transpose()? - .unwrap_or_else(|| "000000".to_string()), - width: line.width.unwrap_or(1), - style: line - .style - .as_deref() - .map(|value| parse_line_style(value, action)) - .transpose()? - .unwrap_or(LineStyle::Solid), - }) -} - -struct ParsedConnectorLine { - color: String, - width: u32, - style: LineStyle, -} - -fn normalize_text_style_with_document( - document: &PresentationDocument, - styling: &TextStylingArgs, - action: &str, -) -> Result { - normalize_text_style_with_palette(Some(&document.theme), styling, action, |style_name| { - document.resolve_named_text_style(style_name, action) - }) -} - -fn normalize_text_layout( - layout: &TextLayoutArgs, - action: &str, -) -> Result { - let wrap = layout - .wrap - .as_deref() - .map(|value| match value { - "square" => Ok(TextWrapMode::Square), - "none" => Ok(TextWrapMode::None), - _ => Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("unsupported wrap `{value}`"), - }), - }) - .transpose()?; - let auto_fit = layout - .auto_fit - .as_deref() - .map(|value| match value { - "none" => Ok(TextAutoFitMode::None), - "shrinkText" | "shrink_text" => Ok(TextAutoFitMode::ShrinkText), - "resizeShapeToFitText" | "resize_shape_to_fit_text" => { - Ok(TextAutoFitMode::ResizeShapeToFitText) - } - _ => Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("unsupported auto_fit `{value}`"), - }), - }) - .transpose()?; - let vertical_alignment = layout - .vertical_alignment - .as_deref() - .map(|value| match value { - "top" => Ok(TextVerticalAlignment::Top), - "middle" | "center" => Ok(TextVerticalAlignment::Middle), - "bottom" => Ok(TextVerticalAlignment::Bottom), - _ => Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("unsupported vertical_alignment `{value}`"), - }), - }) - .transpose()?; - Ok(TextLayoutState { - insets: layout.insets.as_ref().map(|insets| TextInsets { - left: insets.left, - right: insets.right, - top: insets.top, - bottom: insets.bottom, - }), - wrap, - auto_fit, - vertical_alignment, - }) -} - -fn normalize_rich_text_input( - document: &PresentationDocument, - input: RichTextInput, - action: &str, -) -> Result<(String, RichTextState), PresentationArtifactError> { - let mut text = String::new(); - let mut ranges = Vec::new(); - let paragraphs = match input { - RichTextInput::Plain(value) => vec![RichParagraphInput::Plain(value)], - RichTextInput::Paragraphs(paragraphs) => paragraphs, - }; - for (paragraph_index, paragraph) in paragraphs.into_iter().enumerate() { - if paragraph_index > 0 { - text.push('\n'); - } - let paragraph_start = text.chars().count(); - match paragraph { - RichParagraphInput::Plain(value) => text.push_str(&value), - RichParagraphInput::Runs(runs) => { - for run in runs { - match run { - RichRunInput::Plain(value) => text.push_str(&value), - RichRunInput::Styled(value) => { - let start_cp = text.chars().count(); - text.push_str(&value.run); - let length = value.run.chars().count(); - let style = normalize_text_style_with_document( - document, - &value.text_style, - action, - )?; - let hyperlink = value - .link - .as_ref() - .map(|link| parse_rich_text_link(link, action)) - .transpose()?; - if length > 0 - && (!text_style_is_empty(&style) || hyperlink.is_some()) - { - ranges.push(TextRangeAnnotation { - range_id: format!("inline_{paragraph_index}_{start_cp}"), - start_cp, - length, - style, - hyperlink, - spacing_before: None, - spacing_after: None, - line_spacing: None, - }); - } - } - } - } - } - } - let paragraph_len = text.chars().count() - paragraph_start; - if paragraph_len == 0 { - continue; - } - } - Ok(( - text, - RichTextState { - ranges, - layout: TextLayoutState::default(), - }, - )) -} - -fn parse_rich_text_link( - link: &RichTextLinkInput, - action: &str, -) -> Result { - let uri = link - .uri - .as_ref() - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`link.uri` is required".to_string(), - })?; - let target = if link.is_external.unwrap_or(true) { - HyperlinkTarget::Url(uri.clone()) - } else { - HyperlinkTarget::File(uri.clone()) - }; - Ok(HyperlinkState { - target, - tooltip: None, - highlight_click: true, - }) -} - -fn text_style_is_empty(style: &TextStyle) -> bool { - style.style_name.is_none() - && style.font_size.is_none() - && style.font_family.is_none() - && style.color.is_none() - && style.alignment.is_none() - && !style.bold - && !style.italic - && !style.underline -} - -fn resolve_text_range_selector( - text: &str, - query: Option<&str>, - occurrence: Option, - start_cp: Option, - length: Option, - action: &str, -) -> Result<(usize, usize, Option), PresentationArtifactError> { - if let Some(query) = query { - let occurrence = occurrence.unwrap_or(0); - let haystack = text.chars().collect::>(); - let needle = query.chars().collect::>(); - if needle.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`query` must not be empty".to_string(), - }); - } - let mut matches = Vec::new(); - for start in 0..=haystack.len().saturating_sub(needle.len()) { - if haystack[start..start + needle.len()] == needle[..] { - matches.push(start); - } - } - let Some(found) = matches.get(occurrence).copied() else { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("query `{query}` occurrence {occurrence} was not found"), - }); - }; - return Ok((found, needle.len(), Some(query.to_string()))); - } - let start_cp = start_cp.ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "provide either `query` or `start_cp`".to_string(), - })?; - let length = length.ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`length` is required with `start_cp`".to_string(), - })?; - if start_cp + length > text.chars().count() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "text range is out of bounds".to_string(), - }); - } - Ok((start_cp, length, None)) -} - -fn normalize_text_style_with_palette( - theme: Option<&ThemeState>, - styling: &TextStylingArgs, - action: &str, - resolve_style_name: impl Fn(&str) -> Result, -) -> Result { - let mut style = styling - .style - .as_deref() - .map(resolve_style_name) - .transpose()? - .unwrap_or_default(); - style.font_size = styling.font_size.or(style.font_size); - style.font_family = styling.font_family.clone().or(style.font_family); - style.color = styling - .color - .as_deref() - .map(|value| normalize_color_with_palette(theme, value, action, "color")) - .transpose()? - .or(style.color); - style.alignment = styling - .alignment - .as_deref() - .map(|value| parse_alignment(value, action)) - .transpose()? - .or(style.alignment); - style.bold = styling.bold.unwrap_or(style.bold); - style.italic = styling.italic.unwrap_or(style.italic); - style.underline = styling.underline.unwrap_or(style.underline); - if let Some(style_name) = &styling.style { - style.style_name = Some(normalize_style_name(style_name, action)?); - } - Ok(style) -} - -fn parse_hyperlink_state( - document: &PresentationDocument, - args: &SetHyperlinkArgs, - action: &str, -) -> Result { - let link_type = - args.link_type - .as_deref() - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`link_type` is required unless `clear` is true".to_string(), - })?; - let target = match link_type { - "url" => HyperlinkTarget::Url(required_hyperlink_field(&args.url, action, "url")?.clone()), - "slide" => { - let slide_index = - args.slide_index - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`slide_index` is required for slide hyperlinks".to_string(), - })?; - if slide_index as usize >= document.slides.len() { - return Err(index_out_of_range( - action, - slide_index as usize, - document.slides.len(), - )); - } - HyperlinkTarget::Slide(slide_index) - } - "first_slide" => HyperlinkTarget::FirstSlide, - "last_slide" => HyperlinkTarget::LastSlide, - "next_slide" => HyperlinkTarget::NextSlide, - "previous_slide" => HyperlinkTarget::PreviousSlide, - "end_show" => HyperlinkTarget::EndShow, - "email" => HyperlinkTarget::Email { - address: required_hyperlink_field(&args.address, action, "address")?.clone(), - subject: args.subject.clone(), - }, - "file" => { - HyperlinkTarget::File(required_hyperlink_field(&args.path, action, "path")?.clone()) - } - other => { - return Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("hyperlink type `{other}` is not supported"), - }); - } - }; - Ok(HyperlinkState { - target, - tooltip: args.tooltip.clone(), - highlight_click: args.highlight_click.unwrap_or(true), - }) -} - -fn required_hyperlink_field<'a>( - value: &'a Option, - action: &str, - field: &str, -) -> Result<&'a String, PresentationArtifactError> { - value - .as_ref() - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("`{field}` is required for this hyperlink type"), - }) -} - -fn coerce_table_rows( - rows: Vec>, - action: &str, -) -> Result>, PresentationArtifactError> { - if rows.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`rows` must contain at least one row".to_string(), - }); - } - Ok(rows - .into_iter() - .map(|row| { - row.into_iter() - .map(|value| TableCellSpec { - text: cell_value_to_string(value), - text_style: TextStyle::default(), - background_fill: None, - alignment: None, - rich_text: RichTextState::default(), - borders: None, - }) - .collect() - }) - .collect()) -} - -fn parse_table_border( - document: &PresentationDocument, - border: &TableBorderArgs, - action: &str, - field: &str, -) -> Result { - Ok(TableBorder { - color: normalize_color_with_document(document, &border.color, action, field)?, - width: border.width, - }) -} - -fn parse_table_borders( - document: &PresentationDocument, - borders: Option, - action: &str, -) -> Result, PresentationArtifactError> { - borders - .map(|borders| { - Ok(TableBorders { - outside: borders - .outside - .as_ref() - .map(|border| parse_table_border(document, border, action, "borders.outside")) - .transpose()?, - inside: borders - .inside - .as_ref() - .map(|border| parse_table_border(document, border, action, "borders.inside")) - .transpose()?, - top: borders - .top - .as_ref() - .map(|border| parse_table_border(document, border, action, "borders.top")) - .transpose()?, - bottom: borders - .bottom - .as_ref() - .map(|border| parse_table_border(document, border, action, "borders.bottom")) - .transpose()?, - left: borders - .left - .as_ref() - .map(|border| parse_table_border(document, border, action, "borders.left")) - .transpose()?, - right: borders - .right - .as_ref() - .map(|border| parse_table_border(document, border, action, "borders.right")) - .transpose()?, - }) - }) - .transpose() -} - -fn parse_table_style_options(style_options: Option) -> TableStyleOptions { - style_options - .map(|style_options| TableStyleOptions { - header_row: style_options.header_row.unwrap_or(false), - banded_rows: style_options.banded_rows.unwrap_or(false), - banded_columns: style_options.banded_columns.unwrap_or(false), - first_column: style_options.first_column.unwrap_or(false), - last_column: style_options.last_column.unwrap_or(false), - total_row: style_options.total_row.unwrap_or(false), - }) - .unwrap_or_default() -} - -fn normalize_table_dimensions( - rows: &[Vec], - frame: Rect, - column_widths: Option>, - row_heights: Option>, - action: &str, -) -> Result<(Vec, Vec), PresentationArtifactError> { - let column_count = rows.iter().map(std::vec::Vec::len).max().unwrap_or(1); - let normalized_column_widths = match column_widths { - Some(widths) => { - if widths.len() != column_count { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "`column_widths` must contain {column_count} entries for this table" - ), - }); - } - widths - } - None => split_points(frame.width, column_count), - }; - let normalized_row_heights = match row_heights { - Some(heights) => { - if heights.len() != rows.len() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "`row_heights` must contain {} entries for this table", - rows.len() - ), - }); - } - heights - } - None => split_points(frame.height, rows.len()), - }; - Ok((normalized_column_widths, normalized_row_heights)) -} - -fn split_points(total: u32, count: usize) -> Vec { - if count == 0 { - return Vec::new(); - } - let base = total / count as u32; - let remainder = total % count as u32; - (0..count) - .map(|index| base + u32::from(index < remainder as usize)) - .collect() -} - -fn parse_alignment(value: &str, action: &str) -> Result { - match value { - "left" => Ok(TextAlignment::Left), - "center" | "middle" => Ok(TextAlignment::Center), - "right" => Ok(TextAlignment::Right), - "justify" => Ok(TextAlignment::Justify), - _ => Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("unsupported alignment `{value}`"), - }), - } -} - -fn normalize_theme(args: ThemeArgs, action: &str) -> Result { - let color_scheme = args - .color_scheme - .into_iter() - .map(|(key, value)| { - normalize_color(&value, action, &key) - .map(|normalized| (key.to_ascii_lowercase(), normalized)) - }) - .collect::, _>>()?; - Ok(ThemeState { - color_scheme, - major_font: args.major_font, - minor_font: args.minor_font, - }) -} - -fn normalize_style_name( - style_name: &str, - action: &str, -) -> Result { - let normalized_style_name = style_name.trim().to_ascii_lowercase(); - if normalized_style_name.is_empty() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`name` must not be empty".to_string(), - }); - } - Ok(normalized_style_name) -} - -fn built_in_text_styles(theme: &ThemeState) -> HashMap { - ["title", "heading1", "body", "list", "numberedlist"] - .into_iter() - .filter_map(|name| built_in_text_style(theme, name).map(|style| (name.to_string(), style))) - .collect() -} - -fn built_in_text_style(theme: &ThemeState, style_name: &str) -> Option { - let default_color = theme.resolve_color("tx1"); - let default_font = theme - .major_font - .clone() - .or_else(|| theme.minor_font.clone()); - let body_font = theme - .minor_font - .clone() - .or_else(|| theme.major_font.clone()); - let style = match style_name { - "title" => TextStyle { - style_name: Some("title".to_string()), - font_size: Some(28), - font_family: default_font, - color: default_color, - alignment: Some(TextAlignment::Left), - bold: true, - italic: false, - underline: false, - }, - "heading1" => TextStyle { - style_name: Some("heading1".to_string()), - font_size: Some(22), - font_family: default_font, - color: default_color, - alignment: Some(TextAlignment::Left), - bold: true, - italic: false, - underline: false, - }, - "body" => TextStyle { - style_name: Some("body".to_string()), - font_size: Some(14), - font_family: body_font, - color: default_color, - alignment: Some(TextAlignment::Left), - bold: false, - italic: false, - underline: false, - }, - "list" => TextStyle { - style_name: Some("list".to_string()), - font_size: Some(14), - font_family: body_font, - color: default_color, - alignment: Some(TextAlignment::Left), - bold: false, - italic: false, - underline: false, - }, - "numberedlist" => TextStyle { - style_name: Some("numberedlist".to_string()), - font_size: Some(14), - font_family: body_font, - color: default_color, - alignment: Some(TextAlignment::Left), - bold: false, - italic: false, - underline: false, - }, - _ => return None, - }; - Some(style) -} - -fn named_text_style_to_json(style: &NamedTextStyle, id_prefix: &str) -> Value { - serde_json::json!({ - "kind": "textStyle", - "id": format!("{id_prefix}/{}", style.name), - "name": style.name, - "builtIn": style.built_in, - "style": text_style_to_proto(&style.style), - }) -} - -fn parse_slide_size(value: &Value, action: &str) -> Result { - #[derive(Deserialize)] - struct SlideSizeArgs { - width: u32, - height: u32, - } - - let slide_size: SlideSizeArgs = serde_json::from_value(value.clone()).map_err(|error| { - PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("invalid slide_size: {error}"), - } - })?; - Ok(Rect { - left: 0, - top: 0, - width: slide_size.width, - height: slide_size.height, - }) -} - -fn apply_layout_to_slide( - document: &mut PresentationDocument, - slide: &mut PresentationSlide, - layout_ref: &str, - action: &str, -) -> Result<(), PresentationArtifactError> { - let layout = document.get_layout(layout_ref, action)?.clone(); - let placeholders = resolved_layout_placeholders(document, &layout.layout_id, action)?; - slide.layout_id = Some(layout.layout_id); - for resolved in placeholders { - slide.elements.push(materialize_placeholder_element( - document.next_element_id(), - resolved.definition, - slide.elements.len(), - )); - } - Ok(()) -} - -fn materialize_placeholder_element( - element_id: String, - placeholder: PlaceholderDefinition, - z_order: usize, -) -> PresentationElement { - let placeholder_ref = Some(PlaceholderRef { - name: placeholder.name.clone(), - placeholder_type: placeholder.placeholder_type.clone(), - index: placeholder.index, - }); - if placeholder_is_image(&placeholder.placeholder_type) { - return PresentationElement::Image(ImageElement { - element_id, - frame: placeholder.frame, - payload: None, - fit_mode: ImageFitMode::Stretch, - crop: None, - rotation_degrees: None, - flip_horizontal: false, - flip_vertical: false, - lock_aspect_ratio: true, - alt_text: Some(placeholder.name.clone()), - prompt: placeholder - .text - .clone() - .or_else(|| Some(format!("Image placeholder: {}", placeholder.name))), - is_placeholder: true, - placeholder: placeholder_ref, - z_order, - }); - } - if placeholder.geometry == ShapeGeometry::Rectangle { - PresentationElement::Text(TextElement { - element_id, - text: placeholder.text.unwrap_or_default(), - frame: placeholder.frame, - fill: None, - style: TextStyle::default(), - hyperlink: None, - rich_text: RichTextState::default(), - placeholder: placeholder_ref, - z_order, - }) - } else { - PresentationElement::Shape(ShapeElement { - element_id, - geometry: placeholder.geometry, - frame: placeholder.frame, - fill: None, - stroke: None, - text: placeholder.text, - text_style: TextStyle::default(), - hyperlink: None, - rich_text: None, - placeholder: placeholder_ref, - rotation_degrees: None, - flip_horizontal: false, - flip_vertical: false, - z_order, - }) - } -} - -fn resolved_layout_placeholders( - document: &PresentationDocument, - layout_id: &str, - action: &str, -) -> Result, PresentationArtifactError> { - let mut lineage = Vec::new(); - collect_layout_lineage( - document, - layout_id, - action, - &mut HashSet::new(), - &mut lineage, - )?; - let mut resolved: Vec = Vec::new(); - for layout in lineage { - for placeholder in &layout.placeholders { - if let Some(index) = resolved.iter().position(|entry| { - placeholder_key(&entry.definition) == placeholder_key(placeholder) - }) { - resolved[index] = ResolvedPlaceholder { - source_layout_id: layout.layout_id.clone(), - definition: placeholder.clone(), - }; - } else { - resolved.push(ResolvedPlaceholder { - source_layout_id: layout.layout_id.clone(), - definition: placeholder.clone(), - }); - } - } - } - Ok(resolved) -} - -fn collect_layout_lineage<'a>( - document: &'a PresentationDocument, - layout_id: &str, - action: &str, - seen: &mut HashSet, - lineage: &mut Vec<&'a LayoutDocument>, -) -> Result<(), PresentationArtifactError> { - if !seen.insert(layout_id.to_string()) { - return Err(PresentationArtifactError::UnsupportedFeature { - action: action.to_string(), - message: format!("layout inheritance cycle detected at `{layout_id}`"), - }); - } - let layout = document.get_layout(layout_id, action)?; - if let Some(parent_layout_id) = &layout.parent_layout_id { - collect_layout_lineage(document, parent_layout_id, action, seen, lineage)?; - } - lineage.push(layout); - Ok(()) -} - -fn placeholder_key(placeholder: &PlaceholderDefinition) -> (String, String, Option) { - ( - placeholder.name.to_ascii_lowercase(), - placeholder.placeholder_type.to_ascii_lowercase(), - placeholder.index, - ) -} - -fn layout_placeholder_list( - document: &PresentationDocument, - layout_id: &str, - action: &str, -) -> Result, PresentationArtifactError> { - resolved_layout_placeholders(document, layout_id, action).map(|placeholders| { - placeholders - .into_iter() - .map(|placeholder| PlaceholderListEntry { - scope: "layout".to_string(), - source_layout_id: Some(placeholder.source_layout_id), - slide_index: None, - element_id: None, - name: placeholder.definition.name, - placeholder_type: placeholder.definition.placeholder_type, - index: placeholder.definition.index, - geometry: Some(format!("{:?}", placeholder.definition.geometry)), - text_preview: placeholder.definition.text, - }) - .collect() - }) -} - -fn placeholder_is_image(placeholder_type: &str) -> bool { - matches!( - placeholder_type.to_ascii_lowercase().as_str(), - "image" | "picture" | "pic" | "photo" - ) -} - -fn slide_placeholder_list( - slide: &PresentationSlide, - slide_index: usize, -) -> Vec { - slide - .elements - .iter() - .filter_map(|element| match element { - PresentationElement::Text(text) => { - text.placeholder - .as_ref() - .map(|placeholder| PlaceholderListEntry { - scope: "slide".to_string(), - source_layout_id: slide.layout_id.clone(), - slide_index: Some(slide_index), - element_id: Some(text.element_id.clone()), - name: placeholder.name.clone(), - placeholder_type: placeholder.placeholder_type.clone(), - index: placeholder.index, - geometry: Some("Rectangle".to_string()), - text_preview: Some(text.text.clone()), - }) - } - PresentationElement::Shape(shape) => { - shape - .placeholder - .as_ref() - .map(|placeholder| PlaceholderListEntry { - scope: "slide".to_string(), - source_layout_id: slide.layout_id.clone(), - slide_index: Some(slide_index), - element_id: Some(shape.element_id.clone()), - name: placeholder.name.clone(), - placeholder_type: placeholder.placeholder_type.clone(), - index: placeholder.index, - geometry: Some(format!("{:?}", shape.geometry)), - text_preview: shape.text.clone(), - }) - } - PresentationElement::Image(image) => { - image - .placeholder - .as_ref() - .map(|placeholder| PlaceholderListEntry { - scope: "slide".to_string(), - source_layout_id: slide.layout_id.clone(), - slide_index: Some(slide_index), - element_id: Some(image.element_id.clone()), - name: placeholder.name.clone(), - placeholder_type: placeholder.placeholder_type.clone(), - index: placeholder.index, - geometry: Some("Image".to_string()), - text_preview: image.prompt.clone(), - }) - } - PresentationElement::Connector(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => None, - }) - .collect() -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/pptx.rs b/codex-rs/artifact-presentation/src/presentation_artifact/pptx.rs deleted file mode 100644 index a06abc701..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/pptx.rs +++ /dev/null @@ -1,951 +0,0 @@ -const CODEX_METADATA_ENTRY: &str = "ppt/codex-document.json"; - -fn import_codex_metadata_document(path: &Path) -> Result, String> { - let file = std::fs::File::open(path).map_err(|error| error.to_string())?; - let mut archive = ZipArchive::new(file).map_err(|error| error.to_string())?; - let mut entry = match archive.by_name(CODEX_METADATA_ENTRY) { - Ok(entry) => entry, - Err(zip::result::ZipError::FileNotFound) => return Ok(None), - Err(error) => return Err(error.to_string()), - }; - let mut bytes = Vec::new(); - entry.read_to_end(&mut bytes) - .map_err(|error| error.to_string())?; - serde_json::from_slice(&bytes) - .map(Some) - .map_err(|error| error.to_string()) -} - -fn build_pptx_bytes(document: &PresentationDocument, action: &str) -> Result, String> { - let bytes = document - .to_ppt_rs() - .build() - .map_err(|error| format!("{action}: {error}"))?; - patch_pptx_package(bytes, document).map_err(|error| format!("{action}: {error}")) -} - -struct SlideImageAsset { - xml: String, - relationship_xml: String, - media_path: String, - media_bytes: Vec, - extension: String, -} - -fn normalized_image_extension(format: &str) -> String { - match format.to_ascii_lowercase().as_str() { - "jpeg" => "jpg".to_string(), - other => other.to_string(), - } -} - -fn image_relationship_xml(relationship_id: &str, target: &str) -> String { - format!( - r#""#, - ppt_rs::escape_xml(target) - ) -} - -fn image_picture_xml( - image: &ImageElement, - shape_id: usize, - relationship_id: &str, - frame: Rect, - crop: Option, -) -> String { - let blip_fill = if let Some((crop_left, crop_top, crop_right, crop_bottom)) = crop { - format!( - r#" - - - - - -"#, - (crop_left * 100_000.0).round() as u32, - (crop_top * 100_000.0).round() as u32, - (crop_right * 100_000.0).round() as u32, - (crop_bottom * 100_000.0).round() as u32, - ) - } else { - format!( - r#" - - - - -"# - ) - }; - let descr = image - .alt_text - .as_deref() - .map(|alt| format!(r#" descr="{}""#, ppt_rs::escape_xml(alt))) - .unwrap_or_default(); - let no_change_aspect = if image.lock_aspect_ratio { 1 } else { 0 }; - let rotation = image - .rotation_degrees - .map(|rotation| format!(r#" rot="{}""#, i64::from(rotation) * 60_000)) - .unwrap_or_default(); - let flip_horizontal = if image.flip_horizontal { - r#" flipH="1""# - } else { - "" - }; - let flip_vertical = if image.flip_vertical { - r#" flipV="1""# - } else { - "" - }; - format!( - r#" - - - - - - - -{blip_fill} - - - - - - - - - -"#, - points_to_emu(frame.left), - points_to_emu(frame.top), - points_to_emu(frame.width), - points_to_emu(frame.height), - ) -} - -fn slide_image_assets( - slide: &PresentationSlide, - next_media_index: &mut usize, -) -> Vec { - let mut ordered = slide.elements.iter().collect::>(); - ordered.sort_by_key(|element| element.z_order()); - let shape_count = ordered - .iter() - .filter(|element| { - matches!( - element, - PresentationElement::Text(_) - | PresentationElement::Shape(_) - | PresentationElement::Image(ImageElement { payload: None, .. }) - ) - }) - .count() - + usize::from(slide.background_fill.is_some()); - let mut image_index = 0_usize; - let mut assets = Vec::new(); - for element in ordered { - let PresentationElement::Image(image) = element else { - continue; - }; - let Some(payload) = &image.payload else { - continue; - }; - let (left, top, width, height, fitted_crop) = if image.fit_mode != ImageFitMode::Stretch { - fit_image(image) - } else { - ( - image.frame.left, - image.frame.top, - image.frame.width, - image.frame.height, - None, - ) - }; - image_index += 1; - let relationship_id = format!("rIdImage{image_index}"); - let extension = normalized_image_extension(&payload.format); - let media_name = format!("image{next_media_index}.{extension}"); - *next_media_index += 1; - assets.push(SlideImageAsset { - xml: image_picture_xml( - image, - 20 + shape_count + image_index - 1, - &relationship_id, - Rect { - left, - top, - width, - height, - }, - image.crop.or(fitted_crop), - ), - relationship_xml: image_relationship_xml( - &relationship_id, - &format!("../media/{media_name}"), - ), - media_path: format!("ppt/media/{media_name}"), - media_bytes: payload.bytes.clone(), - extension, - }); - } - assets -} - -fn patch_pptx_package( - source_bytes: Vec, - document: &PresentationDocument, -) -> Result, String> { - let mut archive = - ZipArchive::new(Cursor::new(source_bytes)).map_err(|error| error.to_string())?; - let mut writer = ZipWriter::new(Cursor::new(Vec::new())); - let mut next_media_index = 1_usize; - let mut pending_slide_relationships = HashMap::new(); - let mut pending_slide_images = HashMap::new(); - let mut pending_media = Vec::new(); - let mut image_extensions = BTreeSet::new(); - for (slide_index, slide) in document.slides.iter().enumerate() { - let slide_number = slide_index + 1; - let images = slide_image_assets(slide, &mut next_media_index); - let mut relationships = slide_hyperlink_relationships(slide); - relationships.extend(images.iter().map(|image| image.relationship_xml.clone())); - if !relationships.is_empty() { - pending_slide_relationships.insert(slide_number, relationships); - } - if !images.is_empty() { - image_extensions.extend(images.iter().map(|image| image.extension.clone())); - pending_media.extend( - images - .iter() - .map(|image| (image.media_path.clone(), image.media_bytes.clone())), - ); - pending_slide_images.insert(slide_number, images); - } - } - - for index in 0..archive.len() { - let mut file = archive.by_index(index).map_err(|error| error.to_string())?; - if file.is_dir() { - continue; - } - let name = file.name().to_string(); - if name == CODEX_METADATA_ENTRY { - continue; - } - let options = file.options(); - let mut bytes = Vec::new(); - file.read_to_end(&mut bytes) - .map_err(|error| error.to_string())?; - writer - .start_file(&name, options) - .map_err(|error| error.to_string())?; - if name == "[Content_Types].xml" { - writer - .write_all(update_content_types_xml(bytes, &image_extensions)?.as_bytes()) - .map_err(|error| error.to_string())?; - continue; - } - if name == "ppt/presentation.xml" { - writer - .write_all( - update_presentation_xml_dimensions(bytes, document.slide_size)?.as_bytes(), - ) - .map_err(|error| error.to_string())?; - continue; - } - if let Some(slide_number) = parse_slide_xml_path(&name) { - writer - .write_all( - update_slide_xml( - bytes, - &document.slides[slide_number - 1], - pending_slide_images - .get(&slide_number) - .map(std::vec::Vec::as_slice) - .unwrap_or(&[]), - )? - .as_bytes(), - ) - .map_err(|error| error.to_string())?; - continue; - } - if let Some(slide_number) = parse_slide_relationships_path(&name) - && let Some(relationships) = pending_slide_relationships.remove(&slide_number) - { - writer - .write_all(update_slide_relationships_xml(bytes, &relationships)?.as_bytes()) - .map_err(|error| error.to_string())?; - continue; - } - writer - .write_all(&bytes) - .map_err(|error| error.to_string())?; - } - - for (slide_number, relationships) in pending_slide_relationships { - writer - .start_file( - format!("ppt/slides/_rels/slide{slide_number}.xml.rels"), - SimpleFileOptions::default(), - ) - .map_err(|error| error.to_string())?; - writer - .write_all(slide_relationships_xml(&relationships).as_bytes()) - .map_err(|error| error.to_string())?; - } - - for (path, bytes) in pending_media { - writer - .start_file(path, SimpleFileOptions::default()) - .map_err(|error| error.to_string())?; - writer - .write_all(&bytes) - .map_err(|error| error.to_string())?; - } - - writer - .start_file(CODEX_METADATA_ENTRY, SimpleFileOptions::default()) - .map_err(|error| error.to_string())?; - writer - .write_all( - &serde_json::to_vec(document).map_err(|error| error.to_string())?, - ) - .map_err(|error| error.to_string())?; - - writer - .finish() - .map_err(|error| error.to_string()) - .map(Cursor::into_inner) -} - -fn update_presentation_xml_dimensions( - existing_bytes: Vec, - slide_size: Rect, -) -> Result { - let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?; - let updated = replace_self_closing_xml_tag( - &existing, - "p:sldSz", - &format!( - r#""#, - points_to_emu(slide_size.width), - points_to_emu(slide_size.height) - ), - )?; - replace_self_closing_xml_tag( - &updated, - "p:notesSz", - &format!( - r#""#, - points_to_emu(slide_size.height), - points_to_emu(slide_size.width) - ), - ) -} - -fn replace_self_closing_xml_tag(xml: &str, tag: &str, replacement: &str) -> Result { - let start = xml - .find(&format!("<{tag} ")) - .ok_or_else(|| format!("presentation xml is missing `<{tag} .../>`"))?; - let end = xml[start..] - .find("/>") - .map(|offset| start + offset + 2) - .ok_or_else(|| format!("presentation xml tag `{tag}` is not self-closing"))?; - Ok(format!("{}{replacement}{}", &xml[..start], &xml[end..])) -} - -fn slide_hyperlink_relationships(slide: &PresentationSlide) -> Vec { - let mut ordered = slide.elements.iter().collect::>(); - ordered.sort_by_key(|element| element.z_order()); - let mut hyperlink_index = 1_u32; - let mut relationships = Vec::new(); - for element in ordered { - let Some(hyperlink) = (match element { - PresentationElement::Text(text) => text.hyperlink.as_ref(), - PresentationElement::Shape(shape) => shape.hyperlink.as_ref(), - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => None, - }) else { - continue; - }; - let relationship_id = format!("rIdHyperlink{hyperlink_index}"); - hyperlink_index += 1; - relationships.push(hyperlink.relationship_xml(&relationship_id)); - } - relationships -} - -fn parse_slide_relationships_path(path: &str) -> Option { - path.strip_prefix("ppt/slides/_rels/slide")? - .strip_suffix(".xml.rels")? - .parse::() - .ok() -} - -fn parse_slide_xml_path(path: &str) -> Option { - path.strip_prefix("ppt/slides/slide")? - .strip_suffix(".xml")? - .parse::() - .ok() -} - -fn update_slide_relationships_xml( - existing_bytes: Vec, - relationships: &[String], -) -> Result { - let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?; - let injected = relationships.join("\n"); - existing - .contains("") - .then(|| existing.replace("", &format!("{injected}\n"))) - .ok_or_else(|| { - "slide relationships xml is missing a closing ``".to_string() - }) -} - -fn slide_relationships_xml(relationships: &[String]) -> String { - let body = relationships.join("\n"); - format!( - r#" - -{body} -"# - ) -} - -fn update_content_types_xml( - existing_bytes: Vec, - image_extensions: &BTreeSet, -) -> Result { - let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?; - if image_extensions.is_empty() { - return Ok(existing); - } - let existing_lower = existing.to_ascii_lowercase(); - let additions = image_extensions - .iter() - .filter(|extension| { - !existing_lower.contains(&format!( - r#"extension="{}""#, - extension.to_ascii_lowercase() - )) - }) - .map(|extension| generate_image_content_type(extension)) - .collect::>(); - if additions.is_empty() { - return Ok(existing); - } - existing - .contains("") - .then(|| existing.replace("", &format!("{}\n", additions.join("\n")))) - .ok_or_else(|| "content types xml is missing a closing ``".to_string()) -} - -fn update_slide_xml( - existing_bytes: Vec, - slide: &PresentationSlide, - slide_images: &[SlideImageAsset], -) -> Result { - let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?; - let existing = replace_image_placeholders(existing, slide_images)?; - let existing = apply_shape_block_patches(existing, slide)?; - let table_xml = slide_table_xml(slide); - if table_xml.is_empty() { - return Ok(existing); - } - existing - .contains("") - .then(|| existing.replace("", &format!("{table_xml}\n"))) - .ok_or_else(|| "slide xml is missing a closing ``".to_string()) -} - -fn replace_image_placeholders( - existing: String, - slide_images: &[SlideImageAsset], -) -> Result { - if slide_images.is_empty() { - return Ok(existing); - } - let mut updated = String::with_capacity(existing.len()); - let mut remaining = existing.as_str(); - for image in slide_images { - let marker = remaining - .find("name=\"Image Placeholder: ") - .ok_or_else(|| { - "slide xml is missing an image placeholder block for exported images".to_string() - })?; - let start = remaining[..marker].rfind("").ok_or_else(|| { - "slide xml is missing an opening `` for image placeholder".to_string() - })?; - let end = remaining[marker..] - .find("") - .map(|offset| marker + offset + "".len()) - .ok_or_else(|| { - "slide xml is missing a closing `` for image placeholder".to_string() - })?; - updated.push_str(&remaining[..start]); - updated.push_str(&image.xml); - remaining = &remaining[end..]; - } - updated.push_str(remaining); - Ok(updated) -} - -#[derive(Clone, Copy)] -struct ShapeXmlPatch { - line_style: Option, - flip_horizontal: bool, - flip_vertical: bool, -} - -fn apply_shape_block_patches( - existing: String, - slide: &PresentationSlide, -) -> Result { - let mut patches = Vec::new(); - if slide.background_fill.is_some() { - patches.push(None); - } - let mut ordered = slide.elements.iter().collect::>(); - ordered.sort_by_key(|element| element.z_order()); - for element in ordered { - match element { - PresentationElement::Text(_) => patches.push(None), - PresentationElement::Shape(shape) => patches.push(Some(ShapeXmlPatch { - line_style: shape - .stroke - .as_ref() - .map(|stroke| stroke.style) - .filter(|style| *style != LineStyle::Solid), - flip_horizontal: shape.flip_horizontal, - flip_vertical: shape.flip_vertical, - })), - PresentationElement::Image(ImageElement { payload: None, .. }) => patches.push(None), - PresentationElement::Connector(_) - | PresentationElement::Image(_) - | PresentationElement::Table(_) - | PresentationElement::Chart(_) => {} - } - } - if patches.iter().all(|patch| { - patch.is_none_or(|patch| { - patch.line_style.is_none() && !patch.flip_horizontal && !patch.flip_vertical - }) - }) { - return Ok(existing); - } - - let mut updated = String::with_capacity(existing.len()); - let mut remaining = existing.as_str(); - for patch in patches { - let Some(start) = remaining.find("") else { - return Err("slide xml is missing an expected `` block".to_string()); - }; - let end = remaining[start..] - .find("") - .map(|offset| start + offset + "".len()) - .ok_or_else(|| "slide xml is missing a closing `` block".to_string())?; - updated.push_str(&remaining[..start]); - let block = &remaining[start..end]; - if let Some(patch) = patch { - updated.push_str(&patch_shape_block(block, patch)?); - } else { - updated.push_str(block); - } - remaining = &remaining[end..]; - } - updated.push_str(remaining); - Ok(updated) -} - -fn patch_shape_block(block: &str, patch: ShapeXmlPatch) -> Result { - let block = if let Some(line_style) = patch.line_style { - patch_shape_block_dash(block, line_style)? - } else { - block.to_string() - }; - if patch.flip_horizontal || patch.flip_vertical { - patch_shape_block_flip(&block, patch.flip_horizontal, patch.flip_vertical) - } else { - Ok(block) - } -} - -fn patch_shape_block_dash(block: &str, line_style: LineStyle) -> Result { - let Some(line_start) = block.find("` entry for stroke styling".to_string()); - }; - if let Some(dash_start) = block[line_start..].find("") - .map(|offset| dash_start + offset + 2) - .ok_or_else(|| "shape line dash entry is missing a closing `/>`".to_string())?; - let mut patched = String::with_capacity(block.len() + 32); - patched.push_str(&block[..dash_start]); - patched.push_str(&format!( - r#""#, - line_style.to_ppt_xml() - )); - patched.push_str(&block[dash_end..]); - return Ok(patched); - } - - if let Some(line_end) = block[line_start..].find("") { - let line_end = line_start + line_end; - let mut patched = String::with_capacity(block.len() + 32); - patched.push_str(&block[..line_end]); - patched.push_str(&format!( - r#""#, - line_style.to_ppt_xml() - )); - patched.push_str(&block[line_end..]); - return Ok(patched); - } - - let line_end = block[line_start..] - .find("/>") - .map(|offset| line_start + offset + 2) - .ok_or_else(|| "shape line entry is missing a closing marker".to_string())?; - let line_tag = &block[line_start..line_end - 2]; - let mut patched = String::with_capacity(block.len() + 48); - patched.push_str(&block[..line_start]); - patched.push_str(line_tag); - patched.push('>'); - patched.push_str(&format!( - r#""#, - line_style.to_ppt_xml() - )); - patched.push_str(""); - patched.push_str(&block[line_end..]); - Ok(patched) -} - -fn patch_shape_block_flip( - block: &str, - flip_horizontal: bool, - flip_vertical: bool, -) -> Result { - let Some(xfrm_start) = block.find("` entry for flip styling".to_string()); - }; - let tag_end = block[xfrm_start..] - .find('>') - .map(|offset| xfrm_start + offset) - .ok_or_else(|| "shape transform entry is missing a closing `>`".to_string())?; - let tag = &block[xfrm_start..=tag_end]; - let mut patched_tag = tag.to_string(); - patched_tag = upsert_xml_attribute( - &patched_tag, - "flipH", - if flip_horizontal { "1" } else { "0" }, - ); - patched_tag = - upsert_xml_attribute(&patched_tag, "flipV", if flip_vertical { "1" } else { "0" }); - Ok(format!( - "{}{}{}", - &block[..xfrm_start], - patched_tag, - &block[tag_end + 1..] - )) -} - -fn upsert_xml_attribute(tag: &str, attribute: &str, value: &str) -> String { - let needle = format!(r#"{attribute}=""#); - if let Some(start) = tag.find(&needle) { - let value_start = start + needle.len(); - if let Some(end_offset) = tag[value_start..].find('"') { - let end = value_start + end_offset; - return format!("{}{}{}", &tag[..value_start], value, &tag[end..]); - } - } - let insert_at = tag.len() - 1; - format!(r#"{} {attribute}="{value}""#, &tag[..insert_at]) + &tag[insert_at..] -} - -fn slide_table_xml(slide: &PresentationSlide) -> String { - let mut ordered = slide.elements.iter().collect::>(); - ordered.sort_by_key(|element| element.z_order()); - let mut table_index = 0_usize; - ordered - .into_iter() - .filter_map(|element| { - let PresentationElement::Table(table) = element else { - return None; - }; - table_index += 1; - let rows = table - .rows - .clone() - .into_iter() - .enumerate() - .map(|(row_index, row)| { - let cells = row - .into_iter() - .enumerate() - .map(|(column_index, cell)| { - build_table_cell(cell, &table.merges, row_index, column_index) - }) - .collect::>(); - let mut table_row = TableRow::new(cells); - if let Some(height) = table.row_heights.get(row_index) { - table_row = table_row.with_height(points_to_emu(*height)); - } - Some(table_row) - }) - .collect::>>()?; - Some(ppt_rs::generator::table::generate_table_xml( - &ppt_rs::generator::table::Table::new( - rows, - table - .column_widths - .iter() - .copied() - .map(points_to_emu) - .collect(), - points_to_emu(table.frame.left), - points_to_emu(table.frame.top), - ), - 300 + table_index, - )) - }) - .collect::>() - .join("\n") -} - -fn write_preview_images( - document: &PresentationDocument, - output_dir: &Path, - action: &str, -) -> Result<(), PresentationArtifactError> { - let pptx_path = output_dir.join("preview.pptx"); - let bytes = build_pptx_bytes(document, action).map_err(|message| { - PresentationArtifactError::ExportFailed { - path: pptx_path.clone(), - message, - } - })?; - std::fs::write(&pptx_path, bytes).map_err(|error| PresentationArtifactError::ExportFailed { - path: pptx_path.clone(), - message: error.to_string(), - })?; - render_pptx_to_pngs(&pptx_path, output_dir, action) -} - -fn render_pptx_to_pngs( - pptx_path: &Path, - output_dir: &Path, - action: &str, -) -> Result<(), PresentationArtifactError> { - let soffice_cmd = if cfg!(target_os = "macos") - && Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists() - { - "/Applications/LibreOffice.app/Contents/MacOS/soffice" - } else { - "soffice" - }; - let conversion = Command::new(soffice_cmd) - .arg("--headless") - .arg("--convert-to") - .arg("pdf") - .arg(pptx_path) - .arg("--outdir") - .arg(output_dir) - .output() - .map_err(|error| PresentationArtifactError::ExportFailed { - path: pptx_path.to_path_buf(), - message: format!("{action}: failed to execute LibreOffice: {error}"), - })?; - if !conversion.status.success() { - return Err(PresentationArtifactError::ExportFailed { - path: pptx_path.to_path_buf(), - message: format!( - "{action}: LibreOffice conversion failed: {}", - String::from_utf8_lossy(&conversion.stderr) - ), - }); - } - - let pdf_path = output_dir.join( - pptx_path - .file_stem() - .and_then(|stem| stem.to_str()) - .map(|stem| format!("{stem}.pdf")) - .ok_or_else(|| PresentationArtifactError::ExportFailed { - path: pptx_path.to_path_buf(), - message: format!("{action}: preview pptx filename is invalid"), - })?, - ); - let prefix = output_dir.join("slide"); - let conversion = Command::new("pdftoppm") - .arg("-png") - .arg(&pdf_path) - .arg(&prefix) - .output() - .map_err(|error| PresentationArtifactError::ExportFailed { - path: pdf_path.clone(), - message: format!("{action}: failed to execute pdftoppm: {error}"), - })?; - std::fs::remove_file(&pdf_path).ok(); - if !conversion.status.success() { - return Err(PresentationArtifactError::ExportFailed { - path: output_dir.to_path_buf(), - message: format!( - "{action}: pdftoppm conversion failed: {}", - String::from_utf8_lossy(&conversion.stderr) - ), - }); - } - Ok(()) -} - -pub(crate) fn write_preview_image( - source_path: &Path, - target_path: &Path, - format: PreviewOutputFormat, - scale: f32, - quality: u8, - action: &str, -) -> Result<(), PresentationArtifactError> { - if matches!(format, PreviewOutputFormat::Png) && scale == 1.0 { - std::fs::rename(source_path, target_path).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: target_path.to_path_buf(), - message: error.to_string(), - } - })?; - return Ok(()); - } - let mut preview = - image::open(source_path).map_err(|error| PresentationArtifactError::ExportFailed { - path: source_path.to_path_buf(), - message: format!("{action}: {error}"), - })?; - if scale != 1.0 { - let width = (preview.width() as f32 * scale).round().max(1.0) as u32; - let height = (preview.height() as f32 * scale).round().max(1.0) as u32; - preview = preview.resize_exact(width, height, FilterType::Lanczos3); - } - let file = std::fs::File::create(target_path).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: target_path.to_path_buf(), - message: error.to_string(), - } - })?; - let mut writer = std::io::BufWriter::new(file); - match format { - PreviewOutputFormat::Png => { - preview - .write_to(&mut writer, ImageFormat::Png) - .map_err(|error| PresentationArtifactError::ExportFailed { - path: target_path.to_path_buf(), - message: format!("{action}: {error}"), - })? - } - PreviewOutputFormat::Jpeg => { - let rgb = preview.to_rgb8(); - let mut encoder = JpegEncoder::new_with_quality(&mut writer, quality); - encoder.encode_image(&rgb).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: target_path.to_path_buf(), - message: format!("{action}: {error}"), - } - })?; - } - PreviewOutputFormat::Svg => { - let mut png_bytes = Cursor::new(Vec::new()); - preview - .write_to(&mut png_bytes, ImageFormat::Png) - .map_err(|error| PresentationArtifactError::ExportFailed { - path: target_path.to_path_buf(), - message: format!("{action}: {error}"), - })?; - let embedded_png = BASE64_STANDARD.encode(png_bytes.into_inner()); - let svg = format!( - r#""#, - preview.width(), - preview.height(), - preview.width(), - preview.height(), - preview.width(), - preview.height(), - ); - writer.write_all(svg.as_bytes()).map_err(|error| { - PresentationArtifactError::ExportFailed { - path: target_path.to_path_buf(), - message: format!("{action}: {error}"), - } - })?; - } - } - std::fs::remove_file(source_path).ok(); - Ok(()) -} - -fn collect_pngs(output_dir: &Path) -> Result, PresentationArtifactError> { - let mut files = std::fs::read_dir(output_dir) - .map_err(|error| PresentationArtifactError::ExportFailed { - path: output_dir.to_path_buf(), - message: error.to_string(), - })? - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("png")) - .collect::>(); - files.sort(); - Ok(files) -} - -fn parse_preview_output_format( - format: Option<&str>, - path: &Path, - action: &str, -) -> Result { - let value = format - .map(str::to_owned) - .or_else(|| { - path.extension() - .and_then(|extension| extension.to_str()) - .map(str::to_owned) - }) - .unwrap_or_else(|| "png".to_string()); - match value.to_ascii_lowercase().as_str() { - "png" => Ok(PreviewOutputFormat::Png), - "jpg" | "jpeg" => Ok(PreviewOutputFormat::Jpeg), - "svg" => Ok(PreviewOutputFormat::Svg), - other => Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("preview format `{other}` is not supported"), - }), - } -} - -fn normalize_preview_scale( - scale: Option, - action: &str, -) -> Result { - let scale = scale.unwrap_or(1.0); - if !scale.is_finite() || scale <= 0.0 { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`scale` must be a positive number".to_string(), - }); - } - Ok(scale) -} - -fn normalize_preview_quality( - quality: Option, - action: &str, -) -> Result { - let quality = quality.unwrap_or(90); - if quality == 0 || quality > 100 { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "`quality` must be between 1 and 100".to_string(), - }); - } - Ok(quality) -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/proto.rs b/codex-rs/artifact-presentation/src/presentation_artifact/proto.rs deleted file mode 100644 index 05741303a..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/proto.rs +++ /dev/null @@ -1,614 +0,0 @@ -fn document_to_proto( - document: &PresentationDocument, - action: &str, -) -> Result { - let layouts = document - .layouts - .iter() - .map(|layout| layout_to_proto(document, layout, action)) - .collect::, _>>()?; - let slides = document - .slides - .iter() - .enumerate() - .map(|(slide_index, slide)| slide_to_proto(slide, slide_index)) - .collect::>(); - Ok(serde_json::json!({ - "kind": "presentation", - "artifactId": document.artifact_id, - "anchor": format!("pr/{}", document.artifact_id), - "name": document.name, - "slideSize": rect_to_proto(document.slide_size), - "activeSlideIndex": document.active_slide_index, - "activeSlideId": document.active_slide_index.and_then(|index| document.slides.get(index)).map(|slide| slide.slide_id.clone()), - "theme": serde_json::json!({ - "colorScheme": document.theme.color_scheme, - "hexColorMap": document.theme.color_scheme, - "majorFont": document.theme.major_font, - "minorFont": document.theme.minor_font, - }), - "styles": document - .named_text_styles() - .iter() - .map(|style| named_text_style_to_json(style, "st")) - .collect::>(), - "masters": document.layouts.iter().filter(|layout| layout.kind == LayoutKind::Master).map(|layout| layout.layout_id.clone()).collect::>(), - "layouts": layouts, - "slides": slides, - "commentAuthor": document.comment_self.as_ref().map(comment_author_to_proto), - "commentThreads": document - .comment_threads - .iter() - .map(comment_thread_to_proto) - .collect::>(), - })) -} - -fn layout_to_proto( - document: &PresentationDocument, - layout: &LayoutDocument, - action: &str, -) -> Result { - let placeholders = layout - .placeholders - .iter() - .map(placeholder_definition_to_proto) - .collect::>(); - let resolved_placeholders = resolved_layout_placeholders(document, &layout.layout_id, action)? - .into_iter() - .map(|placeholder| { - let mut value = placeholder_definition_to_proto(&placeholder.definition); - value["sourceLayoutId"] = Value::String(placeholder.source_layout_id); - value - }) - .collect::>(); - Ok(serde_json::json!({ - "layoutId": layout.layout_id, - "anchor": format!("ly/{}", layout.layout_id), - "name": layout.name, - "kind": match layout.kind { - LayoutKind::Layout => "layout", - LayoutKind::Master => "master", - }, - "parentLayoutId": layout.parent_layout_id, - "placeholders": placeholders, - "resolvedPlaceholders": resolved_placeholders, - })) -} - -fn placeholder_definition_to_proto(placeholder: &PlaceholderDefinition) -> Value { - serde_json::json!({ - "name": placeholder.name, - "placeholderType": placeholder.placeholder_type, - "index": placeholder.index, - "text": placeholder.text, - "geometry": format!("{:?}", placeholder.geometry), - "frame": rect_to_proto(placeholder.frame), - }) -} - -fn slide_to_proto(slide: &PresentationSlide, slide_index: usize) -> Value { - serde_json::json!({ - "slideId": slide.slide_id, - "anchor": format!("sl/{}", slide.slide_id), - "index": slide_index, - "layoutId": slide.layout_id, - "backgroundFill": slide.background_fill, - "notes": serde_json::json!({ - "anchor": format!("nt/{}", slide.slide_id), - "text": slide.notes.text, - "visible": slide.notes.visible, - "textPreview": slide.notes.text.replace('\n', " | "), - "textChars": slide.notes.text.chars().count(), - "textLines": slide.notes.text.lines().count(), - "richText": rich_text_to_proto(&slide.notes.text, &slide.notes.rich_text), - }), - "elements": slide.elements.iter().map(element_to_proto).collect::>(), - }) -} - -fn element_to_proto(element: &PresentationElement) -> Value { - match element { - PresentationElement::Text(text) => { - let mut record = serde_json::json!({ - "kind": "text", - "elementId": text.element_id, - "anchor": format!("sh/{}", text.element_id), - "frame": rect_to_proto(text.frame), - "text": text.text, - "textPreview": text.text.replace('\n', " | "), - "textChars": text.text.chars().count(), - "textLines": text.text.lines().count(), - "fill": text.fill, - "style": text_style_to_proto(&text.style), - "richText": rich_text_to_proto(&text.text, &text.rich_text), - "zOrder": text.z_order, - }); - if let Some(placeholder) = &text.placeholder { - record["placeholder"] = placeholder_ref_to_proto(placeholder); - } - if let Some(hyperlink) = &text.hyperlink { - record["hyperlink"] = hyperlink.to_json(); - } - record - } - PresentationElement::Shape(shape) => { - let mut record = serde_json::json!({ - "kind": "shape", - "elementId": shape.element_id, - "anchor": format!("sh/{}", shape.element_id), - "geometry": format!("{:?}", shape.geometry), - "frame": rect_to_proto(shape.frame), - "fill": shape.fill, - "stroke": shape.stroke.as_ref().map(stroke_to_proto), - "text": shape.text, - "textStyle": text_style_to_proto(&shape.text_style), - "richText": shape - .text - .as_ref() - .zip(shape.rich_text.as_ref()) - .map(|(text, rich_text)| rich_text_to_proto(text, rich_text)) - .unwrap_or(Value::Null), - "rotation": shape.rotation_degrees, - "flipHorizontal": shape.flip_horizontal, - "flipVertical": shape.flip_vertical, - "zOrder": shape.z_order, - }); - if let Some(text) = &shape.text { - record["textPreview"] = Value::String(text.replace('\n', " | ")); - record["textChars"] = Value::from(text.chars().count()); - record["textLines"] = Value::from(text.lines().count()); - } - if let Some(placeholder) = &shape.placeholder { - record["placeholder"] = placeholder_ref_to_proto(placeholder); - } - if let Some(hyperlink) = &shape.hyperlink { - record["hyperlink"] = hyperlink.to_json(); - } - record - } - PresentationElement::Connector(connector) => serde_json::json!({ - "kind": "connector", - "elementId": connector.element_id, - "anchor": format!("cn/{}", connector.element_id), - "connectorType": format!("{:?}", connector.connector_type), - "start": serde_json::json!({ - "left": connector.start.left, - "top": connector.start.top, - "unit": "points", - }), - "end": serde_json::json!({ - "left": connector.end.left, - "top": connector.end.top, - "unit": "points", - }), - "line": stroke_to_proto(&connector.line), - "lineStyle": connector.line_style.as_api_str(), - "startArrow": format!("{:?}", connector.start_arrow), - "endArrow": format!("{:?}", connector.end_arrow), - "arrowSize": format!("{:?}", connector.arrow_size), - "label": connector.label, - "zOrder": connector.z_order, - }), - PresentationElement::Image(image) => { - let mut record = serde_json::json!({ - "kind": "image", - "elementId": image.element_id, - "anchor": format!("im/{}", image.element_id), - "frame": rect_to_proto(image.frame), - "fit": format!("{:?}", image.fit_mode), - "crop": image.crop.map(|(left, top, right, bottom)| serde_json::json!({ - "left": left, - "top": top, - "right": right, - "bottom": bottom, - })), - "rotation": image.rotation_degrees, - "flipHorizontal": image.flip_horizontal, - "flipVertical": image.flip_vertical, - "lockAspectRatio": image.lock_aspect_ratio, - "alt": image.alt_text, - "prompt": image.prompt, - "isPlaceholder": image.is_placeholder, - "payload": image.payload.as_ref().map(image_payload_to_proto), - "zOrder": image.z_order, - }); - if let Some(placeholder) = &image.placeholder { - record["placeholder"] = placeholder_ref_to_proto(placeholder); - } - record - } - PresentationElement::Table(table) => serde_json::json!({ - "kind": "table", - "elementId": table.element_id, - "anchor": format!("tb/{}", table.element_id), - "frame": rect_to_proto(table.frame), - "rows": table.rows.iter().map(|row| { - row.iter().map(table_cell_to_proto).collect::>() - }).collect::>(), - "columnWidths": table.column_widths, - "rowHeights": table.row_heights, - "style": table.style, - "styleOptions": table_style_options_to_proto(&table.style_options), - "borders": table.borders.as_ref().map(table_borders_to_proto), - "rightToLeft": table.right_to_left, - "merges": table.merges.iter().map(|merge| serde_json::json!({ - "startRow": merge.start_row, - "endRow": merge.end_row, - "startColumn": merge.start_column, - "endColumn": merge.end_column, - })).collect::>(), - "zOrder": table.z_order, - }), - PresentationElement::Chart(chart) => serde_json::json!({ - "kind": "chart", - "elementId": chart.element_id, - "anchor": format!("ch/{}", chart.element_id), - "frame": rect_to_proto(chart.frame), - "chartType": format!("{:?}", chart.chart_type), - "title": chart.title, - "categories": chart.categories, - "styleIndex": chart.style_index, - "hasLegend": chart.has_legend, - "legend": chart.legend.as_ref().map(chart_legend_to_proto), - "xAxis": chart.x_axis.as_ref().map(chart_axis_to_proto), - "yAxis": chart.y_axis.as_ref().map(chart_axis_to_proto), - "dataLabels": chart.data_labels.as_ref().map(chart_data_labels_to_proto), - "chartFill": chart.chart_fill, - "plotAreaFill": chart.plot_area_fill, - "series": chart.series.iter().map(|series| serde_json::json!({ - "name": series.name, - "values": series.values, - "categories": series.categories, - "xValues": series.x_values, - "fill": series.fill, - "stroke": series.stroke.as_ref().map(stroke_to_proto), - "marker": series.marker.as_ref().map(chart_marker_to_proto), - "dataLabelOverrides": series - .data_label_overrides - .iter() - .map(chart_data_label_override_to_proto) - .collect::>(), - })).collect::>(), - "zOrder": chart.z_order, - }), - } -} - -fn rect_to_proto(rect: Rect) -> Value { - serde_json::json!({ - "left": rect.left, - "top": rect.top, - "width": rect.width, - "height": rect.height, - "unit": "points", - }) -} - -fn stroke_to_proto(stroke: &StrokeStyle) -> Value { - serde_json::json!({ - "color": stroke.color, - "width": stroke.width, - "style": stroke.style.as_api_str(), - "unit": "points", - }) -} - -fn text_style_to_proto(style: &TextStyle) -> Value { - serde_json::json!({ - "styleName": style.style_name, - "fontSize": style.font_size, - "fontFamily": style.font_family, - "color": style.color, - "alignment": style.alignment, - "bold": style.bold, - "italic": style.italic, - "underline": style.underline, - }) -} - -fn rich_text_to_proto(text: &str, rich_text: &RichTextState) -> Value { - serde_json::json!({ - "layout": text_layout_to_proto(&rich_text.layout), - "ranges": rich_text - .ranges - .iter() - .map(|range| text_range_to_proto(text, range)) - .collect::>(), - }) -} - -fn text_range_to_proto(text: &str, range: &TextRangeAnnotation) -> Value { - serde_json::json!({ - "rangeId": range.range_id, - "anchor": format!("tr/{}", range.range_id), - "startCp": range.start_cp, - "length": range.length, - "text": text_slice_by_codepoint_range(text, range.start_cp, range.length), - "style": text_style_to_proto(&range.style), - "hyperlink": range.hyperlink.as_ref().map(HyperlinkState::to_json), - "spacingBefore": range.spacing_before, - "spacingAfter": range.spacing_after, - "lineSpacing": range.line_spacing, - }) -} - -fn text_layout_to_proto(layout: &TextLayoutState) -> Value { - serde_json::json!({ - "insets": layout.insets.map(|insets| serde_json::json!({ - "left": insets.left, - "right": insets.right, - "top": insets.top, - "bottom": insets.bottom, - "unit": "points", - })), - "wrap": layout.wrap.map(text_wrap_mode_to_proto), - "autoFit": layout.auto_fit.map(text_auto_fit_mode_to_proto), - "verticalAlignment": layout - .vertical_alignment - .map(text_vertical_alignment_to_proto), - }) -} - -fn text_wrap_mode_to_proto(mode: TextWrapMode) -> &'static str { - match mode { - TextWrapMode::Square => "square", - TextWrapMode::None => "none", - } -} - -fn text_auto_fit_mode_to_proto(mode: TextAutoFitMode) -> &'static str { - match mode { - TextAutoFitMode::None => "none", - TextAutoFitMode::ShrinkText => "shrinkText", - TextAutoFitMode::ResizeShapeToFitText => "resizeShapeToFitText", - } -} - -fn text_vertical_alignment_to_proto(alignment: TextVerticalAlignment) -> &'static str { - match alignment { - TextVerticalAlignment::Top => "top", - TextVerticalAlignment::Middle => "middle", - TextVerticalAlignment::Bottom => "bottom", - } -} - -fn placeholder_ref_to_proto(placeholder: &PlaceholderRef) -> Value { - serde_json::json!({ - "name": placeholder.name, - "placeholderType": placeholder.placeholder_type, - "index": placeholder.index, - }) -} - -fn image_payload_to_proto(payload: &ImagePayload) -> Value { - serde_json::json!({ - "format": payload.format, - "widthPx": payload.width_px, - "heightPx": payload.height_px, - "bytesBase64": BASE64_STANDARD.encode(&payload.bytes), - }) -} - -fn table_cell_to_proto(cell: &TableCellSpec) -> Value { - serde_json::json!({ - "text": cell.text, - "textStyle": text_style_to_proto(&cell.text_style), - "richText": rich_text_to_proto(&cell.text, &cell.rich_text), - "backgroundFill": cell.background_fill, - "alignment": cell.alignment, - "borders": cell.borders.as_ref().map(table_borders_to_proto), - }) -} - -fn table_style_options_to_proto(style_options: &TableStyleOptions) -> Value { - serde_json::json!({ - "headerRow": style_options.header_row, - "bandedRows": style_options.banded_rows, - "bandedColumns": style_options.banded_columns, - "firstColumn": style_options.first_column, - "lastColumn": style_options.last_column, - "totalRow": style_options.total_row, - }) -} - -fn table_borders_to_proto(borders: &TableBorders) -> Value { - serde_json::json!({ - "outside": borders.outside.as_ref().map(table_border_to_proto), - "inside": borders.inside.as_ref().map(table_border_to_proto), - "top": borders.top.as_ref().map(table_border_to_proto), - "bottom": borders.bottom.as_ref().map(table_border_to_proto), - "left": borders.left.as_ref().map(table_border_to_proto), - "right": borders.right.as_ref().map(table_border_to_proto), - }) -} - -fn table_border_to_proto(border: &TableBorder) -> Value { - serde_json::json!({ - "color": border.color, - "width": border.width, - "unit": "points", - }) -} - -fn chart_marker_to_proto(marker: &ChartMarkerStyle) -> Value { - serde_json::json!({ - "symbol": marker.symbol, - "size": marker.size, - }) -} - -fn chart_data_labels_to_proto(data_labels: &ChartDataLabels) -> Value { - serde_json::json!({ - "showValue": data_labels.show_value, - "showCategoryName": data_labels.show_category_name, - "showLeaderLines": data_labels.show_leader_lines, - "position": data_labels.position, - "textStyle": text_style_to_proto(&data_labels.text_style), - }) -} - -fn chart_legend_to_proto(legend: &ChartLegend) -> Value { - serde_json::json!({ - "position": legend.position, - "textStyle": text_style_to_proto(&legend.text_style), - }) -} - -fn chart_axis_to_proto(axis: &ChartAxisSpec) -> Value { - serde_json::json!({ - "title": axis.title, - }) -} - -fn chart_data_label_override_to_proto(override_spec: &ChartDataLabelOverride) -> Value { - serde_json::json!({ - "idx": override_spec.idx, - "text": override_spec.text, - "position": override_spec.position, - "textStyle": text_style_to_proto(&override_spec.text_style), - "fill": override_spec.fill, - "stroke": override_spec.stroke.as_ref().map(stroke_to_proto), - }) -} - -fn comment_author_to_proto(author: &CommentAuthorProfile) -> Value { - serde_json::json!({ - "displayName": author.display_name, - "initials": author.initials, - "email": author.email, - }) -} - -fn comment_thread_to_proto(thread: &CommentThread) -> Value { - serde_json::json!({ - "kind": "comment", - "threadId": thread.thread_id, - "anchor": format!("th/{}", thread.thread_id), - "target": comment_target_to_proto(&thread.target), - "position": thread.position.as_ref().map(comment_position_to_proto), - "status": comment_status_to_proto(thread.status), - "messages": thread.messages.iter().map(comment_message_to_proto).collect::>(), - }) -} - -fn comment_target_to_proto(target: &CommentTarget) -> Value { - match target { - CommentTarget::Slide { slide_id } => serde_json::json!({ - "type": "slide", - "slideId": slide_id, - "slideAnchor": format!("sl/{slide_id}"), - }), - CommentTarget::Element { - slide_id, - element_id, - } => serde_json::json!({ - "type": "element", - "slideId": slide_id, - "slideAnchor": format!("sl/{slide_id}"), - "elementId": element_id, - "elementAnchor": format!("sh/{element_id}"), - }), - CommentTarget::TextRange { - slide_id, - element_id, - start_cp, - length, - context, - } => serde_json::json!({ - "type": "textRange", - "slideId": slide_id, - "slideAnchor": format!("sl/{slide_id}"), - "elementId": element_id, - "elementAnchor": format!("sh/{element_id}"), - "startCp": start_cp, - "length": length, - "context": context, - }), - } -} - -fn comment_position_to_proto(position: &CommentPosition) -> Value { - serde_json::json!({ - "x": position.x, - "y": position.y, - "unit": "points", - }) -} - -fn comment_message_to_proto(message: &CommentMessage) -> Value { - serde_json::json!({ - "messageId": message.message_id, - "author": comment_author_to_proto(&message.author), - "text": message.text, - "createdAt": message.created_at, - "reactions": message.reactions, - }) -} - -fn comment_status_to_proto(status: CommentThreadStatus) -> &'static str { - match status { - CommentThreadStatus::Active => "active", - CommentThreadStatus::Resolved => "resolved", - } -} - -fn text_slice_by_codepoint_range(text: &str, start_cp: usize, length: usize) -> String { - text.chars().skip(start_cp).take(length).collect() -} - -fn build_table_cell( - cell: TableCellSpec, - merges: &[TableMergeRegion], - row_index: usize, - column_index: usize, -) -> TableCell { - let mut table_cell = TableCell::new(&cell.text); - if cell.text_style.bold { - table_cell = table_cell.bold(); - } - if cell.text_style.italic { - table_cell = table_cell.italic(); - } - if cell.text_style.underline { - table_cell = table_cell.underline(); - } - if let Some(color) = cell.text_style.color { - table_cell = table_cell.text_color(&color); - } - if let Some(fill) = cell.background_fill { - table_cell = table_cell.background_color(&fill); - } - if let Some(size) = cell.text_style.font_size { - table_cell = table_cell.font_size(size); - } - if let Some(font_family) = cell.text_style.font_family { - table_cell = table_cell.font_family(&font_family); - } - if let Some(alignment) = cell.alignment.or(cell.text_style.alignment) { - table_cell = match alignment { - TextAlignment::Left => table_cell.align_left(), - TextAlignment::Center => table_cell.align_center(), - TextAlignment::Right => table_cell.align_right(), - TextAlignment::Justify => table_cell.align(CellAlign::Justify), - }; - } - for merge in merges { - if row_index == merge.start_row && column_index == merge.start_column { - table_cell = table_cell - .grid_span((merge.end_column - merge.start_column + 1) as u32) - .row_span((merge.end_row - merge.start_row + 1) as u32); - } else if row_index >= merge.start_row - && row_index <= merge.end_row - && column_index >= merge.start_column - && column_index <= merge.end_column - { - if row_index == merge.start_row { - table_cell = table_cell.h_merge(); - } else { - table_cell = table_cell.v_merge(); - } - } - } - table_cell -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/response.rs b/codex-rs/artifact-presentation/src/presentation_artifact/response.rs deleted file mode 100644 index 241cce570..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/response.rs +++ /dev/null @@ -1,138 +0,0 @@ -#[derive(Debug, Clone, Serialize)] -pub struct PresentationArtifactResponse { - pub artifact_id: String, - pub action: String, - pub summary: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub executed_actions: Option>, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub exported_paths: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub artifact_snapshot: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub slide_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub layout_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub placeholder_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub theme: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub inspect_ndjson: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub resolved_record: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub proto_json: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub patch: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub active_slide_index: Option, -} - -impl PresentationArtifactResponse { - fn new( - artifact_id: String, - action: String, - summary: String, - artifact_snapshot: ArtifactSnapshot, - ) -> Self { - Self { - artifact_id, - action, - summary, - executed_actions: None, - exported_paths: Vec::new(), - artifact_snapshot: Some(artifact_snapshot), - slide_list: None, - layout_list: None, - placeholder_list: None, - theme: None, - inspect_ndjson: None, - resolved_record: None, - proto_json: None, - patch: None, - active_slide_index: None, - } - } -} - -fn response_for_document_state( - artifact_id: String, - action: String, - summary: String, - document: Option<&PresentationDocument>, -) -> PresentationArtifactResponse { - PresentationArtifactResponse { - artifact_id, - action, - summary, - executed_actions: None, - exported_paths: Vec::new(), - artifact_snapshot: document.map(snapshot_for_document), - slide_list: None, - layout_list: None, - placeholder_list: None, - theme: document.map(PresentationDocument::theme_snapshot), - inspect_ndjson: None, - resolved_record: None, - proto_json: None, - patch: None, - active_slide_index: document.and_then(|current| current.active_slide_index), - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct ArtifactSnapshot { - pub slide_count: usize, - pub slides: Vec, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SlideSnapshot { - pub slide_id: String, - pub index: usize, - pub element_ids: Vec, - pub element_types: Vec, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SlideListEntry { - pub slide_id: String, - pub index: usize, - pub is_active: bool, - pub notes: Option, - pub notes_visible: bool, - pub background_fill: Option, - pub layout_id: Option, - pub element_count: usize, -} - -#[derive(Debug, Clone, Serialize)] -pub struct LayoutListEntry { - pub layout_id: String, - pub name: String, - pub kind: String, - pub parent_layout_id: Option, - pub placeholder_count: usize, -} - -#[derive(Debug, Clone, Serialize)] -pub struct PlaceholderListEntry { - pub scope: String, - pub source_layout_id: Option, - pub slide_index: Option, - pub element_id: Option, - pub name: String, - pub placeholder_type: String, - pub index: Option, - pub geometry: Option, - pub text_preview: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ThemeSnapshot { - pub color_scheme: HashMap, - pub hex_color_map: HashMap, - pub major_font: Option, - pub minor_font: Option, -} diff --git a/codex-rs/artifact-presentation/src/presentation_artifact/snapshot.rs b/codex-rs/artifact-presentation/src/presentation_artifact/snapshot.rs deleted file mode 100644 index 8606649c3..000000000 --- a/codex-rs/artifact-presentation/src/presentation_artifact/snapshot.rs +++ /dev/null @@ -1,339 +0,0 @@ -fn cell_value_to_string(value: Value) -> String { - match value { - Value::Null => String::new(), - Value::String(text) => text, - Value::Bool(boolean) => boolean.to_string(), - Value::Number(number) => number.to_string(), - other => other.to_string(), - } -} - -fn snapshot_for_document(document: &PresentationDocument) -> ArtifactSnapshot { - ArtifactSnapshot { - slide_count: document.slides.len(), - slides: document - .slides - .iter() - .enumerate() - .map(|(index, slide)| SlideSnapshot { - slide_id: slide.slide_id.clone(), - index, - element_ids: slide - .elements - .iter() - .map(|element| element.element_id().to_string()) - .collect(), - element_types: slide - .elements - .iter() - .map(|element| element.kind().to_string()) - .collect(), - }) - .collect(), - } -} - -fn slide_list(document: &PresentationDocument) -> Vec { - document - .slides - .iter() - .enumerate() - .map(|(index, slide)| SlideListEntry { - slide_id: slide.slide_id.clone(), - index, - is_active: document.active_slide_index == Some(index), - notes: (slide.notes.visible && !slide.notes.text.is_empty()) - .then(|| slide.notes.text.clone()), - notes_visible: slide.notes.visible, - background_fill: slide.background_fill.clone(), - layout_id: slide.layout_id.clone(), - element_count: slide.elements.len(), - }) - .collect() -} - -fn layout_list(document: &PresentationDocument) -> Vec { - document - .layouts - .iter() - .map(|layout| LayoutListEntry { - layout_id: layout.layout_id.clone(), - name: layout.name.clone(), - kind: match layout.kind { - LayoutKind::Layout => "layout".to_string(), - LayoutKind::Master => "master".to_string(), - }, - parent_layout_id: layout.parent_layout_id.clone(), - placeholder_count: layout.placeholders.len(), - }) - .collect() -} - -fn points_to_emu(points: u32) -> u32 { - points.saturating_mul(POINT_TO_EMU) -} - -fn emu_to_points(emu: u32) -> u32 { - emu / POINT_TO_EMU -} - -type ImageCrop = (f64, f64, f64, f64); -type FittedImage = (u32, u32, u32, u32, Option); - -pub(crate) fn fit_image(image: &ImageElement) -> FittedImage { - let Some(payload) = image.payload.as_ref() else { - return ( - image.frame.left, - image.frame.top, - image.frame.width, - image.frame.height, - None, - ); - }; - let frame = image.frame; - let source_width = payload.width_px as f64; - let source_height = payload.height_px as f64; - let target_width = frame.width as f64; - let target_height = frame.height as f64; - let source_ratio = source_width / source_height; - let target_ratio = target_width / target_height; - - match image.fit_mode { - ImageFitMode::Stretch => (frame.left, frame.top, frame.width, frame.height, None), - ImageFitMode::Contain => { - let scale = if source_ratio > target_ratio { - target_width / source_width - } else { - target_height / source_height - }; - let width = (source_width * scale).round() as u32; - let height = (source_height * scale).round() as u32; - let left = frame.left + frame.width.saturating_sub(width) / 2; - let top = frame.top + frame.height.saturating_sub(height) / 2; - (left, top, width, height, None) - } - ImageFitMode::Cover => { - let scale = if source_ratio > target_ratio { - target_height / source_height - } else { - target_width / source_width - }; - let width = source_width * scale; - let height = source_height * scale; - let crop_x = ((width - target_width).max(0.0) / width) / 2.0; - let crop_y = ((height - target_height).max(0.0) / height) / 2.0; - ( - frame.left, - frame.top, - frame.width, - frame.height, - Some((crop_x, crop_y, crop_x, crop_y)), - ) - } - } -} - -fn normalize_image_crop( - crop: ImageCropArgs, - action: &str, -) -> Result { - for (name, value) in [ - ("left", crop.left), - ("top", crop.top), - ("right", crop.right), - ("bottom", crop.bottom), - ] { - if !(0.0..=1.0).contains(&value) { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("image crop `{name}` must be between 0.0 and 1.0"), - }); - } - } - Ok((crop.left, crop.top, crop.right, crop.bottom)) -} - -fn load_image_payload_from_path( - path: &Path, - action: &str, -) -> Result { - let bytes = std::fs::read(path).map_err(|error| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to read image `{}`: {error}", path.display()), - })?; - build_image_payload( - bytes, - path.file_name() - .and_then(|name| name.to_str()) - .unwrap_or("image") - .to_string(), - action, - ) -} - -fn load_image_payload_from_data_url( - data_url: &str, - action: &str, -) -> Result { - let (header, payload) = - data_url - .split_once(',') - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "data_url must include a MIME header and base64 payload".to_string(), - })?; - let mime = header - .strip_prefix("data:") - .and_then(|prefix| prefix.strip_suffix(";base64")) - .ok_or_else(|| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: "data_url must be base64-encoded".to_string(), - })?; - let bytes = BASE64_STANDARD.decode(payload).map_err(|error| { - PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to decode image data_url: {error}"), - } - })?; - build_image_payload( - bytes, - format!("image.{}", image_extension_from_mime(mime)), - action, - ) -} - -fn load_image_payload_from_blob( - blob: &str, - action: &str, -) -> Result { - let bytes = BASE64_STANDARD.decode(blob.trim()).map_err(|error| { - PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to decode image blob: {error}"), - } - })?; - let extension = image::guess_format(&bytes) - .ok() - .map(image_extension_from_format) - .unwrap_or("png"); - build_image_payload(bytes, format!("image.{extension}"), action) -} - -fn load_image_payload_from_uri( - uri: &str, - action: &str, -) -> Result { - let response = - reqwest::blocking::get(uri).map_err(|error| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to fetch image `{uri}`: {error}"), - })?; - let status = response.status(); - if !status.is_success() { - return Err(PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to fetch image `{uri}`: HTTP {status}"), - }); - } - let content_type = response - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .map(|value| value.split(';').next().unwrap_or(value).trim().to_string()); - let bytes = response - .bytes() - .map_err(|error| PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to read image `{uri}`: {error}"), - })?; - build_image_payload( - bytes.to_vec(), - infer_remote_image_filename(uri, content_type.as_deref()), - action, - ) -} - -fn infer_remote_image_filename(uri: &str, content_type: Option<&str>) -> String { - let path_name = reqwest::Url::parse(uri) - .ok() - .and_then(|url| { - url.path_segments() - .and_then(Iterator::last) - .map(str::to_owned) - }) - .filter(|segment| !segment.is_empty()); - match (path_name, content_type) { - (Some(path_name), _) if Path::new(&path_name).extension().is_some() => path_name, - (Some(path_name), Some(content_type)) => { - format!("{path_name}.{}", image_extension_from_mime(content_type)) - } - (Some(path_name), None) => path_name, - (None, Some(content_type)) => format!("image.{}", image_extension_from_mime(content_type)), - (None, None) => "image.png".to_string(), - } -} - -fn build_image_payload( - bytes: Vec, - filename: String, - action: &str, -) -> Result { - let image = image::load_from_memory(&bytes).map_err(|error| { - PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("failed to decode image bytes: {error}"), - } - })?; - let (width_px, height_px) = image.dimensions(); - let format = Path::new(&filename) - .extension() - .and_then(|extension| extension.to_str()) - .unwrap_or("png") - .to_uppercase(); - Ok(ImagePayload { - bytes, - format, - width_px, - height_px, - }) -} - -fn image_extension_from_mime(mime: &str) -> &'static str { - match mime { - "image/jpeg" => "jpg", - "image/gif" => "gif", - "image/webp" => "webp", - _ => "png", - } -} - -fn image_extension_from_format(format: image::ImageFormat) -> &'static str { - match format { - image::ImageFormat::Jpeg => "jpg", - image::ImageFormat::Gif => "gif", - image::ImageFormat::WebP => "webp", - image::ImageFormat::Bmp => "bmp", - image::ImageFormat::Tiff => "tiff", - _ => "png", - } -} - -fn index_out_of_range(action: &str, index: usize, len: usize) -> PresentationArtifactError { - PresentationArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("slide index {index} is out of range for {len} slides"), - } -} - -fn to_index(value: u32) -> Result { - usize::try_from(value).map_err(|_| PresentationArtifactError::InvalidArgs { - action: "insert_slide".to_string(), - message: "index does not fit in usize".to_string(), - }) -} - -fn resequence_z_order(slide: &mut PresentationSlide) { - for (index, element) in slide.elements.iter_mut().enumerate() { - element.set_z_order(index); - } -} diff --git a/codex-rs/artifact-presentation/src/tests.rs b/codex-rs/artifact-presentation/src/tests.rs deleted file mode 100644 index 2ced6ad64..000000000 --- a/codex-rs/artifact-presentation/src/tests.rs +++ /dev/null @@ -1,3512 +0,0 @@ -use super::presentation_artifact::*; -use base64::Engine; -use pretty_assertions::assert_eq; -use std::io::Read; - -fn zip_entry_text( - path: &std::path::Path, - entry_name: &str, -) -> Result> { - let file = std::fs::File::open(path)?; - let mut archive = zip::ZipArchive::new(file)?; - let mut entry = archive.by_name(entry_name)?; - let mut text = String::new(); - entry.read_to_string(&mut text)?; - Ok(text) -} - -fn zip_entry_names(path: &std::path::Path) -> Result, Box> { - let file = std::fs::File::open(path)?; - let archive = zip::ZipArchive::new(file)?; - Ok(archive.file_names().map(str::to_owned).collect()) -} - -fn parse_ndjson_lines(ndjson: &str) -> Result, serde_json::Error> { - ndjson - .lines() - .filter(|line| !line.is_empty()) - .map(serde_json::from_str) - .collect() -} - -#[test] -fn manager_can_create_add_text_and_export() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let create_response = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Demo" }), - }, - temp_dir.path(), - )?; - let artifact_id = create_response.artifact_id; - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - - let add_text = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "hello", - "position": { "left": 40, "top": 40, "width": 200, "height": 80 } - }), - }, - temp_dir.path(), - )?; - assert_eq!( - add_text - .artifact_snapshot - .as_ref() - .map(|snapshot| snapshot.slide_count), - Some(1) - ); - - let export_path = temp_dir.path().join("deck.pptx"); - let export = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - assert_eq!(export.exported_paths.len(), 1); - assert!(export.exported_paths[0].exists()); - Ok(()) -} - -#[test] -fn manager_can_import_exported_presentation() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Roundtrip" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id.clone(); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "add_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "geometry": "rectangle", - "position": { "left": 24, "top": 24, "width": 180, "height": 120 }, - "text": "shape" - }), - }, - temp_dir.path(), - )?; - let export_path = temp_dir.path().join("roundtrip.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - - let imported = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "import_pptx".to_string(), - args: serde_json::json!({ "path": "roundtrip.pptx" }), - }, - temp_dir.path(), - )?; - assert_eq!( - imported - .artifact_snapshot - .as_ref() - .map(|snapshot| snapshot.slide_count), - Some(1) - ); - Ok(()) -} - -#[test] -fn custom_slide_size_is_written_to_exported_pptx() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ - "name": "Custom Size", - "slide_size": { "width": 960, "height": 540 } - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let export_path = temp_dir.path().join("custom-size.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - - let presentation_xml = zip_entry_text( - &temp_dir.path().join("custom-size.pptx"), - "ppt/presentation.xml", - )?; - assert!(presentation_xml.contains(r#"cx="12192000" cy="6858000""#)); - assert!(presentation_xml.contains(r#"p:notesSz cx="6858000" cy="12192000""#)); - Ok(()) -} - -#[test] -fn exported_images_are_real_pictures_with_media_parts() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let source_path = temp_dir.path().join("source.png"); - image::RgbaImage::from_pixel(24, 16, image::Rgba([0x20, 0x90, 0xD0, 0xFF])) - .save(&source_path)?; - - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Image Export" }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "add_image".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "path": "source.png", - "position": { "left": 36, "top": 48, "width": 144, "height": 96 }, - "rotation": 15, - "flip_horizontal": true, - "alt": "Company logo" - }), - }, - temp_dir.path(), - )?; - - let export_path = temp_dir.path().join("images.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - - let pptx_path = temp_dir.path().join("images.pptx"); - let slide_xml = zip_entry_text(&pptx_path, "ppt/slides/slide1.xml")?; - let rels_xml = zip_entry_text(&pptx_path, "ppt/slides/_rels/slide1.xml.rels")?; - let content_types_xml = zip_entry_text(&pptx_path, "[Content_Types].xml")?; - let entry_names = zip_entry_names(&pptx_path)?; - - assert!(slide_xml.contains("")); - assert!(slide_xml.contains(r#"descr="Company logo""#)); - assert!(slide_xml.contains(r#"r:embed="rIdImage1""#)); - assert!(slide_xml.contains(r#""#)); - assert!(!slide_xml.contains("Image Placeholder:")); - assert!(rels_xml.contains("relationships/image")); - assert!(rels_xml.contains(r#"Target="../media/image1.png""#)); - assert!(content_types_xml.contains(r#"Extension="png" ContentType="image/png""#)); - assert!(entry_names.contains(&"ppt/media/image1.png".to_string())); - Ok(()) -} - -#[test] -fn tool_request_accepts_sequential_actions() -> Result<(), Box> { - let request: PresentationArtifactToolRequest = serde_json::from_value(serde_json::json!({ - "actions": [ - { - "action": "create", - "args": { "name": "Batch Deck" } - }, - { - "action": "export_pptx", - "args": { "path": "deck.pptx" } - } - ] - }))?; - - let execution = request.into_execution_request()?; - assert_eq!(execution.artifact_id, None); - assert_eq!(execution.requests.len(), 2); - assert_eq!(execution.requests[0].action, "create"); - assert_eq!(execution.requests[1].action, "export_pptx"); - Ok(()) -} - -#[test] -fn manager_can_execute_sequential_actions() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let response = manager.execute_requests( - PresentationArtifactExecutionRequest { - artifact_id: None, - requests: vec![ - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Batch Deck" }), - }, - PresentationArtifactRequest { - artifact_id: None, - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - PresentationArtifactRequest { - artifact_id: None, - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "hello", - "position": { "left": 40, "top": 40, "width": 200, "height": 80 } - }), - }, - ], - }, - temp_dir.path(), - )?; - - assert_eq!(response.action, "batch"); - assert_eq!( - response.executed_actions, - Some(vec![ - "create".to_string(), - "add_slide".to_string(), - "add_text_shape".to_string(), - ]) - ); - assert_eq!( - response - .artifact_snapshot - .as_ref() - .map(|snapshot| snapshot.slide_count), - Some(1) - ); - assert!( - response - .summary - .contains("Executed 3 actions sequentially.") - ); - Ok(()) -} - -#[test] -fn imported_pptx_surfaces_image_elements() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let source_path = temp_dir.path().join("import-source.png"); - image::RgbaImage::from_pixel(20, 20, image::Rgba([0xD0, 0x60, 0x20, 0xFF])) - .save(&source_path)?; - - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Image Import" }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "add_image".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "path": "import-source.png", - "position": { "left": 40, "top": 52, "width": 120, "height": 120 }, - "crop": { "left": 0.1, "top": 0.0, "right": 0.05, "bottom": 0.0 }, - "rotation": -10, - "flip_horizontal": true, - "flip_vertical": true, - "lock_aspect_ratio": true, - "alt": "Imported logo" - }), - }, - temp_dir.path(), - )?; - let export_path = temp_dir.path().join("image-import-roundtrip.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - - let imported = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "import_pptx".to_string(), - args: serde_json::json!({ "path": "image-import-roundtrip.pptx" }), - }, - temp_dir.path(), - )?; - assert_eq!( - imported - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.element_types.clone()), - Some(vec!["image".to_string()]) - ); - let image_anchor = imported - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .map(|id| format!("im/{id}")) - .expect("image anchor"); - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(imported.artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": image_anchor }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("alt")) - .and_then(serde_json::Value::as_str), - Some("Imported logo") - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("rotation")) - .and_then(serde_json::Value::as_i64), - Some(-10) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("flipHorizontal")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("flipVertical")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("lockAspectRatio")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("crop")) - .and_then(|crop| crop.get("left")) - .and_then(serde_json::Value::as_f64), - Some(0.1) - ); - Ok(()) -} - -#[test] -fn image_fit_contain_preserves_aspect_ratio() { - let image = ImageElement { - element_id: "element_1".to_string(), - frame: Rect { - left: 10, - top: 10, - width: 200, - height: 200, - }, - payload: Some(ImagePayload { - bytes: Vec::new(), - format: "PNG".to_string(), - width_px: 400, - height_px: 200, - }), - fit_mode: ImageFitMode::Contain, - crop: None, - rotation_degrees: None, - flip_horizontal: false, - flip_vertical: false, - lock_aspect_ratio: true, - alt_text: None, - prompt: None, - is_placeholder: false, - placeholder: None, - z_order: 0, - }; - - let (left, top, width, height, crop) = fit_image(&image); - assert_eq!((left, top, width, height), (10, 60, 200, 100)); - assert_eq!(crop, None); -} - -#[test] -fn preview_image_writer_supports_jpeg_scale_and_svg() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let source_path = temp_dir.path().join("preview.png"); - image::RgbaImage::from_pixel(80, 40, image::Rgba([0x22, 0x66, 0xAA, 0xFF])) - .save(&source_path)?; - let target_path = temp_dir.path().join("preview.jpg"); - write_preview_image( - &source_path, - &target_path, - PreviewOutputFormat::Jpeg, - 0.5, - 82, - "test", - )?; - let rendered = image::open(&target_path)?; - assert_eq!((rendered.width(), rendered.height()), (40, 20)); - assert_eq!( - image::ImageFormat::from_path(&target_path)?, - image::ImageFormat::Jpeg - ); - assert!(!source_path.exists()); - - let svg_source_path = temp_dir.path().join("preview-svg.png"); - image::RgbaImage::from_pixel(32, 16, image::Rgba([0x55, 0xAA, 0x44, 0xFF])) - .save(&svg_source_path)?; - let svg_target_path = temp_dir.path().join("preview.svg"); - write_preview_image( - &svg_source_path, - &svg_target_path, - PreviewOutputFormat::Svg, - 0.5, - 90, - "test", - )?; - let svg = std::fs::read_to_string(&svg_target_path)?; - assert!(svg.contains(r#" Result<(), Box> { - let mut image_bytes = std::io::Cursor::new(Vec::new()); - image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( - 16, - 8, - image::Rgba([0x11, 0x88, 0xCC, 0xFF]), - )) - .write_to(&mut image_bytes, image::ImageFormat::Png)?; - let png = image_bytes.into_inner(); - - let server = tiny_http::Server::http("127.0.0.1:0").expect("server"); - let port = server.server_addr().to_ip().expect("ip addr").port(); - let server_thread = std::thread::spawn(move || { - for request in server.incoming_requests().take(2) { - let response = tiny_http::Response::from_data(png.clone()).with_header( - tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"image/png"[..]) - .expect("header"), - ); - request.respond(response).expect("respond"); - } - }); - - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Remote Images" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let remote_uri = format!("http://127.0.0.1:{port}/image.png"); - let added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_image".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "uri": remote_uri, - "position": { "left": 32, "top": 48, "width": 120, "height": 60 } - }), - }, - temp_dir.path(), - )?; - let element_id = added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("image id"); - assert_eq!( - added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.element_types.clone()), - Some(vec!["image".to_string()]) - ); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "replace_image".to_string(), - args: serde_json::json!({ - "element_id": format!("im/{element_id}"), - "uri": format!("http://127.0.0.1:{port}/updated.png"), - "fit": "contain" - }), - }, - temp_dir.path(), - )?; - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "inspect".to_string(), - args: serde_json::json!({ "kind": "image" }), - }, - temp_dir.path(), - )?; - assert!( - inspect - .inspect_ndjson - .expect("image inspect") - .contains("\"fit\":\"Contain\"") - ); - server_thread.join().expect("server thread"); - Ok(()) -} - -#[test] -fn image_blobs_can_add_images() -> Result<(), Box> { - let mut image_bytes = std::io::Cursor::new(Vec::new()); - image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel( - 10, - 6, - image::Rgba([0xAA, 0x55, 0x22, 0xFF]), - )) - .write_to(&mut image_bytes, image::ImageFormat::Png)?; - let blob = base64::engine::general_purpose::STANDARD.encode(image_bytes.into_inner()); - - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Blob Images" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_image".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "blob": blob, - "position": { "left": 32, "top": 48, "width": 100, "height": 60 } - }), - }, - temp_dir.path(), - )?; - let image_id = added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("image id"); - let proto = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "to_proto".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!( - proto - .proto_json - .as_ref() - .and_then(|proto| proto.get("slides")) - .and_then(serde_json::Value::as_array) - .and_then(|slides| slides.first()) - .and_then(|slide| slide.get("elements")) - .and_then(serde_json::Value::as_array) - .and_then(|elements| elements.iter().find(|element| { - element.get("elementId").and_then(serde_json::Value::as_str) - == Some(image_id.as_str()) - })) - .and_then(|record| record.get("payload")) - .and_then(|payload| payload.get("format")) - .and_then(serde_json::Value::as_str), - Some("PNG") - ); - Ok(()) -} - -#[test] -fn active_slide_can_be_set_and_tracks_reorders() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Active Slide" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - for _ in 0..3 { - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - } - let set_active = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_active_slide".to_string(), - args: serde_json::json!({ "slide_index": 2 }), - }, - temp_dir.path(), - )?; - assert_eq!(set_active.active_slide_index, Some(2)); - assert_eq!( - set_active.slide_list.as_ref().map(|slides| slides - .iter() - .map(|slide| slide.is_active) - .collect::>()), - Some(vec![false, false, true]) - ); - - let moved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "move_slide".to_string(), - args: serde_json::json!({ "from_index": 2, "to_index": 0 }), - }, - temp_dir.path(), - )?; - let summary = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_summary".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!(summary.active_slide_index, Some(0)); - assert_eq!( - summary.slide_list.as_ref().map(|slides| slides - .iter() - .map(|slide| slide.is_active) - .collect::>()), - Some(vec![true, false, false]) - ); - - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({ "kind": "deck,slide" }), - }, - temp_dir.path(), - )?; - let inspect_ndjson = inspect.inspect_ndjson.expect("inspect"); - assert!(inspect_ndjson.contains("\"activeSlideIndex\":0")); - assert!(inspect_ndjson.contains("\"isActive\":true")); - - let active_slide_id = moved - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.slide_id.clone()) - .expect("active slide id"); - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sl/{active_slide_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("isActive")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - - let deleted = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "delete_slide".to_string(), - args: serde_json::json!({ "slide_index": 0 }), - }, - temp_dir.path(), - )?; - assert_eq!( - deleted - .artifact_snapshot - .as_ref() - .map(|snapshot| snapshot.slide_count), - Some(2) - ); - let after_delete = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(deleted.artifact_id), - action: "list_slides".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!(after_delete.active_slide_index, Some(0)); - assert_eq!( - after_delete.slide_list.as_ref().map(|slides| slides - .iter() - .map(|slide| slide.is_active) - .collect::>()), - Some(vec![true, false]) - ); - Ok(()) -} - -#[test] -fn text_replace_and_insert_helpers_update_text_elements() -> Result<(), Box> -{ - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Text Helpers" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Revenue up 24%", - "position": { "left": 24, "top": 24, "width": 240, "height": 80 } - }), - }, - temp_dir.path(), - )?; - let element_id = added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("text id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "replace_text".to_string(), - args: serde_json::json!({ - "element_id": format!("sh/{element_id}"), - "search": "24%", - "replace": "31%" - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "insert_text_after".to_string(), - args: serde_json::json!({ - "element_id": format!("sh/{element_id}"), - "after": "Revenue", - "insert": " QoQ" - }), - }, - temp_dir.path(), - )?; - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{element_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("text")) - .and_then(serde_json::Value::as_str), - Some("Revenue QoQ up 31%") - ); - Ok(()) -} - -#[test] -fn hyperlinks_are_inspectable_and_exported() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Hyperlinks" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - for _ in 0..2 { - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - } - - let text = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Open roadmap", - "position": { "left": 24, "top": 24, "width": 220, "height": 60 } - }), - }, - temp_dir.path(), - )?; - let text_id = text - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("text id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_hyperlink".to_string(), - args: serde_json::json!({ - "element_id": format!("sh/{text_id}"), - "link_type": "url", - "url": "https://example.com/roadmap", - "tooltip": "Roadmap", - "highlight_click": false - }), - }, - temp_dir.path(), - )?; - - let shape = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "geometry": "rounded_rectangle", - "position": { "left": 24, "top": 120, "width": 220, "height": 72 }, - "text": "Jump to appendix" - }), - }, - temp_dir.path(), - )?; - let shape_id = shape - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("shape id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_hyperlink".to_string(), - args: serde_json::json!({ - "element_id": format!("sh/{shape_id}"), - "link_type": "slide", - "slide_index": 1, - "tooltip": "Appendix" - }), - }, - temp_dir.path(), - )?; - - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({ "kind": "textbox,shape" }), - }, - temp_dir.path(), - )?; - let inspect_ndjson = inspect.inspect_ndjson.expect("inspect"); - assert!(inspect_ndjson.contains("\"type\":\"url\"")); - assert!(inspect_ndjson.contains("\"url\":\"https://example.com/roadmap\"")); - assert!(inspect_ndjson.contains("\"type\":\"slide\"")); - assert!(inspect_ndjson.contains("\"slideIndex\":1")); - - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{text_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("hyperlink")) - .and_then(|hyperlink| hyperlink.get("url")) - .and_then(serde_json::Value::as_str), - Some("https://example.com/roadmap") - ); - - let export_path = temp_dir.path().join("hyperlinks.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - let slide_xml = zip_entry_text( - &temp_dir.path().join("hyperlinks.pptx"), - "ppt/slides/slide1.xml", - )?; - let rels_xml = zip_entry_text( - &temp_dir.path().join("hyperlinks.pptx"), - "ppt/slides/_rels/slide1.xml.rels", - )?; - assert!(slide_xml.contains("hlinkClick")); - assert!(rels_xml.contains("https://example.com/roadmap")); - assert!(rels_xml.contains("slide2.xml")); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_hyperlink".to_string(), - args: serde_json::json!({ - "element_id": format!("sh/{text_id}"), - "clear": true - }), - }, - temp_dir.path(), - )?; - let cleared = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{text_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - cleared - .resolved_record - .as_ref() - .and_then(|record| record.get("hyperlink")), - None - ); - Ok(()) -} - -#[test] -fn manager_supports_layout_theme_notes_and_inspect() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ - "name": "Deck", - "theme": { - "color_scheme": { - "accent1": "#123456", - "bg1": "#FFFFFF", - "tx1": "#111111" - }, - "major_font": "Aptos" - } - }), - }, - temp_dir.path(), - )?; - assert_eq!( - created - .theme - .as_ref() - .map(|theme| theme.hex_color_map.clone()), - Some( - [ - ("accent1".to_string(), "123456".to_string()), - ("bg1".to_string(), "FFFFFF".to_string()), - ("tx1".to_string(), "111111".to_string()), - ] - .into_iter() - .collect() - ) - ); - let artifact_id = created.artifact_id.clone(); - - let master_layouts = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_layout".to_string(), - args: serde_json::json!({ "name": "Brand Master", "kind": "master" }), - }, - temp_dir.path(), - )?; - assert_eq!(master_layouts.layout_list.as_ref().map(Vec::len), Some(1)); - let master_id = master_layouts.layout_list.unwrap()[0].layout_id.clone(); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_layout_placeholder".to_string(), - args: serde_json::json!({ - "layout_id": master_id, - "name": "title", - "placeholder_type": "title", - "text": "Placeholder title", - "position": { "left": 48, "top": 48, "width": 500, "height": 60 } - }), - }, - temp_dir.path(), - )?; - - let child_layouts = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_layout".to_string(), - args: serde_json::json!({ - "name": "Title Slide", - "kind": "layout", - "parent_layout_id": master_id - }), - }, - temp_dir.path(), - )?; - assert_eq!(child_layouts.layout_list.as_ref().map(Vec::len), Some(2)); - let layout_id = child_layouts - .layout_list - .as_ref() - .and_then(|layouts| layouts.iter().find(|layout| layout.kind == "layout")) - .map(|layout| layout.layout_id.clone()) - .expect("child layout id"); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_layout_placeholder".to_string(), - args: serde_json::json!({ - "layout_id": "Title Slide", - "name": "subtitle", - "placeholder_type": "subtitle", - "text": "Placeholder subtitle", - "position": { "left": 48, "top": 128, "width": 500, "height": 48 } - }), - }, - temp_dir.path(), - )?; - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_layout_placeholder".to_string(), - args: serde_json::json!({ - "layout_id": "title slide", - "name": "hero-image", - "placeholder_type": "picture", - "text": "Add cover image", - "position": { "left": 420, "top": 40, "width": 180, "height": 120 } - }), - }, - temp_dir.path(), - )?; - - let added_slide = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({ "layout": layout_id }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_notes".to_string(), - args: serde_json::json!({ "slide_index": 0, "text": "Speaker notes" }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "append_notes".to_string(), - args: serde_json::json!({ "slide_index": 0, "text": "More context" }), - }, - temp_dir.path(), - )?; - let layout_placeholders = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "list_layout_placeholders".to_string(), - args: serde_json::json!({ "layout_id": layout_id }), - }, - temp_dir.path(), - )?; - assert_eq!( - layout_placeholders.placeholder_list.as_ref().map(Vec::len), - Some(3) - ); - - let slide_placeholders = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "list_slide_placeholders".to_string(), - args: serde_json::json!({ "slide_index": 0 }), - }, - temp_dir.path(), - )?; - assert_eq!( - slide_placeholders.placeholder_list.as_ref().map(Vec::len), - Some(3) - ); - assert_eq!( - slide_placeholders - .placeholder_list - .as_ref() - .and_then(|placeholders| placeholders - .iter() - .find(|placeholder| placeholder.placeholder_type == "picture") - .map(|placeholder| ( - placeholder.geometry.clone(), - placeholder.text_preview.clone() - ))), - Some(( - Some("Image".to_string()), - Some("Add cover image".to_string()) - )) - ); - let image_placeholder_id = added_slide - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("image placeholder id"); - - let resolved_layout = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("ly/{layout_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_layout - .resolved_record - .as_ref() - .and_then(|record| record.get("kind")) - .and_then(serde_json::Value::as_str), - Some("layout") - ); - - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({ "kind": "deck,slide,textbox,image,notes,layoutList" }), - }, - temp_dir.path(), - )?; - let inspect_ndjson = inspect.inspect_ndjson.expect("inspect output"); - assert!(inspect_ndjson.contains("\"kind\":\"layout\"")); - assert!(inspect_ndjson.contains("\"kind\":\"notes\"")); - assert!(inspect_ndjson.contains("\"placeholder\":\"title\"")); - assert!(inspect_ndjson.contains("\"placeholder\":\"picture\"")); - assert!(inspect_ndjson.contains("\"kind\":\"image\"")); - - let resolved_image_placeholder = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("im/{image_placeholder_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_image_placeholder - .resolved_record - .as_ref() - .and_then(|record| record.get("isPlaceholder")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - resolved_image_placeholder - .resolved_record - .as_ref() - .and_then(|record| record.get("placeholder")) - .and_then(serde_json::Value::as_str), - Some("picture") - ); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_slide_layout".to_string(), - args: serde_json::json!({ "slide_index": 1, "layout_id": "title slide" }), - }, - temp_dir.path(), - )?; - let inherited_placeholders = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "list_slide_placeholders".to_string(), - args: serde_json::json!({ "slide_index": 1 }), - }, - temp_dir.path(), - )?; - assert_eq!( - inherited_placeholders - .placeholder_list - .as_ref() - .map(Vec::len), - Some(3) - ); - assert_eq!( - inherited_placeholders - .placeholder_list - .as_ref() - .and_then(|placeholders| placeholders - .iter() - .find(|placeholder| placeholder.placeholder_type == "title") - .map(|placeholder| placeholder.name.clone())), - Some("title".to_string()) - ); - assert_eq!( - inherited_placeholders - .placeholder_list - .as_ref() - .and_then(|placeholders| placeholders - .iter() - .find(|placeholder| placeholder.placeholder_type == "picture") - .map(|placeholder| placeholder.geometry.clone())), - Some(Some("Image".to_string())) - ); - - let truncated = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id), - action: "inspect".to_string(), - args: serde_json::json!({ - "kind": "deck,slide,textbox,image,notes,layoutList", - "max_chars": 250 - }), - }, - temp_dir.path(), - )?; - assert!( - truncated - .inspect_ndjson - .expect("truncated inspect") - .contains("\"noticeType\":\"truncation\"") - ); - Ok(()) -} - -#[test] -fn named_text_styles_apply_to_text_and_tables() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ - "name": "Styles", - "theme": { - "color_scheme": { - "tx1": "#222222" - }, - "major_font": "Aptos Display", - "minor_font": "Aptos" - } - }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - let described = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "describe_styles".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!( - described - .resolved_record - .as_ref() - .and_then(|record| record.get("styles")) - .and_then(serde_json::Value::as_array) - .map(Vec::len), - Some(5) - ); - let title_style = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_style".to_string(), - args: serde_json::json!({ "name": "title" }), - }, - temp_dir.path(), - )?; - assert_eq!( - title_style.resolved_record, - Some(serde_json::json!({ - "kind": "textStyle", - "id": "st/title", - "name": "title", - "builtIn": true, - "style": { - "styleName": "title", - "fontSize": 28, - "fontFamily": "Aptos Display", - "color": "222222", - "alignment": "left", - "bold": true, - "italic": false, - "underline": false - } - })) - ); - let custom_style = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_style".to_string(), - args: serde_json::json!({ - "name": "callout", - "font_size": 18, - "color": "#336699", - "italic": true, - "underline": true - }), - }, - temp_dir.path(), - )?; - assert_eq!( - custom_style.resolved_record, - Some(serde_json::json!({ - "kind": "textStyle", - "id": "st/callout", - "name": "callout", - "builtIn": false, - "style": { - "styleName": "callout", - "fontSize": 18, - "fontFamily": serde_json::Value::Null, - "color": "336699", - "alignment": serde_json::Value::Null, - "bold": false, - "italic": true, - "underline": true - } - })) - ); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let text_added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Styled title", - "position": { "left": 40, "top": 40, "width": 220, "height": 50 }, - "style": "title", - "underline": true - }), - }, - temp_dir.path(), - )?; - let text_id = text_added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("text id"); - let table_added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_table".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "position": { "left": 40, "top": 120, "width": 180, "height": 80 }, - "rows": [["A"]], - }), - }, - temp_dir.path(), - )?; - let table_id = table_added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("table id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_table_cell".to_string(), - args: serde_json::json!({ - "element_id": table_id, - "row": 0, - "column": 0, - "value": "Styled cell", - "styling": { - "style": "callout" - } - }), - }, - temp_dir.path(), - )?; - - let resolved_text = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{text_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_text - .resolved_record - .as_ref() - .and_then(|record| record.get("textStyle")) - .cloned(), - Some(serde_json::json!({ - "styleName": "title", - "fontSize": 28, - "fontFamily": "Aptos Display", - "color": "222222", - "alignment": "left", - "bold": true, - "italic": false, - "underline": true - })) - ); - let resolved_table = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("tb/{table_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_table - .resolved_record - .as_ref() - .and_then(|record| record.get("cellTextStyles")) - .cloned(), - Some(serde_json::json!([ - [ - { - "styleName": "callout", - "fontSize": 18, - "fontFamily": serde_json::Value::Null, - "color": "336699", - "alignment": serde_json::Value::Null, - "bold": false, - "italic": true, - "underline": true - } - ] - ])) - ); - Ok(()) -} - -#[test] -fn layout_names_resolve_in_slide_actions_and_insert_defaults_after_active_slide() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Layouts by Name" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - - let layout_created = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_layout".to_string(), - args: serde_json::json!({ "name": "Title Slide" }), - }, - temp_dir.path(), - )?; - let layout_id = layout_created - .layout_list - .as_ref() - .and_then(|layouts| layouts.first()) - .map(|layout| layout.layout_id.clone()) - .expect("layout id"); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_layout_placeholder".to_string(), - args: serde_json::json!({ - "layout_id": layout_id, - "name": "title", - "placeholder_type": "title", - "text": "Placeholder title" - }), - }, - temp_dir.path(), - )?; - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({ "layout": "Title Slide" }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_active_slide".to_string(), - args: serde_json::json!({ "slide_index": 0 }), - }, - temp_dir.path(), - )?; - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "insert_slide".to_string(), - args: serde_json::json!({ "layout": "title slide" }), - }, - temp_dir.path(), - )?; - let inserted = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "list_slides".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!( - inserted.slide_list.as_ref().map(|slides| slides - .iter() - .map(|slide| slide.layout_id.clone()) - .collect::>()), - Some(vec![ - Some("layout_1".to_string()), - Some("layout_1".to_string()), - None - ]) - ); - assert_eq!( - inserted.slide_list.as_ref().map(|slides| slides - .iter() - .map(|slide| slide.is_active) - .collect::>()), - Some(vec![true, false, false]) - ); - - let placeholders = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "list_layout_placeholders".to_string(), - args: serde_json::json!({ "layout_id": "TITLE SLIDE" }), - }, - temp_dir.path(), - )?; - assert_eq!( - placeholders.placeholder_list.as_ref().map(|entries| entries - .iter() - .map(|entry| entry.name.clone()) - .collect::>()), - Some(vec!["title".to_string()]) - ); - - let child_layout = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(placeholders.artifact_id), - action: "create_layout".to_string(), - args: serde_json::json!({ - "name": "Child Layout", - "parent_layout_id": "title slide" - }), - }, - temp_dir.path(), - )?; - assert_eq!( - child_layout.layout_list.as_ref().map(|layouts| layouts - .iter() - .find(|layout| layout.name == "Child Layout") - .and_then(|layout| layout.parent_layout_id.clone())), - Some(Some("layout_1".to_string())) - ); - Ok(()) -} - -#[test] -fn inspect_supports_filters_target_windows_and_shape_text_metadata() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Inspect Filters" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - - let first_text = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "First KPI", - "position": { "left": 10, "top": 10, "width": 120, "height": 40 } - }), - }, - temp_dir.path(), - )?; - let second_text = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Second KPI", - "position": { "left": 10, "top": 60, "width": 120, "height": 40 } - }), - }, - temp_dir.path(), - )?; - let third_text = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Third KPI", - "position": { "left": 10, "top": 110, "width": 120, "height": 40 } - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_notes".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Speaker note" - }), - }, - temp_dir.path(), - )?; - let shape = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "geometry": "rectangle", - "position": { "left": 180, "top": 10, "width": 180, "height": 90 }, - "text": "Detailed\nShape KPI" - }), - }, - temp_dir.path(), - )?; - - let middle_text_id = second_text - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.get(1)) - .cloned() - .expect("middle text id"); - let first_text_id = first_text - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("first text id"); - let third_text_id = third_text - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.get(2)) - .cloned() - .expect("third text id"); - let shape_id = shape - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.get(3)) - .cloned() - .expect("shape id"); - let slide_id = shape - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.slide_id.clone()) - .expect("slide id"); - - let filtered = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({ - "include": "shape,notes", - "exclude": "notes", - "search": "Detailed" - }), - }, - temp_dir.path(), - )?; - let filtered_records = parse_ndjson_lines( - filtered - .inspect_ndjson - .as_deref() - .expect("filtered inspect"), - )?; - assert_eq!(filtered_records.len(), 1); - assert_eq!( - filtered_records[0], - serde_json::json!({ - "kind": "shape", - "id": format!("sh/{shape_id}"), - "slide": 1, - "geometry": "Rectangle", - "text": "Detailed\nShape KPI", - "textStyle": { - "styleName": serde_json::Value::Null, - "fontSize": serde_json::Value::Null, - "fontFamily": serde_json::Value::Null, - "color": serde_json::Value::Null, - "alignment": serde_json::Value::Null, - "bold": false, - "italic": false, - "underline": false - }, - "richText": { - "layout": { - "insets": serde_json::Value::Null, - "wrap": serde_json::Value::Null, - "autoFit": serde_json::Value::Null, - "verticalAlignment": serde_json::Value::Null - }, - "ranges": [] - }, - "rotation": serde_json::Value::Null, - "flipHorizontal": false, - "flipVertical": false, - "bbox": [180, 10, 180, 90], - "bboxUnit": "points", - "textPreview": "Detailed | Shape KPI", - "textChars": 18, - "textLines": 2 - }) - ); - - let targeted = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({ - "include": "textbox", - "target": { - "id": format!("sh/{middle_text_id}"), - "before_lines": 1, - "after_lines": 1 - } - }), - }, - temp_dir.path(), - )?; - let targeted_records = parse_ndjson_lines( - targeted - .inspect_ndjson - .as_deref() - .expect("targeted inspect"), - )?; - assert_eq!( - targeted_records - .iter() - .filter_map(|record| record.get("id").and_then(serde_json::Value::as_str)) - .map(str::to_owned) - .collect::>(), - vec![ - format!("sh/{first_text_id}"), - format!("sh/{middle_text_id}"), - format!("sh/{third_text_id}") - ] - ); - - let missing_target = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "inspect".to_string(), - args: serde_json::json!({ - "include": "textbox", - "target": { - "id": "sh/missing", - "before_lines": 1, - "after_lines": 1 - } - }), - }, - temp_dir.path(), - )?; - let missing_target_records = parse_ndjson_lines( - missing_target - .inspect_ndjson - .as_deref() - .expect("missing target inspect"), - )?; - assert_eq!( - missing_target_records, - vec![serde_json::json!({ - "kind": "notice", - "noticeType": "targetNotFound", - "target": { - "id": "sh/missing", - "beforeLines": 1, - "afterLines": 1 - }, - "message": "No inspect records matched target `sh/missing`." - })] - ); - - let resolved_shape = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(missing_target.artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{shape_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_shape - .resolved_record - .as_ref() - .and_then(|record| record.get("textPreview")) - .and_then(serde_json::Value::as_str), - Some("Detailed | Shape KPI") - ); - assert_eq!( - resolved_shape - .resolved_record - .as_ref() - .and_then(|record| record.get("textChars")) - .and_then(serde_json::Value::as_u64), - Some(18) - ); - assert_eq!( - resolved_shape - .resolved_record - .as_ref() - .and_then(|record| record.get("textLines")) - .and_then(serde_json::Value::as_u64), - Some(2) - ); - - let resolved_notes = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(missing_target.artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("nt/{slide_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_notes - .resolved_record - .as_ref() - .and_then(|record| record.get("textPreview")) - .and_then(serde_json::Value::as_str), - Some("Speaker note") - ); - assert_eq!( - resolved_notes - .resolved_record - .as_ref() - .and_then(|record| record.get("textChars")) - .and_then(serde_json::Value::as_u64), - Some(12) - ); - assert_eq!( - resolved_notes - .resolved_record - .as_ref() - .and_then(|record| record.get("textLines")) - .and_then(serde_json::Value::as_u64), - Some(1) - ); - Ok(()) -} - -#[test] -fn notes_visibility_controls_exported_notes() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Notes" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({ "notes": "Hidden notes" }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_notes_visibility".to_string(), - args: serde_json::json!({ "slide_index": 0, "visible": false }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": "notes-hidden.pptx" }), - }, - temp_dir.path(), - )?; - - let imported = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "import_pptx".to_string(), - args: serde_json::json!({ "path": "notes-hidden.pptx" }), - }, - temp_dir.path(), - )?; - let summary = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(imported.artifact_id), - action: "list_slides".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!( - summary - .slide_list - .as_ref() - .and_then(|slides| slides.first()) - .and_then(|slide| slide.notes.clone()), - None - ); - Ok(()) -} - -#[test] -fn image_placeholders_and_anchor_updates_work() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Images" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let placeholder = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_image".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "position": { "left": 24, "top": 24, "width": 200, "height": 120 }, - "fit": "contain", - "prompt": "Generate a hero illustration", - "alt": "Hero placeholder" - }), - }, - temp_dir.path(), - )?; - let image_anchor = placeholder - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .map(|id| format!("im/{id}")) - .expect("image anchor"); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_shape_style".to_string(), - args: serde_json::json!({ - "element_id": image_anchor, - "fit": "cover", - "crop": { "left": 0.1, "top": 0.0, "right": 0.1, "bottom": 0.0 }, - "rotation": 12, - "flip_horizontal": true, - "lock_aspect_ratio": true - }), - }, - temp_dir.path(), - )?; - - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": image_anchor }), - }, - temp_dir.path(), - )?; - let record = resolved.resolved_record.expect("resolved image"); - assert_eq!( - record.get("kind").and_then(serde_json::Value::as_str), - Some("image") - ); - assert_eq!( - record - .get("isPlaceholder") - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - record - .get("lockAspectRatio") - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - record.get("rotation").and_then(serde_json::Value::as_i64), - Some(12) - ); - assert_eq!( - record - .get("flipHorizontal") - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - record - .get("flipVertical") - .and_then(serde_json::Value::as_bool), - Some(false) - ); - Ok(()) -} - -#[test] -fn image_partial_resize_respects_lock_aspect_ratio() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Image Resize" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_image".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "position": { "left": 10, "top": 10, "width": 200, "height": 100 }, - "prompt": "Placeholder image", - "lock_aspect_ratio": true - }), - }, - temp_dir.path(), - )?; - let image_anchor = added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .map(|id| format!("im/{id}")) - .expect("image anchor"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_shape_style".to_string(), - args: serde_json::json!({ - "element_id": image_anchor, - "position": { "width": 120 } - }), - }, - temp_dir.path(), - )?; - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": image_anchor }), - }, - temp_dir.path(), - )?; - let bbox = resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("bbox")) - .and_then(serde_json::Value::as_array) - .expect("bbox"); - assert_eq!(bbox[2].as_u64(), Some(120)); - assert_eq!(bbox[3].as_u64(), Some(60)); - Ok(()) -} - -#[test] -fn connectors_support_arrows_and_inspect() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Connectors" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_connector".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "connector_type": "elbow", - "start": { "left": 20, "top": 20 }, - "end": { "left": 180, "top": 160 }, - "line": { "color": "#ff0000", "width": 2, "style": "dash-dot" }, - "start_arrow": "none", - "end_arrow": "triangle", - "arrow_size": "large", - "label": "flow" - }), - }, - temp_dir.path(), - )?; - let connector_id = added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .map(|id| format!("cn/{id}")) - .expect("connector id"); - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": connector_id }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("kind")) - .and_then(serde_json::Value::as_str), - Some("connector") - ); - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "inspect".to_string(), - args: serde_json::json!({ "kind": "connector" }), - }, - temp_dir.path(), - )?; - assert!( - inspect - .inspect_ndjson - .expect("connector inspect") - .contains("\"kind\":\"connector\"") - ); - Ok(()) -} - -#[test] -fn shapes_support_stroke_dash_styles() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Shape Strokes" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let added_shape = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "geometry": "rectangle", - "position": { - "left": 24, - "top": 24, - "width": 180, - "height": 120, - "rotation": 15, - "flip_horizontal": true, - "flip_vertical": true - }, - "stroke": { "color": "#ff0000", "width": 2, "style": "dash-dot" } - }), - }, - temp_dir.path(), - )?; - let shape_id = added_shape - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("shape id"); - - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{shape_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("stroke")) - .and_then(|stroke| stroke.get("style")) - .and_then(serde_json::Value::as_str), - Some("dash-dot") - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("rotation")) - .and_then(serde_json::Value::as_i64), - Some(15) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("flipHorizontal")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("flipVertical")) - .and_then(serde_json::Value::as_bool), - Some(true) - ); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_shape_style".to_string(), - args: serde_json::json!({ - "element_id": format!("sh/{shape_id}"), - "position": { - "rotation": 30, - "flip_horizontal": false, - "flip_vertical": true - } - }), - }, - temp_dir.path(), - )?; - let updated = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("sh/{shape_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - updated - .resolved_record - .as_ref() - .and_then(|record| record.get("rotation")) - .and_then(serde_json::Value::as_i64), - Some(30) - ); - assert_eq!( - updated - .resolved_record - .as_ref() - .and_then(|record| record.get("flipHorizontal")) - .and_then(serde_json::Value::as_bool), - Some(false) - ); - - let export_path = temp_dir.path().join("shape-strokes.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - - let slide_xml = zip_entry_text( - &temp_dir.path().join("shape-strokes.pptx"), - "ppt/slides/slide1.xml", - )?; - assert!(slide_xml.contains(r#""#)); - assert!(slide_xml.contains(r#""#)); - Ok(()) -} - -#[test] -fn z_order_helpers_resequence_elements() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Z Order" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let first = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "A", - "position": { "left": 10, "top": 10, "width": 100, "height": 40 } - }), - }, - temp_dir.path(), - )?; - let second = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "B", - "position": { "left": 20, "top": 20, "width": 100, "height": 40 } - }), - }, - temp_dir.path(), - )?; - let first_id = first - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("first id"); - let second_id = second - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("second id"); - let sent_back = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "send_to_back".to_string(), - args: serde_json::json!({ "element_id": format!("sh/{second_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - sent_back - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.element_ids.clone()), - Some(vec![second_id.clone(), first_id.clone()]) - ); - let brought_front = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "bring_to_front".to_string(), - args: serde_json::json!({ "element_id": format!("sh/{second_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - brought_front - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.element_ids.clone()), - Some(vec![first_id, second_id]) - ); - Ok(()) -} - -#[test] -fn manager_supports_table_cell_updates_and_merges() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Tables" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let table = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_table".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "position": { "left": 24, "top": 24, "width": 240, "height": 120 }, - "rows": [["A", "B"], ["C", "D"]], - "column_widths": [90, 150], - "row_heights": [40, 80], - "style": "TableStyleMedium9" - }), - }, - temp_dir.path(), - )?; - let table_id = table - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("table id"); - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_table_cell".to_string(), - args: serde_json::json!({ - "element_id": table_id, - "row": 0, - "column": 1, - "value": "Updated", - "background_fill": "#eeeeee", - "alignment": "right", - "styling": { "bold": true } - }), - }, - temp_dir.path(), - )?; - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({ "kind": "table" }), - }, - temp_dir.path(), - )?; - assert!( - inspect - .inspect_ndjson - .expect("inspect") - .contains("\"kind\":\"table\"") - ); - let resolved = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": format!("tb/{table_id}") }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("columnWidths")) - .and_then(serde_json::Value::as_array) - .map(|widths| widths - .iter() - .filter_map(serde_json::Value::as_u64) - .collect::>()), - Some(vec![90, 150]) - ); - assert_eq!( - resolved - .resolved_record - .as_ref() - .and_then(|record| record.get("rowHeights")) - .and_then(serde_json::Value::as_array) - .map(|heights| heights - .iter() - .filter_map(serde_json::Value::as_u64) - .collect::>()), - Some(vec![40, 80]) - ); - - let merged = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "merge_table_cells".to_string(), - args: serde_json::json!({ - "element_id": table_id, - "start_row": 0, - "end_row": 0, - "start_column": 0, - "end_column": 1 - }), - }, - temp_dir.path(), - )?; - assert_eq!( - merged - .artifact_snapshot - .as_ref() - .map(|snapshot| snapshot.slide_count), - Some(1) - ); - let export_path = temp_dir.path().join("tables.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(merged.artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - let slide_xml = zip_entry_text( - &temp_dir.path().join("tables.pptx"), - "ppt/slides/slide1.xml", - )?; - assert!( - slide_xml.contains(r#""#), - "{slide_xml}" - ); - assert!(slide_xml.contains(r#""#)); - assert!(slide_xml.contains(r#""#)); - assert!(slide_xml.contains(r#""#)); - Ok(()) -} - -#[test] -fn rich_text_comments_tables_and_charts_roundtrip_through_metadata() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Parity Roundtrip" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - - let text_added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_text_shape".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "text": "Placeholder", - "position": { "left": 32, "top": 28, "width": 280, "height": 72 } - }), - }, - temp_dir.path(), - )?; - let text_id = text_added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.first()) - .cloned() - .expect("text id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_rich_text".to_string(), - args: serde_json::json!({ - "element_id": text_id, - "text": [[ - { - "run": "Quarterly ", - "text_style": { "bold": true, "color": "#114488" } - }, - "update pipeline" - ]], - "text_layout": { - "wrap": "square", - "auto_fit": "shrinkText", - "vertical_alignment": "middle", - "insets": { "left": 6, "right": 6, "top": 4, "bottom": 4 } - } - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "format_text_range".to_string(), - args: serde_json::json!({ - "element_id": text_id, - "query": "update", - "styling": { "italic": true }, - "link": { "uri": "https://example.com/update", "is_external": true } - }), - }, - temp_dir.path(), - )?; - - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_comment_author".to_string(), - args: serde_json::json!({ - "display_name": "Jamie Fox", - "initials": "JF", - "email": "jamie@example.com" - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_comment_thread".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "element_id": text_id, - "query": "Quarterly", - "text": "Tighten this headline", - "position": { "x": 240, "y": 44 } - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_comment_reply".to_string(), - args: serde_json::json!({ - "thread_id": "thread_1", - "text": "Applied to the slide draft." - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "toggle_comment_reaction".to_string(), - args: serde_json::json!({ - "thread_id": "thread_1", - "emoji": "eyes" - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "resolve_comment_thread".to_string(), - args: serde_json::json!({ "thread_id": "thread_1" }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "reopen_comment_thread".to_string(), - args: serde_json::json!({ "thread_id": "thread_1" }), - }, - temp_dir.path(), - )?; - - let table_added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_table".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "position": { "left": 32, "top": 124, "width": 320, "height": 120 }, - "rows": [["Metric", "Value"], ["Status", "Beta"]], - "style": "TableStyleMedium2", - "style_options": { - "header_row": true, - "banded_rows": true, - "first_column": true - }, - "borders": { - "outside": { "color": "#222222", "width": 2 } - }, - "right_to_left": true - }), - }, - temp_dir.path(), - )?; - let table_id = table_added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("table id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_table_style".to_string(), - args: serde_json::json!({ - "element_id": table_id, - "style_options": { "last_column": true, "total_row": true }, - "borders": { - "inside": { "color": "#999999", "width": 1 } - } - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "style_table_block".to_string(), - args: serde_json::json!({ - "element_id": table_id, - "row": 1, - "column": 0, - "row_count": 1, - "column_count": 2, - "background_fill": "#FFF2CC", - "alignment": "center", - "styling": { "bold": true } - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "format_text_range".to_string(), - args: serde_json::json!({ - "element_id": table_id, - "row": 1, - "column": 1, - "query": "Beta", - "styling": { "italic": true, "color": "#AA0000" } - }), - }, - temp_dir.path(), - )?; - - let chart_added = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_chart".to_string(), - args: serde_json::json!({ - "slide_index": 0, - "position": { "left": 372, "top": 120, "width": 280, "height": 210 }, - "chart_type": "bar", - "categories": ["Q1", "Q2"], - "series": [{ - "name": "Revenue", - "values": [10.0, 12.0], - "fill": "#4472C4", - "stroke": { "color": "#1F4E79", "width": 2 }, - "marker": { "symbol": "circle", "size": 7 }, - "data_label_overrides": [{ - "idx": 1, - "text": "12M", - "position": "outsideEnd", - "fill": "#FFFFFF" - }] - }], - "title": "Revenue", - "style_index": 7, - "has_legend": true, - "legend_position": "right", - "legend_text_style": { "italic": true }, - "x_axis_title": "Quarter", - "y_axis_title": "USD", - "data_labels": { - "show_value": true, - "position": "outsideEnd", - "text_style": { "bold": true } - }, - "chart_fill": "#F8F8F8", - "plot_area_fill": "#FFFFFF" - }), - }, - temp_dir.path(), - )?; - let chart_id = chart_added - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .and_then(|slide| slide.element_ids.last()) - .cloned() - .expect("chart id"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "update_chart".to_string(), - args: serde_json::json!({ - "element_id": chart_id, - "title": "Revenue outlook", - "style_index": 12, - "legend_position": "bottom", - "y_axis_title": "USD (millions)" - }), - }, - temp_dir.path(), - )?; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_chart_series".to_string(), - args: serde_json::json!({ - "element_id": chart_id, - "name": "Target", - "values": [11.0, 13.0], - "fill": "#70AD47", - "marker": { "symbol": "diamond", "size": 6 } - }), - }, - temp_dir.path(), - )?; - - let export_path = temp_dir.path().join("parity-roundtrip.pptx"); - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "export_pptx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - let metadata = zip_entry_text( - &temp_dir.path().join("parity-roundtrip.pptx"), - "ppt/codex-document.json", - )?; - assert!(metadata.contains("\"thread_id\":\"thread_1\"")); - assert!(metadata.contains("\"style_index\":12")); - assert!(metadata.contains("\"right_to_left\":true")); - - let imported = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "import_pptx".to_string(), - args: serde_json::json!({ "path": "parity-roundtrip.pptx" }), - }, - temp_dir.path(), - )?; - let imported_artifact_id = imported.artifact_id; - - let proto = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(imported_artifact_id.clone()), - action: "to_proto".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let proto = proto.proto_json.expect("proto"); - let comments = proto["commentThreads"].as_array().expect("comments"); - assert_eq!(comments.len(), 1); - assert_eq!(comments[0]["status"], "active"); - assert_eq!(comments[0]["messages"].as_array().map(Vec::len), Some(2)); - assert_eq!( - comments[0]["messages"][1]["reactions"], - serde_json::json!(["eyes"]) - ); - - let elements = proto["slides"][0]["elements"].as_array().expect("elements"); - let text_record = elements - .iter() - .find(|element| element["elementId"] == text_id) - .expect("text record"); - assert_eq!(text_record["richText"]["layout"]["wrap"], "square"); - assert_eq!(text_record["richText"]["layout"]["autoFit"], "shrinkText"); - assert_eq!( - text_record["richText"]["layout"]["verticalAlignment"], - "middle" - ); - assert_eq!( - text_record["richText"]["ranges"].as_array().map(Vec::len), - Some(2) - ); - assert!( - text_record["richText"]["ranges"] - .as_array() - .expect("text ranges") - .iter() - .any(|range| range["text"] == "update") - ); - - let table_record = elements - .iter() - .find(|element| element["elementId"] == table_id) - .expect("table record"); - assert_eq!(table_record["styleOptions"]["headerRow"], true); - assert_eq!(table_record["styleOptions"]["lastColumn"], true); - assert_eq!(table_record["rightToLeft"], true); - assert_eq!(table_record["borders"]["outside"]["color"], "222222"); - assert_eq!( - table_record["rows"][1][1]["richText"]["ranges"] - .as_array() - .map(Vec::len), - Some(1) - ); - - let chart_record = elements - .iter() - .find(|element| element["elementId"] == chart_id) - .expect("chart record"); - assert_eq!(chart_record["styleIndex"], 12); - assert_eq!(chart_record["legend"]["position"], "bottom"); - assert_eq!(chart_record["series"].as_array().map(Vec::len), Some(2)); - assert_eq!(chart_record["series"][1]["name"], "Target"); - - let inspect = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(imported_artifact_id.clone()), - action: "inspect".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let inspect_ndjson = inspect.inspect_ndjson.expect("inspect"); - assert!(inspect_ndjson.contains("\"kind\":\"comment\"")); - assert!(inspect_ndjson.contains("\"kind\":\"textRange\"")); - assert!(inspect_ndjson.contains("\"chartType\":\"Bar\"")); - - let resolved_thread = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(imported_artifact_id.clone()), - action: "resolve".to_string(), - args: serde_json::json!({ "id": "th/thread_1" }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_thread - .resolved_record - .as_ref() - .and_then(|record| record.get("target")) - .and_then(|target| target.get("type")), - Some(&serde_json::json!("textRange")) - ); - - let resolved_range = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(imported_artifact_id), - action: "resolve".to_string(), - args: serde_json::json!({ "id": "tr/range_1" }), - }, - temp_dir.path(), - )?; - assert_eq!( - resolved_range - .resolved_record - .as_ref() - .and_then(|record| record.get("text")), - Some(&serde_json::json!("update")) - ); - Ok(()) -} - -#[test] -fn history_can_undo_and_redo_created_artifact() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "History" }), - }, - temp_dir.path(), - )?; - - let undone = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "undo".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!(undone.artifact_id, created.artifact_id); - assert!(undone.artifact_snapshot.is_none()); - assert!( - manager - .execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "get_summary".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - ) - .is_err() - ); - - let redone = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(created.artifact_id.clone()), - action: "redo".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!(redone.artifact_id, created.artifact_id); - assert_eq!( - redone - .artifact_snapshot - .as_ref() - .map(|snapshot| snapshot.slide_count), - Some(0) - ); - Ok(()) -} - -#[test] -fn proto_and_patch_actions_work_and_patch_history_is_atomic() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = PresentationArtifactManager::default(); - let created = manager.execute( - PresentationArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Proto Patch" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "add_slide".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - - let patch_ops = serde_json::json!([ - { - "action": "add_text_shape", - "args": { - "slide_index": 0, - "text": "Patch text", - "position": { "left": 40, "top": 60, "width": 180, "height": 50 } - } - }, - { - "action": "set_slide_background", - "args": { - "slide_index": 0, - "fill": "#ffeecc" - } - } - ]); - let recorded = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "record_patch".to_string(), - args: serde_json::json!({ "operations": patch_ops }), - }, - temp_dir.path(), - )?; - let expected_patch = serde_json::json!({ - "version": 1, - "artifactId": artifact_id, - "operations": [ - { - "action": "add_text_shape", - "args": { - "slide_index": 0, - "text": "Patch text", - "position": { "left": 40, "top": 60, "width": 180, "height": 50 } - } - }, - { - "action": "set_slide_background", - "args": { - "slide_index": 0, - "fill": "#ffeecc" - } - } - ] - }); - assert_eq!(recorded.patch.as_ref(), Some(&expected_patch)); - - let applied = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "apply_patch".to_string(), - args: serde_json::json!({ "patch": expected_patch }), - }, - temp_dir.path(), - )?; - let slide_snapshot = applied - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .cloned() - .expect("slide snapshot"); - let slide_id = slide_snapshot.slide_id.clone(); - let element_id = slide_snapshot - .element_ids - .first() - .cloned() - .expect("element id"); - - let proto = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "to_proto".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - let proto = proto.proto_json.expect("proto"); - assert_eq!(proto["kind"], "presentation"); - assert_eq!(proto["artifactId"], artifact_id); - assert_eq!(proto["activeSlideId"], slide_id); - assert_eq!(proto["commentAuthor"], serde_json::Value::Null); - assert_eq!(proto["commentThreads"], serde_json::json!([])); - assert_eq!(proto["masters"], serde_json::json!([])); - assert_eq!(proto["layouts"], serde_json::json!([])); - assert_eq!(proto["theme"]["hexColorMap"], serde_json::json!({})); - assert_eq!(proto["styles"].as_array().map(Vec::len), Some(5)); - assert_eq!(proto["slides"].as_array().map(Vec::len), Some(1)); - assert_eq!(proto["slides"][0]["slideId"], slide_id); - assert_eq!(proto["slides"][0]["backgroundFill"], "FFEECC"); - assert_eq!( - proto["slides"][0]["notes"]["richText"]["ranges"], - serde_json::json!([]) - ); - assert_eq!( - proto["slides"][0]["elements"].as_array().map(Vec::len), - Some(1) - ); - assert_eq!(proto["slides"][0]["elements"][0]["elementId"], element_id); - assert_eq!(proto["slides"][0]["elements"][0]["text"], "Patch text"); - assert_eq!( - proto["slides"][0]["elements"][0]["richText"]["ranges"], - serde_json::json!([]) - ); - - let undone = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "undo".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!( - undone - .artifact_snapshot - .as_ref() - .and_then(|snapshot| snapshot.slides.first()) - .map(|slide| slide.element_ids.clone()), - Some(Vec::new()) - ); - let undone_proto = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "to_proto".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!( - undone_proto - .proto_json - .as_ref() - .and_then(|proto| proto.get("slides")) - .and_then(serde_json::Value::as_array) - .and_then(|slides| slides.first()) - .and_then(|slide| slide.get("backgroundFill")), - Some(&serde_json::Value::Null) - ); - assert_eq!( - undone_proto - .proto_json - .as_ref() - .and_then(|proto| proto.get("slides")) - .and_then(serde_json::Value::as_array) - .and_then(|slides| slides.first()) - .and_then(|slide| slide.get("elements")) - .and_then(serde_json::Value::as_array) - .map(Vec::len), - Some(0) - ); - - let redone = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "redo".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!(redone.patch, None); - let redone_proto = manager.execute( - PresentationArtifactRequest { - artifact_id: Some(artifact_id), - action: "to_proto".to_string(), - args: serde_json::json!({}), - }, - temp_dir.path(), - )?; - assert_eq!(redone_proto.proto_json, Some(proto)); - Ok(()) -} diff --git a/codex-rs/artifact-spreadsheet/BUILD.bazel b/codex-rs/artifact-spreadsheet/BUILD.bazel deleted file mode 100644 index 07d133728..000000000 --- a/codex-rs/artifact-spreadsheet/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "artifact-spreadsheet", - crate_name = "codex_artifact_spreadsheet", -) diff --git a/codex-rs/artifact-spreadsheet/Cargo.toml b/codex-rs/artifact-spreadsheet/Cargo.toml deleted file mode 100644 index 66e030ea4..000000000 --- a/codex-rs/artifact-spreadsheet/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "codex-artifact-spreadsheet" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -name = "codex_artifact_spreadsheet" -path = "src/lib.rs" - -[lints] -workspace = true - -[dependencies] -base64 = { workspace = true } -regex = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -uuid = { workspace = true, features = ["v4"] } -zip = { workspace = true } - -[dev-dependencies] -pretty_assertions = { workspace = true } -tempfile = { workspace = true } diff --git a/codex-rs/artifact-spreadsheet/src/address.rs b/codex-rs/artifact-spreadsheet/src/address.rs deleted file mode 100644 index 28e4da5e3..000000000 --- a/codex-rs/artifact-spreadsheet/src/address.rs +++ /dev/null @@ -1,245 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; - -use crate::SpreadsheetArtifactError; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct CellAddress { - pub column: u32, - pub row: u32, -} - -impl CellAddress { - pub fn parse(address: &str) -> Result { - let trimmed = address.trim(); - if trimmed.is_empty() { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: address.to_string(), - message: "address is empty".to_string(), - }); - } - - let mut split = 0usize; - for (index, ch) in trimmed.char_indices() { - if ch.is_ascii_alphabetic() { - split = index + ch.len_utf8(); - } else { - break; - } - } - - let (letters, digits) = trimmed.split_at(split); - if letters.is_empty() || digits.is_empty() { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: address.to_string(), - message: "expected A1-style address".to_string(), - }); - } - - if !letters.chars().all(|ch| ch.is_ascii_alphabetic()) - || !digits.chars().all(|ch| ch.is_ascii_digit()) - { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: address.to_string(), - message: "expected letters followed by digits".to_string(), - }); - } - - let column = column_letters_to_index(letters)?; - let row = digits - .parse::() - .map_err(|_| SpreadsheetArtifactError::InvalidAddress { - address: address.to_string(), - message: "row must be a positive integer".to_string(), - })?; - - if row == 0 { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: address.to_string(), - message: "row must be positive".to_string(), - }); - } - - Ok(Self { column, row }) - } - - pub fn to_a1(self) -> String { - format!("{}{}", column_index_to_letters(self.column), self.row) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CellRange { - pub start: CellAddress, - pub end: CellAddress, -} - -impl CellRange { - pub fn parse(address: &str) -> Result { - let trimmed = address.trim(); - if trimmed.is_empty() { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: address.to_string(), - message: "range is empty".to_string(), - }); - } - - let (start, end) = if let Some((left, right)) = trimmed.split_once(':') { - (CellAddress::parse(left)?, CellAddress::parse(right)?) - } else { - let cell = CellAddress::parse(trimmed)?; - (cell, cell) - }; - - let normalized = Self { - start: CellAddress { - column: start.column.min(end.column), - row: start.row.min(end.row), - }, - end: CellAddress { - column: start.column.max(end.column), - row: start.row.max(end.row), - }, - }; - Ok(normalized) - } - - pub fn from_start_end(start: CellAddress, end: CellAddress) -> Self { - Self { - start: CellAddress { - column: start.column.min(end.column), - row: start.row.min(end.row), - }, - end: CellAddress { - column: start.column.max(end.column), - row: start.row.max(end.row), - }, - } - } - - pub fn to_a1(&self) -> String { - if self.is_single_cell() { - self.start.to_a1() - } else { - format!("{}:{}", self.start.to_a1(), self.end.to_a1()) - } - } - - pub fn is_single_cell(&self) -> bool { - self.start == self.end - } - - pub fn is_single_row(&self) -> bool { - self.start.row == self.end.row - } - - pub fn is_single_column(&self) -> bool { - self.start.column == self.end.column - } - - pub fn width(&self) -> usize { - (self.end.column - self.start.column + 1) as usize - } - - pub fn height(&self) -> usize { - (self.end.row - self.start.row + 1) as usize - } - - pub fn contains(&self, address: CellAddress) -> bool { - self.start.column <= address.column - && address.column <= self.end.column - && self.start.row <= address.row - && address.row <= self.end.row - } - - pub fn contains_range(&self, other: &CellRange) -> bool { - self.contains(other.start) && self.contains(other.end) - } - - pub fn intersects(&self, other: &CellRange) -> bool { - !(self.end.column < other.start.column - || other.end.column < self.start.column - || self.end.row < other.start.row - || other.end.row < self.start.row) - } - - pub fn addresses(&self) -> impl Iterator { - let range = self.clone(); - (range.start.row..=range.end.row).flat_map(move |row| { - let range = range.clone(); - (range.start.column..=range.end.column).map(move |column| CellAddress { column, row }) - }) - } -} - -pub fn column_letters_to_index(column: &str) -> Result { - let trimmed = column.trim(); - if trimmed.is_empty() { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: column.to_string(), - message: "column is empty".to_string(), - }); - } - - let mut result = 0u32; - for ch in trimmed.chars() { - if !ch.is_ascii_alphabetic() { - return Err(SpreadsheetArtifactError::InvalidAddress { - address: column.to_string(), - message: "column must contain only letters".to_string(), - }); - } - result = result - .checked_mul(26) - .and_then(|value| value.checked_add((ch.to_ascii_uppercase() as u8 - b'A' + 1) as u32)) - .ok_or_else(|| SpreadsheetArtifactError::InvalidAddress { - address: column.to_string(), - message: "column is too large".to_string(), - })?; - } - Ok(result) -} - -pub fn column_index_to_letters(mut index: u32) -> String { - if index == 0 { - return String::new(); - } - - let mut letters = Vec::new(); - while index > 0 { - let remainder = (index - 1) % 26; - letters.push((b'A' + remainder as u8) as char); - index = (index - 1) / 26; - } - letters.iter().rev().collect() -} - -pub fn parse_column_reference(reference: &str) -> Result<(u32, u32), SpreadsheetArtifactError> { - let trimmed = reference.trim(); - if let Some((left, right)) = trimmed.split_once(':') { - let start = column_letters_to_index(left)?; - let end = column_letters_to_index(right)?; - Ok((start.min(end), start.max(end))) - } else { - let column = column_letters_to_index(trimmed)?; - Ok((column, column)) - } -} - -pub fn is_valid_cell_reference(address: &str) -> bool { - CellAddress::parse(address).is_ok() -} - -pub fn is_valid_range_reference(address: &str) -> bool { - CellRange::parse(address).is_ok() -} - -pub fn is_valid_row_reference(address: &str) -> bool { - CellRange::parse(address) - .map(|range| range.is_single_row()) - .unwrap_or(false) -} - -pub fn is_valid_column_reference(address: &str) -> bool { - parse_column_reference(address).is_ok() -} diff --git a/codex-rs/artifact-spreadsheet/src/chart.rs b/codex-rs/artifact-spreadsheet/src/chart.rs deleted file mode 100644 index 83b02b6be..000000000 --- a/codex-rs/artifact-spreadsheet/src/chart.rs +++ /dev/null @@ -1,357 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetSheet; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum SpreadsheetChartType { - Area, - Bar, - Doughnut, - Line, - Pie, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum SpreadsheetChartLegendPosition { - Bottom, - Top, - Left, - Right, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetChartLegend { - pub visible: bool, - pub position: SpreadsheetChartLegendPosition, - pub overlay: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetChartAxis { - pub linked_number_format: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetChartSeries { - pub id: u32, - pub name: Option, - pub category_sheet_name: Option, - pub category_range: String, - pub value_sheet_name: Option, - pub value_range: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetChart { - pub id: u32, - pub chart_type: SpreadsheetChartType, - pub source_sheet_name: Option, - pub source_range: Option, - pub title: Option, - pub style_index: u32, - pub display_blanks_as: String, - pub legend: SpreadsheetChartLegend, - pub category_axis: SpreadsheetChartAxis, - pub value_axis: SpreadsheetChartAxis, - #[serde(default)] - pub series: Vec, -} - -#[derive(Debug, Clone, Default)] -pub struct SpreadsheetChartLookup { - pub id: Option, - pub index: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetChartCreateOptions { - pub id: Option, - pub title: Option, - pub legend_visible: Option, - pub legend_position: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetChartProperties { - pub title: Option, - pub legend_visible: Option, - pub legend_position: Option, -} - -impl SpreadsheetSheet { - pub fn list_charts( - &self, - range: Option<&CellRange>, - ) -> Result, SpreadsheetArtifactError> { - Ok(self - .charts - .iter() - .filter(|chart| { - range.is_none_or(|target| { - chart - .source_range - .as_deref() - .map(CellRange::parse) - .transpose() - .ok() - .flatten() - .is_some_and(|chart_range| chart_range.intersects(target)) - }) - }) - .cloned() - .collect()) - } - - pub fn get_chart( - &self, - action: &str, - lookup: SpreadsheetChartLookup, - ) -> Result<&SpreadsheetChart, SpreadsheetArtifactError> { - if let Some(id) = lookup.id { - return self - .charts - .iter() - .find(|chart| chart.id == id) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("chart id `{id}` was not found"), - }); - } - if let Some(index) = lookup.index { - return self.charts.get(index).ok_or_else(|| { - SpreadsheetArtifactError::IndexOutOfRange { - action: action.to_string(), - index, - len: self.charts.len(), - } - }); - } - Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "chart id or index is required".to_string(), - }) - } - - pub fn create_chart( - &mut self, - action: &str, - chart_type: SpreadsheetChartType, - source_sheet_name: Option, - source_range: &CellRange, - options: SpreadsheetChartCreateOptions, - ) -> Result { - if source_range.width() < 2 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "chart source range must include at least two columns".to_string(), - }); - } - let id = if let Some(id) = options.id { - if self.charts.iter().any(|chart| chart.id == id) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("chart id `{id}` already exists"), - }); - } - id - } else { - self.charts.iter().map(|chart| chart.id).max().unwrap_or(0) + 1 - }; - let series = (source_range.start.column + 1..=source_range.end.column) - .enumerate() - .map(|(index, value_column)| SpreadsheetChartSeries { - id: index as u32 + 1, - name: None, - category_sheet_name: source_sheet_name.clone(), - category_range: CellRange::from_start_end( - source_range.start, - CellAddress { - column: source_range.start.column, - row: source_range.end.row, - }, - ) - .to_a1(), - value_sheet_name: source_sheet_name.clone(), - value_range: CellRange::from_start_end( - CellAddress { - column: value_column, - row: source_range.start.row, - }, - CellAddress { - column: value_column, - row: source_range.end.row, - }, - ) - .to_a1(), - }) - .collect::>(); - self.charts.push(SpreadsheetChart { - id, - chart_type, - source_sheet_name, - source_range: Some(source_range.to_a1()), - title: options.title, - style_index: 102, - display_blanks_as: "gap".to_string(), - legend: SpreadsheetChartLegend { - visible: options.legend_visible.unwrap_or(true), - position: options - .legend_position - .unwrap_or(SpreadsheetChartLegendPosition::Bottom), - overlay: false, - }, - category_axis: SpreadsheetChartAxis { - linked_number_format: true, - }, - value_axis: SpreadsheetChartAxis { - linked_number_format: true, - }, - series, - }); - Ok(id) - } - - pub fn add_chart_series( - &mut self, - action: &str, - lookup: SpreadsheetChartLookup, - mut series: SpreadsheetChartSeries, - ) -> Result { - validate_chart_series(action, &series)?; - let chart = self.get_chart_mut(action, lookup)?; - let next_id = chart.series.iter().map(|entry| entry.id).max().unwrap_or(0) + 1; - series.id = next_id; - chart.series.push(series); - Ok(next_id) - } - - pub fn delete_chart( - &mut self, - action: &str, - lookup: SpreadsheetChartLookup, - ) -> Result<(), SpreadsheetArtifactError> { - let index = if let Some(id) = lookup.id { - self.charts - .iter() - .position(|chart| chart.id == id) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("chart id `{id}` was not found"), - })? - } else if let Some(index) = lookup.index { - if index >= self.charts.len() { - return Err(SpreadsheetArtifactError::IndexOutOfRange { - action: action.to_string(), - index, - len: self.charts.len(), - }); - } - index - } else { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "chart id or index is required".to_string(), - }); - }; - self.charts.remove(index); - Ok(()) - } - - pub fn set_chart_properties( - &mut self, - action: &str, - lookup: SpreadsheetChartLookup, - properties: SpreadsheetChartProperties, - ) -> Result<(), SpreadsheetArtifactError> { - let chart = self.get_chart_mut(action, lookup)?; - if let Some(title) = properties.title { - chart.title = Some(title); - } - if let Some(visible) = properties.legend_visible { - chart.legend.visible = visible; - } - if let Some(position) = properties.legend_position { - chart.legend.position = position; - } - Ok(()) - } - - pub fn validate_charts(&self, action: &str) -> Result<(), SpreadsheetArtifactError> { - for chart in &self.charts { - if let Some(source_range) = &chart.source_range { - let range = CellRange::parse(source_range)?; - if range.width() < 2 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "chart `{}` source range `{source_range}` is too narrow", - chart.id - ), - }); - } - } - for series in &chart.series { - validate_chart_series(action, series)?; - } - } - Ok(()) - } - - fn get_chart_mut( - &mut self, - action: &str, - lookup: SpreadsheetChartLookup, - ) -> Result<&mut SpreadsheetChart, SpreadsheetArtifactError> { - if let Some(id) = lookup.id { - return self - .charts - .iter_mut() - .find(|chart| chart.id == id) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("chart id `{id}` was not found"), - }); - } - if let Some(index) = lookup.index { - let len = self.charts.len(); - return self.charts.get_mut(index).ok_or_else(|| { - SpreadsheetArtifactError::IndexOutOfRange { - action: action.to_string(), - index, - len, - } - }); - } - Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "chart id or index is required".to_string(), - }) - } -} - -fn validate_chart_series( - action: &str, - series: &SpreadsheetChartSeries, -) -> Result<(), SpreadsheetArtifactError> { - let category_range = CellRange::parse(&series.category_range)?; - let value_range = CellRange::parse(&series.value_range)?; - if !category_range.is_single_column() || !value_range.is_single_column() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "chart category and value ranges must be single-column ranges".to_string(), - }); - } - if category_range.height() != value_range.height() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "chart category and value series lengths must match".to_string(), - }); - } - Ok(()) -} diff --git a/codex-rs/artifact-spreadsheet/src/conditional.rs b/codex-rs/artifact-spreadsheet/src/conditional.rs deleted file mode 100644 index 96c0c8ebd..000000000 --- a/codex-rs/artifact-spreadsheet/src/conditional.rs +++ /dev/null @@ -1,308 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; - -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetSheet; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum SpreadsheetConditionalFormatType { - Expression, - CellIs, - ColorScale, - DataBar, - IconSet, - Top10, - UniqueValues, - DuplicateValues, - ContainsText, - NotContainsText, - BeginsWith, - EndsWith, - ContainsBlanks, - NotContainsBlanks, - ContainsErrors, - NotContainsErrors, - TimePeriod, - AboveAverage, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetColorScale { - pub min_type: Option, - pub mid_type: Option, - pub max_type: Option, - pub min_value: Option, - pub mid_value: Option, - pub max_value: Option, - pub min_color: String, - pub mid_color: Option, - pub max_color: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetDataBar { - pub color: String, - pub min_length: Option, - pub max_length: Option, - pub show_value: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetIconSet { - pub style: String, - pub show_value: Option, - pub reverse_order: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetConditionalFormat { - pub id: u32, - pub range: String, - pub rule_type: SpreadsheetConditionalFormatType, - pub operator: Option, - #[serde(default)] - pub formulas: Vec, - pub text: Option, - pub dxf_id: Option, - pub stop_if_true: bool, - pub priority: u32, - pub rank: Option, - pub percent: Option, - pub time_period: Option, - pub above_average: Option, - pub equal_average: Option, - pub color_scale: Option, - pub data_bar: Option, - pub icon_set: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetConditionalFormatCollection { - pub sheet_name: String, - pub range: String, -} - -impl SpreadsheetConditionalFormatCollection { - pub fn new(sheet_name: String, range: &CellRange) -> Self { - Self { - sheet_name, - range: range.to_a1(), - } - } - - pub fn range(&self) -> Result { - CellRange::parse(&self.range) - } - - pub fn list( - &self, - artifact: &SpreadsheetArtifact, - ) -> Result, SpreadsheetArtifactError> { - let sheet = artifact.sheet_lookup( - "conditional_format_collection", - Some(&self.sheet_name), - None, - )?; - Ok(sheet.list_conditional_formats(Some(&self.range()?))) - } - - pub fn add( - &self, - artifact: &mut SpreadsheetArtifact, - mut format: SpreadsheetConditionalFormat, - ) -> Result { - format.range = self.range.clone(); - artifact.add_conditional_format("conditional_format_collection", &self.sheet_name, format) - } - - pub fn delete( - &self, - artifact: &mut SpreadsheetArtifact, - id: u32, - ) -> Result<(), SpreadsheetArtifactError> { - artifact.delete_conditional_format("conditional_format_collection", &self.sheet_name, id) - } -} - -impl SpreadsheetArtifact { - pub fn validate_conditional_formats( - &self, - action: &str, - sheet_name: &str, - ) -> Result<(), SpreadsheetArtifactError> { - let sheet = self.sheet_lookup(action, Some(sheet_name), None)?; - for format in &sheet.conditional_formats { - validate_conditional_format(self, format, action)?; - } - Ok(()) - } - - pub fn add_conditional_format( - &mut self, - action: &str, - sheet_name: &str, - mut format: SpreadsheetConditionalFormat, - ) -> Result { - validate_conditional_format(self, &format, action)?; - let sheet = self.sheet_lookup_mut(action, Some(sheet_name), None)?; - let next_id = sheet - .conditional_formats - .iter() - .map(|entry| entry.id) - .max() - .unwrap_or(0) - + 1; - format.id = next_id; - format.priority = if format.priority == 0 { - next_id - } else { - format.priority - }; - sheet.conditional_formats.push(format); - Ok(next_id) - } - - pub fn delete_conditional_format( - &mut self, - action: &str, - sheet_name: &str, - id: u32, - ) -> Result<(), SpreadsheetArtifactError> { - let sheet = self.sheet_lookup_mut(action, Some(sheet_name), None)?; - let previous_len = sheet.conditional_formats.len(); - sheet.conditional_formats.retain(|entry| entry.id != id); - if sheet.conditional_formats.len() == previous_len { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("conditional format `{id}` was not found"), - }); - } - Ok(()) - } -} - -impl SpreadsheetSheet { - pub fn conditional_format_collection( - &self, - range: &CellRange, - ) -> SpreadsheetConditionalFormatCollection { - SpreadsheetConditionalFormatCollection::new(self.name.clone(), range) - } - - pub fn list_conditional_formats( - &self, - range: Option<&CellRange>, - ) -> Vec { - self.conditional_formats - .iter() - .filter(|entry| { - range.is_none_or(|target| { - CellRange::parse(&entry.range) - .map(|entry_range| entry_range.intersects(target)) - .unwrap_or(false) - }) - }) - .cloned() - .collect() - } -} - -fn validate_conditional_format( - artifact: &SpreadsheetArtifact, - format: &SpreadsheetConditionalFormat, - action: &str, -) -> Result<(), SpreadsheetArtifactError> { - CellRange::parse(&format.range)?; - if let Some(dxf_id) = format.dxf_id - && artifact.get_differential_format(dxf_id).is_none() - { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("differential format `{dxf_id}` was not found"), - }); - } - - let has_style = format.dxf_id.is_some(); - let has_intrinsic_visual = - format.color_scale.is_some() || format.data_bar.is_some() || format.icon_set.is_some(); - - match format.rule_type { - SpreadsheetConditionalFormatType::Expression | SpreadsheetConditionalFormatType::CellIs => { - if format.formulas.is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "conditional format formulas are required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::ContainsText - | SpreadsheetConditionalFormatType::NotContainsText - | SpreadsheetConditionalFormatType::BeginsWith - | SpreadsheetConditionalFormatType::EndsWith => { - if format.text.as_deref().unwrap_or_default().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "conditional format text is required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::ColorScale => { - if format.color_scale.is_none() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "color scale settings are required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::DataBar => { - if format.data_bar.is_none() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "data bar settings are required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::IconSet => { - if format.icon_set.is_none() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "icon set settings are required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::Top10 => { - if format.rank.is_none() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "top10 rank is required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::TimePeriod => { - if format.time_period.as_deref().unwrap_or_default().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "time period is required".to_string(), - }); - } - } - SpreadsheetConditionalFormatType::AboveAverage => {} - SpreadsheetConditionalFormatType::UniqueValues - | SpreadsheetConditionalFormatType::DuplicateValues - | SpreadsheetConditionalFormatType::ContainsBlanks - | SpreadsheetConditionalFormatType::NotContainsBlanks - | SpreadsheetConditionalFormatType::ContainsErrors - | SpreadsheetConditionalFormatType::NotContainsErrors => {} - } - - if !has_style && !has_intrinsic_visual { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "conditional formatting requires at least one style component".to_string(), - }); - } - Ok(()) -} diff --git a/codex-rs/artifact-spreadsheet/src/error.rs b/codex-rs/artifact-spreadsheet/src/error.rs deleted file mode 100644 index d6d0b31bc..000000000 --- a/codex-rs/artifact-spreadsheet/src/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::path::PathBuf; - -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum SpreadsheetArtifactError { - #[error("missing `artifact_id` for action `{action}`")] - MissingArtifactId { action: String }, - #[error("unknown artifact id `{artifact_id}` for action `{action}`")] - UnknownArtifactId { action: String, artifact_id: String }, - #[error("unknown action `{0}`")] - UnknownAction(String), - #[error("invalid args for action `{action}`: {message}")] - InvalidArgs { action: String, message: String }, - #[error("invalid address `{address}`: {message}")] - InvalidAddress { address: String, message: String }, - #[error("sheet lookup failed for action `{action}`: {message}")] - SheetLookup { action: String, message: String }, - #[error("index `{index}` is out of range for action `{action}`; len={len}")] - IndexOutOfRange { - action: String, - index: usize, - len: usize, - }, - #[error("merge conflict for action `{action}` on range `{range}` with `{conflict}`")] - MergeConflict { - action: String, - range: String, - conflict: String, - }, - #[error("formula error at `{location}`: {message}")] - Formula { location: String, message: String }, - #[error("serialization failed: {message}")] - Serialization { message: String }, - #[error("failed to import XLSX `{path}`: {message}")] - ImportFailed { path: PathBuf, message: String }, - #[error("failed to export XLSX `{path}`: {message}")] - ExportFailed { path: PathBuf, message: String }, -} diff --git a/codex-rs/artifact-spreadsheet/src/formula.rs b/codex-rs/artifact-spreadsheet/src/formula.rs deleted file mode 100644 index 9ee8d0824..000000000 --- a/codex-rs/artifact-spreadsheet/src/formula.rs +++ /dev/null @@ -1,535 +0,0 @@ -use std::collections::BTreeSet; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetCellValue; - -#[derive(Debug, Clone)] -enum Token { - Number(f64), - Cell(String), - Ident(String), - Plus, - Minus, - Star, - Slash, - LParen, - RParen, - Colon, - Comma, -} - -#[derive(Debug, Clone)] -enum Expr { - Number(f64), - Cell(CellAddress), - Range(CellRange), - UnaryMinus(Box), - Binary { - op: BinaryOp, - left: Box, - right: Box, - }, - Function { - name: String, - args: Vec, - }, -} - -#[derive(Debug, Clone, Copy)] -enum BinaryOp { - Add, - Subtract, - Multiply, - Divide, -} - -#[derive(Debug, Clone)] -enum EvalValue { - Scalar(Option), - Range(Vec>), -} - -pub(crate) fn recalculate_workbook(artifact: &mut SpreadsheetArtifact) { - let updates = artifact - .sheets - .iter() - .enumerate() - .flat_map(|(sheet_index, sheet)| { - sheet.cells.iter().filter_map(move |(address, cell)| { - cell.formula - .as_ref() - .map(|formula| (sheet_index, *address, formula.clone())) - }) - }) - .map(|(sheet_index, address, formula)| { - let mut stack = BTreeSet::new(); - let value = evaluate_formula(artifact, sheet_index, &formula, &mut stack) - .unwrap_or_else(|error| { - Some(SpreadsheetCellValue::Error(map_error_to_code(&error))) - }); - (sheet_index, address, value) - }) - .collect::>(); - - for (sheet_index, address, value) in updates { - if let Some(sheet) = artifact.sheets.get_mut(sheet_index) - && let Some(cell) = sheet.cells.get_mut(&address) - { - cell.value = value; - } - } -} - -fn evaluate_formula( - artifact: &SpreadsheetArtifact, - sheet_index: usize, - formula: &str, - stack: &mut BTreeSet<(usize, CellAddress)>, -) -> Result, SpreadsheetArtifactError> { - let source = formula.trim().trim_start_matches('='); - let tokens = tokenize(source)?; - let mut parser = Parser::new(tokens); - let expr = parser.parse_expression()?; - if parser.has_remaining() { - return Err(SpreadsheetArtifactError::Formula { - location: formula.to_string(), - message: "unexpected trailing tokens".to_string(), - }); - } - match evaluate_expr(artifact, sheet_index, &expr, stack)? { - EvalValue::Scalar(value) => Ok(value), - EvalValue::Range(_) => Err(SpreadsheetArtifactError::Formula { - location: formula.to_string(), - message: "range expressions are only allowed inside functions".to_string(), - }), - } -} - -fn evaluate_expr( - artifact: &SpreadsheetArtifact, - sheet_index: usize, - expr: &Expr, - stack: &mut BTreeSet<(usize, CellAddress)>, -) -> Result { - match expr { - Expr::Number(value) => Ok(EvalValue::Scalar(Some(number_to_value(*value)))), - Expr::Cell(address) => evaluate_cell_reference(artifact, sheet_index, *address, stack), - Expr::Range(range) => { - let sheet = artifact.sheets.get(sheet_index).ok_or_else(|| { - SpreadsheetArtifactError::Formula { - location: range.to_a1(), - message: "sheet index was not found".to_string(), - } - })?; - let values = range - .addresses() - .map(|address| sheet.get_cell(address).and_then(|cell| cell.value.clone())) - .collect::>(); - Ok(EvalValue::Range(values)) - } - Expr::UnaryMinus(inner) => { - let value = evaluate_scalar(artifact, sheet_index, inner, stack)?; - Ok(EvalValue::Scalar(match value { - None => Some(SpreadsheetCellValue::Integer(0)), - Some(SpreadsheetCellValue::Integer(value)) => { - Some(SpreadsheetCellValue::Integer(-value)) - } - Some(SpreadsheetCellValue::Float(value)) => { - Some(SpreadsheetCellValue::Float(-value)) - } - Some(SpreadsheetCellValue::Error(value)) => { - Some(SpreadsheetCellValue::Error(value)) - } - Some(_) => Some(SpreadsheetCellValue::Error("#VALUE!".to_string())), - })) - } - Expr::Binary { op, left, right } => { - let left = evaluate_scalar(artifact, sheet_index, left, stack)?; - let right = evaluate_scalar(artifact, sheet_index, right, stack)?; - Ok(EvalValue::Scalar(Some(apply_binary_op(*op, left, right)?))) - } - Expr::Function { name, args } => { - let mut numeric = Vec::new(); - for arg in args { - match evaluate_expr(artifact, sheet_index, arg, stack)? { - EvalValue::Scalar(value) => { - if let Some(number) = scalar_to_number(value.clone())? { - numeric.push(number); - } - } - EvalValue::Range(values) => { - for value in values { - if let Some(number) = scalar_to_number(value.clone())? { - numeric.push(number); - } - } - } - } - } - let upper = name.to_ascii_uppercase(); - let result = match upper.as_str() { - "SUM" => numeric.iter().sum::(), - "AVERAGE" => { - if numeric.is_empty() { - return Ok(EvalValue::Scalar(None)); - } - numeric.iter().sum::() / numeric.len() as f64 - } - "MIN" => numeric.iter().copied().reduce(f64::min).unwrap_or(0.0), - "MAX" => numeric.iter().copied().reduce(f64::max).unwrap_or(0.0), - _ => { - return Ok(EvalValue::Scalar(Some(SpreadsheetCellValue::Error( - "#NAME?".to_string(), - )))); - } - }; - Ok(EvalValue::Scalar(Some(number_to_value(result)))) - } - } -} - -fn evaluate_scalar( - artifact: &SpreadsheetArtifact, - sheet_index: usize, - expr: &Expr, - stack: &mut BTreeSet<(usize, CellAddress)>, -) -> Result, SpreadsheetArtifactError> { - match evaluate_expr(artifact, sheet_index, expr, stack)? { - EvalValue::Scalar(value) => Ok(value), - EvalValue::Range(_) => Err(SpreadsheetArtifactError::Formula { - location: format!("{expr:?}"), - message: "expected a scalar expression".to_string(), - }), - } -} - -fn evaluate_cell_reference( - artifact: &SpreadsheetArtifact, - sheet_index: usize, - address: CellAddress, - stack: &mut BTreeSet<(usize, CellAddress)>, -) -> Result { - let Some(sheet) = artifact.sheets.get(sheet_index) else { - return Err(SpreadsheetArtifactError::Formula { - location: address.to_a1(), - message: "sheet index was not found".to_string(), - }); - }; - let key = (sheet_index, address); - if !stack.insert(key) { - return Ok(EvalValue::Scalar(Some(SpreadsheetCellValue::Error( - "#CYCLE!".to_string(), - )))); - } - - let value = if let Some(cell) = sheet.get_cell(address) { - if let Some(formula) = &cell.formula { - evaluate_formula(artifact, sheet_index, formula, stack)? - } else { - cell.value.clone() - } - } else { - None - }; - stack.remove(&key); - Ok(EvalValue::Scalar(value)) -} - -fn apply_binary_op( - op: BinaryOp, - left: Option, - right: Option, -) -> Result { - if let Some(SpreadsheetCellValue::Error(value)) = &left { - return Ok(SpreadsheetCellValue::Error(value.clone())); - } - if let Some(SpreadsheetCellValue::Error(value)) = &right { - return Ok(SpreadsheetCellValue::Error(value.clone())); - } - - let left = scalar_to_number(left)?; - let right = scalar_to_number(right)?; - let left = left.unwrap_or(0.0); - let right = right.unwrap_or(0.0); - let result = match op { - BinaryOp::Add => left + right, - BinaryOp::Subtract => left - right, - BinaryOp::Multiply => left * right, - BinaryOp::Divide => { - if right == 0.0 { - return Ok(SpreadsheetCellValue::Error("#DIV/0!".to_string())); - } - left / right - } - }; - Ok(number_to_value(result)) -} - -fn scalar_to_number( - value: Option, -) -> Result, SpreadsheetArtifactError> { - match value { - None => Ok(None), - Some(SpreadsheetCellValue::Integer(value)) => Ok(Some(value as f64)), - Some(SpreadsheetCellValue::Float(value)) => Ok(Some(value)), - Some(SpreadsheetCellValue::Bool(value)) => Ok(Some(if value { 1.0 } else { 0.0 })), - Some(SpreadsheetCellValue::Error(value)) => Err(SpreadsheetArtifactError::Formula { - location: value, - message: "encountered error value".to_string(), - }), - Some(other) => Err(SpreadsheetArtifactError::Formula { - location: format!("{other:?}"), - message: "value is not numeric".to_string(), - }), - } -} - -fn number_to_value(number: f64) -> SpreadsheetCellValue { - if number.fract() == 0.0 { - SpreadsheetCellValue::Integer(number as i64) - } else { - SpreadsheetCellValue::Float(number) - } -} - -fn map_error_to_code(error: &SpreadsheetArtifactError) -> String { - match error { - SpreadsheetArtifactError::Formula { message, .. } => { - if message.contains("cycle") { - "#CYCLE!".to_string() - } else if message.contains("not numeric") || message.contains("scalar") { - "#VALUE!".to_string() - } else { - "#ERROR!".to_string() - } - } - SpreadsheetArtifactError::InvalidAddress { .. } => "#REF!".to_string(), - _ => "#ERROR!".to_string(), - } -} - -fn tokenize(source: &str) -> Result, SpreadsheetArtifactError> { - let chars = source.chars().collect::>(); - let mut index = 0usize; - let mut tokens = Vec::new(); - while index < chars.len() { - let ch = chars[index]; - if ch.is_ascii_whitespace() { - index += 1; - continue; - } - match ch { - '+' => { - tokens.push(Token::Plus); - index += 1; - } - '-' => { - tokens.push(Token::Minus); - index += 1; - } - '*' => { - tokens.push(Token::Star); - index += 1; - } - '/' => { - tokens.push(Token::Slash); - index += 1; - } - '(' => { - tokens.push(Token::LParen); - index += 1; - } - ')' => { - tokens.push(Token::RParen); - index += 1; - } - ':' => { - tokens.push(Token::Colon); - index += 1; - } - ',' => { - tokens.push(Token::Comma); - index += 1; - } - '0'..='9' | '.' => { - let start = index; - index += 1; - while index < chars.len() && (chars[index].is_ascii_digit() || chars[index] == '.') - { - index += 1; - } - let number = source[start..index].parse::().map_err(|_| { - SpreadsheetArtifactError::Formula { - location: source.to_string(), - message: "invalid numeric literal".to_string(), - } - })?; - tokens.push(Token::Number(number)); - } - 'A'..='Z' | 'a'..='z' | '_' => { - let start = index; - index += 1; - while index < chars.len() - && (chars[index].is_ascii_alphanumeric() || chars[index] == '_') - { - index += 1; - } - let text = source[start..index].to_string(); - if text.chars().any(|part| part.is_ascii_digit()) - && text.chars().any(|part| part.is_ascii_alphabetic()) - { - tokens.push(Token::Cell(text)); - } else { - tokens.push(Token::Ident(text)); - } - } - other => { - return Err(SpreadsheetArtifactError::Formula { - location: source.to_string(), - message: format!("unsupported token `{other}`"), - }); - } - } - } - Ok(tokens) -} - -struct Parser { - tokens: Vec, - index: usize, -} - -impl Parser { - fn new(tokens: Vec) -> Self { - Self { tokens, index: 0 } - } - - fn has_remaining(&self) -> bool { - self.index < self.tokens.len() - } - - fn parse_expression(&mut self) -> Result { - let mut expr = self.parse_term()?; - while let Some(token) = self.peek() { - let op = match token { - Token::Plus => BinaryOp::Add, - Token::Minus => BinaryOp::Subtract, - _ => break, - }; - self.index += 1; - let right = self.parse_term()?; - expr = Expr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - fn parse_term(&mut self) -> Result { - let mut expr = self.parse_factor()?; - while let Some(token) = self.peek() { - let op = match token { - Token::Star => BinaryOp::Multiply, - Token::Slash => BinaryOp::Divide, - _ => break, - }; - self.index += 1; - let right = self.parse_factor()?; - expr = Expr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - fn parse_factor(&mut self) -> Result { - match self.peek() { - Some(Token::Minus) => { - self.index += 1; - Ok(Expr::UnaryMinus(Box::new(self.parse_factor()?))) - } - _ => self.parse_primary(), - } - } - - fn parse_primary(&mut self) -> Result { - match self.next().cloned() { - Some(Token::Number(value)) => Ok(Expr::Number(value)), - Some(Token::Cell(address)) => { - let start = CellAddress::parse(&address)?; - if matches!(self.peek(), Some(Token::Colon)) { - self.index += 1; - let Some(Token::Cell(end)) = self.next().cloned() else { - return Err(SpreadsheetArtifactError::Formula { - location: address, - message: "expected cell after `:`".to_string(), - }); - }; - Ok(Expr::Range(CellRange::from_start_end( - start, - CellAddress::parse(&end)?, - ))) - } else { - Ok(Expr::Cell(start)) - } - } - Some(Token::Ident(name)) => { - if !matches!(self.next(), Some(Token::LParen)) { - return Err(SpreadsheetArtifactError::Formula { - location: name, - message: "expected `(` after function name".to_string(), - }); - } - let mut args = Vec::new(); - if !matches!(self.peek(), Some(Token::RParen)) { - loop { - args.push(self.parse_expression()?); - if matches!(self.peek(), Some(Token::Comma)) { - self.index += 1; - continue; - } - break; - } - } - if !matches!(self.next(), Some(Token::RParen)) { - return Err(SpreadsheetArtifactError::Formula { - location: name, - message: "expected `)`".to_string(), - }); - } - Ok(Expr::Function { name, args }) - } - Some(Token::LParen) => { - let expr = self.parse_expression()?; - if !matches!(self.next(), Some(Token::RParen)) { - return Err(SpreadsheetArtifactError::Formula { - location: format!("{expr:?}"), - message: "expected `)`".to_string(), - }); - } - Ok(expr) - } - other => Err(SpreadsheetArtifactError::Formula { - location: format!("{other:?}"), - message: "unexpected token".to_string(), - }), - } - } - - fn peek(&self) -> Option<&Token> { - self.tokens.get(self.index) - } - - fn next(&mut self) -> Option<&Token> { - let token = self.tokens.get(self.index); - self.index += usize::from(token.is_some()); - token - } -} diff --git a/codex-rs/artifact-spreadsheet/src/lib.rs b/codex-rs/artifact-spreadsheet/src/lib.rs deleted file mode 100644 index bd9f6ed8f..000000000 --- a/codex-rs/artifact-spreadsheet/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -mod address; -mod chart; -mod conditional; -mod error; -mod formula; -mod manager; -mod model; -mod pivot; -mod render; -mod style; -mod table; -mod xlsx; - -#[cfg(test)] -mod tests; - -pub use address::*; -pub use chart::*; -pub use conditional::*; -pub use error::*; -pub use manager::*; -pub use model::*; -pub use pivot::*; -pub use render::*; -pub use style::*; -pub use table::*; diff --git a/codex-rs/artifact-spreadsheet/src/manager.rs b/codex-rs/artifact-spreadsheet/src/manager.rs deleted file mode 100644 index 392b685c4..000000000 --- a/codex-rs/artifact-spreadsheet/src/manager.rs +++ /dev/null @@ -1,3346 +0,0 @@ -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; - -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetCellRangeRef; -use crate::SpreadsheetCellRef; -use crate::SpreadsheetCellValue; -use crate::SpreadsheetCitation; -use crate::SpreadsheetRangeView; -use crate::SpreadsheetSheetSummary; -use crate::SpreadsheetSummary; - -#[derive(Debug, Clone, Deserialize)] -pub struct SpreadsheetArtifactRequest { - pub artifact_id: Option, - pub action: String, - #[serde(default)] - pub args: Value, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PathAccessKind { - Read, - Write, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PathAccessRequirement { - pub action: String, - pub kind: PathAccessKind, - pub path: PathBuf, -} - -impl SpreadsheetArtifactRequest { - pub fn required_path_accesses( - &self, - cwd: &Path, - ) -> Result, SpreadsheetArtifactError> { - let access = match self.action.as_str() { - "import_xlsx" | "load" | "read" => { - let args: PathArgs = parse_args(&self.action, &self.args)?; - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Read, - path: resolve_path(cwd, &args.path), - }] - } - "export_xlsx" => { - let args: PathArgs = parse_args(&self.action, &self.args)?; - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Write, - path: resolve_path(cwd, &args.path), - }] - } - "save" => { - let args: SaveArgs = parse_args(&self.action, &self.args)?; - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Write, - path: resolve_path(cwd, &args.path), - }] - } - "render_workbook" | "render_sheet" | "render_range" => { - let args: RenderArgs = parse_args(&self.action, &self.args)?; - args.output_path - .map(|path| { - vec![PathAccessRequirement { - action: self.action.clone(), - kind: PathAccessKind::Write, - path: resolve_path(cwd, &path), - }] - }) - .unwrap_or_default() - } - _ => Vec::new(), - }; - Ok(access) - } -} - -#[derive(Debug, Default)] -pub struct SpreadsheetArtifactManager { - documents: HashMap, -} - -impl SpreadsheetArtifactManager { - pub fn execute( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - match request.action.as_str() { - "create" => self.create(request), - "import_xlsx" | "load" | "read" => self.import_xlsx(request, cwd), - "export_xlsx" => self.export_xlsx(request, cwd), - "save" => self.save(request, cwd), - "render_workbook" => self.render_workbook(request, cwd), - "render_sheet" => self.render_sheet(request, cwd), - "render_range" => self.render_range(request, cwd), - "get_summary" => self.get_summary(request), - "list_sheets" => self.list_sheets(request), - "get_sheet" => self.get_sheet(request), - "inspect" => self.inspect(request), - "list_charts" => self.list_charts(request), - "get_chart" => self.get_chart(request), - "create_chart" => self.create_chart(request), - "add_chart_series" => self.add_chart_series(request), - "set_chart_properties" => self.set_chart_properties(request), - "delete_chart" => self.delete_chart(request), - "list_tables" => self.list_tables(request), - "get_table" => self.get_table(request), - "create_table" => self.create_table(request), - "set_table_style" => self.set_table_style(request), - "clear_table_filters" => self.clear_table_filters(request), - "reapply_table_filters" => self.reapply_table_filters(request), - "rename_table_column" => self.rename_table_column(request), - "set_table_column_totals" => self.set_table_column_totals(request), - "delete_table" => self.delete_table(request), - "list_conditional_formats" => self.list_conditional_formats(request), - "add_conditional_format" => self.add_conditional_format(request), - "delete_conditional_format" => self.delete_conditional_format(request), - "list_pivot_tables" => self.list_pivot_tables(request), - "get_pivot_table" => self.get_pivot_table(request), - "create_sheet" => self.create_sheet(request), - "rename_sheet" => self.rename_sheet(request), - "delete_sheet" => self.delete_sheet(request), - "set_sheet_properties" => self.set_sheet_properties(request), - "set_column_widths" => self.set_column_widths(request), - "set_column_widths_bulk" => self.set_column_widths_bulk(request), - "set_row_height" => self.set_row_height(request), - "set_row_heights" => self.set_row_heights(request), - "set_row_heights_bulk" => self.set_row_heights_bulk(request), - "get_row_height" => self.get_row_height(request), - "cleanup_and_validate_sheet" => self.cleanup_and_validate_sheet(request), - "create_text_style" => self.create_text_style(request), - "get_text_style" => self.get_text_style(request), - "create_fill" => self.create_fill(request), - "get_fill" => self.get_fill(request), - "create_border" => self.create_border(request), - "get_border" => self.get_border(request), - "create_number_format" => self.create_number_format(request), - "get_number_format" => self.get_number_format(request), - "create_cell_format" => self.create_cell_format(request), - "get_cell_format" => self.get_cell_format(request), - "create_differential_format" => self.create_differential_format(request), - "get_differential_format" => self.get_differential_format(request), - "get_cell_format_summary" => self.get_cell_format_summary(request), - "get_range_format_summary" => self.get_range_format_summary(request), - "get_reference" => self.get_reference(request), - "get_cell" => self.get_cell(request), - "get_cell_by_indices" => self.get_cell_by_indices(request), - "get_cell_field" => self.get_cell_field(request), - "get_cell_field_by_indices" => self.get_cell_field_by_indices(request), - "get_range" => self.get_range(request), - "set_cell_value" => self.set_cell_value(request), - "set_range_value" => self.set_range_value(request), - "set_range_values" => self.set_range_values(request), - "set_cell_formula" => self.set_cell_formula(request), - "set_range_formula" => self.set_range_formula(request), - "set_range_formulas" => self.set_range_formulas(request), - "set_cell_style" => self.set_cell_style(request), - "set_range_style" => self.set_range_style(request), - "clear_range" => self.clear_range(request), - "merge_range" => self.merge_range(request), - "unmerge_range" => self.unmerge_range(request), - "cite_cell" => self.cite_cell(request), - "cite_range" => self.cite_range(request), - "calculate" | "recalculate" => self.calculate(request), - "serialize_dict" => self.serialize_dict(request), - "serialize_json" => self.serialize_json(request), - "serialize_bytes" => self.serialize_bytes(request), - "deserialize_dict" => self.deserialize_dict(request), - "deserialize_json" => self.deserialize_json(request), - "deserialize_bytes" => self.deserialize_bytes(request), - "delete_artifact" => self.delete_artifact(request), - other => Err(SpreadsheetArtifactError::UnknownAction(other.to_string())), - } - } - - fn create( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateArgs = parse_args(&request.action, &request.args)?; - let mut artifact = SpreadsheetArtifact::new(args.name); - if let Some(auto_recalculate) = args.auto_recalculate { - artifact.auto_recalculate = auto_recalculate; - } - let artifact_id = artifact.artifact_id.clone(); - let summary = format!("Created spreadsheet artifact `{artifact_id}`"); - let snapshot = snapshot_for_artifact(&artifact); - self.documents.insert(artifact_id.clone(), artifact); - Ok(SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - summary, - snapshot, - )) - } - - fn import_xlsx( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let args: PathArgs = parse_args(&request.action, &request.args)?; - let path = resolve_path(cwd, &args.path); - let artifact = SpreadsheetArtifact::from_source_file(&path, None)?; - let artifact_id = artifact.artifact_id.clone(); - let snapshot = snapshot_for_artifact(&artifact); - let summary = format!( - "Imported `{}` as spreadsheet artifact `{artifact_id}` with {} sheets", - path.display(), - artifact.sheets.len() - ); - self.documents.insert(artifact_id.clone(), artifact); - Ok(SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - summary, - snapshot, - )) - } - - fn export_xlsx( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let args: PathArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let path = resolve_path(cwd, &args.path); - let exported = artifact.export(&path)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Exported spreadsheet to `{}`", exported.display()), - snapshot_for_artifact(artifact), - ); - response.exported_paths.push(exported); - response.workbook_summary = Some(artifact.summary()); - response.sheet_list = Some( - artifact - .sheets - .iter() - .map(super::model::SpreadsheetSheet::summary) - .collect(), - ); - Ok(response) - } - - fn save( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let args: SaveArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let path = resolve_path(cwd, &args.path); - let exported = artifact.save(&path, args.file_type.as_deref())?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Saved spreadsheet to `{}`", exported.display()), - snapshot_for_artifact(artifact), - ); - response.exported_paths.push(exported); - response.workbook_summary = Some(artifact.summary()); - Ok(response) - } - - fn get_summary( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Spreadsheet has {} sheets and {} bytes of serialized state", - artifact.sheets.len(), - artifact.summary().size_bytes - ), - snapshot_for_artifact(artifact), - ); - response.workbook_summary = Some(artifact.summary()); - response.sheet_list = Some( - artifact - .sheets - .iter() - .map(super::model::SpreadsheetSheet::summary) - .collect(), - ); - Ok(response) - } - - fn render_workbook( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let args: RenderArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let rendered = artifact.render_workbook_previews(cwd, &render_options_from_args(args)?)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Rendered workbook to {} preview files", rendered.len()), - snapshot_for_artifact(artifact), - ); - response.exported_paths = rendered.into_iter().map(|output| output.path).collect(); - Ok(response) - } - - fn render_sheet( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let args: RenderArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let rendered = - artifact.render_sheet_preview(cwd, sheet, &render_options_from_args(args)?)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Rendered sheet `{}`", sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.exported_paths.push(rendered.path); - response.rendered_html = Some(rendered.html); - response.rendered_text = Some(sheet.to_rendered_text(None)); - Ok(response) - } - - fn render_range( - &mut self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let args: RenderArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range_text = - args.range - .clone() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: "range is required".to_string(), - })?; - let range = CellRange::parse(&range_text)?; - let rendered = - artifact.render_range_preview(cwd, sheet, &range, &render_options_from_args(args)?)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Rendered range `{range_text}` from `{}`", sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - response.exported_paths.push(rendered.path); - response.rendered_html = Some(rendered.html); - response.rendered_text = Some(sheet.to_rendered_text(Some(&range))); - Ok(response) - } - - fn list_sheets( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Listed {} sheets", artifact.sheets.len()), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some( - artifact - .sheets - .iter() - .map(super::model::SpreadsheetSheet::summary) - .collect(), - ); - Ok(response) - } - - fn get_sheet( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = if let Some(range) = args.range.as_deref() { - Some(CellRange::parse(range)?) - } else { - sheet.minimum_range() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved sheet `{}`", sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet.summary()]); - response.sheet_ref = Some(sheet_reference(sheet)); - response.range_ref = range - .as_ref() - .map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry)); - response.rendered_text = Some(sheet.to_rendered_text(range.as_ref())); - response.range = range.as_ref().map(|entry| sheet.get_range_view(entry)); - response.top_left_style_index = range - .as_ref() - .map(|entry| sheet.top_left_style_index(entry)); - response.range_format = range.as_ref().map(|entry| sheet.range_format(entry)); - response.cell_format_summary = response - .top_left_style_index - .and_then(|style_index| artifact.cell_format_summary(style_index)); - response.serialized_dict = Some(sheet.to_dict()?); - Ok(response) - } - - fn inspect( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let inspected = if args.sheet_name.is_none() && args.sheet_index.is_none() { - artifact.to_dict()? - } else { - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - serde_json::to_value(sheet).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Generated inspection snapshot".to_string(), - snapshot_for_artifact(artifact), - ); - response.serialized_dict = Some(inspected); - Ok(response) - } - - fn create_sheet( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateSheetArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let name = args.name.clone(); - artifact.create_sheet(args.name)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created sheet `{name}`"), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some( - artifact - .sheets - .iter() - .map(super::model::SpreadsheetSheet::summary) - .collect(), - ); - Ok(response) - } - - fn list_charts( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = args.range.as_deref().map(CellRange::parse).transpose()?; - let charts = sheet.list_charts(range.as_ref())?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Listed {} charts on `{}`", charts.len(), sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.range_ref = range - .as_ref() - .map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry)); - response.chart_list = Some(charts); - Ok(response) - } - - fn get_chart( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: ChartLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let chart = sheet - .get_chart(&request.action, chart_lookup_from_args(&args))? - .clone(); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved chart `{}` from `{}`", chart.id, sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.chart_list = Some(vec![chart]); - Ok(response) - } - - fn create_chart( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateChartArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - if let Some(source_sheet_name) = args.source_sheet_name.as_deref() { - artifact.sheet_lookup(&request.action, Some(source_sheet_name), None)?; - } - let source_range = CellRange::parse(&args.source_range)?; - let chart = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let chart_id = sheet.create_chart( - &request.action, - args.chart_type, - args.source_sheet_name.or_else(|| Some(sheet.name.clone())), - &source_range, - args.options.unwrap_or_default(), - )?; - sheet - .get_chart( - &request.action, - crate::SpreadsheetChartLookup { - id: Some(chart_id), - index: None, - }, - )? - .clone() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created chart `{}`", chart.id), - snapshot_for_artifact(artifact), - ); - response.chart_list = Some(vec![chart]); - Ok(response) - } - - fn add_chart_series( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: AddChartSeriesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let chart = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let series_id = sheet.add_chart_series( - &request.action, - crate::SpreadsheetChartLookup { - id: args.chart_id, - index: args.chart_index.map(|value| value as usize), - }, - args.series, - )?; - let chart = sheet.get_chart( - &request.action, - crate::SpreadsheetChartLookup { - id: args.chart_id.or(Some(series_id).and(None)), - index: args.chart_index.map(|value| value as usize), - }, - ); - match chart { - Ok(chart) => chart.clone(), - Err(_) => sheet - .get_chart( - &request.action, - crate::SpreadsheetChartLookup { - id: None, - index: args.chart_index.map(|value| value as usize), - }, - )? - .clone(), - } - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Added chart series to chart `{}`", chart.id), - snapshot_for_artifact(artifact), - ); - response.chart_list = Some(vec![chart]); - Ok(response) - } - - fn set_chart_properties( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetChartPropertiesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let chart = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.lookup.sheet_name.as_deref(), - args.lookup.sheet_index.map(|value| value as usize), - )?; - let lookup = chart_lookup_from_args(&args.lookup); - sheet.set_chart_properties(&request.action, lookup.clone(), args.properties)?; - sheet.get_chart(&request.action, lookup)?.clone() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Updated chart `{}`", chart.id), - snapshot_for_artifact(artifact), - ); - response.chart_list = Some(vec![chart]); - Ok(response) - } - - fn delete_chart( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: ChartLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_name = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let name = sheet.name.clone(); - sheet.delete_chart(&request.action, chart_lookup_from_args(&args))?; - name - }; - let action = request.action.clone(); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Deleted chart from `{sheet_name}`"), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![ - artifact - .sheet_lookup(&action, Some(&sheet_name), None)? - .summary(), - ]); - Ok(response) - } - - fn list_tables( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = args.range.as_deref().map(CellRange::parse).transpose()?; - let tables = sheet.list_tables(range.as_ref())?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Listed {} tables on `{}`", tables.len(), sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.range_ref = range - .as_ref() - .map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry)); - response.table_list = Some(tables); - Ok(response) - } - - fn get_table( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: TableLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let table = sheet.get_table_view(&request.action, table_lookup_from_args(&args))?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved table `{}` from `{}`", table.name, sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.table_list = Some(vec![table]); - Ok(response) - } - - fn create_table( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateTableArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let table = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let table_id = sheet.create_table(&request.action, &range, args.options)?; - sheet.get_table_view( - &request.action, - crate::SpreadsheetTableLookup { - name: None, - display_name: None, - id: Some(table_id), - }, - )? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created table `{}`", table.name), - snapshot_for_artifact(artifact), - ); - response.table_list = Some(vec![table]); - Ok(response) - } - - fn set_table_style( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetTableStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let table = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.lookup.sheet_name.as_deref(), - args.lookup.sheet_index.map(|value| value as usize), - )?; - let lookup = table_lookup_from_args(&args.lookup); - sheet.set_table_style(&request.action, lookup.clone(), args.options)?; - sheet.get_table_view(&request.action, lookup)? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Updated table style for `{}`", table.name), - snapshot_for_artifact(artifact), - ); - response.table_list = Some(vec![table]); - Ok(response) - } - - fn clear_table_filters( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: TableLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let table = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let lookup = table_lookup_from_args(&args); - sheet.clear_table_filters(&request.action, lookup.clone())?; - sheet.get_table_view(&request.action, lookup)? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Cleared table filters for `{}`", table.name), - snapshot_for_artifact(artifact), - ); - response.table_list = Some(vec![table]); - Ok(response) - } - - fn reapply_table_filters( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: TableLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let table = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let lookup = table_lookup_from_args(&args); - sheet.reapply_table_filters(&request.action, lookup.clone())?; - sheet.get_table_view(&request.action, lookup)? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Reapplied table filters for `{}`", table.name), - snapshot_for_artifact(artifact), - ); - response.table_list = Some(vec![table]); - Ok(response) - } - - fn rename_table_column( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: RenameTableColumnArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let column = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.lookup.sheet_name.as_deref(), - args.lookup.sheet_index.map(|value| value as usize), - )?; - sheet.rename_table_column( - &request.action, - table_lookup_from_args(&args.lookup), - args.column_id, - args.column_name.as_deref(), - args.new_name, - )? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Renamed table column to `{}`", column.name), - snapshot_for_artifact(artifact), - ); - response.serialized_dict = Some(to_serialized_value(column)?); - Ok(response) - } - - fn set_table_column_totals( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetTableColumnTotalsArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let column = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.lookup.sheet_name.as_deref(), - args.lookup.sheet_index.map(|value| value as usize), - )?; - sheet.set_table_column_totals( - &request.action, - table_lookup_from_args(&args.lookup), - args.column_id, - args.column_name.as_deref(), - args.totals_row_label, - args.totals_row_function, - )? - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Updated totals metadata for column `{}`", column.name), - snapshot_for_artifact(artifact), - ); - response.serialized_dict = Some(to_serialized_value(column)?); - Ok(response) - } - - fn delete_table( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: TableLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_name = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let name = sheet.name.clone(); - sheet.delete_table(&request.action, table_lookup_from_args(&args))?; - name - }; - let action = request.action.clone(); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Deleted table from `{sheet_name}`"), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![ - artifact - .sheet_lookup(&action, Some(&sheet_name), None)? - .summary(), - ]); - Ok(response) - } - - fn list_conditional_formats( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = args.range.as_deref().map(CellRange::parse).transpose()?; - let formats = sheet.list_conditional_formats(range.as_ref()); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Listed {} conditional formats on `{}`", - formats.len(), - sheet.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.range_ref = range - .as_ref() - .map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry)); - response.conditional_format_list = Some(formats); - Ok(response) - } - - fn add_conditional_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: AddConditionalFormatArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_name = artifact - .sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )? - .name - .clone(); - let format_id = - artifact.add_conditional_format(&request.action, &sheet_name, args.format)?; - let format = artifact - .sheet_lookup(&request.action, Some(&sheet_name), None)? - .conditional_formats - .iter() - .find(|entry| entry.id == format_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::Serialization { - message: format!("created conditional format `{format_id}` was not available"), - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created conditional format `{format_id}`"), - snapshot_for_artifact(artifact), - ); - response.conditional_format_list = Some(vec![format]); - Ok(response) - } - - fn delete_conditional_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: DeleteConditionalFormatArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_name = artifact - .sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )? - .name - .clone(); - artifact.delete_conditional_format(&request.action, &sheet_name, args.id)?; - let action = request.action.clone(); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Deleted conditional format `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![ - artifact - .sheet_lookup(&action, Some(&sheet_name), None)? - .summary(), - ]); - Ok(response) - } - - fn list_pivot_tables( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = args.range.as_deref().map(CellRange::parse).transpose()?; - let pivots = sheet.list_pivot_tables(range.as_ref())?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Listed {} pivot tables on `{}`", pivots.len(), sheet.name), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.range_ref = range - .as_ref() - .map(|entry| SpreadsheetCellRangeRef::new(sheet.name.clone(), entry)); - response.pivot_table_list = Some(pivots); - Ok(response) - } - - fn get_pivot_table( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: PivotTableLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let pivot = sheet - .get_pivot_table(&request.action, pivot_lookup_from_args(&args))? - .clone(); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Retrieved pivot table `{}` from `{}`", - pivot.name, sheet.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.pivot_table_list = Some(vec![pivot]); - Ok(response) - } - - fn rename_sheet( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: RenameSheetArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let new_name = args.new_name.clone(); - artifact.rename_sheet( - args.new_name, - args.old_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Renamed sheet to `{new_name}`"), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some( - artifact - .sheets - .iter() - .map(super::model::SpreadsheetSheet::summary) - .collect(), - ); - Ok(response) - } - - fn delete_sheet( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: DeleteSheetArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - artifact.delete_sheet( - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Deleted sheet".to_string(), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some( - artifact - .sheets - .iter() - .map(super::model::SpreadsheetSheet::summary) - .collect(), - ); - Ok(response) - } - - fn set_sheet_properties( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetSheetPropertiesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_summary = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - if let Some(default_row_height) = args.default_row_height { - sheet.default_row_height = Some(default_row_height); - } - if let Some(default_column_width) = args.default_column_width { - sheet.default_column_width = Some(default_column_width); - } - if let Some(show_grid_lines) = args.show_grid_lines { - sheet.show_grid_lines = show_grid_lines; - } - sheet.summary() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Updated sheet `{}` properties", sheet_summary.name), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - Ok(response) - } - - fn set_column_widths( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetColumnWidthsArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_summary = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_column_widths(&args.reference, args.width)?; - sheet.summary() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Updated column widths `{}` on `{}`", - args.reference, sheet_summary.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - Ok(response) - } - - fn set_column_widths_bulk( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetColumnWidthsBulkArgs = parse_args(&request.action, &request.args)?; - let width_count = args.widths.len(); - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_summary = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_column_widths_bulk(&args.widths)?; - sheet.summary() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Updated {width_count} column width references on `{}`", - sheet_summary.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - Ok(response) - } - - fn set_row_height( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetRowHeightArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let (sheet_summary, row_height) = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_row_height(args.row_index, args.height)?; - (sheet.summary(), sheet.get_row_height(args.row_index)) - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Updated row height for row {} on `{}`", - args.row_index, sheet_summary.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - response.row_height = row_height; - Ok(response) - } - - fn set_row_heights( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetRowHeightsArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let start = args.start_row_index.min(args.end_row_index); - let end = args.start_row_index.max(args.end_row_index); - let sheet_summary = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_row_heights(args.start_row_index, args.end_row_index, args.height)?; - sheet.summary() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Updated row heights {start}:{end} on `{}`", - sheet_summary.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - Ok(response) - } - - fn set_row_heights_bulk( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetRowHeightsBulkArgs = parse_args(&request.action, &request.args)?; - let height_count = args.heights.len(); - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_summary = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_row_heights_bulk(&args.heights)?; - sheet.summary() - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Updated {height_count} row height entries on `{}`", - sheet_summary.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - Ok(response) - } - - fn get_row_height( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetRowHeightArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Retrieved row height for row {} from `{}`", - args.row_index, sheet.name - ), - snapshot_for_artifact(artifact), - ); - response.sheet_ref = Some(sheet_reference(sheet)); - response.sheet_list = Some(vec![sheet.summary()]); - response.row_height = sheet.get_row_height(args.row_index); - Ok(response) - } - - fn cleanup_and_validate_sheet( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SheetLookupArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let sheet_summary = { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.cleanup_and_validate_sheet()?; - sheet.summary() - }; - artifact.validate_conditional_formats(&request.action, &sheet_summary.name)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Cleaned and validated `{}`", sheet_summary.name), - snapshot_for_artifact(artifact), - ); - response.sheet_list = Some(vec![sheet_summary]); - Ok(response) - } - - fn create_text_style( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateTextStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let style_id = artifact.create_text_style( - args.style, - args.source_style_id, - args.merge_with_existing_components.unwrap_or(false), - )?; - let style = artifact.get_text_style(style_id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::Serialization { - message: format!("created text style `{style_id}` was not available"), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created text style `{style_id}`"), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(style_id); - response.serialized_dict = Some(to_serialized_value(style)?); - Ok(response) - } - - fn get_text_style( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let style = artifact.get_text_style(args.id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("text style `{}` was not found", args.id), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved text style `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(args.id); - response.serialized_dict = Some(to_serialized_value(style)?); - Ok(response) - } - - fn create_fill( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateFillArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let style_id = artifact.create_fill( - args.fill, - args.source_fill_id, - args.merge_with_existing_components.unwrap_or(false), - )?; - let fill = artifact.get_fill(style_id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::Serialization { - message: format!("created fill `{style_id}` was not available"), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created fill `{style_id}`"), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(style_id); - response.serialized_dict = Some(to_serialized_value(fill)?); - Ok(response) - } - - fn get_fill( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let fill = artifact.get_fill(args.id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("fill `{}` was not found", args.id), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved fill `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(args.id); - response.serialized_dict = Some(to_serialized_value(fill)?); - Ok(response) - } - - fn create_border( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateBorderArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let style_id = artifact.create_border( - args.border, - args.source_border_id, - args.merge_with_existing_components.unwrap_or(false), - )?; - let border = artifact.get_border(style_id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::Serialization { - message: format!("created border `{style_id}` was not available"), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created border `{style_id}`"), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(style_id); - response.serialized_dict = Some(to_serialized_value(border)?); - Ok(response) - } - - fn get_border( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let border = artifact.get_border(args.id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("border `{}` was not found", args.id), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved border `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(args.id); - response.serialized_dict = Some(to_serialized_value(border)?); - Ok(response) - } - - fn create_number_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateNumberFormatArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let style_id = artifact.create_number_format( - args.number_format, - args.source_number_format_id, - args.merge_with_existing_components.unwrap_or(false), - )?; - let number_format = artifact - .get_number_format(style_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::Serialization { - message: format!("created number format `{style_id}` was not available"), - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created number format `{style_id}`"), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(style_id); - response.serialized_dict = Some(to_serialized_value(number_format)?); - Ok(response) - } - - fn get_number_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let number_format = artifact - .get_number_format(args.id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("number format `{}` was not found", args.id), - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved number format `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(args.id); - response.serialized_dict = Some(to_serialized_value(number_format)?); - Ok(response) - } - - fn create_cell_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateCellFormatArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let style_id = artifact.create_cell_format( - args.format, - args.source_format_id, - args.merge_with_existing_components.unwrap_or(false), - )?; - let format = artifact.get_cell_format(style_id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::Serialization { - message: format!("created cell format `{style_id}` was not available"), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created cell format `{style_id}`"), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(style_id); - response.serialized_dict = Some(to_serialized_value(format)?); - response.cell_format_summary = artifact.cell_format_summary(style_id); - Ok(response) - } - - fn get_cell_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let format = artifact.get_cell_format(args.id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("cell format `{}` was not found", args.id), - } - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved cell format `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(args.id); - response.serialized_dict = Some(to_serialized_value(format)?); - response.cell_format_summary = artifact.cell_format_summary(args.id); - Ok(response) - } - - fn create_differential_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CreateDifferentialFormatArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let style_id = artifact.create_differential_format(args.format); - let format = artifact - .get_differential_format(style_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::Serialization { - message: format!("created differential format `{style_id}` was not available"), - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Created differential format `{style_id}`"), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(style_id); - response.serialized_dict = Some(to_serialized_value(format)?); - Ok(response) - } - - fn get_differential_format( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: GetStyleArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let format = artifact - .get_differential_format(args.id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: request.action.clone(), - message: format!("differential format `{}` was not found", args.id), - })?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved differential format `{}`", args.id), - snapshot_for_artifact(artifact), - ); - response.style_id = Some(args.id); - response.serialized_dict = Some(to_serialized_value(format)?); - Ok(response) - } - - fn get_cell_format_summary( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CellAddressArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let cell = sheet.get_cell_view(CellAddress::parse(&args.address)?); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved cell format summary for `{}`", args.address), - snapshot_for_artifact(artifact), - ); - response.cell_ref = Some(sheet.cell_ref(&args.address)?); - response.cell = Some(cell.clone()); - response.cell_format_summary = artifact.cell_format_summary(cell.style_index); - Ok(response) - } - - fn get_range_format_summary( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: RangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = CellRange::parse(&args.range)?; - let top_left_style_index = sheet.top_left_style_index(&range); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved range format summary for `{}`", args.range), - snapshot_for_artifact(artifact), - ); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - response.range_format = Some(sheet.range_format(&range)); - response.range = Some(sheet.get_range_view(&range)); - response.top_left_style_index = Some(top_left_style_index); - response.cell_format_summary = artifact.cell_format_summary(top_left_style_index); - Ok(response) - } - - fn get_reference( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: ReferenceArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Resolved reference `{}` on `{}`", - args.reference, sheet.name - ), - snapshot_for_artifact(artifact), - ); - match sheet.reference(&args.reference)? { - crate::SpreadsheetSheetReference::Cell { cell_ref } => { - let address = cell_ref.cell_address()?; - let cell = sheet.get_cell_view(address); - response.cell_format_summary = artifact.cell_format_summary(cell.style_index); - response.cell = Some(cell); - response.raw_cell = sheet.get_raw_cell(address); - response.cell_ref = Some(cell_ref); - } - crate::SpreadsheetSheetReference::Range { range_ref } => { - let range = range_ref.range()?; - let top_left_style_index = sheet.top_left_style_index(&range); - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(range_ref); - response.range_format = Some(sheet.range_format(&range)); - response.top_left_style_index = Some(top_left_style_index); - response.cell_format_summary = artifact.cell_format_summary(top_left_style_index); - response.rendered_text = Some(sheet.to_rendered_text(Some(&range))); - } - } - Ok(response) - } - - fn get_cell( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CellAddressArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let cell = sheet.get_cell_view(CellAddress::parse(&args.address)?); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved cell `{}` from `{}`", args.address, sheet.name), - snapshot_for_artifact(artifact), - ); - response.cell_format_summary = artifact.cell_format_summary(cell.style_index); - response.cell = Some(cell); - response.raw_cell = sheet.get_raw_cell(CellAddress::parse(&args.address)?); - response.cell_ref = Some(sheet.cell_ref(&args.address)?); - Ok(response) - } - - fn get_cell_by_indices( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CellIndicesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let address = CellAddress { - column: args.column_index, - row: args.row_index, - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Retrieved cell by indices ({}, {}) from `{}`", - args.column_index, args.row_index, sheet.name - ), - snapshot_for_artifact(artifact), - ); - let cell = sheet.get_cell_view_by_indices(args.column_index, args.row_index); - response.cell_format_summary = artifact.cell_format_summary(cell.style_index); - response.cell = Some(cell); - response.raw_cell = sheet.get_raw_cell(address); - response.cell_ref = Some(sheet.cell_ref(address.to_a1())?); - Ok(response) - } - - fn get_cell_field( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CellFieldArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let value = sheet.get_cell_field(CellAddress::parse(&args.address)?, &args.field)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Retrieved field `{}` from `{}` on `{}`", - args.field, args.address, sheet.name - ), - snapshot_for_artifact(artifact), - ); - response.cell_field = value; - response.cell_ref = Some(sheet.cell_ref(&args.address)?); - Ok(response) - } - - fn get_cell_field_by_indices( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: CellFieldByIndicesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let value = - sheet.get_cell_field_by_indices(args.column_index, args.row_index, &args.field)?; - let address = CellAddress { - column: args.column_index, - row: args.row_index, - }; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!( - "Retrieved field `{}` from indices ({}, {}) on `{}`", - args.field, args.column_index, args.row_index, sheet.name - ), - snapshot_for_artifact(artifact), - ); - response.cell_field = value; - response.cell_ref = Some(sheet.cell_ref(address.to_a1())?); - Ok(response) - } - - fn get_range( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: RangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let sheet = artifact.sheet_lookup( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let range = CellRange::parse(&args.range)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - format!("Retrieved range `{}` from `{}`", args.range, sheet.name), - snapshot_for_artifact(artifact), - ); - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - response.range_format = Some(sheet.range_format(&range)); - response.top_left_style_index = Some(sheet.top_left_style_index(&range)); - response.cell_format_summary = - artifact.cell_format_summary(sheet.top_left_style_index(&range)); - response.rendered_text = Some(sheet.to_rendered_text(Some(&range))); - Ok(response) - } - - fn set_cell_value( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: SetCellValueArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let address = CellAddress::parse(&args.address)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let value = normalize_optional_cell_value(args.value)?; - sheet.set_value(address, value)?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated cell `{}`", args.address), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.cell = Some(sheet.get_cell_view(address)); - response.cell_ref = Some(sheet.cell_ref(&args.address)?); - Ok(response) - } - - fn set_range_value( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: SetRangeValueArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_range_to_value(&range, normalize_optional_cell_value(args.value)?)?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated range `{}` to a single value", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn set_range_values( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: SetRangeValuesArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - let values = normalize_value_matrix(args.values, &request.action)?; - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_values_matrix(&range, &values)?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated range `{}`", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn set_cell_formula( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: SetCellFormulaArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let address = CellAddress::parse(&args.address)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_formula(address, Some(normalize_formula(args.formula)))?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated formula in `{}`", args.address), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.cell = Some(sheet.get_cell_view(address)); - response.cell_ref = Some(sheet.cell_ref(&args.address)?); - Ok(response) - } - - fn set_range_formula( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: SetRangeFormulaArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_range_to_formula(&range, Some(normalize_formula(args.formula)))?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated range `{}` to a single formula", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn set_range_formulas( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: SetRangeFormulasArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - let formulas = args - .formulas - .into_iter() - .map(|row| { - row.into_iter() - .map(|value| value.map(normalize_formula)) - .collect() - }) - .collect::>>>(); - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.set_formulas_matrix(&range, &formulas)?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated formulas in `{}`", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn set_cell_style( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetCellStyleArgs = parse_args(&request.action, &request.args)?; - let range = CellRange::parse(&args.address)?; - self.set_style_impl( - request, - args.sheet_name, - args.sheet_index, - range, - args.style_index, - ) - } - - fn set_range_style( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: SetRangeStyleArgs = parse_args(&request.action, &request.args)?; - let range = CellRange::parse(&args.range)?; - self.set_style_impl( - request, - args.sheet_name, - args.sheet_index, - range, - args.style_index, - ) - } - - fn set_style_impl( - &mut self, - request: SpreadsheetArtifactRequest, - sheet_name: Option, - sheet_index: Option, - range: CellRange, - style_index: u32, - ) -> Result { - let action = request.action.clone(); - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - sheet_name.as_deref(), - sheet_index.map(|value| value as usize), - )?; - sheet.set_style_index(&range, style_index)?; - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Updated style index {} on `{}`", style_index, range.to_a1()), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - sheet_name.as_deref(), - sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn clear_range( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: ClearRangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let recalculate = args.recalculate.unwrap_or(artifact.auto_recalculate); - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.clear_range(&range, args.fields.as_deref())?; - } - if recalculate { - artifact.recalculate(); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Cleared range `{}`", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn merge_range( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: MergeRangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.merge_cells(&range, args.raise_on_conflict.unwrap_or(false))?; - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Merged `{}`", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - response.range_ref = Some(SpreadsheetCellRangeRef::new(sheet.name.clone(), &range)); - Ok(response) - } - - fn cite_cell( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: CiteCellArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let address = CellAddress::parse(&args.address)?; - let citation = SpreadsheetCitation { - tether_id: args.tether_id, - start_line: args.start_line, - end_line: args.end_line, - content_reference_type: args.content_reference_type, - source_type: args.source_type, - }; - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - let cell_ref = sheet.cell_ref(&args.address)?; - cell_ref.cite(sheet, citation)?; - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Attached citation to cell `{}`", args.address), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.cell = Some(sheet.get_cell_view(address)); - response.cell_ref = Some(sheet.cell_ref(&args.address)?); - Ok(response) - } - - fn unmerge_range( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: RangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.unmerge_cells(&range); - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Unmerged `{}`", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - Ok(response) - } - - fn cite_range( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let action = request.action.clone(); - let args: CiteRangeArgs = parse_args(&request.action, &request.args)?; - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - let range = CellRange::parse(&args.range)?; - let citation = SpreadsheetCitation { - tether_id: args.tether_id, - start_line: args.start_line, - end_line: args.end_line, - content_reference_type: args.content_reference_type, - source_type: args.source_type, - }; - { - let sheet = artifact.sheet_lookup_mut( - &request.action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - sheet.cite_range(&range, citation)?; - } - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - action.clone(), - format!("Attached citation to `{}`", args.range), - snapshot_for_artifact(artifact), - ); - let sheet = artifact.sheet_lookup( - &action, - args.sheet_name.as_deref(), - args.sheet_index.map(|value| value as usize), - )?; - response.range = Some(sheet.get_range_view(&range)); - Ok(response) - } - - fn calculate( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact_mut(&artifact_id, &request.action)?; - artifact.recalculate(); - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Recalculated workbook".to_string(), - snapshot_for_artifact(artifact), - ); - response.workbook_summary = Some(artifact.summary()); - Ok(response) - } - - fn serialize_dict( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Serialized workbook to dict".to_string(), - snapshot_for_artifact(artifact), - ); - response.serialized_dict = Some(artifact.to_dict()?); - Ok(response) - } - - fn serialize_json( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Serialized workbook to JSON".to_string(), - snapshot_for_artifact(artifact), - ); - response.serialized_json = Some(artifact.to_json()?); - Ok(response) - } - - fn serialize_bytes( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.get_artifact(&artifact_id, &request.action)?; - let mut response = SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Serialized workbook to bytes".to_string(), - snapshot_for_artifact(artifact), - ); - response.serialized_bytes_base64 = Some(artifact.to_bytes_base64()); - Ok(response) - } - - fn deserialize_dict( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: DeserializeDictArgs = parse_args(&request.action, &request.args)?; - let artifact = SpreadsheetArtifact::from_dict(args.data, args.artifact_id)?; - let artifact_id = artifact.artifact_id.clone(); - let snapshot = snapshot_for_artifact(&artifact); - self.documents.insert(artifact_id.clone(), artifact); - Ok(SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Deserialized workbook from dict".to_string(), - snapshot, - )) - } - - fn deserialize_json( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: DeserializeJsonArgs = parse_args(&request.action, &request.args)?; - let artifact = SpreadsheetArtifact::from_json(args.json, args.artifact_id)?; - let artifact_id = artifact.artifact_id.clone(); - let snapshot = snapshot_for_artifact(&artifact); - self.documents.insert(artifact_id.clone(), artifact); - Ok(SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Deserialized workbook from JSON".to_string(), - snapshot, - )) - } - - fn deserialize_bytes( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let args: DeserializeBytesArgs = parse_args(&request.action, &request.args)?; - let bytes = BASE64_STANDARD.decode(args.bytes_base64).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - let artifact = SpreadsheetArtifact::from_bytes(&bytes, args.artifact_id)?; - let artifact_id = artifact.artifact_id.clone(); - let snapshot = snapshot_for_artifact(&artifact); - self.documents.insert(artifact_id.clone(), artifact); - Ok(SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Deserialized workbook from bytes".to_string(), - snapshot, - )) - } - - fn delete_artifact( - &mut self, - request: SpreadsheetArtifactRequest, - ) -> Result { - let artifact_id = required_artifact_id(&request)?; - let artifact = self.documents.remove(&artifact_id).ok_or_else(|| { - SpreadsheetArtifactError::UnknownArtifactId { - action: request.action.clone(), - artifact_id: artifact_id.clone(), - } - })?; - let snapshot = snapshot_for_artifact(&artifact); - Ok(SpreadsheetArtifactResponse::new( - artifact_id, - request.action, - "Deleted spreadsheet artifact".to_string(), - snapshot, - )) - } - - fn get_artifact( - &self, - artifact_id: &str, - action: &str, - ) -> Result<&SpreadsheetArtifact, SpreadsheetArtifactError> { - self.documents - .get(artifact_id) - .ok_or_else(|| SpreadsheetArtifactError::UnknownArtifactId { - action: action.to_string(), - artifact_id: artifact_id.to_string(), - }) - } - - fn get_artifact_mut( - &mut self, - artifact_id: &str, - action: &str, - ) -> Result<&mut SpreadsheetArtifact, SpreadsheetArtifactError> { - self.documents.get_mut(artifact_id).ok_or_else(|| { - SpreadsheetArtifactError::UnknownArtifactId { - action: action.to_string(), - artifact_id: artifact_id.to_string(), - } - }) - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct SpreadsheetArtifactResponse { - pub artifact_id: String, - pub action: String, - pub summary: String, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub exported_paths: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub artifact_snapshot: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub workbook_summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sheet_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub chart_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub table_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub conditional_format_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub pivot_table_list: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub sheet_ref: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cell_ref: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub range_ref: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub range_format: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cell: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub raw_cell: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub style_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cell_field: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub range: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub top_left_style_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cell_format_summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rendered_text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rendered_html: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub row_height: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub serialized_dict: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub serialized_json: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub serialized_bytes_base64: Option, -} - -impl SpreadsheetArtifactResponse { - fn new( - artifact_id: String, - action: String, - summary: String, - artifact_snapshot: SpreadsheetArtifactSnapshot, - ) -> Self { - Self { - artifact_id, - action, - summary, - exported_paths: Vec::new(), - artifact_snapshot: Some(artifact_snapshot), - workbook_summary: None, - sheet_list: None, - chart_list: None, - table_list: None, - conditional_format_list: None, - pivot_table_list: None, - sheet_ref: None, - cell_ref: None, - range_ref: None, - range_format: None, - cell: None, - raw_cell: None, - style_id: None, - cell_field: None, - range: None, - top_left_style_index: None, - cell_format_summary: None, - rendered_text: None, - rendered_html: None, - row_height: None, - serialized_dict: None, - serialized_json: None, - serialized_bytes_base64: None, - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct SpreadsheetSheetRef { - pub sheet_name: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SpreadsheetArtifactSnapshot { - pub sheet_count: usize, - pub sheet_names: Vec, - pub sheets: Vec, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SpreadsheetSheetSnapshot { - pub sheet_id: String, - pub name: String, - pub filled_rows: usize, - pub filled_columns: usize, - pub minimum_range_filled: String, - pub merged_range_count: usize, - pub chart_count: usize, - pub table_count: usize, - pub conditional_format_count: usize, - pub pivot_table_count: usize, -} - -#[derive(Debug, Deserialize)] -struct CreateArgs { - name: Option, - auto_recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct PathArgs { - path: PathBuf, -} - -#[derive(Debug, Deserialize)] -struct SaveArgs { - path: PathBuf, - file_type: Option, -} - -#[derive(Debug, Deserialize)] -struct RenderArgs { - output_path: Option, - sheet_name: Option, - sheet_index: Option, - range: Option, - center_address: Option, - width: Option, - height: Option, - include_headers: Option, - scale: Option, - performance_mode: Option, -} - -#[derive(Debug, Deserialize)] -struct SheetLookupArgs { - sheet_name: Option, - sheet_index: Option, - range: Option, -} - -#[derive(Debug, Deserialize)] -struct ChartLookupArgs { - sheet_name: Option, - sheet_index: Option, - chart_id: Option, - chart_index: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateChartArgs { - sheet_name: Option, - sheet_index: Option, - chart_type: crate::SpreadsheetChartType, - source_sheet_name: Option, - source_range: String, - options: Option, -} - -#[derive(Debug, Deserialize)] -struct AddChartSeriesArgs { - sheet_name: Option, - sheet_index: Option, - chart_id: Option, - chart_index: Option, - series: crate::SpreadsheetChartSeries, -} - -#[derive(Debug, Deserialize)] -struct SetChartPropertiesArgs { - #[serde(flatten)] - lookup: ChartLookupArgs, - properties: crate::SpreadsheetChartProperties, -} - -#[derive(Debug, Deserialize)] -struct TableLookupArgs { - sheet_name: Option, - sheet_index: Option, - name: Option, - display_name: Option, - id: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateTableArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - options: crate::SpreadsheetCreateTableOptions, -} - -#[derive(Debug, Deserialize)] -struct SetTableStyleArgs { - #[serde(flatten)] - lookup: TableLookupArgs, - options: crate::SpreadsheetTableStyleOptions, -} - -#[derive(Debug, Deserialize)] -struct RenameTableColumnArgs { - #[serde(flatten)] - lookup: TableLookupArgs, - column_id: Option, - column_name: Option, - new_name: String, -} - -#[derive(Debug, Deserialize)] -struct SetTableColumnTotalsArgs { - #[serde(flatten)] - lookup: TableLookupArgs, - column_id: Option, - column_name: Option, - totals_row_label: Option, - totals_row_function: Option, -} - -#[derive(Debug, Deserialize)] -struct AddConditionalFormatArgs { - sheet_name: Option, - sheet_index: Option, - format: crate::SpreadsheetConditionalFormat, -} - -#[derive(Debug, Deserialize)] -struct DeleteConditionalFormatArgs { - sheet_name: Option, - sheet_index: Option, - id: u32, -} - -#[derive(Debug, Deserialize)] -struct PivotTableLookupArgs { - sheet_name: Option, - sheet_index: Option, - name: Option, - index: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateSheetArgs { - name: String, -} - -#[derive(Debug, Deserialize)] -struct RenameSheetArgs { - old_name: Option, - sheet_index: Option, - new_name: String, -} - -#[derive(Debug, Deserialize)] -struct DeleteSheetArgs { - sheet_name: Option, - sheet_index: Option, -} - -#[derive(Debug, Deserialize)] -struct SetSheetPropertiesArgs { - sheet_name: Option, - sheet_index: Option, - default_row_height: Option, - default_column_width: Option, - show_grid_lines: Option, -} - -#[derive(Debug, Deserialize)] -struct SetColumnWidthsArgs { - sheet_name: Option, - sheet_index: Option, - reference: String, - width: f64, -} - -#[derive(Debug, Deserialize)] -struct SetColumnWidthsBulkArgs { - sheet_name: Option, - sheet_index: Option, - widths: BTreeMap, -} - -#[derive(Debug, Deserialize)] -struct SetRowHeightArgs { - sheet_name: Option, - sheet_index: Option, - row_index: u32, - height: Option, -} - -#[derive(Debug, Deserialize)] -struct SetRowHeightsArgs { - sheet_name: Option, - sheet_index: Option, - start_row_index: u32, - end_row_index: u32, - height: Option, -} - -#[derive(Debug, Deserialize)] -struct SetRowHeightsBulkArgs { - sheet_name: Option, - sheet_index: Option, - heights: BTreeMap>, -} - -#[derive(Debug, Deserialize)] -struct GetRowHeightArgs { - sheet_name: Option, - sheet_index: Option, - row_index: u32, -} - -#[derive(Debug, Deserialize)] -struct CreateTextStyleArgs { - style: crate::SpreadsheetTextStyle, - source_style_id: Option, - merge_with_existing_components: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateFillArgs { - fill: crate::SpreadsheetFill, - source_fill_id: Option, - merge_with_existing_components: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateBorderArgs { - border: crate::SpreadsheetBorder, - source_border_id: Option, - merge_with_existing_components: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateNumberFormatArgs { - number_format: crate::SpreadsheetNumberFormat, - source_number_format_id: Option, - merge_with_existing_components: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateCellFormatArgs { - format: crate::SpreadsheetCellFormat, - source_format_id: Option, - merge_with_existing_components: Option, -} - -#[derive(Debug, Deserialize)] -struct CreateDifferentialFormatArgs { - format: crate::SpreadsheetDifferentialFormat, -} - -#[derive(Debug, Deserialize)] -struct GetStyleArgs { - id: u32, -} - -#[derive(Debug, Deserialize)] -struct CellAddressArgs { - sheet_name: Option, - sheet_index: Option, - address: String, -} - -#[derive(Debug, Deserialize)] -struct ReferenceArgs { - sheet_name: Option, - sheet_index: Option, - reference: String, -} - -#[derive(Debug, Deserialize)] -struct CellIndicesArgs { - sheet_name: Option, - sheet_index: Option, - column_index: u32, - row_index: u32, -} - -#[derive(Debug, Deserialize)] -struct CellFieldArgs { - sheet_name: Option, - sheet_index: Option, - address: String, - field: String, -} - -#[derive(Debug, Deserialize)] -struct CellFieldByIndicesArgs { - sheet_name: Option, - sheet_index: Option, - column_index: u32, - row_index: u32, - field: String, -} - -#[derive(Debug, Deserialize)] -struct RangeArgs { - sheet_name: Option, - sheet_index: Option, - range: String, -} - -#[derive(Debug, Deserialize)] -struct SetCellValueArgs { - sheet_name: Option, - sheet_index: Option, - address: String, - value: Value, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct SetRangeValuesArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - values: Vec>, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct SetRangeValueArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - value: Value, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct SetCellFormulaArgs { - sheet_name: Option, - sheet_index: Option, - address: String, - formula: String, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct SetRangeFormulasArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - formulas: Vec>>, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct SetRangeFormulaArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - formula: String, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct SetCellStyleArgs { - sheet_name: Option, - sheet_index: Option, - address: String, - style_index: u32, -} - -#[derive(Debug, Deserialize)] -struct SetRangeStyleArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - style_index: u32, -} - -#[derive(Debug, Deserialize)] -struct ClearRangeArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - fields: Option>, - recalculate: Option, -} - -#[derive(Debug, Deserialize)] -struct MergeRangeArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - raise_on_conflict: Option, -} - -#[derive(Debug, Deserialize)] -struct CiteRangeArgs { - sheet_name: Option, - sheet_index: Option, - range: String, - tether_id: String, - start_line: Option, - end_line: Option, - content_reference_type: Option, - source_type: Option, -} - -#[derive(Debug, Deserialize)] -struct CiteCellArgs { - sheet_name: Option, - sheet_index: Option, - address: String, - tether_id: String, - start_line: Option, - end_line: Option, - content_reference_type: Option, - source_type: Option, -} - -#[derive(Debug, Deserialize)] -struct DeserializeDictArgs { - data: Value, - artifact_id: Option, -} - -#[derive(Debug, Deserialize)] -struct DeserializeJsonArgs { - json: String, - artifact_id: Option, -} - -#[derive(Debug, Deserialize)] -struct DeserializeBytesArgs { - bytes_base64: String, - artifact_id: Option, -} - -fn snapshot_for_artifact(artifact: &SpreadsheetArtifact) -> SpreadsheetArtifactSnapshot { - SpreadsheetArtifactSnapshot { - sheet_count: artifact.sheets.len(), - sheet_names: artifact.list_sheet_names(), - sheets: artifact - .sheets - .iter() - .map(|sheet| SpreadsheetSheetSnapshot { - sheet_id: sheet.sheet_id.clone(), - name: sheet.name.clone(), - filled_rows: sheet.filled_rows(), - filled_columns: sheet.filled_columns(), - minimum_range_filled: sheet.minimum_range_filled(), - merged_range_count: sheet.merged_ranges.len(), - chart_count: sheet.charts.len(), - table_count: sheet.tables.len(), - conditional_format_count: sheet.conditional_formats.len(), - pivot_table_count: sheet.pivot_tables.len(), - }) - .collect(), - } -} - -fn chart_lookup_from_args(args: &ChartLookupArgs) -> crate::SpreadsheetChartLookup { - crate::SpreadsheetChartLookup { - id: args.chart_id, - index: args.chart_index.map(|value| value as usize), - } -} - -fn table_lookup_from_args(args: &TableLookupArgs) -> crate::SpreadsheetTableLookup<'_> { - crate::SpreadsheetTableLookup { - name: args.name.as_deref(), - display_name: args.display_name.as_deref(), - id: args.id, - } -} - -fn pivot_lookup_from_args(args: &PivotTableLookupArgs) -> crate::SpreadsheetPivotTableLookup<'_> { - crate::SpreadsheetPivotTableLookup { - name: args.name.as_deref(), - index: args.index.map(|value| value as usize), - } -} - -fn sheet_reference(sheet: &crate::SpreadsheetSheet) -> SpreadsheetSheetRef { - SpreadsheetSheetRef { - sheet_name: sheet.name.clone(), - } -} - -fn normalize_optional_cell_value( - value: Value, -) -> Result, SpreadsheetArtifactError> { - if value.is_null() { - Ok(None) - } else { - Ok(Some(SpreadsheetCellValue::try_from(value)?)) - } -} - -fn normalize_value_matrix( - values: Vec>, - action: &str, -) -> Result>>, SpreadsheetArtifactError> { - if values.is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "values matrix cannot be empty".to_string(), - }); - } - let width = values.first().map(Vec::len).unwrap_or(0); - if width == 0 || values.iter().any(|row| row.len() != width) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "values matrix must be rectangular".to_string(), - }); - } - values - .into_iter() - .map(|row| { - row.into_iter() - .map(normalize_optional_cell_value) - .collect::, _>>() - }) - .collect::, _>>() -} - -fn normalize_formula(formula: String) -> String { - let trimmed = formula.trim(); - if trimmed.starts_with('=') { - trimmed.to_string() - } else { - format!("={trimmed}") - } -} - -fn render_options_from_args( - args: RenderArgs, -) -> Result { - let scale = args.scale.unwrap_or(1.0); - if scale <= 0.0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "render".to_string(), - message: "render scale must be positive".to_string(), - }); - } - Ok(crate::SpreadsheetRenderOptions { - output_path: args.output_path, - center_address: args.center_address, - width: args.width, - height: args.height, - include_headers: args.include_headers.unwrap_or(true), - scale, - performance_mode: args.performance_mode.unwrap_or(false), - }) -} - -fn required_artifact_id( - request: &SpreadsheetArtifactRequest, -) -> Result { - request - .artifact_id - .clone() - .ok_or_else(|| SpreadsheetArtifactError::MissingArtifactId { - action: request.action.clone(), - }) -} - -fn parse_args Deserialize<'de>>( - action: &str, - value: &Value, -) -> Result { - serde_json::from_value(value.clone()).map_err(|error| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: error.to_string(), - }) -} - -fn resolve_path(cwd: &Path, path: &Path) -> PathBuf { - if path.is_absolute() { - path.to_path_buf() - } else { - cwd.join(path) - } -} - -fn to_serialized_value(value: T) -> Result { - serde_json::to_value(value).map_err(|error| SpreadsheetArtifactError::Serialization { - message: error.to_string(), - }) -} diff --git a/codex-rs/artifact-spreadsheet/src/model.rs b/codex-rs/artifact-spreadsheet/src/model.rs deleted file mode 100644 index 747eedbce..000000000 --- a/codex-rs/artifact-spreadsheet/src/model.rs +++ /dev/null @@ -1,1787 +0,0 @@ -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::path::Path; -use std::path::PathBuf; - -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value; -use uuid::Uuid; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifactError; -use crate::formula::recalculate_workbook; -use crate::parse_column_reference; -use crate::xlsx::import_xlsx; -use crate::xlsx::write_xlsx; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SpreadsheetFileType { - Xlsx, - Json, - Binary, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "kind", content = "value", rename_all = "snake_case")] -pub enum SpreadsheetCellValue { - Bool(bool), - Integer(i64), - Float(f64), - String(String), - DateTime(String), - Error(String), -} - -impl SpreadsheetCellValue { - pub fn to_json_value(&self) -> Value { - match self { - Self::Bool(value) => Value::Bool(*value), - Self::Integer(value) => Value::Number((*value).into()), - Self::Float(value) => serde_json::Number::from_f64(*value) - .map(Value::Number) - .unwrap_or_else(|| Value::String(value.to_string())), - Self::String(value) | Self::DateTime(value) | Self::Error(value) => { - Value::String(value.clone()) - } - } - } - - pub fn as_f64(&self) -> Option { - match self { - Self::Integer(value) => Some(*value as f64), - Self::Float(value) => Some(*value), - _ => None, - } - } -} - -impl TryFrom for SpreadsheetCellValue { - type Error = SpreadsheetArtifactError; - - fn try_from(value: Value) -> Result { - match value { - Value::Object(_) => serde_json::from_value(value).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - }), - Value::Bool(value) => Ok(Self::Bool(value)), - Value::Number(value) => { - if let Some(integer) = value.as_i64() { - Ok(Self::Integer(integer)) - } else if let Some(float) = value.as_f64() { - Ok(Self::Float(float)) - } else { - Err(SpreadsheetArtifactError::Serialization { - message: "unsupported JSON number".to_string(), - }) - } - } - Value::String(value) => Ok(Self::String(value)), - Value::Null => Err(SpreadsheetArtifactError::Serialization { - message: "null is represented as an empty cell, not a cell value".to_string(), - }), - other => Err(SpreadsheetArtifactError::Serialization { - message: format!("unsupported JSON cell value `{other}`"), - }), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetCitation { - pub tether_id: String, - pub start_line: Option, - pub end_line: Option, - pub content_reference_type: Option, - pub source_type: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetCell { - pub value: Option, - pub formula: Option, - pub style_index: u32, - #[serde(default)] - pub citations: Vec, -} - -impl SpreadsheetCell { - pub fn to_dict(&self) -> Result { - serde_json::to_value(self).map_err(|error| SpreadsheetArtifactError::Serialization { - message: error.to_string(), - }) - } - - pub fn data(&self) -> Option { - if let Some(formula) = &self.formula { - return Some(Value::String(formula.clone())); - } - self.value.as_ref().map(SpreadsheetCellValue::to_json_value) - } - - pub fn is_empty(&self) -> bool { - self.value.is_none() - && self.formula.is_none() - && self.style_index == 0 - && self.citations.is_empty() - } - - pub fn is_calculation_error(&self) -> bool { - matches!( - self.value, - Some(SpreadsheetCellValue::Error(ref value)) - if matches!( - value.as_str(), - "#DIV/0!" - | "#N/A" - | "#NAME?" - | "#NUM!" - | "#REF!" - | "#VALUE!" - | "#CYCLE!" - | "#ERROR!" - | "#LIC!" - ) - ) - } - - pub fn calculation_error_message(&self) -> Option<&'static str> { - match self.value.as_ref() { - Some(SpreadsheetCellValue::Error(value)) => match value.as_str() { - "#DIV/0!" => Some("Division by zero"), - "#N/A" => Some("Value is not available"), - "#NAME?" => Some("Unknown function or name"), - "#NUM!" => Some("Invalid numeric result"), - "#REF!" => Some("Invalid cell reference"), - "#VALUE!" => Some("Invalid value type"), - "#CYCLE!" => Some("Formula cycle detected"), - "#ERROR!" => Some("Formula parse error"), - "#LIC!" => Some("Calculation engine license error"), - _ => None, - }, - _ => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetCellRef { - pub sheet_name: String, - pub address: String, -} - -impl SpreadsheetCellRef { - pub fn new(sheet_name: String, address: CellAddress) -> Self { - Self { - sheet_name, - address: address.to_a1(), - } - } - - pub fn cell_address(&self) -> Result { - CellAddress::parse(&self.address) - } - - pub fn get( - &self, - sheet: &SpreadsheetSheet, - ) -> Result { - self.ensure_sheet(sheet)?; - Ok(sheet.get_cell_view(self.cell_address()?)) - } - - pub fn data( - &self, - sheet: &SpreadsheetSheet, - ) -> Result, SpreadsheetArtifactError> { - Ok(self.get(sheet)?.data) - } - - pub fn raw_cell( - &self, - sheet: &SpreadsheetSheet, - ) -> Result, SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - Ok(sheet.get_cell(self.cell_address()?).cloned()) - } - - pub fn to_dict(&self, sheet: &SpreadsheetSheet) -> Result { - Ok(match self.raw_cell(sheet)? { - Some(cell) => cell.to_dict()?, - None => Value::Object(Default::default()), - }) - } - - pub fn set_value( - &self, - sheet: &mut SpreadsheetSheet, - value: Option, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_value(self.cell_address()?, value) - } - - pub fn set_formula( - &self, - sheet: &mut SpreadsheetSheet, - formula: Option, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_formula(self.cell_address()?, formula) - } - - pub fn cite( - &self, - sheet: &mut SpreadsheetSheet, - citation: SpreadsheetCitation, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.cite_range( - &CellRange::from_start_end(self.cell_address()?, self.cell_address()?), - citation, - ) - } - - pub fn set_style_index( - &self, - sheet: &mut SpreadsheetSheet, - style_index: u32, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_style_index( - &CellRange::from_start_end(self.cell_address()?, self.cell_address()?), - style_index, - ) - } - - pub fn is_calculation_error( - &self, - sheet: &SpreadsheetSheet, - ) -> Result { - Ok(self.get(sheet)?.is_calculation_error) - } - - pub fn get_calculation_error_message( - &self, - sheet: &SpreadsheetSheet, - ) -> Result, SpreadsheetArtifactError> { - Ok(self.get(sheet)?.calculation_error_message) - } - - fn ensure_sheet(&self, sheet: &SpreadsheetSheet) -> Result<(), SpreadsheetArtifactError> { - if self.sheet_name != sheet.name { - return Err(SpreadsheetArtifactError::SheetLookup { - action: "cell_ref".to_string(), - message: format!( - "cell ref points to `{}` but sheet is `{}`", - self.sheet_name, sheet.name - ), - }); - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetCellRangeRef { - pub sheet_name: String, - pub address: String, -} - -impl SpreadsheetCellRangeRef { - pub fn new(sheet_name: String, range: &CellRange) -> Self { - Self { - sheet_name, - address: range.to_a1(), - } - } - - pub fn range(&self) -> Result { - CellRange::parse(&self.address) - } - - pub fn get( - &self, - sheet: &SpreadsheetSheet, - ) -> Result { - self.ensure_sheet(sheet)?; - Ok(sheet.get_range_view(&self.range()?)) - } - - pub fn get_values( - &self, - sheet: &SpreadsheetSheet, - ) -> Result>>, SpreadsheetArtifactError> { - Ok(self.get(sheet)?.values) - } - - pub fn get_formulas( - &self, - sheet: &SpreadsheetSheet, - ) -> Result>>, SpreadsheetArtifactError> { - Ok(self.get(sheet)?.formulas) - } - - pub fn get_style_indices( - &self, - sheet: &SpreadsheetSheet, - ) -> Result>, SpreadsheetArtifactError> { - Ok(self.get(sheet)?.style_indices) - } - - pub fn get_data( - &self, - sheet: &SpreadsheetSheet, - ) -> Result>>, SpreadsheetArtifactError> { - Ok(self.get(sheet)?.data) - } - - pub fn top_left_style_index( - &self, - sheet: &SpreadsheetSheet, - ) -> Result { - self.ensure_sheet(sheet)?; - Ok(sheet.top_left_style_index(&self.range()?)) - } - - pub fn set_value( - &self, - sheet: &mut SpreadsheetSheet, - value: Option, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_range_to_value(&self.range()?, value) - } - - pub fn set_values( - &self, - sheet: &mut SpreadsheetSheet, - values: &[Vec>], - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_values_matrix(&self.range()?, values) - } - - pub fn set_formula( - &self, - sheet: &mut SpreadsheetSheet, - formula: Option, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_range_to_formula(&self.range()?, formula) - } - - pub fn set_formulas( - &self, - sheet: &mut SpreadsheetSheet, - formulas: &[Vec>], - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_formulas_matrix(&self.range()?, formulas) - } - - pub fn set_style_index( - &self, - sheet: &mut SpreadsheetSheet, - style_index: u32, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.set_style_index(&self.range()?, style_index) - } - - pub fn merge( - &self, - sheet: &mut SpreadsheetSheet, - raise_on_conflict: bool, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.merge_cells(&self.range()?, raise_on_conflict) - } - - pub fn unmerge(&self, sheet: &mut SpreadsheetSheet) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.unmerge_cells(&self.range()?); - Ok(()) - } - - pub fn cite( - &self, - sheet: &mut SpreadsheetSheet, - citation: SpreadsheetCitation, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_sheet(sheet)?; - sheet.cite_range(&self.range()?, citation) - } - - fn ensure_sheet(&self, sheet: &SpreadsheetSheet) -> Result<(), SpreadsheetArtifactError> { - if self.sheet_name != sheet.name { - return Err(SpreadsheetArtifactError::SheetLookup { - action: "range_ref".to_string(), - message: format!( - "range ref points to `{}` but sheet is `{}`", - self.sheet_name, sheet.name - ), - }); - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SpreadsheetSheetReference { - Cell { cell_ref: SpreadsheetCellRef }, - Range { range_ref: SpreadsheetCellRangeRef }, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetSheetSummary { - pub name: String, - pub filled_rows: usize, - pub filled_columns: usize, - pub minimum_range_filled: String, - pub min_row_idx: Option, - pub max_row_idx: Option, - pub min_column_idx: Option, - pub max_column_idx: Option, - pub min_column_letter: Option, - pub max_column_letter: Option, - pub first_row_address_range: Option, - pub default_row_height: Option, - pub default_column_width: Option, - pub show_grid_lines: bool, - pub merged_range_count: usize, - pub chart_count: usize, - pub table_count: usize, - pub conditional_format_count: usize, - pub pivot_table_count: usize, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetSummary { - pub artifact_id: String, - pub sheets: Vec, - pub size_bytes: usize, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetCellView { - pub sheet_name: String, - pub address: String, - pub effective_address: String, - pub exists: bool, - pub value: Option, - pub formula: Option, - pub style_index: u32, - pub data: Option, - pub is_calculation_error: bool, - pub calculation_error_message: Option, - pub citations: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetRangeView { - pub sheet_name: String, - pub address: String, - pub values: Vec>>, - pub formulas: Vec>>, - pub style_indices: Vec>, - pub data: Vec>>, - pub is_single_cell: bool, - pub is_single_row: bool, - pub is_single_column: bool, - pub contains_merged_cells: bool, - pub is_exactly_one_merged_cell: bool, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetSheet { - pub sheet_id: String, - pub name: String, - #[serde(default, with = "cell_map_serde")] - pub cells: BTreeMap, - #[serde(default)] - pub merged_ranges: Vec, - #[serde(default)] - pub charts: Vec, - #[serde(default)] - pub tables: Vec, - #[serde(default)] - pub conditional_formats: Vec, - #[serde(default)] - pub pivot_tables: Vec, - pub default_row_height: Option, - pub default_column_width: Option, - pub show_grid_lines: bool, - #[serde(default)] - pub column_widths: BTreeMap, - #[serde(default)] - pub row_heights: BTreeMap, -} - -mod cell_map_serde { - use std::collections::BTreeMap; - - use serde::Deserialize; - use serde::Deserializer; - use serde::Serialize; - use serde::Serializer; - - use crate::CellAddress; - use crate::SpreadsheetCell; - #[derive(Serialize, Deserialize)] - struct CellEntry { - address: String, - cell: SpreadsheetCell, - } - - pub fn serialize( - cells: &BTreeMap, - serializer: S, - ) -> Result - where - S: Serializer, - { - let entries = cells - .iter() - .map(|(address, cell)| CellEntry { - address: address.to_a1(), - cell: cell.clone(), - }) - .collect::>(); - entries.serialize(serializer) - } - - pub fn deserialize<'de, D>( - deserializer: D, - ) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let entries = Vec::::deserialize(deserializer)?; - let mut cells = BTreeMap::new(); - for entry in entries { - let address = CellAddress::parse(&entry.address).map_err(serde::de::Error::custom)?; - cells.insert(address, entry.cell); - } - Ok(cells) - } -} - -impl SpreadsheetSheet { - pub fn new(name: String) -> Self { - Self { - sheet_id: format!("sheet_{}", Uuid::new_v4().simple()), - name, - cells: BTreeMap::new(), - merged_ranges: Vec::new(), - charts: Vec::new(), - tables: Vec::new(), - conditional_formats: Vec::new(), - pivot_tables: Vec::new(), - default_row_height: None, - default_column_width: None, - show_grid_lines: true, - column_widths: BTreeMap::new(), - row_heights: BTreeMap::new(), - } - } - - pub fn filled_rows(&self) -> usize { - self.cells - .keys() - .map(|address| address.row) - .collect::>() - .len() - } - - pub fn filled_columns(&self) -> usize { - self.cells - .keys() - .map(|address| address.column) - .collect::>() - .len() - } - - pub fn is_empty(&self) -> bool { - self.cells.is_empty() - } - - pub fn minimum_range(&self) -> Option { - let mut iter = self.cells.keys(); - let first = *iter.next()?; - let mut min_row = first.row; - let mut max_row = first.row; - let mut min_column = first.column; - let mut max_column = first.column; - - for address in iter { - min_row = min_row.min(address.row); - max_row = max_row.max(address.row); - min_column = min_column.min(address.column); - max_column = max_column.max(address.column); - } - - Some(CellRange::from_start_end( - CellAddress { - column: min_column, - row: min_row, - }, - CellAddress { - column: max_column, - row: max_row, - }, - )) - } - - pub fn minimum_range_ref(&self) -> Option { - self.minimum_range() - .map(|range| SpreadsheetCellRangeRef::new(self.name.clone(), &range)) - } - - pub fn minimum_range_filled(&self) -> String { - self.minimum_range() - .map(|range| range.to_a1()) - .unwrap_or_default() - } - - pub fn min_column_letter(&self) -> Option { - self.minimum_range() - .map(|range| crate::column_index_to_letters(range.start.column)) - } - - pub fn max_column_letter(&self) -> Option { - self.minimum_range() - .map(|range| crate::column_index_to_letters(range.end.column)) - } - - pub fn first_row_address_range(&self) -> Option { - self.minimum_range().map(|range| { - CellRange::from_start_end( - CellAddress { - column: range.start.column, - row: range.start.row, - }, - CellAddress { - column: range.end.column, - row: range.start.row, - }, - ) - .to_a1() - }) - } - - pub fn summary(&self) -> SpreadsheetSheetSummary { - let minimum = self.minimum_range(); - SpreadsheetSheetSummary { - name: self.name.clone(), - filled_rows: self.filled_rows(), - filled_columns: self.filled_columns(), - minimum_range_filled: self.minimum_range_filled(), - min_row_idx: minimum.as_ref().map(|range| range.start.row), - max_row_idx: minimum.as_ref().map(|range| range.end.row), - min_column_idx: minimum.as_ref().map(|range| range.start.column), - max_column_idx: minimum.as_ref().map(|range| range.end.column), - min_column_letter: self.min_column_letter(), - max_column_letter: self.max_column_letter(), - first_row_address_range: self.first_row_address_range(), - default_row_height: self.default_row_height, - default_column_width: self.default_column_width, - show_grid_lines: self.show_grid_lines, - merged_range_count: self.merged_ranges.len(), - chart_count: self.charts.len(), - table_count: self.tables.len(), - conditional_format_count: self.conditional_formats.len(), - pivot_table_count: self.pivot_tables.len(), - } - } - - pub fn to_dict(&self) -> Result { - serde_json::to_value(self).map_err(|error| SpreadsheetArtifactError::Serialization { - message: error.to_string(), - }) - } - - pub fn cell_ref( - &self, - address: impl AsRef, - ) -> Result { - Ok(SpreadsheetCellRef::new( - self.name.clone(), - CellAddress::parse(address.as_ref())?, - )) - } - - pub fn range_ref( - &self, - address: impl AsRef, - ) -> Result { - let range = CellRange::parse(address.as_ref())?; - Ok(SpreadsheetCellRangeRef::new(self.name.clone(), &range)) - } - - pub fn reference( - &self, - address: impl AsRef, - ) -> Result { - let address = address.as_ref(); - if let Ok(cell_ref) = self.cell_ref(address) { - return Ok(SpreadsheetSheetReference::Cell { cell_ref }); - } - Ok(SpreadsheetSheetReference::Range { - range_ref: self.range_ref(address)?, - }) - } - - pub fn set_column_widths( - &mut self, - reference: &str, - width: f64, - ) -> Result<(), SpreadsheetArtifactError> { - if width <= 0.0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "set_column_widths".to_string(), - message: "column width must be positive".to_string(), - }); - } - let (start, end) = parse_column_reference(reference)?; - for column in start..=end { - self.column_widths.insert(column, width); - } - Ok(()) - } - - pub fn set_column_widths_bulk( - &mut self, - widths: &BTreeMap, - ) -> Result<(), SpreadsheetArtifactError> { - for (reference, width) in widths { - self.set_column_widths(reference, *width)?; - } - Ok(()) - } - - pub fn get_column_width( - &self, - reference: &str, - ) -> Result, SpreadsheetArtifactError> { - let (start, end) = parse_column_reference(reference)?; - if start != end { - return Ok((start..=end) - .find_map(|column| self.column_widths.get(&column).copied()) - .or(self.default_column_width)); - } - Ok(self - .column_widths - .get(&start) - .copied() - .or(self.default_column_width)) - } - - pub fn set_row_height( - &mut self, - row_index: u32, - height: Option, - ) -> Result<(), SpreadsheetArtifactError> { - if row_index == 0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "set_row_height".to_string(), - message: "row index must be positive".to_string(), - }); - } - if let Some(height) = height { - if height <= 0.0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "set_row_height".to_string(), - message: "row height must be positive".to_string(), - }); - } - self.row_heights.insert(row_index, height); - } else { - self.row_heights.remove(&row_index); - } - Ok(()) - } - - pub fn set_row_heights( - &mut self, - start_row_index: u32, - end_row_index: u32, - height: Option, - ) -> Result<(), SpreadsheetArtifactError> { - if start_row_index == 0 || end_row_index == 0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "set_row_heights".to_string(), - message: "row indices must be positive".to_string(), - }); - } - let start = start_row_index.min(end_row_index); - let end = start_row_index.max(end_row_index); - for row_index in start..=end { - self.set_row_height(row_index, height)?; - } - Ok(()) - } - - pub fn set_row_heights_bulk( - &mut self, - heights: &BTreeMap>, - ) -> Result<(), SpreadsheetArtifactError> { - for (row_index, height) in heights { - self.set_row_height(*row_index, *height)?; - } - Ok(()) - } - - pub fn get_row_height(&self, row_index: u32) -> Option { - self.row_heights - .get(&row_index) - .copied() - .or(self.default_row_height) - } - - pub fn cell_exists(&self, address: CellAddress) -> bool { - self.cells.contains_key(&address) - } - - pub fn merged_range_for(&self, address: CellAddress) -> Option<&CellRange> { - self.merged_ranges - .iter() - .find(|range| range.contains(address)) - } - - pub fn effective_address(&self, address: CellAddress) -> CellAddress { - self.merged_range_for(address) - .map(|range| range.start) - .unwrap_or(address) - } - - pub fn get_cell(&self, address: CellAddress) -> Option<&SpreadsheetCell> { - let effective = self.effective_address(address); - self.cells.get(&effective) - } - - pub fn get_cell_by_indices(&self, column: u32, row: u32) -> Option<&SpreadsheetCell> { - self.get_cell(CellAddress { column, row }) - } - - pub fn get_cell_mut(&mut self, address: CellAddress) -> Option<&mut SpreadsheetCell> { - let effective = self.effective_address(address); - self.cells.get_mut(&effective) - } - - pub fn get_or_create_cell_mut(&mut self, address: CellAddress) -> &mut SpreadsheetCell { - let effective = self.effective_address(address); - self.cells.entry(effective).or_insert(SpreadsheetCell { - value: None, - formula: None, - style_index: 0, - citations: Vec::new(), - }) - } - - pub fn clear_range( - &mut self, - range: &CellRange, - fields: Option<&[String]>, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, false, "clear_range")?; - for address in range.addresses() { - if let Some(cell) = self.get_cell_mut(address) { - match fields { - Some(fields) => { - for field in fields { - match field.as_str() { - "value" => cell.value = None, - "formula" => cell.formula = None, - "style_index" => cell.style_index = 0, - "citations" => cell.citations.clear(), - other => { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "clear_range".to_string(), - message: format!("unsupported cell field `{other}`"), - }); - } - } - } - } - None => { - cell.value = None; - cell.formula = None; - cell.style_index = 0; - cell.citations.clear(); - } - } - } - } - self.cells.retain(|_, cell| !cell.is_empty()); - Ok(()) - } - - pub fn set_value( - &mut self, - address: CellAddress, - value: Option, - ) -> Result<(), SpreadsheetArtifactError> { - let range = CellRange::from_start_end(address, address); - self.ensure_range_write_allowed(&range, false, "set_value")?; - let cell = self.get_or_create_cell_mut(address); - cell.formula = None; - cell.value = value; - if cell.is_empty() { - self.cells.remove(&address); - } - Ok(()) - } - - pub fn set_range_to_value( - &mut self, - range: &CellRange, - value: Option, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, false, "set_range_to_value")?; - for address in range.addresses() { - let cell = self.get_or_create_cell_mut(address); - cell.formula = None; - cell.value = value.clone(); - } - self.cells.retain(|_, cell| !cell.is_empty()); - Ok(()) - } - - pub fn set_formula( - &mut self, - address: CellAddress, - formula: Option, - ) -> Result<(), SpreadsheetArtifactError> { - let range = CellRange::from_start_end(address, address); - self.ensure_range_write_allowed(&range, false, "set_formula")?; - let cell = self.get_or_create_cell_mut(address); - cell.formula = formula; - if cell.formula.is_none() { - cell.value = None; - } - if cell.is_empty() { - self.cells.remove(&address); - } - Ok(()) - } - - pub fn set_range_to_formula( - &mut self, - range: &CellRange, - formula: Option, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, false, "set_range_to_formula")?; - for address in range.addresses() { - let cell = self.get_or_create_cell_mut(address); - cell.formula = formula.clone(); - } - self.cells.retain(|_, cell| !cell.is_empty()); - Ok(()) - } - - pub fn set_cell_values_to( - &mut self, - address: &str, - value: Option, - ) -> Result<(), SpreadsheetArtifactError> { - let range = CellRange::parse(address)?; - self.set_range_to_value(&range, value) - } - - pub fn set_cell_formulas_to( - &mut self, - address: &str, - formula: Option, - ) -> Result<(), SpreadsheetArtifactError> { - let range = CellRange::parse(address)?; - self.set_range_to_formula(&range, formula) - } - - pub fn set_style_index( - &mut self, - range: &CellRange, - style_index: u32, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, false, "set_style_index")?; - for address in range.addresses() { - let cell = self.get_or_create_cell_mut(address); - cell.style_index = style_index; - } - self.cells.retain(|_, cell| !cell.is_empty()); - Ok(()) - } - - pub fn cite_range( - &mut self, - range: &CellRange, - citation: SpreadsheetCitation, - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, true, "cite_range")?; - for address in range.addresses() { - let cell = self.get_or_create_cell_mut(address); - cell.citations.push(citation.clone()); - } - Ok(()) - } - - pub fn set_values_matrix( - &mut self, - range: &CellRange, - values: &[Vec>], - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, false, "set_values_matrix")?; - if values.len() != range.height() || values.iter().any(|row| row.len() != range.width()) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "set_range_values".to_string(), - message: format!( - "matrix dimensions {}x{} do not match range {}x{}", - values.len(), - values.first().map(Vec::len).unwrap_or(0), - range.height(), - range.width() - ), - }); - } - - for (row_offset, row) in values.iter().enumerate() { - for (column_offset, value) in row.iter().enumerate() { - let address = CellAddress { - column: range.start.column + column_offset as u32, - row: range.start.row + row_offset as u32, - }; - let cell = self.get_or_create_cell_mut(address); - cell.formula = None; - cell.value = value.clone(); - } - } - self.cells.retain(|_, cell| !cell.is_empty()); - Ok(()) - } - - pub fn set_formulas_matrix( - &mut self, - range: &CellRange, - formulas: &[Vec>], - ) -> Result<(), SpreadsheetArtifactError> { - self.ensure_range_write_allowed(range, false, "set_formulas_matrix")?; - if formulas.len() != range.height() || formulas.iter().any(|row| row.len() != range.width()) - { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "set_range_formulas".to_string(), - message: format!( - "matrix dimensions {}x{} do not match range {}x{}", - formulas.len(), - formulas.first().map(Vec::len).unwrap_or(0), - range.height(), - range.width() - ), - }); - } - - for (row_offset, row) in formulas.iter().enumerate() { - for (column_offset, formula) in row.iter().enumerate() { - let address = CellAddress { - column: range.start.column + column_offset as u32, - row: range.start.row + row_offset as u32, - }; - let cell = self.get_or_create_cell_mut(address); - cell.formula = formula.clone(); - } - } - self.cells.retain(|_, cell| !cell.is_empty()); - Ok(()) - } - - pub fn merge_cells( - &mut self, - range: &CellRange, - raise_on_conflict: bool, - ) -> Result<(), SpreadsheetArtifactError> { - for existing in &self.merged_ranges { - if existing.intersects(range) && existing != range { - if raise_on_conflict { - return Err(SpreadsheetArtifactError::MergeConflict { - action: "merge_range".to_string(), - range: range.to_a1(), - conflict: existing.to_a1(), - }); - } - return Ok(()); - } - } - self.merged_ranges.push(range.clone()); - self.merged_ranges - .sort_by_key(|entry| (entry.start.row, entry.start.column)); - Ok(()) - } - - pub fn unmerge_cells(&mut self, range: &CellRange) { - self.merged_ranges.retain(|entry| entry != range); - } - - pub fn contains_merged_cells(&self, range: &CellRange) -> bool { - self.merged_ranges - .iter() - .any(|entry| entry.intersects(range)) - } - - pub fn is_exactly_one_merged_cell(&self, range: &CellRange) -> bool { - self.merged_ranges.iter().any(|entry| entry == range) - } - - pub fn get_cell_view(&self, address: CellAddress) -> SpreadsheetCellView { - let effective = self.effective_address(address); - let cell = self.get_cell(address); - SpreadsheetCellView { - sheet_name: self.name.clone(), - address: address.to_a1(), - effective_address: effective.to_a1(), - exists: cell.is_some(), - value: cell.and_then(|entry| entry.value.clone()), - formula: cell.and_then(|entry| entry.formula.clone()), - style_index: cell.map(|entry| entry.style_index).unwrap_or(0), - data: cell.and_then(SpreadsheetCell::data), - is_calculation_error: cell - .map(SpreadsheetCell::is_calculation_error) - .unwrap_or(false), - calculation_error_message: cell - .and_then(SpreadsheetCell::calculation_error_message) - .map(str::to_string), - citations: cell - .map(|entry| entry.citations.clone()) - .unwrap_or_default(), - } - } - - pub fn get_cell_view_by_indices(&self, column: u32, row: u32) -> SpreadsheetCellView { - self.get_cell_view(CellAddress { column, row }) - } - - pub fn get_cell_field( - &self, - address: CellAddress, - field: &str, - ) -> Result, SpreadsheetArtifactError> { - let cell = self.get_cell(address); - Ok(match field { - "value" => cell.and_then(|entry| { - entry - .value - .as_ref() - .map(SpreadsheetCellValue::to_json_value) - }), - "formula" => cell.and_then(|entry| entry.formula.clone().map(Value::String)), - "style_index" => cell.map(|entry| Value::Number(entry.style_index.into())), - "data" => cell.and_then(SpreadsheetCell::data), - "citations" => cell - .map(|entry| serde_json::to_value(&entry.citations)) - .transpose() - .map_err(|error| SpreadsheetArtifactError::Serialization { - message: error.to_string(), - })?, - other => { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "get_cell_field".to_string(), - message: format!("unsupported field `{other}`"), - }); - } - }) - } - - pub fn get_cell_field_by_indices( - &self, - column: u32, - row: u32, - field: &str, - ) -> Result, SpreadsheetArtifactError> { - self.get_cell_field(CellAddress { column, row }, field) - } - - pub fn get_raw_cell(&self, address: CellAddress) -> Option { - self.get_cell(address).cloned() - } - - pub fn top_left_style_index(&self, range: &CellRange) -> u32 { - self.get_cell(range.start) - .map(|cell| cell.style_index) - .unwrap_or(0) - } - - pub fn get_range_view(&self, range: &CellRange) -> SpreadsheetRangeView { - let mut values = Vec::new(); - let mut formulas = Vec::new(); - let mut style_indices = Vec::new(); - let mut data = Vec::new(); - - for row in range.start.row..=range.end.row { - let mut value_row = Vec::new(); - let mut formula_row = Vec::new(); - let mut style_row = Vec::new(); - let mut data_row = Vec::new(); - for column in range.start.column..=range.end.column { - let cell = self.get_cell(CellAddress { column, row }); - value_row.push(cell.and_then(|entry| entry.value.clone())); - formula_row.push(cell.and_then(|entry| entry.formula.clone())); - style_row.push(cell.map(|entry| entry.style_index).unwrap_or(0)); - data_row.push(cell.and_then(SpreadsheetCell::data)); - } - values.push(value_row); - formulas.push(formula_row); - style_indices.push(style_row); - data.push(data_row); - } - - SpreadsheetRangeView { - sheet_name: self.name.clone(), - address: range.to_a1(), - values, - formulas, - style_indices, - data, - is_single_cell: range.is_single_cell(), - is_single_row: range.is_single_row(), - is_single_column: range.is_single_column(), - contains_merged_cells: self.contains_merged_cells(range), - is_exactly_one_merged_cell: self.is_exactly_one_merged_cell(range), - } - } - - pub fn to_rendered_text(&self, range: Option<&CellRange>) -> String { - let target = range - .cloned() - .or_else(|| self.minimum_range()) - .unwrap_or_else(|| { - CellRange::from_start_end( - CellAddress { column: 1, row: 1 }, - CellAddress { column: 1, row: 1 }, - ) - }); - let mut lines = Vec::new(); - for row in target.start.row..=target.end.row { - let mut entries = Vec::new(); - for column in target.start.column..=target.end.column { - let view = self.get_cell_view(CellAddress { column, row }); - let text = match view.data { - Some(Value::String(value)) => value, - Some(Value::Bool(value)) => value.to_string(), - Some(Value::Number(value)) => value.to_string(), - Some(Value::Null) | None => String::new(), - Some(other) => other.to_string(), - }; - entries.push(text); - } - lines.push(entries.join("\t")); - } - lines.join("\n") - } - - pub fn cleanup_and_validate_sheet(&mut self) -> Result<(), SpreadsheetArtifactError> { - self.cells.retain(|_, cell| !cell.is_empty()); - - for (column, width) in &self.column_widths { - if *column == 0 || *width <= 0.0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "cleanup_and_validate_sheet".to_string(), - message: format!("invalid column width entry for column {column}"), - }); - } - } - for (row, height) in &self.row_heights { - if *row == 0 || *height <= 0.0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "cleanup_and_validate_sheet".to_string(), - message: format!("invalid row height entry for row {row}"), - }); - } - } - - self.merged_ranges.sort_by_key(|range| { - ( - range.start.row, - range.start.column, - range.end.row, - range.end.column, - ) - }); - self.merged_ranges.dedup(); - for index in 0..self.merged_ranges.len() { - for other in index + 1..self.merged_ranges.len() { - if self.merged_ranges[index].intersects(&self.merged_ranges[other]) { - return Err(SpreadsheetArtifactError::MergeConflict { - action: "cleanup_and_validate_sheet".to_string(), - range: self.merged_ranges[index].to_a1(), - conflict: self.merged_ranges[other].to_a1(), - }); - } - } - } - self.validate_tables("cleanup_and_validate_sheet")?; - self.validate_charts("cleanup_and_validate_sheet")?; - self.validate_pivot_tables("cleanup_and_validate_sheet")?; - Ok(()) - } - - fn ensure_range_write_allowed( - &self, - range: &CellRange, - allow_exact_merged_cell: bool, - action: &str, - ) -> Result<(), SpreadsheetArtifactError> { - for merged in &self.merged_ranges { - if !merged.intersects(range) { - continue; - } - if allow_exact_merged_cell && merged == range { - continue; - } - if merged.contains_range(range) || range.contains_range(merged) { - if allow_exact_merged_cell || merged != range { - return Err(SpreadsheetArtifactError::MergeConflict { - action: action.to_string(), - range: range.to_a1(), - conflict: merged.to_a1(), - }); - } - } else { - return Err(SpreadsheetArtifactError::MergeConflict { - action: action.to_string(), - range: range.to_a1(), - conflict: merged.to_a1(), - }); - } - } - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetArtifact { - pub artifact_id: String, - pub name: Option, - #[serde(default)] - pub sheets: Vec, - pub auto_recalculate: bool, - #[serde(default)] - pub text_styles: BTreeMap, - #[serde(default)] - pub fills: BTreeMap, - #[serde(default)] - pub borders: BTreeMap, - #[serde(default)] - pub number_formats: BTreeMap, - #[serde(default)] - pub cell_formats: BTreeMap, - #[serde(default)] - pub differential_formats: BTreeMap, -} - -impl SpreadsheetArtifact { - pub fn new(name: Option) -> Self { - Self { - artifact_id: format!("spreadsheet_{}", Uuid::new_v4().simple()), - name, - sheets: Vec::new(), - auto_recalculate: false, - text_styles: BTreeMap::new(), - fills: BTreeMap::new(), - borders: BTreeMap::new(), - number_formats: BTreeMap::new(), - cell_formats: BTreeMap::new(), - differential_formats: BTreeMap::new(), - } - } - - pub fn allowed_file_extensions() -> &'static [&'static str] { - &["xlsx", "json", "bin"] - } - - pub fn allowed_file_mime_types() -> &'static [&'static str] { - &[ - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/json", - "application/octet-stream", - ] - } - - pub fn allowed_file_types() -> &'static [SpreadsheetFileType] { - &[ - SpreadsheetFileType::Xlsx, - SpreadsheetFileType::Json, - SpreadsheetFileType::Binary, - ] - } - - pub fn get_output_file_name( - &self, - suffix: Option<&str>, - file_type: SpreadsheetFileType, - ) -> String { - let extension = match file_type { - SpreadsheetFileType::Xlsx => "xlsx", - SpreadsheetFileType::Json => "json", - SpreadsheetFileType::Binary => "bin", - }; - match suffix { - Some(suffix) => format!("{}_{}.{}", self.artifact_id, suffix, extension), - None => format!("{}.{}", self.artifact_id, extension), - } - } - - pub fn create_sheet( - &mut self, - name: String, - ) -> Result<&mut SpreadsheetSheet, SpreadsheetArtifactError> { - if name.trim().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "create_sheet".to_string(), - message: "sheet name cannot be empty".to_string(), - }); - } - if self.sheets.iter().any(|sheet| sheet.name == name) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "create_sheet".to_string(), - message: format!("sheet `{name}` already exists"), - }); - } - self.sheets.push(SpreadsheetSheet::new(name)); - self.sheets - .last_mut() - .ok_or_else(|| SpreadsheetArtifactError::Serialization { - message: "created sheet was not available".to_string(), - }) - } - - pub fn get_sheet(&self, name: Option<&str>, index: Option) -> Option<&SpreadsheetSheet> { - if let Some(name) = name { - self.sheets.iter().find(|sheet| sheet.name == name) - } else if let Some(index) = index { - self.sheets.get(index) - } else { - None - } - } - - pub fn get_sheet_mut( - &mut self, - name: Option<&str>, - index: Option, - ) -> Option<&mut SpreadsheetSheet> { - if let Some(name) = name { - self.sheets.iter_mut().find(|sheet| sheet.name == name) - } else if let Some(index) = index { - self.sheets.get_mut(index) - } else { - None - } - } - - pub fn sheet_lookup( - &self, - action: &str, - name: Option<&str>, - index: Option, - ) -> Result<&SpreadsheetSheet, SpreadsheetArtifactError> { - self.get_sheet(name, index) - .ok_or_else(|| SpreadsheetArtifactError::SheetLookup { - action: action.to_string(), - message: match (name, index) { - (Some(name), _) => format!("sheet `{name}` was not found"), - (None, Some(index)) => format!("sheet index {index} was not found"), - (None, None) => "sheet name or index is required".to_string(), - }, - }) - } - - pub fn sheet_lookup_mut( - &mut self, - action: &str, - name: Option<&str>, - index: Option, - ) -> Result<&mut SpreadsheetSheet, SpreadsheetArtifactError> { - self.get_sheet_mut(name, index) - .ok_or_else(|| SpreadsheetArtifactError::SheetLookup { - action: action.to_string(), - message: match (name, index) { - (Some(name), _) => format!("sheet `{name}` was not found"), - (None, Some(index)) => format!("sheet index {index} was not found"), - (None, None) => "sheet name or index is required".to_string(), - }, - }) - } - - pub fn rename_sheet( - &mut self, - new_name: String, - old_name: Option<&str>, - index: Option, - ) -> Result<(), SpreadsheetArtifactError> { - if self.sheets.iter().any(|sheet| sheet.name == new_name) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "rename_sheet".to_string(), - message: format!("sheet `{new_name}` already exists"), - }); - } - let sheet = self.sheet_lookup_mut("rename_sheet", old_name, index)?; - sheet.name = new_name; - Ok(()) - } - - pub fn delete_sheet( - &mut self, - name: Option<&str>, - index: Option, - ) -> Result<(), SpreadsheetArtifactError> { - let remove_index = if let Some(name) = name { - self.sheets - .iter() - .position(|sheet| sheet.name == name) - .ok_or_else(|| SpreadsheetArtifactError::SheetLookup { - action: "delete_sheet".to_string(), - message: format!("sheet `{name}` was not found"), - })? - } else if let Some(index) = index { - if index >= self.sheets.len() { - return Err(SpreadsheetArtifactError::IndexOutOfRange { - action: "delete_sheet".to_string(), - index, - len: self.sheets.len(), - }); - } - index - } else { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: "delete_sheet".to_string(), - message: "sheet name or index is required".to_string(), - }); - }; - self.sheets.remove(remove_index); - Ok(()) - } - - pub fn list_sheet_names(&self) -> Vec { - self.sheets.iter().map(|sheet| sheet.name.clone()).collect() - } - - pub fn summary(&self) -> SpreadsheetSummary { - SpreadsheetSummary { - artifact_id: self.artifact_id.clone(), - sheets: self.sheets.iter().map(SpreadsheetSheet::summary).collect(), - size_bytes: self.to_bytes().len(), - } - } - - pub fn calculate(&mut self) { - recalculate_workbook(self); - } - - pub fn recalculate(&mut self) { - self.calculate(); - } - - pub fn to_dict(&self) -> Result { - serde_json::to_value(self).map_err(|error| SpreadsheetArtifactError::Serialization { - message: error.to_string(), - }) - } - - pub fn to_json(&self) -> Result { - serde_json::to_string_pretty(self).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - }) - } - - pub fn to_bytes(&self) -> Vec { - self.to_json() - .unwrap_or_else(|_| "{}".to_string()) - .into_bytes() - } - - pub fn to_bytes_base64(&self) -> String { - BASE64_STANDARD.encode(self.to_bytes()) - } - - pub fn from_dict( - data: Value, - artifact_id: Option, - ) -> Result { - let mut artifact: Self = serde_json::from_value(data).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - if let Some(artifact_id) = artifact_id { - artifact.artifact_id = artifact_id; - } else if artifact.artifact_id.is_empty() { - artifact.artifact_id = format!("spreadsheet_{}", Uuid::new_v4().simple()); - } - Ok(artifact) - } - - pub fn from_json( - json: impl AsRef<[u8]>, - artifact_id: Option, - ) -> Result { - let text = std::str::from_utf8(json.as_ref()).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - Self::from_dict( - serde_json::from_str(text).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?, - artifact_id, - ) - } - - pub fn from_bytes( - bytes: &[u8], - artifact_id: Option, - ) -> Result { - Self::from_json(bytes, artifact_id) - } - - pub fn from_source_file( - path: &Path, - artifact_id: Option, - ) -> Result { - match path - .extension() - .and_then(|value| value.to_str()) - .unwrap_or_default() - .to_ascii_lowercase() - .as_str() - { - "xlsx" => import_xlsx(path, artifact_id), - "json" => { - let bytes = std::fs::read(path).map_err(|error| { - SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - Self::from_json(bytes, artifact_id) - } - "bin" => { - let bytes = std::fs::read(path).map_err(|error| { - SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - Self::from_bytes(&bytes, artifact_id) - } - other => Err(SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: format!("unsupported import file type `{other}`"), - }), - } - } - - pub fn read( - path: &Path, - artifact_id: Option, - ) -> Result { - Self::from_source_file(path, artifact_id) - } - - pub fn load( - path: &Path, - artifact_id: Option, - ) -> Result { - Self::from_source_file(path, artifact_id) - } - - pub fn save( - &mut self, - path: &Path, - file_type: Option<&str>, - ) -> Result { - self.to_source_file(path, file_type) - } - - pub fn export(&mut self, path: &Path) -> Result { - self.to_source_file(path, Some("xlsx")) - } - - pub fn to_source_file( - &mut self, - path: &Path, - file_type: Option<&str>, - ) -> Result { - let selected = file_type - .map(str::to_string) - .or_else(|| { - path.extension() - .and_then(|value| value.to_str()) - .map(str::to_string) - }) - .unwrap_or_else(|| "xlsx".to_string()) - .to_ascii_lowercase(); - - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - } - - match selected.as_str() { - "xlsx" => { - for sheet in &self.sheets { - if !sheet.charts.is_empty() - || !sheet.tables.is_empty() - || !sheet.conditional_formats.is_empty() - || !sheet.pivot_tables.is_empty() - { - return Err(SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: format!( - "xlsx export does not yet support charts, tables, conditional formats, or pivot tables on sheet `{}`; use json or bin export instead", - sheet.name - ), - }); - } - } - write_xlsx(self, path) - } - "json" => { - let json = self.to_json()?; - std::fs::write(path, json).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - Ok(path.to_path_buf()) - } - "bin" => { - std::fs::write(path, self.to_bytes()).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - Ok(path.to_path_buf()) - } - other => Err(SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: format!("unsupported export file type `{other}`"), - }), - } - } -} diff --git a/codex-rs/artifact-spreadsheet/src/pivot.rs b/codex-rs/artifact-spreadsheet/src/pivot.rs deleted file mode 100644 index c0c42985c..000000000 --- a/codex-rs/artifact-spreadsheet/src/pivot.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::collections::BTreeMap; - -use serde::Deserialize; -use serde::Serialize; - -use crate::CellRange; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetCellRangeRef; -use crate::SpreadsheetSheet; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotFieldItem { - pub item_type: Option, - pub index: Option, - pub hidden: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotField { - pub index: u32, - pub name: Option, - pub axis: Option, - #[serde(default)] - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotFieldReference { - pub field_index: u32, - pub field_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotPageField { - pub field_index: u32, - pub field_name: Option, - pub selected_item: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotDataField { - pub field_index: u32, - pub field_name: Option, - pub name: Option, - pub subtotal: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotFilter { - pub field_index: Option, - pub field_name: Option, - pub filter_type: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotTable { - pub name: String, - pub cache_id: u32, - pub address: Option, - #[serde(default)] - pub row_fields: Vec, - #[serde(default)] - pub column_fields: Vec, - #[serde(default)] - pub page_fields: Vec, - #[serde(default)] - pub data_fields: Vec, - #[serde(default)] - pub filters: Vec, - #[serde(default)] - pub pivot_fields: Vec, - pub style_name: Option, - pub part_path: Option, -} - -#[derive(Debug, Clone, Default)] -pub struct SpreadsheetPivotTableLookup<'a> { - pub name: Option<&'a str>, - pub index: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetPivotCacheDefinition { - pub definition_path: String, - #[serde(default)] - pub field_names: Vec>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SpreadsheetPivotPreservation { - #[serde(default)] - pub caches: BTreeMap, - #[serde(default)] - pub parts: BTreeMap, -} - -impl SpreadsheetPivotTable { - pub fn range(&self) -> Result, SpreadsheetArtifactError> { - self.address.as_deref().map(CellRange::parse).transpose() - } - - pub fn range_ref( - &self, - sheet_name: &str, - ) -> Result, SpreadsheetArtifactError> { - Ok(self - .range()? - .map(|range| SpreadsheetCellRangeRef::new(sheet_name.to_string(), &range))) - } -} - -impl SpreadsheetSheet { - pub fn list_pivot_tables( - &self, - range: Option<&CellRange>, - ) -> Result, SpreadsheetArtifactError> { - Ok(self - .pivot_tables - .iter() - .filter(|pivot_table| { - range.is_none_or(|target| { - pivot_table - .range() - .ok() - .flatten() - .is_some_and(|pivot_range| pivot_range.intersects(target)) - }) - }) - .cloned() - .collect()) - } - - pub fn get_pivot_table( - &self, - action: &str, - lookup: SpreadsheetPivotTableLookup, - ) -> Result<&SpreadsheetPivotTable, SpreadsheetArtifactError> { - if let Some(name) = lookup.name { - return self - .pivot_tables - .iter() - .find(|pivot_table| pivot_table.name == name) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("pivot table `{name}` was not found"), - }); - } - if let Some(index) = lookup.index { - return self.pivot_tables.get(index).ok_or_else(|| { - SpreadsheetArtifactError::IndexOutOfRange { - action: action.to_string(), - index, - len: self.pivot_tables.len(), - } - }); - } - Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "pivot table name or index is required".to_string(), - }) - } - - pub fn validate_pivot_tables(&self, action: &str) -> Result<(), SpreadsheetArtifactError> { - for pivot_table in &self.pivot_tables { - if pivot_table.name.trim().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "pivot table name cannot be empty".to_string(), - }); - } - if let Some(address) = &pivot_table.address { - CellRange::parse(address)?; - } - } - Ok(()) - } -} diff --git a/codex-rs/artifact-spreadsheet/src/render.rs b/codex-rs/artifact-spreadsheet/src/render.rs deleted file mode 100644 index 05e41b608..000000000 --- a/codex-rs/artifact-spreadsheet/src/render.rs +++ /dev/null @@ -1,373 +0,0 @@ -use std::fs; -use std::path::Path; -use std::path::PathBuf; - -use serde::Deserialize; -use serde::Serialize; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetSheet; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetRenderOptions { - pub output_path: Option, - pub center_address: Option, - pub width: Option, - pub height: Option, - pub include_headers: bool, - pub scale: f64, - pub performance_mode: bool, -} - -impl Default for SpreadsheetRenderOptions { - fn default() -> Self { - Self { - output_path: None, - center_address: None, - width: None, - height: None, - include_headers: true, - scale: 1.0, - performance_mode: false, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SpreadsheetRenderedOutput { - pub path: PathBuf, - pub html: String, -} - -impl SpreadsheetSheet { - pub fn render_html( - &self, - range: Option<&CellRange>, - options: &SpreadsheetRenderOptions, - ) -> Result { - let center = options - .center_address - .as_deref() - .map(CellAddress::parse) - .transpose()?; - let viewport = render_viewport(self, range, center, options)?; - let title = range - .map(CellRange::to_a1) - .unwrap_or_else(|| self.name.clone()); - Ok(format!( - concat!( - "", - "{}", - "", - "", - "
", - "

{}

{}

", - "
", - "{}
", - "
" - ), - html_escape(&title), - preview_css(), - html_escape(&self.name), - options.performance_mode, - html_escape(&title), - html_escape(&viewport.to_a1()), - viewport_style(options), - render_table(self, &viewport, options), - )) - } -} - -impl SpreadsheetArtifact { - pub fn render_workbook_previews( - &self, - cwd: &Path, - options: &SpreadsheetRenderOptions, - ) -> Result, SpreadsheetArtifactError> { - let sheets = if self.sheets.is_empty() { - vec![SpreadsheetSheet::new("Sheet1".to_string())] - } else { - self.sheets.clone() - }; - let output_paths = workbook_output_paths(self, cwd, options, &sheets); - sheets - .iter() - .zip(output_paths) - .map(|(sheet, path)| { - let html = sheet.render_html(None, options)?; - write_rendered_output(&path, &html)?; - Ok(SpreadsheetRenderedOutput { path, html }) - }) - .collect() - } - - pub fn render_sheet_preview( - &self, - cwd: &Path, - sheet: &SpreadsheetSheet, - options: &SpreadsheetRenderOptions, - ) -> Result { - let path = single_output_path( - cwd, - self, - options.output_path.as_deref(), - &format!("render_{}", sanitize_file_component(&sheet.name)), - ); - let html = sheet.render_html(None, options)?; - write_rendered_output(&path, &html)?; - Ok(SpreadsheetRenderedOutput { path, html }) - } - - pub fn render_range_preview( - &self, - cwd: &Path, - sheet: &SpreadsheetSheet, - range: &CellRange, - options: &SpreadsheetRenderOptions, - ) -> Result { - let path = single_output_path( - cwd, - self, - options.output_path.as_deref(), - &format!( - "render_{}_{}", - sanitize_file_component(&sheet.name), - sanitize_file_component(&range.to_a1()) - ), - ); - let html = sheet.render_html(Some(range), options)?; - write_rendered_output(&path, &html)?; - Ok(SpreadsheetRenderedOutput { path, html }) - } -} - -fn render_viewport( - sheet: &SpreadsheetSheet, - range: Option<&CellRange>, - center: Option, - options: &SpreadsheetRenderOptions, -) -> Result { - let base = range - .cloned() - .or_else(|| sheet.minimum_range()) - .unwrap_or_else(|| { - CellRange::from_start_end( - CellAddress { column: 1, row: 1 }, - CellAddress { column: 1, row: 1 }, - ) - }); - let Some(center) = center else { - return Ok(base); - }; - let visible_columns = options - .width - .map(|width| estimated_visible_count(width, 96.0, options.scale)) - .unwrap_or(base.width() as u32); - let visible_rows = options - .height - .map(|height| estimated_visible_count(height, 28.0, options.scale)) - .unwrap_or(base.height() as u32); - - let half_columns = visible_columns / 2; - let half_rows = visible_rows / 2; - let start_column = center - .column - .saturating_sub(half_columns) - .max(base.start.column); - let start_row = center.row.saturating_sub(half_rows).max(base.start.row); - let end_column = (start_column + visible_columns.saturating_sub(1)).min(base.end.column); - let end_row = (start_row + visible_rows.saturating_sub(1)).min(base.end.row); - Ok(CellRange::from_start_end( - CellAddress { - column: start_column, - row: start_row, - }, - CellAddress { - column: end_column.max(start_column), - row: end_row.max(start_row), - }, - )) -} - -fn estimated_visible_count(dimension: u32, cell_size: f64, scale: f64) -> u32 { - ((dimension as f64 / (cell_size * scale.max(0.1))).floor() as u32).max(1) -} - -fn render_table( - sheet: &SpreadsheetSheet, - range: &CellRange, - options: &SpreadsheetRenderOptions, -) -> String { - let mut rows = Vec::new(); - if options.include_headers { - let mut header = vec!["".to_string()]; - for column in range.start.column..=range.end.column { - header.push(format!( - "{}", - crate::column_index_to_letters(column) - )); - } - header.push("".to_string()); - rows.push(header.join("")); - } - for row in range.start.row..=range.end.row { - let mut cells = Vec::new(); - if options.include_headers { - cells.push(format!("{row}")); - } - for column in range.start.column..=range.end.column { - let address = CellAddress { column, row }; - let view = sheet.get_cell_view(address); - let value = view - .data - .as_ref() - .map(render_data_value) - .unwrap_or_default(); - cells.push(format!( - "{}", - address.to_a1(), - view.style_index, - html_escape(&value) - )); - } - rows.push(format!("{}", cells.join(""))); - } - rows.join("") -} - -fn render_data_value(value: &serde_json::Value) -> String { - match value { - serde_json::Value::String(value) => value.clone(), - serde_json::Value::Bool(value) => value.to_string(), - serde_json::Value::Number(value) => value.to_string(), - serde_json::Value::Null => String::new(), - other => other.to_string(), - } -} - -fn viewport_style(options: &SpreadsheetRenderOptions) -> String { - let mut style = vec![ - format!("--scale: {}", options.scale.max(0.1)), - format!( - "--headers: {}", - if options.include_headers { "1" } else { "0" } - ), - ]; - if let Some(width) = options.width { - style.push(format!("width: {width}px")); - } - if let Some(height) = options.height { - style.push(format!("height: {height}px")); - } - style.push("overflow: auto".to_string()); - style.join("; ") -} - -fn preview_css() -> &'static str { - concat!( - "body{margin:0;padding:24px;background:#f5f3ee;color:#1e1e1e;font-family:Georgia,serif;}", - ".spreadsheet-preview{display:flex;flex-direction:column;gap:16px;}", - "header h1{margin:0;font-size:24px;}header p{margin:0;color:#6b6257;font-size:13px;}", - ".viewport{border:1px solid #d6d0c7;background:#fff;box-shadow:0 12px 30px rgba(0,0,0,.08);}", - "table{border-collapse:collapse;transform:scale(var(--scale));transform-origin:top left;}", - "th,td{border:1px solid #ddd3c6;padding:6px 10px;min-width:72px;max-width:240px;font-size:13px;text-align:left;vertical-align:top;}", - "th{background:#f0ebe3;font-weight:600;position:sticky;top:0;z-index:1;}", - ".corner{background:#e7e0d6;left:0;z-index:2;}", - "td{white-space:pre-wrap;}" - ) -} - -fn write_rendered_output(path: &Path, html: &str) -> Result<(), SpreadsheetArtifactError> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - } - fs::write(path, html).map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - }) -} - -fn workbook_output_paths( - artifact: &SpreadsheetArtifact, - cwd: &Path, - options: &SpreadsheetRenderOptions, - sheets: &[SpreadsheetSheet], -) -> Vec { - if let Some(output_path) = options.output_path.as_deref() { - if output_path.extension().is_some_and(|ext| ext == "html") { - let stem = output_path - .file_stem() - .and_then(|value| value.to_str()) - .unwrap_or("render"); - let parent = output_path.parent().unwrap_or(cwd); - return sheets - .iter() - .map(|sheet| { - parent.join(format!( - "{}_{}.html", - stem, - sanitize_file_component(&sheet.name) - )) - }) - .collect(); - } - return sheets - .iter() - .map(|sheet| output_path.join(format!("{}.html", sanitize_file_component(&sheet.name)))) - .collect(); - } - sheets - .iter() - .map(|sheet| { - cwd.join(format!( - "{}_render_{}.html", - artifact.artifact_id, - sanitize_file_component(&sheet.name) - )) - }) - .collect() -} - -fn single_output_path( - cwd: &Path, - artifact: &SpreadsheetArtifact, - output_path: Option<&Path>, - suffix: &str, -) -> PathBuf { - if let Some(output_path) = output_path { - return if output_path.extension().is_some_and(|ext| ext == "html") { - output_path.to_path_buf() - } else { - output_path.join(format!("{suffix}.html")) - }; - } - cwd.join(format!("{}_{}.html", artifact.artifact_id, suffix)) -} - -fn sanitize_file_component(value: &str) -> String { - value - .chars() - .map(|character| { - if character.is_ascii_alphanumeric() { - character - } else { - '_' - } - }) - .collect() -} - -fn html_escape(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} diff --git a/codex-rs/artifact-spreadsheet/src/style.rs b/codex-rs/artifact-spreadsheet/src/style.rs deleted file mode 100644 index 0c9a3782a..000000000 --- a/codex-rs/artifact-spreadsheet/src/style.rs +++ /dev/null @@ -1,580 +0,0 @@ -use std::collections::BTreeMap; - -use serde::Deserialize; -use serde::Serialize; - -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetCellRangeRef; -use crate::SpreadsheetSheet; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SpreadsheetFontFace { - pub font_family: Option, - pub font_scheme: Option, - pub typeface: Option, -} - -impl SpreadsheetFontFace { - fn merge(&self, patch: &Self) -> Self { - Self { - font_family: patch - .font_family - .clone() - .or_else(|| self.font_family.clone()), - font_scheme: patch - .font_scheme - .clone() - .or_else(|| self.font_scheme.clone()), - typeface: patch.typeface.clone().or_else(|| self.typeface.clone()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct SpreadsheetTextStyle { - pub bold: Option, - pub italic: Option, - pub underline: Option, - pub font_size: Option, - pub font_color: Option, - pub text_alignment: Option, - pub anchor: Option, - pub vertical_text_orientation: Option, - pub text_rotation: Option, - pub paragraph_spacing: Option, - pub bottom_inset: Option, - pub left_inset: Option, - pub right_inset: Option, - pub top_inset: Option, - pub font_family: Option, - pub font_scheme: Option, - pub typeface: Option, - pub font_face: Option, -} - -impl SpreadsheetTextStyle { - fn merge(&self, patch: &Self) -> Self { - Self { - bold: patch.bold.or(self.bold), - italic: patch.italic.or(self.italic), - underline: patch.underline.or(self.underline), - font_size: patch.font_size.or(self.font_size), - font_color: patch.font_color.clone().or_else(|| self.font_color.clone()), - text_alignment: patch - .text_alignment - .clone() - .or_else(|| self.text_alignment.clone()), - anchor: patch.anchor.clone().or_else(|| self.anchor.clone()), - vertical_text_orientation: patch - .vertical_text_orientation - .clone() - .or_else(|| self.vertical_text_orientation.clone()), - text_rotation: patch.text_rotation.or(self.text_rotation), - paragraph_spacing: patch.paragraph_spacing.or(self.paragraph_spacing), - bottom_inset: patch.bottom_inset.or(self.bottom_inset), - left_inset: patch.left_inset.or(self.left_inset), - right_inset: patch.right_inset.or(self.right_inset), - top_inset: patch.top_inset.or(self.top_inset), - font_family: patch - .font_family - .clone() - .or_else(|| self.font_family.clone()), - font_scheme: patch - .font_scheme - .clone() - .or_else(|| self.font_scheme.clone()), - typeface: patch.typeface.clone().or_else(|| self.typeface.clone()), - font_face: match (&self.font_face, &patch.font_face) { - (Some(base), Some(update)) => Some(base.merge(update)), - (None, Some(update)) => Some(update.clone()), - (Some(base), None) => Some(base.clone()), - (None, None) => None, - }, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetGradientStop { - pub position: f64, - pub color: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetFillRectangle { - pub left: f64, - pub right: f64, - pub top: f64, - pub bottom: f64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct SpreadsheetFill { - pub solid_fill_color: Option, - pub pattern_type: Option, - pub pattern_foreground_color: Option, - pub pattern_background_color: Option, - #[serde(default)] - pub color_transforms: Vec, - pub gradient_fill_type: Option, - #[serde(default)] - pub gradient_stops: Vec, - pub gradient_kind: Option, - pub angle: Option, - pub scaled: Option, - pub path_type: Option, - pub fill_rectangle: Option, - pub image_reference: Option, -} - -impl SpreadsheetFill { - fn merge(&self, patch: &Self) -> Self { - Self { - solid_fill_color: patch - .solid_fill_color - .clone() - .or_else(|| self.solid_fill_color.clone()), - pattern_type: patch - .pattern_type - .clone() - .or_else(|| self.pattern_type.clone()), - pattern_foreground_color: patch - .pattern_foreground_color - .clone() - .or_else(|| self.pattern_foreground_color.clone()), - pattern_background_color: patch - .pattern_background_color - .clone() - .or_else(|| self.pattern_background_color.clone()), - color_transforms: if patch.color_transforms.is_empty() { - self.color_transforms.clone() - } else { - patch.color_transforms.clone() - }, - gradient_fill_type: patch - .gradient_fill_type - .clone() - .or_else(|| self.gradient_fill_type.clone()), - gradient_stops: if patch.gradient_stops.is_empty() { - self.gradient_stops.clone() - } else { - patch.gradient_stops.clone() - }, - gradient_kind: patch - .gradient_kind - .clone() - .or_else(|| self.gradient_kind.clone()), - angle: patch.angle.or(self.angle), - scaled: patch.scaled.or(self.scaled), - path_type: patch.path_type.clone().or_else(|| self.path_type.clone()), - fill_rectangle: patch - .fill_rectangle - .clone() - .or_else(|| self.fill_rectangle.clone()), - image_reference: patch - .image_reference - .clone() - .or_else(|| self.image_reference.clone()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SpreadsheetBorderLine { - pub style: Option, - pub color: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SpreadsheetBorder { - pub top: Option, - pub right: Option, - pub bottom: Option, - pub left: Option, -} - -impl SpreadsheetBorder { - fn merge(&self, patch: &Self) -> Self { - Self { - top: patch.top.clone().or_else(|| self.top.clone()), - right: patch.right.clone().or_else(|| self.right.clone()), - bottom: patch.bottom.clone().or_else(|| self.bottom.clone()), - left: patch.left.clone().or_else(|| self.left.clone()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SpreadsheetAlignment { - pub horizontal: Option, - pub vertical: Option, -} - -impl SpreadsheetAlignment { - fn merge(&self, patch: &Self) -> Self { - Self { - horizontal: patch.horizontal.clone().or_else(|| self.horizontal.clone()), - vertical: patch.vertical.clone().or_else(|| self.vertical.clone()), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SpreadsheetNumberFormat { - pub format_id: Option, - pub format_code: Option, -} - -impl SpreadsheetNumberFormat { - fn merge(&self, patch: &Self) -> Self { - Self { - format_id: patch.format_id.or(self.format_id), - format_code: patch - .format_code - .clone() - .or_else(|| self.format_code.clone()), - } - } - - fn normalized(mut self) -> Self { - if self.format_code.is_none() { - self.format_code = self.format_id.and_then(builtin_number_format_code); - } - self - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct SpreadsheetCellFormat { - pub text_style_id: Option, - pub fill_id: Option, - pub border_id: Option, - pub alignment: Option, - pub number_format_id: Option, - pub wrap_text: Option, - pub base_cell_style_format_id: Option, -} - -impl SpreadsheetCellFormat { - pub fn wrap(mut self) -> Self { - self.wrap_text = Some(true); - self - } - - pub fn unwrap(mut self) -> Self { - self.wrap_text = Some(false); - self - } - - fn merge(&self, patch: &Self) -> Self { - Self { - text_style_id: patch.text_style_id.or(self.text_style_id), - fill_id: patch.fill_id.or(self.fill_id), - border_id: patch.border_id.or(self.border_id), - alignment: match (&self.alignment, &patch.alignment) { - (Some(base), Some(update)) => Some(base.merge(update)), - (None, Some(update)) => Some(update.clone()), - (Some(base), None) => Some(base.clone()), - (None, None) => None, - }, - number_format_id: patch.number_format_id.or(self.number_format_id), - wrap_text: patch.wrap_text.or(self.wrap_text), - base_cell_style_format_id: patch - .base_cell_style_format_id - .or(self.base_cell_style_format_id), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct SpreadsheetDifferentialFormat { - pub text_style_id: Option, - pub fill_id: Option, - pub border_id: Option, - pub alignment: Option, - pub number_format_id: Option, - pub wrap_text: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetCellFormatSummary { - pub style_index: u32, - pub text_style: Option, - pub fill: Option, - pub border: Option, - pub alignment: Option, - pub number_format: Option, - pub wrap_text: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetRangeFormat { - pub sheet_name: String, - pub range: String, -} - -impl SpreadsheetRangeFormat { - pub fn new(sheet_name: String, range: &CellRange) -> Self { - Self { - sheet_name, - range: range.to_a1(), - } - } - - pub fn range_ref(&self) -> Result { - let range = CellRange::parse(&self.range)?; - Ok(SpreadsheetCellRangeRef::new( - self.sheet_name.clone(), - &range, - )) - } - - pub fn top_left_style_index( - &self, - sheet: &SpreadsheetSheet, - ) -> Result { - self.range_ref()?.top_left_style_index(sheet) - } - - pub fn top_left_cell_format( - &self, - artifact: &SpreadsheetArtifact, - sheet: &SpreadsheetSheet, - ) -> Result, SpreadsheetArtifactError> { - let range = self.range_ref()?.range()?; - Ok(artifact.cell_format_summary(sheet.top_left_style_index(&range))) - } -} - -impl SpreadsheetArtifact { - pub fn create_text_style( - &mut self, - style: SpreadsheetTextStyle, - source_style_id: Option, - merge_with_existing_components: bool, - ) -> Result { - let created = if let Some(source_style_id) = source_style_id { - let source = self - .text_styles - .get(&source_style_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: "create_text_style".to_string(), - message: format!("text style `{source_style_id}` was not found"), - })?; - if merge_with_existing_components { - source.merge(&style) - } else { - style - } - } else { - style - }; - Ok(insert_with_next_id(&mut self.text_styles, created)) - } - - pub fn get_text_style(&self, style_id: u32) -> Option<&SpreadsheetTextStyle> { - self.text_styles.get(&style_id) - } - - pub fn create_fill( - &mut self, - fill: SpreadsheetFill, - source_fill_id: Option, - merge_with_existing_components: bool, - ) -> Result { - let created = if let Some(source_fill_id) = source_fill_id { - let source = self.fills.get(&source_fill_id).cloned().ok_or_else(|| { - SpreadsheetArtifactError::InvalidArgs { - action: "create_fill".to_string(), - message: format!("fill `{source_fill_id}` was not found"), - } - })?; - if merge_with_existing_components { - source.merge(&fill) - } else { - fill - } - } else { - fill - }; - Ok(insert_with_next_id(&mut self.fills, created)) - } - - pub fn get_fill(&self, fill_id: u32) -> Option<&SpreadsheetFill> { - self.fills.get(&fill_id) - } - - pub fn create_border( - &mut self, - border: SpreadsheetBorder, - source_border_id: Option, - merge_with_existing_components: bool, - ) -> Result { - let created = if let Some(source_border_id) = source_border_id { - let source = self - .borders - .get(&source_border_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: "create_border".to_string(), - message: format!("border `{source_border_id}` was not found"), - })?; - if merge_with_existing_components { - source.merge(&border) - } else { - border - } - } else { - border - }; - Ok(insert_with_next_id(&mut self.borders, created)) - } - - pub fn get_border(&self, border_id: u32) -> Option<&SpreadsheetBorder> { - self.borders.get(&border_id) - } - - pub fn create_number_format( - &mut self, - format: SpreadsheetNumberFormat, - source_number_format_id: Option, - merge_with_existing_components: bool, - ) -> Result { - let created = if let Some(source_number_format_id) = source_number_format_id { - let source = self - .number_formats - .get(&source_number_format_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: "create_number_format".to_string(), - message: format!("number format `{source_number_format_id}` was not found"), - })?; - if merge_with_existing_components { - source.merge(&format) - } else { - format - } - } else { - format - }; - Ok(insert_with_next_id( - &mut self.number_formats, - created.normalized(), - )) - } - - pub fn get_number_format(&self, number_format_id: u32) -> Option<&SpreadsheetNumberFormat> { - self.number_formats.get(&number_format_id) - } - - pub fn create_cell_format( - &mut self, - format: SpreadsheetCellFormat, - source_format_id: Option, - merge_with_existing_components: bool, - ) -> Result { - let created = if let Some(source_format_id) = source_format_id { - let source = self - .cell_formats - .get(&source_format_id) - .cloned() - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: "create_cell_format".to_string(), - message: format!("cell format `{source_format_id}` was not found"), - })?; - if merge_with_existing_components { - source.merge(&format) - } else { - format - } - } else { - format - }; - Ok(insert_with_next_id(&mut self.cell_formats, created)) - } - - pub fn get_cell_format(&self, format_id: u32) -> Option<&SpreadsheetCellFormat> { - self.cell_formats.get(&format_id) - } - - pub fn create_differential_format(&mut self, format: SpreadsheetDifferentialFormat) -> u32 { - insert_with_next_id(&mut self.differential_formats, format) - } - - pub fn get_differential_format( - &self, - format_id: u32, - ) -> Option<&SpreadsheetDifferentialFormat> { - self.differential_formats.get(&format_id) - } - - pub fn resolve_cell_format(&self, style_index: u32) -> Option { - let format = self.cell_formats.get(&style_index)?.clone(); - resolve_cell_format_recursive(&self.cell_formats, &format, 0) - } - - pub fn cell_format_summary(&self, style_index: u32) -> Option { - let resolved = self.resolve_cell_format(style_index)?; - Some(SpreadsheetCellFormatSummary { - style_index, - text_style: resolved - .text_style_id - .and_then(|id| self.text_styles.get(&id).cloned()), - fill: resolved.fill_id.and_then(|id| self.fills.get(&id).cloned()), - border: resolved - .border_id - .and_then(|id| self.borders.get(&id).cloned()), - alignment: resolved.alignment, - number_format: resolved - .number_format_id - .and_then(|id| self.number_formats.get(&id).cloned()), - wrap_text: resolved.wrap_text, - }) - } -} - -impl SpreadsheetSheet { - pub fn range_format(&self, range: &CellRange) -> SpreadsheetRangeFormat { - SpreadsheetRangeFormat::new(self.name.clone(), range) - } -} - -fn insert_with_next_id(map: &mut BTreeMap, value: T) -> u32 { - let next_id = map.last_key_value().map(|(key, _)| key + 1).unwrap_or(1); - map.insert(next_id, value); - next_id -} - -fn resolve_cell_format_recursive( - cell_formats: &BTreeMap, - format: &SpreadsheetCellFormat, - depth: usize, -) -> Option { - if depth > 32 { - return None; - } - let base = format - .base_cell_style_format_id - .and_then(|id| cell_formats.get(&id)) - .and_then(|base| resolve_cell_format_recursive(cell_formats, base, depth + 1)); - Some(match base { - Some(base) => base.merge(format), - None => format.clone(), - }) -} - -fn builtin_number_format_code(format_id: u32) -> Option { - match format_id { - 0 => Some("General".to_string()), - 1 => Some("0".to_string()), - 2 => Some("0.00".to_string()), - 3 => Some("#,##0".to_string()), - 4 => Some("#,##0.00".to_string()), - 9 => Some("0%".to_string()), - 10 => Some("0.00%".to_string()), - _ => None, - } -} diff --git a/codex-rs/artifact-spreadsheet/src/table.rs b/codex-rs/artifact-spreadsheet/src/table.rs deleted file mode 100644 index be04c188e..000000000 --- a/codex-rs/artifact-spreadsheet/src/table.rs +++ /dev/null @@ -1,630 +0,0 @@ -use std::collections::BTreeMap; -use std::collections::BTreeSet; - -use serde::Deserialize; -use serde::Serialize; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetCellValue; -use crate::SpreadsheetSheet; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetTableColumn { - pub id: u32, - pub name: String, - pub totals_row_label: Option, - pub totals_row_function: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetTable { - pub id: u32, - pub name: String, - pub display_name: String, - pub range: String, - pub header_row_count: u32, - pub totals_row_count: u32, - pub style_name: Option, - pub show_first_column: bool, - pub show_last_column: bool, - pub show_row_stripes: bool, - pub show_column_stripes: bool, - #[serde(default)] - pub columns: Vec, - #[serde(default)] - pub filters: BTreeMap, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SpreadsheetTableView { - pub id: u32, - pub name: String, - pub display_name: String, - pub address: String, - pub full_range: String, - pub header_row_count: u32, - pub totals_row_count: u32, - pub totals_row_visible: bool, - pub header_row_range: Option, - pub data_body_range: Option, - pub totals_row_range: Option, - pub style_name: Option, - pub show_first_column: bool, - pub show_last_column: bool, - pub show_row_stripes: bool, - pub show_column_stripes: bool, - pub columns: Vec, -} - -#[derive(Debug, Clone, Default)] -pub struct SpreadsheetTableLookup<'a> { - pub name: Option<&'a str>, - pub display_name: Option<&'a str>, - pub id: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetCreateTableOptions { - pub name: Option, - pub display_name: Option, - pub header_row_count: u32, - pub totals_row_count: u32, - pub style_name: Option, - pub show_first_column: bool, - pub show_last_column: bool, - pub show_row_stripes: bool, - pub show_column_stripes: bool, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SpreadsheetTableStyleOptions { - pub style_name: Option, - pub show_first_column: Option, - pub show_last_column: Option, - pub show_row_stripes: Option, - pub show_column_stripes: Option, -} - -impl SpreadsheetTable { - pub fn range(&self) -> Result { - CellRange::parse(&self.range) - } - - pub fn address(&self) -> String { - self.range.clone() - } - - pub fn full_range(&self) -> String { - self.range.clone() - } - - pub fn totals_row_visible(&self) -> bool { - self.totals_row_count > 0 - } - - pub fn header_row_range(&self) -> Result, SpreadsheetArtifactError> { - if self.header_row_count == 0 { - return Ok(None); - } - let range = self.range()?; - Ok(Some(CellRange::from_start_end( - range.start, - CellAddress { - column: range.end.column, - row: range.start.row + self.header_row_count - 1, - }, - ))) - } - - pub fn data_body_range(&self) -> Result, SpreadsheetArtifactError> { - let range = self.range()?; - let start_row = range.start.row + self.header_row_count; - let end_row = range.end.row.saturating_sub(self.totals_row_count); - if start_row > end_row { - return Ok(None); - } - Ok(Some(CellRange::from_start_end( - CellAddress { - column: range.start.column, - row: start_row, - }, - CellAddress { - column: range.end.column, - row: end_row, - }, - ))) - } - - pub fn totals_row_range(&self) -> Result, SpreadsheetArtifactError> { - if self.totals_row_count == 0 { - return Ok(None); - } - let range = self.range()?; - Ok(Some(CellRange::from_start_end( - CellAddress { - column: range.start.column, - row: range.end.row - self.totals_row_count + 1, - }, - range.end, - ))) - } - - pub fn view(&self) -> Result { - Ok(SpreadsheetTableView { - id: self.id, - name: self.name.clone(), - display_name: self.display_name.clone(), - address: self.address(), - full_range: self.full_range(), - header_row_count: self.header_row_count, - totals_row_count: self.totals_row_count, - totals_row_visible: self.totals_row_visible(), - header_row_range: self.header_row_range()?.map(|range| range.to_a1()), - data_body_range: self.data_body_range()?.map(|range| range.to_a1()), - totals_row_range: self.totals_row_range()?.map(|range| range.to_a1()), - style_name: self.style_name.clone(), - show_first_column: self.show_first_column, - show_last_column: self.show_last_column, - show_row_stripes: self.show_row_stripes, - show_column_stripes: self.show_column_stripes, - columns: self.columns.clone(), - }) - } -} - -impl SpreadsheetSheet { - pub fn create_table( - &mut self, - action: &str, - range: &CellRange, - options: SpreadsheetCreateTableOptions, - ) -> Result { - validate_table_geometry( - action, - range, - options.header_row_count, - options.totals_row_count, - )?; - for table in &self.tables { - let table_range = table.range()?; - if table_range.intersects(range) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "table range `{}` intersects existing table `{}`", - range.to_a1(), - table.name - ), - }); - } - } - - let next_id = self.tables.iter().map(|table| table.id).max().unwrap_or(0) + 1; - let name = options.name.unwrap_or_else(|| format!("Table{next_id}")); - if name.trim().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "table name cannot be empty".to_string(), - }); - } - let display_name = options.display_name.unwrap_or_else(|| name.clone()); - if display_name.trim().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "table display_name cannot be empty".to_string(), - }); - } - ensure_unique_table_name(&self.tables, action, &name, &display_name, None)?; - - let columns = build_table_columns(self, range, options.header_row_count); - self.tables.push(SpreadsheetTable { - id: next_id, - name, - display_name, - range: range.to_a1(), - header_row_count: options.header_row_count, - totals_row_count: options.totals_row_count, - style_name: options.style_name, - show_first_column: options.show_first_column, - show_last_column: options.show_last_column, - show_row_stripes: options.show_row_stripes, - show_column_stripes: options.show_column_stripes, - columns, - filters: BTreeMap::new(), - }); - Ok(next_id) - } - - pub fn list_tables( - &self, - range: Option<&CellRange>, - ) -> Result, SpreadsheetArtifactError> { - self.tables - .iter() - .filter(|table| { - range.is_none_or(|target| { - table - .range() - .map(|table_range| table_range.intersects(target)) - .unwrap_or(false) - }) - }) - .map(SpreadsheetTable::view) - .collect() - } - - pub fn get_table( - &self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result<&SpreadsheetTable, SpreadsheetArtifactError> { - self.table_lookup_internal(action, lookup) - } - - pub fn get_table_view( - &self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result { - self.get_table(action, lookup)?.view() - } - - pub fn delete_table( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result<(), SpreadsheetArtifactError> { - let index = self.table_index(action, lookup)?; - self.tables.remove(index); - Ok(()) - } - - pub fn set_table_style( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - options: SpreadsheetTableStyleOptions, - ) -> Result<(), SpreadsheetArtifactError> { - let table = self.table_lookup_mut(action, lookup)?; - table.style_name = options.style_name; - if let Some(value) = options.show_first_column { - table.show_first_column = value; - } - if let Some(value) = options.show_last_column { - table.show_last_column = value; - } - if let Some(value) = options.show_row_stripes { - table.show_row_stripes = value; - } - if let Some(value) = options.show_column_stripes { - table.show_column_stripes = value; - } - Ok(()) - } - - pub fn clear_table_filters( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result<(), SpreadsheetArtifactError> { - self.table_lookup_mut(action, lookup)?.filters.clear(); - Ok(()) - } - - pub fn reapply_table_filters( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result<(), SpreadsheetArtifactError> { - let _ = self.table_lookup_mut(action, lookup)?; - Ok(()) - } - - pub fn rename_table_column( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - column_id: Option, - column_name: Option<&str>, - new_name: String, - ) -> Result { - if new_name.trim().is_empty() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "table column name cannot be empty".to_string(), - }); - } - let table = self.table_lookup_mut(action, lookup)?; - if table - .columns - .iter() - .any(|column| column.name == new_name && Some(column.id) != column_id) - { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("table column `{new_name}` already exists"), - }); - } - let column = table_column_lookup_mut(&mut table.columns, action, column_id, column_name)?; - column.name = new_name; - Ok(column.clone()) - } - - pub fn set_table_column_totals( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - column_id: Option, - column_name: Option<&str>, - totals_row_label: Option, - totals_row_function: Option, - ) -> Result { - let table = self.table_lookup_mut(action, lookup)?; - let column = table_column_lookup_mut(&mut table.columns, action, column_id, column_name)?; - column.totals_row_label = totals_row_label; - column.totals_row_function = totals_row_function; - Ok(column.clone()) - } - - pub fn validate_tables(&self, action: &str) -> Result<(), SpreadsheetArtifactError> { - let mut seen_names = BTreeSet::new(); - let mut seen_display_names = BTreeSet::new(); - for table in &self.tables { - let range = table.range()?; - validate_table_geometry( - action, - &range, - table.header_row_count, - table.totals_row_count, - )?; - if !seen_names.insert(table.name.clone()) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("duplicate table name `{}`", table.name), - }); - } - if !seen_display_names.insert(table.display_name.clone()) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("duplicate table display_name `{}`", table.display_name), - }); - } - let column_names = table - .columns - .iter() - .map(|column| column.name.clone()) - .collect::>(); - if column_names.len() != table.columns.len() { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("table `{}` has duplicate column names", table.name), - }); - } - } - - for index in 0..self.tables.len() { - for other in index + 1..self.tables.len() { - if self.tables[index] - .range()? - .intersects(&self.tables[other].range()?) - { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!( - "table `{}` intersects table `{}`", - self.tables[index].name, self.tables[other].name - ), - }); - } - } - } - Ok(()) - } - - fn table_index( - &self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result { - self.tables - .iter() - .position(|table| table_matches_lookup(table, lookup.clone())) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: describe_missing_table(lookup), - }) - } - - fn table_lookup_internal( - &self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result<&SpreadsheetTable, SpreadsheetArtifactError> { - self.tables - .iter() - .find(|table| table_matches_lookup(table, lookup.clone())) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: describe_missing_table(lookup), - }) - } - - fn table_lookup_mut( - &mut self, - action: &str, - lookup: SpreadsheetTableLookup<'_>, - ) -> Result<&mut SpreadsheetTable, SpreadsheetArtifactError> { - let index = self.table_index(action, lookup)?; - Ok(&mut self.tables[index]) - } -} - -fn table_matches_lookup(table: &SpreadsheetTable, lookup: SpreadsheetTableLookup<'_>) -> bool { - if let Some(name) = lookup.name { - table.name == name - } else if let Some(display_name) = lookup.display_name { - table.display_name == display_name - } else if let Some(id) = lookup.id { - table.id == id - } else { - false - } -} - -fn describe_missing_table(lookup: SpreadsheetTableLookup<'_>) -> String { - if let Some(name) = lookup.name { - format!("table name `{name}` was not found") - } else if let Some(display_name) = lookup.display_name { - format!("table display_name `{display_name}` was not found") - } else if let Some(id) = lookup.id { - format!("table id `{id}` was not found") - } else { - "table name, display_name, or id is required".to_string() - } -} - -fn ensure_unique_table_name( - tables: &[SpreadsheetTable], - action: &str, - name: &str, - display_name: &str, - exclude_id: Option, -) -> Result<(), SpreadsheetArtifactError> { - if tables.iter().any(|table| { - Some(table.id) != exclude_id && (table.name == name || table.display_name == name) - }) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("table name `{name}` already exists"), - }); - } - if tables.iter().any(|table| { - Some(table.id) != exclude_id - && (table.display_name == display_name || table.name == display_name) - }) { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: format!("table display_name `{display_name}` already exists"), - }); - } - Ok(()) -} - -fn validate_table_geometry( - action: &str, - range: &CellRange, - header_row_count: u32, - totals_row_count: u32, -) -> Result<(), SpreadsheetArtifactError> { - if range.width() == 0 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "table range must include at least one column".to_string(), - }); - } - if header_row_count + totals_row_count > range.height() as u32 { - return Err(SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: "table range is smaller than header and totals rows".to_string(), - }); - } - Ok(()) -} - -fn build_table_columns( - sheet: &SpreadsheetSheet, - range: &CellRange, - header_row_count: u32, -) -> Vec { - let header_row = range.start.row + header_row_count.saturating_sub(1); - let default_names = (0..range.width()) - .map(|index| format!("Column{}", index + 1)) - .collect::>(); - let names = unique_table_column_names( - (range.start.column..=range.end.column) - .enumerate() - .map(|(index, column)| { - if header_row_count == 0 { - return default_names[index].clone(); - } - sheet - .get_cell(CellAddress { - column, - row: header_row, - }) - .and_then(|cell| cell.value.as_ref()) - .map(cell_value_to_table_header) - .filter(|value| !value.trim().is_empty()) - .unwrap_or_else(|| default_names[index].clone()) - }) - .collect::>(), - ); - names - .into_iter() - .enumerate() - .map(|(index, name)| SpreadsheetTableColumn { - id: index as u32 + 1, - name, - totals_row_label: None, - totals_row_function: None, - }) - .collect() -} - -fn unique_table_column_names(names: Vec) -> Vec { - let mut seen = BTreeMap::::new(); - names - .into_iter() - .map(|name| { - let entry = seen.entry(name.clone()).or_insert(0); - *entry += 1; - if *entry == 1 { - name - } else { - format!("{name}_{}", *entry) - } - }) - .collect() -} - -fn cell_value_to_table_header(value: &SpreadsheetCellValue) -> String { - match value { - SpreadsheetCellValue::Bool(value) => value.to_string(), - SpreadsheetCellValue::Integer(value) => value.to_string(), - SpreadsheetCellValue::Float(value) => value.to_string(), - SpreadsheetCellValue::String(value) - | SpreadsheetCellValue::DateTime(value) - | SpreadsheetCellValue::Error(value) => value.clone(), - } -} - -fn table_column_lookup_mut<'a>( - columns: &'a mut [SpreadsheetTableColumn], - action: &str, - column_id: Option, - column_name: Option<&str>, -) -> Result<&'a mut SpreadsheetTableColumn, SpreadsheetArtifactError> { - columns - .iter_mut() - .find(|column| { - if let Some(column_id) = column_id { - column.id == column_id - } else if let Some(column_name) = column_name { - column.name == column_name - } else { - false - } - }) - .ok_or_else(|| SpreadsheetArtifactError::InvalidArgs { - action: action.to_string(), - message: if let Some(column_id) = column_id { - format!("table column id `{column_id}` was not found") - } else if let Some(column_name) = column_name { - format!("table column `{column_name}` was not found") - } else { - "table column id or name is required".to_string() - }, - }) -} diff --git a/codex-rs/artifact-spreadsheet/src/tests.rs b/codex-rs/artifact-spreadsheet/src/tests.rs deleted file mode 100644 index 608ac7b39..000000000 --- a/codex-rs/artifact-spreadsheet/src/tests.rs +++ /dev/null @@ -1,1228 +0,0 @@ -use std::collections::BTreeMap; - -use pretty_assertions::assert_eq; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactManager; -use crate::SpreadsheetArtifactRequest; -use crate::SpreadsheetCell; -use crate::SpreadsheetCellFormat; -use crate::SpreadsheetCellFormatSummary; -use crate::SpreadsheetCellValue; -use crate::SpreadsheetFileType; -use crate::SpreadsheetFill; -use crate::SpreadsheetFontFace; -use crate::SpreadsheetNumberFormat; -use crate::SpreadsheetRenderOptions; -use crate::SpreadsheetSheet; -use crate::SpreadsheetSheetReference; -use crate::SpreadsheetTextStyle; - -#[test] -fn manager_can_create_edit_recalculate_and_export() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = SpreadsheetArtifactManager::default(); - - let created = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Budget" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_sheet".to_string(), - args: serde_json::json!({ "name": "Sheet1" }), - }, - temp_dir.path(), - )?; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_range_values".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "range": "A1:B2", - "values": [[1, 2], [3, 4]] - }), - }, - temp_dir.path(), - )?; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_cell_formula".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "address": "C1", - "formula": "=SUM(A1:B2)", - "recalculate": true - }), - }, - temp_dir.path(), - )?; - - let cell = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_cell".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "address": "C1" - }), - }, - temp_dir.path(), - )?; - assert_eq!( - cell.cell.and_then(|entry| entry.value), - Some(SpreadsheetCellValue::Integer(10)) - ); - - let export_path = temp_dir.path().join("budget.xlsx"); - let export = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id), - action: "export_xlsx".to_string(), - args: serde_json::json!({ "path": export_path }), - }, - temp_dir.path(), - )?; - assert_eq!(export.exported_paths.len(), 1); - assert!(export.exported_paths[0].exists()); - Ok(()) -} - -#[test] -fn spreadsheet_serialization_roundtrip_preserves_cells() -> Result<(), Box> { - let mut artifact = SpreadsheetArtifact::new(Some("Roundtrip".to_string())); - let sheet = artifact.create_sheet("Sheet1".to_string())?; - sheet.set_value( - crate::CellAddress::parse("A1")?, - Some(SpreadsheetCellValue::String("hello".to_string())), - )?; - sheet.set_formula(crate::CellAddress::parse("B1")?, Some("=A1".to_string()))?; - artifact.recalculate(); - - let json = artifact.to_json()?; - let restored = SpreadsheetArtifact::from_json(json, None)?; - let restored_sheet = restored.get_sheet(Some("Sheet1"), None).expect("sheet"); - let cell = restored_sheet.get_cell_view(crate::CellAddress::parse("A1")?); - assert_eq!( - cell.value, - Some(SpreadsheetCellValue::String("hello".to_string())) - ); - Ok(()) -} - -#[test] -fn xlsx_roundtrip_preserves_merged_ranges_and_style_indices() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let path = temp_dir.path().join("styled.xlsx"); - - let mut artifact = SpreadsheetArtifact::new(Some("Styled".to_string())); - let sheet = artifact.create_sheet("Sheet1".to_string())?; - sheet.set_value( - crate::CellAddress::parse("A1")?, - Some(SpreadsheetCellValue::Integer(42)), - )?; - sheet.set_style_index(&crate::CellRange::parse("A1:B1")?, 3)?; - sheet.merge_cells(&crate::CellRange::parse("A1:B1")?, true)?; - artifact.export(&path)?; - - let restored = SpreadsheetArtifact::from_source_file(&path, None)?; - let restored_sheet = restored.get_sheet(Some("Sheet1"), None).expect("sheet"); - assert_eq!(restored_sheet.merged_ranges.len(), 1); - assert_eq!( - restored_sheet - .get_cell_view(crate::CellAddress::parse("A1")?) - .style_index, - 3 - ); - Ok(()) -} - -#[test] -fn path_accesses_cover_import_and_export() -> Result<(), Box> { - let cwd = tempfile::tempdir()?; - let request = crate::SpreadsheetArtifactRequest { - artifact_id: Some("spreadsheet_1".to_string()), - action: "export_xlsx".to_string(), - args: serde_json::json!({ "path": "out/report.xlsx" }), - }; - let accesses = request.required_path_accesses(cwd.path())?; - assert_eq!(accesses.len(), 1); - assert!(accesses[0].path.ends_with("out/report.xlsx")); - Ok(()) -} - -#[test] -fn render_options_write_deterministic_html_previews() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut artifact = SpreadsheetArtifact::new(Some("Preview".to_string())); - artifact.create_sheet("Sheet 1".to_string())?; - { - let sheet = artifact - .get_sheet_mut(Some("Sheet 1"), None) - .expect("sheet"); - sheet.set_value( - CellAddress::parse("A1")?, - Some(SpreadsheetCellValue::String("Name".to_string())), - )?; - sheet.set_value( - CellAddress::parse("B1")?, - Some(SpreadsheetCellValue::String("Value".to_string())), - )?; - sheet.set_value( - CellAddress::parse("A2")?, - Some(SpreadsheetCellValue::String("Alpha".to_string())), - )?; - sheet.set_value( - CellAddress::parse("B2")?, - Some(SpreadsheetCellValue::Integer(42)), - )?; - } - - let rendered = artifact.render_range_preview( - temp_dir.path(), - artifact.get_sheet(Some("Sheet 1"), None).expect("sheet"), - &CellRange::parse("A1:B2")?, - &SpreadsheetRenderOptions { - output_path: Some(temp_dir.path().join("range-preview.html")), - width: Some(320), - height: Some(200), - include_headers: true, - scale: 1.25, - performance_mode: true, - ..Default::default() - }, - )?; - assert!(rendered.path.exists()); - assert_eq!(std::fs::read_to_string(&rendered.path)?, rendered.html); - assert!(rendered.html.contains("")); - assert!(rendered.html.contains("data-performance-mode=\"true\"")); - assert!(rendered.html.contains( - "style=\"--scale: 1.25; --headers: 1; width: 320px; height: 200px; overflow: auto\"" - )); - assert!(rendered.html.contains("A")); - assert!(rendered.html.contains("data-address=\"B2\"")); - assert!(rendered.html.contains(">42")); - - let workbook = artifact.render_workbook_previews( - temp_dir.path(), - &SpreadsheetRenderOptions { - output_path: Some(temp_dir.path().join("workbook")), - include_headers: false, - ..Default::default() - }, - )?; - assert_eq!(workbook.len(), 1); - assert!(workbook[0].path.ends_with("Sheet_1.html")); - assert!(!workbook[0].html.contains("A")); - Ok(()) -} - -#[test] -fn sheet_refs_support_handle_and_field_apis() -> Result<(), Box> { - let mut artifact = SpreadsheetArtifact::new(Some("Handles".to_string())); - let (range_ref, cell_ref) = { - let sheet = artifact.create_sheet("Sheet1".to_string())?; - let range_ref = sheet.range_ref("A1:B2")?; - range_ref.set_value(sheet, Some(SpreadsheetCellValue::Integer(7)))?; - let cell_ref = sheet.cell_ref("B2")?; - cell_ref.set_formula(sheet, Some("=SUM(A1:B2)".to_string()))?; - (range_ref, cell_ref) - }; - artifact.recalculate(); - let sheet = artifact.get_sheet(Some("Sheet1"), None).expect("sheet"); - - let values = range_ref.get_values(sheet)?; - assert_eq!(values[0][0], Some(SpreadsheetCellValue::Integer(7))); - assert_eq!( - cell_ref.get(sheet)?.value, - Some(SpreadsheetCellValue::Integer(28)) - ); - assert_eq!( - sheet.get_cell_field_by_indices(2, 2, "formula")?, - Some(serde_json::Value::String("=SUM(A1:B2)".to_string())) - ); - assert_eq!( - sheet.minimum_range_ref().map(|entry| entry.address), - Some("A1:B2".to_string()) - ); - assert!(matches!( - sheet.to_dict()?, - serde_json::Value::Object(_) | serde_json::Value::Array(_) - )); - Ok(()) -} - -#[test] -fn manager_supports_single_value_formula_and_cite_cell_actions() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = SpreadsheetArtifactManager::default(); - let created = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Actions" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_sheet".to_string(), - args: serde_json::json!({ "name": "Sheet1" }), - }, - temp_dir.path(), - )?; - - let uniform = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_range_value".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "range": "A1:B2", - "value": 5 - }), - }, - temp_dir.path(), - )?; - assert_eq!( - uniform - .range_ref - .as_ref() - .map(|entry| entry.address.clone()), - Some("A1:B2".to_string()) - ); - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_range_formula".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "range": "C1:C2", - "formula": "=SUM(A1:B2)", - "recalculate": true - }), - }, - temp_dir.path(), - )?; - - let cited = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "cite_cell".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "address": "C1", - "tether_id": "source-1", - "start_line": 3, - "end_line": 8 - }), - }, - temp_dir.path(), - )?; - assert_eq!( - cited.cell.as_ref().map(|entry| entry.citations.len()), - Some(1) - ); - - let by_indices = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_cell_by_indices".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "column_index": 3, - "row_index": 1 - }), - }, - temp_dir.path(), - )?; - assert_eq!( - by_indices - .cell - .as_ref() - .and_then(|entry| entry.value.clone()), - Some(SpreadsheetCellValue::Integer(20)) - ); - - let field = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id), - action: "get_cell_field".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "address": "C1", - "field": "formula" - }), - }, - temp_dir.path(), - )?; - assert_eq!( - field.cell_field, - Some(serde_json::Value::String("=SUM(A1:B2)".to_string())) - ); - Ok(()) -} - -#[test] -fn artifact_file_type_helpers_and_source_files_work() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut artifact = SpreadsheetArtifact::new(Some("Files".to_string())); - artifact.artifact_id = "spreadsheet_fixed".to_string(); - artifact.create_sheet("Sheet1".to_string())?.set_value( - CellAddress::parse("A1")?, - Some(SpreadsheetCellValue::String("hello".to_string())), - )?; - - assert_eq!( - SpreadsheetArtifact::allowed_file_extensions(), - &["xlsx", "json", "bin"] - ); - assert_eq!( - SpreadsheetArtifact::allowed_file_mime_types(), - &[ - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/json", - "application/octet-stream", - ] - ); - assert_eq!( - SpreadsheetArtifact::allowed_file_types().to_vec(), - vec![ - SpreadsheetFileType::Xlsx, - SpreadsheetFileType::Json, - SpreadsheetFileType::Binary, - ] - ); - assert_eq!( - artifact.get_output_file_name(Some("preview"), SpreadsheetFileType::Json), - "spreadsheet_fixed_preview.json".to_string() - ); - - let json_path = temp_dir - .path() - .join(artifact.get_output_file_name(None, SpreadsheetFileType::Json)); - artifact.save(&json_path, Some("json"))?; - let restored_json = SpreadsheetArtifact::load(&json_path, None)?; - assert_eq!(restored_json.to_dict()?, artifact.to_dict()?); - - let bytes_path = temp_dir - .path() - .join(artifact.get_output_file_name(Some("bytes"), SpreadsheetFileType::Binary)); - artifact.save(&bytes_path, Some("bin"))?; - let restored_bytes = SpreadsheetArtifact::read(&bytes_path, None)?; - assert_eq!(restored_bytes.to_dict()?, artifact.to_dict()?); - Ok(()) -} - -#[test] -fn sheet_cleanup_and_row_sizing_helpers_work() -> Result<(), Box> { - let mut sheet = SpreadsheetSheet::new("Sheet1".to_string()); - sheet.default_row_height = Some(15.0); - sheet.set_column_widths_bulk(&BTreeMap::from([ - ("A".to_string(), 12.0), - ("C:D".to_string(), 20.0), - ]))?; - sheet.set_row_height(2, Some(18.0))?; - sheet.set_row_heights(3, 4, Some(22.0))?; - sheet.set_row_heights_bulk(&BTreeMap::from([(4, None), (5, Some(30.0))]))?; - - assert_eq!(sheet.get_column_width("A")?, Some(12.0)); - assert_eq!(sheet.get_column_width("B")?, None); - assert_eq!(sheet.get_column_width("D")?, Some(20.0)); - assert_eq!(sheet.get_row_height(2), Some(18.0)); - assert_eq!(sheet.get_row_height(3), Some(22.0)); - assert_eq!(sheet.get_row_height(4), Some(15.0)); - assert_eq!(sheet.get_row_height(5), Some(30.0)); - - sheet.cells.insert( - CellAddress::parse("A1")?, - SpreadsheetCell { - value: None, - formula: None, - style_index: 0, - citations: Vec::new(), - }, - ); - let merged = CellRange::parse("B2:C3")?; - sheet.merged_ranges = vec![merged.clone(), merged.clone()]; - sheet.cleanup_and_validate_sheet()?; - - assert_eq!(sheet.cells, BTreeMap::new()); - assert_eq!(sheet.merged_ranges, vec![merged]); - Ok(()) -} - -#[test] -fn xlsx_roundtrip_preserves_row_and_column_sizes() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let path = temp_dir.path().join("sizing.xlsx"); - - let mut artifact = SpreadsheetArtifact::new(Some("Sizing".to_string())); - let (expected_column_widths, expected_row_heights, expected_show_grid_lines) = { - let sheet = artifact.create_sheet("Sheet1".to_string())?; - sheet.show_grid_lines = false; - sheet.set_value( - CellAddress::parse("A1")?, - Some(SpreadsheetCellValue::Integer(42)), - )?; - sheet.set_column_widths_bulk(&BTreeMap::from([ - ("A:B".to_string(), 12.5), - ("D".to_string(), 18.0), - ]))?; - sheet.set_row_heights_bulk(&BTreeMap::from([(2, Some(24.0)), (6, Some(19.5))]))?; - ( - sheet.column_widths.clone(), - sheet.row_heights.clone(), - sheet.show_grid_lines, - ) - }; - artifact.export(&path)?; - - let restored = SpreadsheetArtifact::from_source_file(&path, None)?; - let restored_sheet = restored.get_sheet(Some("Sheet1"), None).expect("sheet"); - assert_eq!(restored_sheet.column_widths, expected_column_widths); - assert_eq!(restored_sheet.row_heights, expected_row_heights); - assert_eq!(restored_sheet.show_grid_lines, expected_show_grid_lines); - Ok(()) -} - -#[test] -fn manager_supports_bulk_sizes_and_row_heights() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = SpreadsheetArtifactManager::default(); - let created = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Sizing" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_sheet".to_string(), - args: serde_json::json!({ "name": "Sheet1" }), - }, - temp_dir.path(), - )?; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_column_widths_bulk".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "widths": { - "A:B": 12.0, - "D": 20.0 - } - }), - }, - temp_dir.path(), - )?; - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_row_height".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "row_index": 2, - "height": 18.0 - }), - }, - temp_dir.path(), - )?; - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_row_heights".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "start_row_index": 3, - "end_row_index": 4, - "height": 21.0 - }), - }, - temp_dir.path(), - )?; - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_row_heights_bulk".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "heights": { - "4": null, - "5": 25.0 - } - }), - }, - temp_dir.path(), - )?; - - let row_height = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_row_height".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "row_index": 5 - }), - }, - temp_dir.path(), - )?; - assert_eq!(row_height.row_height, Some(25.0)); - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "cleanup_and_validate_sheet".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1" - }), - }, - temp_dir.path(), - )?; - - let sheet = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id), - action: "get_sheet".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1" - }), - }, - temp_dir.path(), - )?; - let restored: SpreadsheetSheet = - serde_json::from_value(sheet.serialized_dict.expect("sheet dict"))?; - assert_eq!( - restored.column_widths, - BTreeMap::from([(1, 12.0), (2, 12.0), (4, 20.0)]) - ); - assert_eq!( - restored.row_heights, - BTreeMap::from([(2, 18.0), (3, 21.0), (5, 25.0)]) - ); - Ok(()) -} - -#[test] -fn manager_style_registry_and_format_summaries_work() -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = SpreadsheetArtifactManager::default(); - let created = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Styles" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_sheet".to_string(), - args: serde_json::json!({ "name": "Sheet1" }), - }, - temp_dir.path(), - )?; - - let text_style = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_text_style".to_string(), - args: serde_json::json!({ - "style": { - "bold": true, - "italic": true, - "underline": true, - "font_size": 14.0, - "font_color": "#112233", - "text_alignment": "center", - "anchor": "middle", - "vertical_text_orientation": "stacked", - "text_rotation": 90, - "paragraph_spacing": true, - "bottom_inset": 1.0, - "left_inset": 2.0, - "right_inset": 3.0, - "top_inset": 4.0, - "font_family": "IBM Plex Sans", - "font_scheme": "minor", - "typeface": "IBM Plex Sans", - "font_face": { - "font_family": "IBM Plex Sans", - "font_scheme": "minor", - "typeface": "IBM Plex Sans" - } - } - }), - }, - temp_dir.path(), - )?; - let text_style_id = text_style.style_id.expect("text style id"); - - let fill = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_fill".to_string(), - args: serde_json::json!({ - "fill": { - "solid_fill_color": "#ffeeaa", - "pattern_type": "solid", - "pattern_foreground_color": "#ffeeaa", - "pattern_background_color": "#221100", - "color_transforms": ["tint:0.2"], - "gradient_fill_type": "linear", - "gradient_stops": [ - { "position": 0.0, "color": "#ffeeaa" }, - { "position": 1.0, "color": "#aa5500" } - ], - "gradient_kind": "linear", - "angle": 45.0, - "scaled": true, - "path_type": "rect", - "fill_rectangle": { - "left": 0.0, - "right": 1.0, - "top": 0.0, - "bottom": 1.0 - }, - "image_reference": "image://fill" - } - }), - }, - temp_dir.path(), - )?; - let fill_id = fill.style_id.expect("fill id"); - - let border = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_border".to_string(), - args: serde_json::json!({ - "border": { - "top": { "style": "solid", "color": "#111111" }, - "right": { "style": "dashed", "color": "#222222" }, - "bottom": { "style": "double", "color": "#333333" }, - "left": { "style": "solid", "color": "#444444" } - } - }), - }, - temp_dir.path(), - )?; - let border_id = border.style_id.expect("border id"); - - let number_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_number_format".to_string(), - args: serde_json::json!({ - "number_format": { - "format_id": 4 - } - }), - }, - temp_dir.path(), - )?; - let number_format_id = number_format.style_id.expect("number format id"); - - let base_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_cell_format".to_string(), - args: serde_json::json!({ - "format": { - "text_style_id": text_style_id, - "number_format_id": number_format_id, - "alignment": { - "horizontal": "center", - "vertical": "middle" - } - } - }), - }, - temp_dir.path(), - )?; - let base_format_id = base_format.style_id.expect("base format id"); - - let derived_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_cell_format".to_string(), - args: serde_json::json!({ - "format": { - "fill_id": fill_id, - "border_id": border_id, - "wrap_text": true, - "base_cell_style_format_id": base_format_id - } - }), - }, - temp_dir.path(), - )?; - let derived_format_id = derived_format.style_id.expect("derived format id"); - - let merged_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_cell_format".to_string(), - args: serde_json::json!({ - "source_format_id": derived_format_id, - "merge_with_existing_components": true, - "format": { - "alignment": { - "vertical": "bottom" - } - } - }), - }, - temp_dir.path(), - )?; - let merged_format_id = merged_format.style_id.expect("merged format id"); - - let differential_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_differential_format".to_string(), - args: serde_json::json!({ - "format": { - "fill_id": fill_id, - "wrap_text": true - } - }), - }, - temp_dir.path(), - )?; - let differential_format_id = differential_format.style_id.expect("dxf id"); - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_cell_style".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "address": "A1", - "style_index": merged_format_id - }), - }, - temp_dir.path(), - )?; - - let summary = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_cell_format_summary".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "address": "A1" - }), - }, - temp_dir.path(), - )?; - assert_eq!( - summary.cell_format_summary, - Some(SpreadsheetCellFormatSummary { - style_index: merged_format_id, - text_style: Some(SpreadsheetTextStyle { - bold: Some(true), - italic: Some(true), - underline: Some(true), - font_size: Some(14.0), - font_color: Some("#112233".to_string()), - text_alignment: Some("center".to_string()), - anchor: Some("middle".to_string()), - vertical_text_orientation: Some("stacked".to_string()), - text_rotation: Some(90), - paragraph_spacing: Some(true), - bottom_inset: Some(1.0), - left_inset: Some(2.0), - right_inset: Some(3.0), - top_inset: Some(4.0), - font_family: Some("IBM Plex Sans".to_string()), - font_scheme: Some("minor".to_string()), - typeface: Some("IBM Plex Sans".to_string()), - font_face: Some(SpreadsheetFontFace { - font_family: Some("IBM Plex Sans".to_string()), - font_scheme: Some("minor".to_string()), - typeface: Some("IBM Plex Sans".to_string()), - }), - }), - fill: Some(SpreadsheetFill { - solid_fill_color: Some("#ffeeaa".to_string()), - pattern_type: Some("solid".to_string()), - pattern_foreground_color: Some("#ffeeaa".to_string()), - pattern_background_color: Some("#221100".to_string()), - color_transforms: vec!["tint:0.2".to_string()], - gradient_fill_type: Some("linear".to_string()), - gradient_stops: vec![ - crate::SpreadsheetGradientStop { - position: 0.0, - color: "#ffeeaa".to_string(), - }, - crate::SpreadsheetGradientStop { - position: 1.0, - color: "#aa5500".to_string(), - }, - ], - gradient_kind: Some("linear".to_string()), - angle: Some(45.0), - scaled: Some(true), - path_type: Some("rect".to_string()), - fill_rectangle: Some(crate::SpreadsheetFillRectangle { - left: 0.0, - right: 1.0, - top: 0.0, - bottom: 1.0, - }), - image_reference: Some("image://fill".to_string()), - }), - border: Some(crate::SpreadsheetBorder { - top: Some(crate::SpreadsheetBorderLine { - style: Some("solid".to_string()), - color: Some("#111111".to_string()), - }), - right: Some(crate::SpreadsheetBorderLine { - style: Some("dashed".to_string()), - color: Some("#222222".to_string()), - }), - bottom: Some(crate::SpreadsheetBorderLine { - style: Some("double".to_string()), - color: Some("#333333".to_string()), - }), - left: Some(crate::SpreadsheetBorderLine { - style: Some("solid".to_string()), - color: Some("#444444".to_string()), - }), - }), - alignment: Some(crate::SpreadsheetAlignment { - horizontal: Some("center".to_string()), - vertical: Some("bottom".to_string()), - }), - number_format: Some(SpreadsheetNumberFormat { - format_id: Some(4), - format_code: Some("#,##0.00".to_string()), - }), - wrap_text: Some(true), - }) - ); - - let retrieved_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_cell_format".to_string(), - args: serde_json::json!({ "id": merged_format_id }), - }, - temp_dir.path(), - )?; - let retrieved_format: SpreadsheetCellFormat = - serde_json::from_value(retrieved_format.serialized_dict.expect("cell format"))?; - assert_eq!( - retrieved_format, - SpreadsheetCellFormat { - text_style_id: None, - fill_id: Some(fill_id), - border_id: Some(border_id), - alignment: Some(crate::SpreadsheetAlignment { - horizontal: None, - vertical: Some("bottom".to_string()), - }), - number_format_id: None, - wrap_text: Some(true), - base_cell_style_format_id: Some(base_format_id), - } - ); - - let retrieved_number_format = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_number_format".to_string(), - args: serde_json::json!({ "id": number_format_id }), - }, - temp_dir.path(), - )?; - assert_eq!( - serde_json::from_value::( - retrieved_number_format - .serialized_dict - .expect("number format") - )?, - SpreadsheetNumberFormat { - format_id: Some(4), - format_code: Some("#,##0.00".to_string()), - } - ); - - let retrieved_text_style = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_text_style".to_string(), - args: serde_json::json!({ "id": text_style_id }), - }, - temp_dir.path(), - )?; - assert_eq!( - serde_json::from_value::( - retrieved_text_style.serialized_dict.expect("text style") - )? - .bold, - Some(true) - ); - - let range_summary = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_range_format_summary".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "range": "A1:B2" - }), - }, - temp_dir.path(), - )?; - assert_eq!(range_summary.top_left_style_index, Some(merged_format_id)); - assert_eq!( - range_summary - .range_format - .as_ref() - .map(|format| format.range.clone()), - Some("A1:B2".to_string()) - ); - - let retrieved_dxf = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id), - action: "get_differential_format".to_string(), - args: serde_json::json!({ "id": differential_format_id }), - }, - temp_dir.path(), - )?; - assert_eq!( - serde_json::from_value::( - retrieved_dxf.serialized_dict.expect("differential format") - )? - .wrap_text, - Some(true) - ); - Ok(()) -} - -#[test] -fn sheet_references_resolve_cells_and_ranges() -> Result<(), Box> { - let sheet = SpreadsheetSheet::new("Sheet1".to_string()); - assert_eq!( - sheet.reference("A1")?, - SpreadsheetSheetReference::Cell { - cell_ref: sheet.cell_ref("A1")?, - } - ); - assert_eq!( - sheet.reference("A1:B2")?, - SpreadsheetSheetReference::Range { - range_ref: sheet.range_ref("A1:B2")?, - } - ); - Ok(()) -} - -#[test] -fn manager_get_reference_and_xlsx_import_preserve_workbook_name() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let path = temp_dir.path().join("named.xlsx"); - - let mut artifact = SpreadsheetArtifact::new(Some("Named Workbook".to_string())); - artifact.create_sheet("Sheet1".to_string())?.set_value( - CellAddress::parse("A1")?, - Some(SpreadsheetCellValue::Integer(9)), - )?; - artifact.export(&path)?; - - let restored = SpreadsheetArtifact::from_source_file(&path, None)?; - assert_eq!(restored.name, Some("Named Workbook".to_string())); - - let mut manager = SpreadsheetArtifactManager::default(); - let imported = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: None, - action: "read".to_string(), - args: serde_json::json!({ "path": path }), - }, - temp_dir.path(), - )?; - let artifact_id = imported.artifact_id; - - let cell_reference = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "get_reference".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "reference": "A1" - }), - }, - temp_dir.path(), - )?; - assert_eq!( - cell_reference - .raw_cell - .as_ref() - .and_then(|cell| cell.value.clone()), - Some(SpreadsheetCellValue::Integer(9)) - ); - - let range_reference = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id), - action: "get_reference".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "reference": "A1:B2" - }), - }, - temp_dir.path(), - )?; - assert_eq!( - range_reference - .range_ref - .as_ref() - .map(|range_ref| range_ref.address.clone()), - Some("A1:B2".to_string()) - ); - Ok(()) -} - -#[test] -fn manager_render_actions_support_workbook_sheet_and_range() --> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - let mut manager = SpreadsheetArtifactManager::default(); - let created = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: None, - action: "create".to_string(), - args: serde_json::json!({ "name": "Render" }), - }, - temp_dir.path(), - )?; - let artifact_id = created.artifact_id; - - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "create_sheet".to_string(), - args: serde_json::json!({ "name": "Sheet1" }), - }, - temp_dir.path(), - )?; - manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "set_range_values".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "range": "A1:C4", - "values": [ - ["h1", "h2", "h3"], - ["a", 1, 2], - ["b", 3, 4], - ["c", 5, 6] - ] - }), - }, - temp_dir.path(), - )?; - - let workbook = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "render_workbook".to_string(), - args: serde_json::json!({ - "output_path": temp_dir.path().join("workbook-previews"), - "include_headers": false - }), - }, - temp_dir.path(), - )?; - assert_eq!(workbook.exported_paths.len(), 1); - assert!(workbook.exported_paths[0].exists()); - - let sheet = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id.clone()), - action: "render_sheet".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "output_path": temp_dir.path().join("sheet-preview.html"), - "center_address": "B3", - "width": 220, - "height": 90, - "scale": 1.5, - "performance_mode": true - }), - }, - temp_dir.path(), - )?; - assert_eq!(sheet.exported_paths.len(), 1); - assert!(sheet.exported_paths[0].exists()); - assert!( - sheet - .rendered_html - .as_ref() - .is_some_and(|html| html.contains("data-performance-mode=\"true\"")) - ); - - let range = manager.execute( - SpreadsheetArtifactRequest { - artifact_id: Some(artifact_id), - action: "render_range".to_string(), - args: serde_json::json!({ - "sheet_name": "Sheet1", - "range": "A2:C4", - "output_path": temp_dir.path().join("range-preview.html"), - "include_headers": true - }), - }, - temp_dir.path(), - )?; - assert_eq!(range.exported_paths.len(), 1); - assert_eq!( - range - .range_ref - .as_ref() - .map(|range_ref| range_ref.address.clone()), - Some("A2:C4".to_string()) - ); - assert!( - range - .rendered_html - .as_ref() - .is_some_and(|html| html.contains("A")) - ); - Ok(()) -} diff --git a/codex-rs/artifact-spreadsheet/src/xlsx.rs b/codex-rs/artifact-spreadsheet/src/xlsx.rs deleted file mode 100644 index 5175f3b51..000000000 --- a/codex-rs/artifact-spreadsheet/src/xlsx.rs +++ /dev/null @@ -1,860 +0,0 @@ -use std::collections::BTreeMap; -use std::fs::File; -use std::io::Read; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; - -use regex::Regex; -use zip::ZipArchive; -use zip::ZipWriter; -use zip::write::SimpleFileOptions; - -use crate::CellAddress; -use crate::CellRange; -use crate::SpreadsheetArtifact; -use crate::SpreadsheetArtifactError; -use crate::SpreadsheetCell; -use crate::SpreadsheetCellValue; -use crate::SpreadsheetSheet; - -pub(crate) fn write_xlsx( - artifact: &mut SpreadsheetArtifact, - path: &Path, -) -> Result { - if artifact.auto_recalculate { - artifact.recalculate(); - } - for sheet in &mut artifact.sheets { - sheet.cleanup_and_validate_sheet()?; - } - - let file = File::create(path).map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - let mut zip = ZipWriter::new(file); - let options = SimpleFileOptions::default(); - - let sheet_count = artifact.sheets.len().max(1); - zip.start_file("[Content_Types].xml", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(content_types_xml(sheet_count).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.add_directory("_rels/", options).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - zip.start_file("_rels/.rels", options).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - zip.write_all(root_relationships_xml().as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.add_directory("docProps/", options).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - zip.start_file("docProps/app.xml", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(app_xml(artifact).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.start_file("docProps/core.xml", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(core_xml(artifact).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.add_directory("xl/", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.start_file("xl/workbook.xml", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(workbook_xml(artifact).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.add_directory("xl/_rels/", options).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - zip.start_file("xl/_rels/workbook.xml.rels", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(workbook_relationships_xml(artifact).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.start_file("xl/styles.xml", options).map_err(|error| { - SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - } - })?; - zip.write_all(styles_xml(artifact).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - zip.add_directory("xl/worksheets/", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - if artifact.sheets.is_empty() { - let empty = SpreadsheetSheet::new("Sheet1".to_string()); - zip.start_file("xl/worksheets/sheet1.xml", options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(sheet_xml(&empty).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - } else { - for (index, sheet) in artifact.sheets.iter().enumerate() { - zip.start_file(format!("xl/worksheets/sheet{}.xml", index + 1), options) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - zip.write_all(sheet_xml(sheet).as_bytes()) - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - } - } - - zip.finish() - .map_err(|error| SpreadsheetArtifactError::ExportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - Ok(path.to_path_buf()) -} - -pub(crate) fn import_xlsx( - path: &Path, - artifact_id: Option, -) -> Result { - let file = File::open(path).map_err(|error| SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - let mut archive = - ZipArchive::new(file).map_err(|error| SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - - let workbook_xml = read_zip_entry(&mut archive, "xl/workbook.xml", path)?; - let workbook_rels = read_zip_entry(&mut archive, "xl/_rels/workbook.xml.rels", path)?; - let workbook_name = if archive.by_name("docProps/core.xml").is_ok() { - let title = - extract_workbook_title(&read_zip_entry(&mut archive, "docProps/core.xml", path)?); - (!title.trim().is_empty()).then_some(title) - } else { - None - }; - let shared_strings = if archive.by_name("xl/sharedStrings.xml").is_ok() { - Some(parse_shared_strings(&read_zip_entry( - &mut archive, - "xl/sharedStrings.xml", - path, - )?)?) - } else { - None - }; - - let relationships = parse_relationships(&workbook_rels)?; - let sheets = parse_sheet_definitions(&workbook_xml)? - .into_iter() - .map(|(name, relation)| { - let target = relationships.get(&relation).ok_or_else(|| { - SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: format!("missing relationship `{relation}` for sheet `{name}`"), - } - })?; - let normalized = if target.starts_with('/') { - target.trim_start_matches('/').to_string() - } else if target.starts_with("xl/") { - target.clone() - } else { - format!("xl/{target}") - }; - Ok((name, normalized)) - }) - .collect::, SpreadsheetArtifactError>>()?; - - let mut artifact = SpreadsheetArtifact::new(workbook_name.or_else(|| { - path.file_stem() - .and_then(|value| value.to_str()) - .map(str::to_string) - })); - if let Some(artifact_id) = artifact_id { - artifact.artifact_id = artifact_id; - } - artifact.sheets.clear(); - - for (name, target) in sheets { - let xml = read_zip_entry(&mut archive, &target, path)?; - let sheet = parse_sheet(&name, &xml, shared_strings.as_deref())?; - artifact.sheets.push(sheet); - } - - Ok(artifact) -} - -fn read_zip_entry( - archive: &mut ZipArchive, - entry: &str, - path: &Path, -) -> Result { - let mut file = - archive - .by_name(entry) - .map_err(|error| SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - let mut text = String::new(); - file.read_to_string(&mut text) - .map_err(|error| SpreadsheetArtifactError::ImportFailed { - path: path.to_path_buf(), - message: error.to_string(), - })?; - Ok(text) -} - -fn parse_sheet_definitions( - workbook_xml: &str, -) -> Result, SpreadsheetArtifactError> { - let regex = Regex::new(r#"]*)/?>"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - let mut sheets = Vec::new(); - for captures in regex.captures_iter(workbook_xml) { - let Some(attributes) = captures.get(1).map(|value| value.as_str()) else { - continue; - }; - let Some(name) = extract_attribute(attributes, "name") else { - continue; - }; - let relation = extract_attribute(attributes, "r:id") - .or_else(|| extract_attribute(attributes, "id")) - .unwrap_or_default(); - sheets.push((xml_unescape(&name), relation)); - } - Ok(sheets) -} - -fn parse_relationships(xml: &str) -> Result, SpreadsheetArtifactError> { - let regex = Regex::new(r#"]*)/?>"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - Ok(regex - .captures_iter(xml) - .filter_map(|captures| { - let attributes = captures.get(1)?.as_str(); - let id = extract_attribute(attributes, "Id")?; - let target = extract_attribute(attributes, "Target")?; - Some((id, target)) - }) - .collect()) -} - -fn parse_shared_strings(xml: &str) -> Result, SpreadsheetArtifactError> { - let regex = Regex::new(r#"(?s)]*>(.*?)"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - regex - .captures_iter(xml) - .filter_map(|captures| captures.get(1).map(|value| value.as_str())) - .map(all_text_nodes) - .collect() -} - -fn parse_sheet( - name: &str, - xml: &str, - shared_strings: Option<&[String]>, -) -> Result { - let mut sheet = SpreadsheetSheet::new(name.to_string()); - - if let Some(sheet_view) = first_tag_attributes(xml, "sheetView") - && let Some(show_grid_lines) = extract_attribute(&sheet_view, "showGridLines") - { - sheet.show_grid_lines = show_grid_lines != "0"; - } - if let Some(format_pr) = first_tag_attributes(xml, "sheetFormatPr") { - sheet.default_row_height = extract_attribute(&format_pr, "defaultRowHeight") - .and_then(|value| value.parse::().ok()); - sheet.default_column_width = extract_attribute(&format_pr, "defaultColWidth") - .and_then(|value| value.parse::().ok()); - } - - let col_regex = Regex::new(r#"]*)/?>"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - for captures in col_regex.captures_iter(xml) { - let Some(attributes) = captures.get(1).map(|value| value.as_str()) else { - continue; - }; - let Some(min) = - extract_attribute(attributes, "min").and_then(|value| value.parse::().ok()) - else { - continue; - }; - let Some(max) = - extract_attribute(attributes, "max").and_then(|value| value.parse::().ok()) - else { - continue; - }; - let Some(width) = - extract_attribute(attributes, "width").and_then(|value| value.parse::().ok()) - else { - continue; - }; - for column in min..=max { - sheet.column_widths.insert(column, width); - } - } - - let row_regex = Regex::new(r#"(?s)]*)>(.*?)"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - let cell_regex = Regex::new(r#"(?s)]*)>(.*?)"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - for row_captures in row_regex.captures_iter(xml) { - let row_attributes = row_captures - .get(1) - .map(|value| value.as_str()) - .unwrap_or_default(); - if let Some(row_index) = - extract_attribute(row_attributes, "r").and_then(|value| value.parse::().ok()) - && let Some(height) = - extract_attribute(row_attributes, "ht").and_then(|value| value.parse::().ok()) - && row_index > 0 - && height > 0.0 - { - sheet.row_heights.insert(row_index, height); - } - let Some(row_body) = row_captures.get(2).map(|value| value.as_str()) else { - continue; - }; - for cell_captures in cell_regex.captures_iter(row_body) { - let Some(attributes) = cell_captures.get(1).map(|value| value.as_str()) else { - continue; - }; - let Some(body) = cell_captures.get(2).map(|value| value.as_str()) else { - continue; - }; - let Some(address) = extract_attribute(attributes, "r") else { - continue; - }; - let address = CellAddress::parse(&address)?; - let style_index = extract_attribute(attributes, "s") - .and_then(|value| value.parse::().ok()) - .unwrap_or(0); - let cell_type = extract_attribute(attributes, "t").unwrap_or_default(); - let formula = first_tag_text(body, "f").map(|value| format!("={value}")); - let value = parse_cell_value(body, &cell_type, shared_strings)?; - - let cell = SpreadsheetCell { - value, - formula, - style_index, - citations: Vec::new(), - }; - if !cell.is_empty() { - sheet.cells.insert(address, cell); - } - } - } - - let merge_regex = Regex::new(r#"]*)/?>"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - for captures in merge_regex.captures_iter(xml) { - let Some(attributes) = captures.get(1).map(|value| value.as_str()) else { - continue; - }; - if let Some(reference) = extract_attribute(attributes, "ref") { - sheet.merged_ranges.push(CellRange::parse(&reference)?); - } - } - - Ok(sheet) -} - -fn parse_cell_value( - body: &str, - cell_type: &str, - shared_strings: Option<&[String]>, -) -> Result, SpreadsheetArtifactError> { - let inline_text = first_tag_text(body, "t").map(|value| xml_unescape(&value)); - let raw_value = first_tag_text(body, "v").map(|value| xml_unescape(&value)); - - let parsed = match cell_type { - "inlineStr" => inline_text.map(SpreadsheetCellValue::String), - "s" => raw_value - .and_then(|value| value.parse::().ok()) - .and_then(|index| shared_strings.and_then(|entries| entries.get(index).cloned())) - .map(SpreadsheetCellValue::String), - "b" => raw_value.map(|value| SpreadsheetCellValue::Bool(value == "1")), - "str" => raw_value.map(SpreadsheetCellValue::String), - "e" => raw_value.map(SpreadsheetCellValue::Error), - _ => match raw_value { - Some(value) => { - if let Ok(integer) = value.parse::() { - Some(SpreadsheetCellValue::Integer(integer)) - } else if let Ok(float) = value.parse::() { - Some(SpreadsheetCellValue::Float(float)) - } else { - Some(SpreadsheetCellValue::String(value)) - } - } - None => None, - }, - }; - Ok(parsed) -} - -fn content_types_xml(sheet_count: usize) -> String { - let mut overrides = String::new(); - for index in 1..=sheet_count { - overrides.push_str(&format!( - r#""# - )); - } - format!( - "{}{}{}{}{}{}{}{}{}{}", - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - overrides, - r#""# - ) -} - -fn root_relationships_xml() -> &'static str { - concat!( - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - r#""# - ) -} - -fn app_xml(artifact: &SpreadsheetArtifact) -> String { - let title = artifact - .name - .clone() - .unwrap_or_else(|| "Spreadsheet".to_string()); - format!( - concat!( - r#""#, - r#""#, - r#"Codex"#, - r#"0"#, - r#"false"#, - r#"Worksheets{}"#, - r#"{}"#, - r#"OpenAI"#, - r#"{}"#, - r#""# - ), - artifact.sheets.len(), - artifact.sheets.len(), - artifact - .sheets - .iter() - .map(|sheet| format!(r#"{}"#, xml_escape(&sheet.name))) - .collect::>() - .join(""), - xml_escape(&title), - ) -} - -fn core_xml(artifact: &SpreadsheetArtifact) -> String { - let title = artifact - .name - .clone() - .unwrap_or_else(|| artifact.artifact_id.clone()); - format!( - concat!( - r#""#, - r#""#, - r#"{}"#, - r#"Codex"#, - r#"Codex"#, - r#""# - ), - xml_escape(&title), - ) -} - -fn workbook_xml(artifact: &SpreadsheetArtifact) -> String { - let sheets = if artifact.sheets.is_empty() { - r#""#.to_string() - } else { - artifact - .sheets - .iter() - .enumerate() - .map(|(index, sheet)| { - format!( - r#""#, - xml_escape(&sheet.name), - index + 1, - index + 1 - ) - }) - .collect::>() - .join("") - }; - format!( - "{}{}{}{}{}", - r#""#, - r#""#, - r#""#, - sheets, - r#""# - ) -} - -fn workbook_relationships_xml(artifact: &SpreadsheetArtifact) -> String { - let sheet_relationships = if artifact.sheets.is_empty() { - r#""#.to_string() - } else { - artifact - .sheets - .iter() - .enumerate() - .map(|(index, _)| { - format!( - r#""#, - index + 1, - index + 1 - ) - }) - .collect::>() - .join("") - }; - let style_relation_id = artifact.sheets.len().max(1) + 1; - format!( - "{}{}{}{}", - r#""#, - r#""#, - sheet_relationships, - style_relation_id, - r#""# - ) -} - -fn styles_xml(artifact: &SpreadsheetArtifact) -> String { - let max_style_index = artifact - .sheets - .iter() - .flat_map(|sheet| sheet.cells.values().map(|cell| cell.style_index)) - .max() - .unwrap_or(0); - let cell_xfs = (0..=max_style_index) - .map(|_| r#""#) - .collect::>() - .join(""); - format!( - concat!( - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - r#""#, - r#"{}"#, - r#""#, - r#""# - ), - max_style_index + 1, - cell_xfs, - ) -} - -fn sheet_xml(sheet: &SpreadsheetSheet) -> String { - let mut rows = BTreeMap::>::new(); - for row_index in sheet.row_heights.keys() { - rows.entry(*row_index).or_default(); - } - for (address, cell) in &sheet.cells { - rows.entry(address.row).or_default().push((*address, cell)); - } - - let sheet_data = rows - .into_iter() - .map(|(row_index, mut entries)| { - entries.sort_by_key(|(address, _)| address.column); - let cells = entries - .into_iter() - .map(|(address, cell)| cell_xml(address, cell)) - .collect::>() - .join(""); - let height = sheet - .row_heights - .get(&row_index) - .map(|value| format!(r#" ht="{value}" customHeight="1""#)) - .unwrap_or_default(); - format!(r#"{cells}"#) - }) - .collect::>() - .join(""); - - let cols = if sheet.column_widths.is_empty() { - String::new() - } else { - let mut groups = Vec::new(); - let mut iter = sheet.column_widths.iter().peekable(); - while let Some((&start, &width)) = iter.next() { - let mut end = start; - while let Some((next_column, next_width)) = - iter.peek().map(|(column, width)| (**column, **width)) - { - if next_column == end + 1 && (next_width - width).abs() < f64::EPSILON { - end = next_column; - iter.next(); - } else { - break; - } - } - groups.push(format!( - r#""# - )); - } - format!("{}", groups.join("")) - }; - - let merge_cells = if sheet.merged_ranges.is_empty() { - String::new() - } else { - format!( - r#"{}"#, - sheet.merged_ranges.len(), - sheet - .merged_ranges - .iter() - .map(|range| format!(r#""#, range.to_a1())) - .collect::>() - .join("") - ) - }; - - let default_row_height = sheet.default_row_height.unwrap_or(15.0); - let default_column_width = sheet.default_column_width.unwrap_or(8.43); - let grid_lines = if sheet.show_grid_lines { "1" } else { "0" }; - - format!( - "{}{}{}{}{}{}", - r#""#, - r#""#, - grid_lines, - default_row_height, - default_column_width, - cols, - sheet_data, - merge_cells, - r#""# - ) -} - -fn cell_xml(address: CellAddress, cell: &SpreadsheetCell) -> String { - let style = if cell.style_index == 0 { - String::new() - } else { - format!(r#" s="{}""#, cell.style_index) - }; - - if let Some(formula) = &cell.formula { - let formula = xml_escape(formula.trim_start_matches('=')); - let value_xml = match &cell.value { - Some(SpreadsheetCellValue::Bool(value)) => { - format!( - r#" t="b">{formula}{}"#, - usize::from(*value) - ) - } - Some(SpreadsheetCellValue::Integer(value)) => { - format!(r#">{formula}{value}"#) - } - Some(SpreadsheetCellValue::Float(value)) => { - format!(r#">{formula}{value}"#) - } - Some(SpreadsheetCellValue::String(value)) - | Some(SpreadsheetCellValue::DateTime(value)) => format!( - r#" t="str">{formula}{}"#, - xml_escape(value) - ), - Some(SpreadsheetCellValue::Error(value)) => { - format!(r#" t="e">{formula}{}"#, xml_escape(value)) - } - None => format!(r#">{formula}"#), - }; - return format!(r#" format!( - r#"{}"#, - address.to_a1(), - usize::from(*value) - ), - Some(SpreadsheetCellValue::Integer(value)) => { - format!(r#"{value}"#, address.to_a1()) - } - Some(SpreadsheetCellValue::Float(value)) => { - format!(r#"{value}"#, address.to_a1()) - } - Some(SpreadsheetCellValue::String(value)) | Some(SpreadsheetCellValue::DateTime(value)) => { - format!( - r#"{}"#, - address.to_a1(), - xml_escape(value) - ) - } - Some(SpreadsheetCellValue::Error(value)) => format!( - r#"{}"#, - address.to_a1(), - xml_escape(value) - ), - None => format!(r#""#, address.to_a1()), - } -} - -fn first_tag_attributes(xml: &str, tag: &str) -> Option { - let regex = Regex::new(&format!(r#"<{tag}\b([^>]*)/?>"#)).ok()?; - let captures = regex.captures(xml)?; - captures.get(1).map(|value| value.as_str().to_string()) -} - -fn first_tag_text(xml: &str, tag: &str) -> Option { - let regex = Regex::new(&format!(r#"(?s)<{tag}\b[^>]*>(.*?)"#)).ok()?; - let captures = regex.captures(xml)?; - captures.get(1).map(|value| value.as_str().to_string()) -} - -fn extract_workbook_title(xml: &str) -> String { - let Ok(regex) = - Regex::new(r#"(?s)<(?:[A-Za-z0-9_]+:)?title\b[^>]*>(.*?)"#) - else { - return String::new(); - }; - regex - .captures(xml) - .and_then(|captures| captures.get(1).map(|value| xml_unescape(value.as_str()))) - .unwrap_or_default() -} - -fn all_text_nodes(xml: &str) -> Result { - let regex = Regex::new(r#"(?s)]*>(.*?)"#).map_err(|error| { - SpreadsheetArtifactError::Serialization { - message: error.to_string(), - } - })?; - Ok(regex - .captures_iter(xml) - .filter_map(|captures| captures.get(1).map(|value| xml_unescape(value.as_str()))) - .collect::>() - .join("")) -} - -fn extract_attribute(attributes: &str, name: &str) -> Option { - let pattern = format!(r#"{name}="([^"]*)""#); - let regex = Regex::new(&pattern).ok()?; - let captures = regex.captures(attributes)?; - captures.get(1).map(|value| xml_unescape(value.as_str())) -} - -fn xml_escape(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} - -fn xml_unescape(value: &str) -> String { - value - .replace("'", "'") - .replace(""", "\"") - .replace(">", ">") - .replace("<", "<") - .replace("&", "&") -} diff --git a/codex-rs/artifacts/BUILD.bazel b/codex-rs/artifacts/BUILD.bazel new file mode 100644 index 000000000..300e9b0fc --- /dev/null +++ b/codex-rs/artifacts/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "artifacts", + crate_name = "codex_artifacts", +) diff --git a/codex-rs/artifacts/Cargo.toml b/codex-rs/artifacts/Cargo.toml new file mode 100644 index 000000000..fc2e4190a --- /dev/null +++ b/codex-rs/artifacts/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "codex-artifacts" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +codex-package-manager = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs", "io-util", "process", "time"] } +url = { workspace = true } + +[lints] +workspace = true + +[dev-dependencies] +pretty_assertions = { workspace = true } +sha2 = { workspace = true } +tokio = { workspace = true, features = ["fs", "io-util", "macros", "process", "rt", "rt-multi-thread", "time"] } +wiremock = { workspace = true } +zip = { workspace = true } diff --git a/codex-rs/artifacts/src/client.rs b/codex-rs/artifacts/src/client.rs new file mode 100644 index 000000000..7b2ff56da --- /dev/null +++ b/codex-rs/artifacts/src/client.rs @@ -0,0 +1,446 @@ +use crate::ArtifactRuntimeError; +use crate::ArtifactRuntimeManager; +use crate::InstalledArtifactRuntime; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use tempfile::TempDir; +use thiserror::Error; +use tokio::fs; +use tokio::io::AsyncReadExt; +use tokio::process::Command; +use tokio::time::timeout; + +const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30); + +#[derive(Clone, Debug)] +pub struct ArtifactsClient { + runtime_source: RuntimeSource, +} + +#[derive(Clone, Debug)] +enum RuntimeSource { + Managed(ArtifactRuntimeManager), + Installed(InstalledArtifactRuntime), +} + +impl ArtifactsClient { + pub fn from_runtime_manager(runtime_manager: ArtifactRuntimeManager) -> Self { + Self { + runtime_source: RuntimeSource::Managed(runtime_manager), + } + } + + pub fn from_installed_runtime(runtime: InstalledArtifactRuntime) -> Self { + Self { + runtime_source: RuntimeSource::Installed(runtime), + } + } + + pub async fn execute_build( + &self, + request: ArtifactBuildRequest, + ) -> Result { + let runtime = self.resolve_runtime().await?; + let staging_dir = TempDir::new().map_err(|source| ArtifactsError::Io { + context: "failed to create build staging directory".to_string(), + source, + })?; + let script_path = staging_dir.path().join("artifact-build.mjs"); + let wrapped_script = build_wrapped_script(&request.source); + fs::write(&script_path, wrapped_script) + .await + .map_err(|source| ArtifactsError::Io { + context: format!("failed to write {}", script_path.display()), + source, + })?; + + let mut command = Command::new(runtime.node_path()); + command + .arg(&script_path) + .current_dir(&request.cwd) + .env("CODEX_ARTIFACT_BUILD_ENTRYPOINT", runtime.build_js_path()) + .env( + "CODEX_ARTIFACT_RENDER_ENTRYPOINT", + runtime.render_cli_path(), + ) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + for (key, value) in &request.env { + command.env(key, value); + } + + run_command( + command, + request.timeout.unwrap_or(DEFAULT_EXECUTION_TIMEOUT), + ) + .await + } + + pub async fn execute_render( + &self, + request: ArtifactRenderCommandRequest, + ) -> Result { + let runtime = self.resolve_runtime().await?; + let mut command = Command::new(runtime.node_path()); + command + .arg(runtime.render_cli_path()) + .args(request.target.to_args()) + .current_dir(&request.cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + for (key, value) in &request.env { + command.env(key, value); + } + + run_command( + command, + request.timeout.unwrap_or(DEFAULT_EXECUTION_TIMEOUT), + ) + .await + } + + async fn resolve_runtime(&self) -> Result { + match &self.runtime_source { + RuntimeSource::Installed(runtime) => Ok(runtime.clone()), + RuntimeSource::Managed(manager) => manager.ensure_installed().await.map_err(Into::into), + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ArtifactBuildRequest { + pub source: String, + pub cwd: PathBuf, + pub timeout: Option, + pub env: BTreeMap, +} + +#[derive(Clone, Debug)] +pub struct ArtifactRenderCommandRequest { + pub cwd: PathBuf, + pub timeout: Option, + pub env: BTreeMap, + pub target: ArtifactRenderTarget, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ArtifactRenderTarget { + Presentation(PresentationRenderTarget), + Spreadsheet(SpreadsheetRenderTarget), +} + +impl ArtifactRenderTarget { + pub fn to_args(&self) -> Vec { + match self { + Self::Presentation(target) => { + vec![ + "pptx".to_string(), + "render".to_string(), + "--in".to_string(), + target.input_path.display().to_string(), + "--slide".to_string(), + target.slide_number.to_string(), + "--out".to_string(), + target.output_path.display().to_string(), + ] + } + Self::Spreadsheet(target) => { + let mut args = vec![ + "xlsx".to_string(), + "render".to_string(), + "--in".to_string(), + target.input_path.display().to_string(), + "--sheet".to_string(), + target.sheet_name.clone(), + "--out".to_string(), + target.output_path.display().to_string(), + ]; + if let Some(range) = &target.range { + args.push("--range".to_string()); + args.push(range.clone()); + } + args + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PresentationRenderTarget { + pub input_path: PathBuf, + pub output_path: PathBuf, + pub slide_number: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SpreadsheetRenderTarget { + pub input_path: PathBuf, + pub output_path: PathBuf, + pub sheet_name: String, + pub range: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtifactCommandOutput { + pub exit_code: Option, + pub stdout: String, + pub stderr: String, +} + +impl ArtifactCommandOutput { + pub fn success(&self) -> bool { + self.exit_code == Some(0) + } +} + +#[derive(Debug, Error)] +pub enum ArtifactsError { + #[error(transparent)] + Runtime(#[from] ArtifactRuntimeError), + #[error("{context}")] + Io { + context: String, + #[source] + source: std::io::Error, + }, + #[error("artifact command timed out after {timeout:?}")] + TimedOut { timeout: Duration }, +} + +fn build_wrapped_script(source: &str) -> String { + format!( + concat!( + "import {{ pathToFileURL }} from \"node:url\";\n", + "const artifactTool = await import(pathToFileURL(process.env.CODEX_ARTIFACT_BUILD_ENTRYPOINT).href);\n", + "globalThis.artifactTool = artifactTool;\n", + "globalThis.artifacts = artifactTool;\n", + "globalThis.codexArtifacts = artifactTool;\n", + "for (const [name, value] of Object.entries(artifactTool)) {{\n", + " if (name === \"default\" || Object.prototype.hasOwnProperty.call(globalThis, name)) {{\n", + " continue;\n", + " }}\n", + " globalThis[name] = value;\n", + "}}\n\n", + "{}\n" + ), + source + ) +} + +async fn run_command( + mut command: Command, + execution_timeout: Duration, +) -> Result { + let mut child = command.spawn().map_err(|source| ArtifactsError::Io { + context: "failed to spawn artifact command".to_string(), + source, + })?; + let mut stdout = child.stdout.take().ok_or_else(|| ArtifactsError::Io { + context: "artifact command stdout was not captured".to_string(), + source: std::io::Error::other("missing stdout pipe"), + })?; + let mut stderr = child.stderr.take().ok_or_else(|| ArtifactsError::Io { + context: "artifact command stderr was not captured".to_string(), + source: std::io::Error::other("missing stderr pipe"), + })?; + let stdout_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + stdout.read_to_end(&mut bytes).await.map(|_| bytes) + }); + let stderr_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + stderr.read_to_end(&mut bytes).await.map(|_| bytes) + }); + + let status = match timeout(execution_timeout, child.wait()).await { + Ok(result) => result.map_err(|source| ArtifactsError::Io { + context: "failed while waiting for artifact command".to_string(), + source, + })?, + Err(_) => { + let _ = child.kill().await; + let _ = child.wait().await; + return Err(ArtifactsError::TimedOut { + timeout: execution_timeout, + }); + } + }; + let stdout_bytes = stdout_task + .await + .map_err(|source| ArtifactsError::Io { + context: "failed to join stdout reader".to_string(), + source: std::io::Error::other(source.to_string()), + })? + .map_err(|source| ArtifactsError::Io { + context: "failed to read artifact command stdout".to_string(), + source, + })?; + let stderr_bytes = stderr_task + .await + .map_err(|source| ArtifactsError::Io { + context: "failed to join stderr reader".to_string(), + source: std::io::Error::other(source.to_string()), + })? + .map_err(|source| ArtifactsError::Io { + context: "failed to read artifact command stderr".to_string(), + source, + })?; + + Ok(ArtifactCommandOutput { + exit_code: status.code(), + stdout: String::from_utf8_lossy(&stdout_bytes).into_owned(), + stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(unix)] + use crate::ArtifactRuntimePlatform; + #[cfg(unix)] + use crate::ExtractedRuntimeManifest; + #[cfg(unix)] + use crate::RuntimeEntrypoints; + #[cfg(unix)] + use crate::RuntimePathEntry; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + + #[test] + fn wrapped_build_script_exposes_artifact_tool_surface() { + let wrapped = build_wrapped_script("console.log(Object.keys(artifactTool).length);"); + assert!(wrapped.contains("const artifactTool = await import(")); + assert!(wrapped.contains("globalThis.artifactTool = artifactTool;")); + assert!(wrapped.contains("globalThis.artifacts = artifactTool;")); + assert!(wrapped.contains("globalThis.codexArtifacts = artifactTool;")); + assert!(wrapped.contains("Object.entries(artifactTool)")); + assert!(wrapped.contains("globalThis[name] = value;")); + } + + #[test] + fn presentation_render_target_builds_expected_args() { + let args = ArtifactRenderTarget::Presentation(PresentationRenderTarget { + input_path: PathBuf::from("deck.pptx"), + output_path: PathBuf::from("slide.png"), + slide_number: 2, + }) + .to_args(); + + assert_eq!( + args, + vec![ + "pptx", + "render", + "--in", + "deck.pptx", + "--slide", + "2", + "--out", + "slide.png" + ] + ); + } + + #[test] + fn spreadsheet_render_target_builds_expected_args() { + let args = ArtifactRenderTarget::Spreadsheet(SpreadsheetRenderTarget { + input_path: PathBuf::from("book.xlsx"), + output_path: PathBuf::from("sheet.png"), + sheet_name: "Summary".to_string(), + range: Some("A1:C3".to_string()), + }) + .to_args(); + + assert_eq!( + args, + vec![ + "xlsx", + "render", + "--in", + "book.xlsx", + "--sheet", + "Summary", + "--out", + "sheet.png", + "--range", + "A1:C3" + ] + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn execute_build_invokes_runtime_node_with_expected_environment() { + let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); + let cwd = temp.path().join("cwd"); + fs::create_dir_all(&cwd) + .await + .unwrap_or_else(|error| panic!("{error}")); + let log_path = temp.path().join("build.log"); + let fake_node = temp.path().join("fake-node.sh"); + let build_entrypoint = temp.path().join("artifact_tool.mjs"); + let render_entrypoint = temp.path().join("render_cli.mjs"); + fs::write( + &fake_node, + format!( + "#!/bin/sh\nprintf '%s\\n' \"$1\" > \"{}\"\nprintf '%s\\n' \"$CODEX_ARTIFACT_BUILD_ENTRYPOINT\" >> \"{}\"\n", + log_path.display(), + log_path.display() + ), + ) + .await + .unwrap_or_else(|error| panic!("{error}")); + std::fs::set_permissions(&fake_node, std::fs::Permissions::from_mode(0o755)) + .unwrap_or_else(|error| panic!("{error}")); + + let runtime = InstalledArtifactRuntime::new( + temp.path().join("runtime"), + "0.1.0".to_string(), + ArtifactRuntimePlatform::LinuxX64, + sample_manifest("0.1.0"), + fake_node.clone(), + build_entrypoint.clone(), + render_entrypoint, + ); + let client = ArtifactsClient::from_installed_runtime(runtime); + + let output = client + .execute_build(ArtifactBuildRequest { + source: "console.log('hello');".to_string(), + cwd: cwd.clone(), + timeout: Some(Duration::from_secs(5)), + env: BTreeMap::new(), + }) + .await + .unwrap_or_else(|error| panic!("{error}")); + + assert!(output.success()); + let logged = fs::read_to_string(&log_path) + .await + .unwrap_or_else(|error| panic!("{error}")); + assert!(logged.contains("artifact-build.mjs")); + assert!(logged.contains(&build_entrypoint.display().to_string())); + } + + #[cfg(unix)] + fn sample_manifest(runtime_version: &str) -> ExtractedRuntimeManifest { + ExtractedRuntimeManifest { + schema_version: 1, + runtime_version: runtime_version.to_string(), + node: RuntimePathEntry { + relative_path: "node/bin/node".to_string(), + }, + entrypoints: RuntimeEntrypoints { + build_js: RuntimePathEntry { + relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(), + }, + render_cli: RuntimePathEntry { + relative_path: "granola-render/dist/cli.mjs".to_string(), + }, + }, + } + } +} diff --git a/codex-rs/artifacts/src/lib.rs b/codex-rs/artifacts/src/lib.rs new file mode 100644 index 000000000..f17805217 --- /dev/null +++ b/codex-rs/artifacts/src/lib.rs @@ -0,0 +1,23 @@ +mod client; +mod runtime; + +pub use client::ArtifactBuildRequest; +pub use client::ArtifactCommandOutput; +pub use client::ArtifactRenderCommandRequest; +pub use client::ArtifactRenderTarget; +pub use client::ArtifactsClient; +pub use client::ArtifactsError; +pub use client::PresentationRenderTarget; +pub use client::SpreadsheetRenderTarget; +pub use runtime::ArtifactRuntimeError; +pub use runtime::ArtifactRuntimeManager; +pub use runtime::ArtifactRuntimeManagerConfig; +pub use runtime::ArtifactRuntimePlatform; +pub use runtime::ArtifactRuntimeReleaseLocator; +pub use runtime::DEFAULT_CACHE_ROOT_RELATIVE; +pub use runtime::DEFAULT_RELEASE_TAG_PREFIX; +pub use runtime::ExtractedRuntimeManifest; +pub use runtime::InstalledArtifactRuntime; +pub use runtime::ReleaseManifest; +pub use runtime::RuntimeEntrypoints; +pub use runtime::RuntimePathEntry; diff --git a/codex-rs/artifacts/src/runtime.rs b/codex-rs/artifacts/src/runtime.rs new file mode 100644 index 000000000..2ed6b6e11 --- /dev/null +++ b/codex-rs/artifacts/src/runtime.rs @@ -0,0 +1,522 @@ +use codex_package_manager::ManagedPackage; +use codex_package_manager::PackageManager; +use codex_package_manager::PackageManagerConfig; +use codex_package_manager::PackageManagerError; +pub use codex_package_manager::PackagePlatform as ArtifactRuntimePlatform; +use codex_package_manager::PackageReleaseArchive; +use reqwest::Client; +use serde::Deserialize; +use serde::Serialize; +use std::collections::BTreeMap; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use thiserror::Error; +use url::Url; + +pub const DEFAULT_RELEASE_TAG_PREFIX: &str = "artifact-runtime-v"; +pub const DEFAULT_CACHE_ROOT_RELATIVE: &str = "packages/artifacts"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtifactRuntimeReleaseLocator { + base_url: Url, + runtime_version: String, + release_tag_prefix: String, +} + +impl ArtifactRuntimeReleaseLocator { + pub fn new(base_url: Url, runtime_version: impl Into) -> Self { + Self { + base_url, + runtime_version: runtime_version.into(), + release_tag_prefix: DEFAULT_RELEASE_TAG_PREFIX.to_string(), + } + } + + pub fn with_tag_prefix(mut self, release_tag_prefix: impl Into) -> Self { + self.release_tag_prefix = release_tag_prefix.into(); + self + } + + pub fn base_url(&self) -> &Url { + &self.base_url + } + + pub fn runtime_version(&self) -> &str { + &self.runtime_version + } + + pub fn release_tag(&self) -> String { + format!("{}{}", self.release_tag_prefix, self.runtime_version) + } + + pub fn manifest_file_name(&self) -> String { + format!("{}-manifest.json", self.release_tag()) + } + + pub fn manifest_url(&self) -> Result { + self.base_url + .join(&self.manifest_file_name()) + .map_err(PackageManagerError::InvalidBaseUrl) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtifactRuntimeManagerConfig { + package_manager: PackageManagerConfig, +} + +impl ArtifactRuntimeManagerConfig { + pub fn new(codex_home: PathBuf, release: ArtifactRuntimeReleaseLocator) -> Self { + Self { + package_manager: PackageManagerConfig::new( + codex_home, + ArtifactRuntimePackage::new(release), + ), + } + } + + pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self { + self.package_manager = self.package_manager.with_cache_root(cache_root); + self + } + + pub fn cache_root(&self) -> PathBuf { + self.package_manager.cache_root() + } + + pub fn release(&self) -> &ArtifactRuntimeReleaseLocator { + &self.package_manager.package().release + } + + pub fn codex_home(&self) -> &Path { + self.package_manager.codex_home() + } +} + +#[derive(Clone, Debug)] +pub struct ArtifactRuntimeManager { + package_manager: PackageManager, + config: ArtifactRuntimeManagerConfig, +} + +impl ArtifactRuntimeManager { + pub fn new(config: ArtifactRuntimeManagerConfig) -> Self { + let package_manager = PackageManager::new(config.package_manager.clone()); + Self { + package_manager, + config, + } + } + + pub fn with_client(config: ArtifactRuntimeManagerConfig, client: Client) -> Self { + let package_manager = PackageManager::with_client(config.package_manager.clone(), client); + Self { + package_manager, + config, + } + } + + pub fn config(&self) -> &ArtifactRuntimeManagerConfig { + &self.config + } + + pub async fn resolve_cached( + &self, + ) -> Result, ArtifactRuntimeError> { + self.package_manager.resolve_cached().await + } + + pub async fn ensure_installed(&self) -> Result { + self.package_manager.ensure_installed().await + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ArtifactRuntimePackage { + release: ArtifactRuntimeReleaseLocator, +} + +impl ArtifactRuntimePackage { + fn new(release: ArtifactRuntimeReleaseLocator) -> Self { + Self { release } + } +} + +impl ManagedPackage for ArtifactRuntimePackage { + type Error = ArtifactRuntimeError; + type Installed = InstalledArtifactRuntime; + type ReleaseManifest = ReleaseManifest; + + fn default_cache_root_relative(&self) -> &str { + DEFAULT_CACHE_ROOT_RELATIVE + } + + fn version(&self) -> &str { + self.release.runtime_version() + } + + fn manifest_url(&self) -> Result { + self.release.manifest_url() + } + + fn archive_url(&self, archive: &PackageReleaseArchive) -> Result { + self.release + .base_url() + .join(&archive.archive) + .map_err(PackageManagerError::InvalidBaseUrl) + } + + fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str { + &manifest.runtime_version + } + + fn platform_archive( + &self, + manifest: &Self::ReleaseManifest, + platform: ArtifactRuntimePlatform, + ) -> Result { + manifest + .platforms + .get(platform.as_str()) + .cloned() + .ok_or_else(|| { + PackageManagerError::MissingPlatform(platform.as_str().to_string()).into() + }) + } + + fn install_dir(&self, cache_root: &Path, platform: ArtifactRuntimePlatform) -> PathBuf { + cache_root.join(self.version()).join(platform.as_str()) + } + + fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str { + package.runtime_version() + } + + fn load_installed( + &self, + root_dir: PathBuf, + platform: ArtifactRuntimePlatform, + ) -> Result { + InstalledArtifactRuntime::load(root_dir, platform) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct ReleaseManifest { + pub schema_version: u32, + pub runtime_version: String, + pub release_tag: String, + #[serde(default)] + pub node_version: Option, + pub platforms: BTreeMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct ExtractedRuntimeManifest { + pub schema_version: u32, + pub runtime_version: String, + pub node: RuntimePathEntry, + pub entrypoints: RuntimeEntrypoints, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct RuntimePathEntry { + pub relative_path: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct RuntimeEntrypoints { + pub build_js: RuntimePathEntry, + pub render_cli: RuntimePathEntry, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InstalledArtifactRuntime { + root_dir: PathBuf, + runtime_version: String, + platform: ArtifactRuntimePlatform, + manifest: ExtractedRuntimeManifest, + node_path: PathBuf, + build_js_path: PathBuf, + render_cli_path: PathBuf, +} + +impl InstalledArtifactRuntime { + pub fn new( + root_dir: PathBuf, + runtime_version: String, + platform: ArtifactRuntimePlatform, + manifest: ExtractedRuntimeManifest, + node_path: PathBuf, + build_js_path: PathBuf, + render_cli_path: PathBuf, + ) -> Self { + Self { + root_dir, + runtime_version, + platform, + manifest, + node_path, + build_js_path, + render_cli_path, + } + } + + pub fn load( + root_dir: PathBuf, + platform: ArtifactRuntimePlatform, + ) -> Result { + let manifest_path = root_dir.join("manifest.json"); + let manifest_bytes = + std::fs::read(&manifest_path).map_err(|source| ArtifactRuntimeError::Io { + context: format!("failed to read {}", manifest_path.display()), + source, + })?; + let manifest = serde_json::from_slice::(&manifest_bytes) + .map_err(|source| ArtifactRuntimeError::InvalidManifest { + path: manifest_path, + source, + })?; + let node_path = resolve_relative_runtime_path(&root_dir, &manifest.node.relative_path)?; + let build_js_path = + resolve_relative_runtime_path(&root_dir, &manifest.entrypoints.build_js.relative_path)?; + let render_cli_path = resolve_relative_runtime_path( + &root_dir, + &manifest.entrypoints.render_cli.relative_path, + )?; + + Ok(Self::new( + root_dir, + manifest.runtime_version.clone(), + platform, + manifest, + node_path, + build_js_path, + render_cli_path, + )) + } + + pub fn root_dir(&self) -> &Path { + &self.root_dir + } + + pub fn runtime_version(&self) -> &str { + &self.runtime_version + } + + pub fn platform(&self) -> ArtifactRuntimePlatform { + self.platform + } + + pub fn manifest(&self) -> &ExtractedRuntimeManifest { + &self.manifest + } + + pub fn node_path(&self) -> &Path { + &self.node_path + } + + pub fn build_js_path(&self) -> &Path { + &self.build_js_path + } + + pub fn render_cli_path(&self) -> &Path { + &self.render_cli_path + } +} + +#[derive(Debug, Error)] +pub enum ArtifactRuntimeError { + #[error(transparent)] + PackageManager(#[from] PackageManagerError), + #[error("{context}")] + Io { + context: String, + #[source] + source: std::io::Error, + }, + #[error("invalid manifest at {path}")] + InvalidManifest { + path: PathBuf, + #[source] + source: serde_json::Error, + }, + #[error("runtime path `{0}` is invalid")] + InvalidRuntimePath(String), +} + +fn resolve_relative_runtime_path( + root_dir: &Path, + relative_path: &str, +) -> Result { + let relative = Path::new(relative_path); + if relative.as_os_str().is_empty() || relative.is_absolute() { + return Err(ArtifactRuntimeError::InvalidRuntimePath( + relative_path.to_string(), + )); + } + if relative.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::Prefix(_) | Component::RootDir + ) + }) { + return Err(ArtifactRuntimeError::InvalidRuntimePath( + relative_path.to_string(), + )); + } + Ok(root_dir.join(relative)) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use sha2::Digest; + use sha2::Sha256; + use std::io::Cursor; + use std::io::Write; + use tempfile::TempDir; + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + use zip::ZipWriter; + use zip::write::SimpleFileOptions; + + #[test] + fn release_locator_builds_manifest_url() { + let locator = ArtifactRuntimeReleaseLocator::new( + Url::parse("https://example.test/releases/").unwrap_or_else(|error| panic!("{error}")), + "0.1.0", + ); + let url = locator + .manifest_url() + .unwrap_or_else(|error| panic!("{error}")); + assert_eq!( + url.as_str(), + "https://example.test/releases/artifact-runtime-v0.1.0-manifest.json" + ); + } + + #[tokio::test] + async fn ensure_installed_downloads_and_extracts_zip_runtime() { + let server = MockServer::start().await; + let runtime_version = "0.1.0"; + let platform = + ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); + let archive_name = format!( + "artifact-runtime-v{runtime_version}-{}.zip", + platform.as_str() + ); + let archive_bytes = build_zip_archive(runtime_version); + let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); + let manifest = ReleaseManifest { + schema_version: 1, + runtime_version: runtime_version.to_string(), + release_tag: format!("artifact-runtime-v{runtime_version}"), + node_version: Some("22.0.0".to_string()), + platforms: BTreeMap::from([( + platform.as_str().to_string(), + PackageReleaseArchive { + archive: archive_name.clone(), + sha256: archive_sha, + format: codex_package_manager::ArchiveFormat::Zip, + size_bytes: Some(archive_bytes.len() as u64), + }, + )]), + }; + Mock::given(method("GET")) + .and(path(format!( + "/artifact-runtime-v{runtime_version}-manifest.json" + ))) + .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path(format!("/{archive_name}"))) + .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) + .mount(&server) + .await; + + let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); + let locator = ArtifactRuntimeReleaseLocator::new( + Url::parse(&format!("{}/", server.uri())).unwrap_or_else(|error| panic!("{error}")), + runtime_version, + ); + let manager = ArtifactRuntimeManager::new(ArtifactRuntimeManagerConfig::new( + codex_home.path().to_path_buf(), + locator, + )); + + let runtime = manager + .ensure_installed() + .await + .unwrap_or_else(|error| panic!("{error}")); + + assert_eq!(runtime.runtime_version(), runtime_version); + assert_eq!(runtime.platform(), platform); + assert!(runtime.node_path().ends_with(Path::new("node/bin/node"))); + assert!( + runtime + .build_js_path() + .ends_with(Path::new("artifact-tool/dist/artifact_tool.mjs")) + ); + assert!( + runtime + .render_cli_path() + .ends_with(Path::new("granola-render/dist/cli.mjs")) + ); + } + + fn build_zip_archive(runtime_version: &str) -> Vec { + let mut bytes = Cursor::new(Vec::new()); + { + let mut zip = ZipWriter::new(&mut bytes); + let options = SimpleFileOptions::default(); + let manifest = serde_json::to_vec(&sample_extracted_manifest(runtime_version)) + .unwrap_or_else(|error| panic!("{error}")); + zip.start_file("artifact-runtime/manifest.json", options) + .unwrap_or_else(|error| panic!("{error}")); + zip.write_all(&manifest) + .unwrap_or_else(|error| panic!("{error}")); + zip.start_file("artifact-runtime/node/bin/node", options) + .unwrap_or_else(|error| panic!("{error}")); + zip.write_all(b"#!/bin/sh\n") + .unwrap_or_else(|error| panic!("{error}")); + zip.start_file( + "artifact-runtime/artifact-tool/dist/artifact_tool.mjs", + options, + ) + .unwrap_or_else(|error| panic!("{error}")); + zip.write_all(b"export const ok = true;\n") + .unwrap_or_else(|error| panic!("{error}")); + zip.start_file("artifact-runtime/granola-render/dist/cli.mjs", options) + .unwrap_or_else(|error| panic!("{error}")); + zip.write_all(b"export const ok = true;\n") + .unwrap_or_else(|error| panic!("{error}")); + zip.finish().unwrap_or_else(|error| panic!("{error}")); + } + bytes.into_inner() + } + + fn sample_extracted_manifest(runtime_version: &str) -> ExtractedRuntimeManifest { + ExtractedRuntimeManifest { + schema_version: 1, + runtime_version: runtime_version.to_string(), + node: RuntimePathEntry { + relative_path: "node/bin/node".to_string(), + }, + entrypoints: RuntimeEntrypoints { + build_js: RuntimePathEntry { + relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(), + }, + render_cli: RuntimePathEntry { + relative_path: "granola-render/dist/cli.mjs".to_string(), + }, + }, + } + } +} diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index ebb01edc4..e40658652 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -42,8 +42,7 @@ codex-hooks = { workspace = true } codex-keyring-store = { workspace = true } codex-network-proxy = { workspace = true } codex-otel = { workspace = true } -codex-artifact-presentation = { workspace = true } -codex-artifact-spreadsheet = { workspace = true } +codex-artifacts = { workspace = true } codex-protocol = { workspace = true } codex-rmcp-client = { workspace = true } codex-state = { workspace = true } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 1e1d96d4a..5ba2d7f63 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -55,12 +55,6 @@ use async_channel::Receiver; use async_channel::Sender; use chrono::Local; use chrono::Utc; -use codex_artifact_presentation::PresentationArtifactError; -use codex_artifact_presentation::PresentationArtifactExecutionRequest; -use codex_artifact_presentation::PresentationArtifactResponse; -use codex_artifact_spreadsheet::SpreadsheetArtifactError; -use codex_artifact_spreadsheet::SpreadsheetArtifactRequest; -use codex_artifact_spreadsheet::SpreadsheetArtifactResponse; use codex_hooks::HookEvent; use codex_hooks::HookEventAfterAgent; use codex_hooks::HookPayload; @@ -1798,24 +1792,6 @@ impl Session { state.clear_connector_selection(); } - pub(crate) async fn execute_presentation_artifact( - &self, - request: PresentationArtifactExecutionRequest, - cwd: &Path, - ) -> Result { - let mut state = self.state.lock().await; - state.artifacts.presentation.execute_requests(request, cwd) - } - - pub(crate) async fn execute_spreadsheet_artifact( - &self, - request: SpreadsheetArtifactRequest, - cwd: &Path, - ) -> Result { - let mut state = self.state.lock().await; - state.artifacts.spreadsheet.execute(request, cwd) - } - async fn record_initial_history(&self, conversation_history: InitialHistory) { let turn_context = self.new_default_turn().await; self.clear_mcp_tool_selection().await; diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index babfb044d..576db7cf3 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -1,7 +1,5 @@ //! Session-wide mutable state. -use codex_artifact_presentation::PresentationArtifactManager; -use codex_artifact_spreadsheet::SpreadsheetArtifactManager; use codex_protocol::models::ResponseItem; use std::collections::HashMap; use std::collections::HashSet; @@ -18,12 +16,6 @@ use crate::tasks::RegularTask; use crate::truncate::TruncationPolicy; use codex_protocol::protocol::TurnContextItem; -#[derive(Default)] -pub(crate) struct SessionArtifacts { - pub(crate) presentation: PresentationArtifactManager, - pub(crate) spreadsheet: SpreadsheetArtifactManager, -} - /// Persistent, session-scoped state previously stored directly on `Session`. pub(crate) struct SessionState { pub(crate) session_configuration: SessionConfiguration, @@ -40,7 +32,6 @@ pub(crate) struct SessionState { pub(crate) startup_regular_task: Option>>, pub(crate) active_mcp_tool_selection: Option>, pub(crate) active_connector_selection: HashSet, - pub(crate) artifacts: SessionArtifacts, } impl SessionState { @@ -58,7 +49,6 @@ impl SessionState { startup_regular_task: None, active_mcp_tool_selection: None, active_connector_selection: HashSet::new(), - artifacts: SessionArtifacts::default(), } } diff --git a/codex-rs/core/src/tools/handlers/artifacts.rs b/codex-rs/core/src/tools/handlers/artifacts.rs new file mode 100644 index 000000000..156a2b464 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/artifacts.rs @@ -0,0 +1,412 @@ +use async_trait::async_trait; +use codex_artifacts::ArtifactBuildRequest; +use codex_artifacts::ArtifactCommandOutput; +use codex_artifacts::ArtifactRuntimeError; +use codex_artifacts::ArtifactRuntimePlatform; +use codex_artifacts::ArtifactsClient; +use codex_artifacts::ArtifactsError; +use codex_artifacts::InstalledArtifactRuntime; +use serde_json::Value as JsonValue; +use std::path::Path; +use std::time::Duration; +use std::time::Instant; + +use crate::codex::Session; +use crate::codex::TurnContext; +use crate::exec::ExecToolCallOutput; +use crate::exec::StreamOutput; +use crate::features::Feature; +use crate::function_tool::FunctionCallError; +use crate::protocol::ExecCommandSource; +use crate::tools::context::ToolInvocation; +use crate::tools::context::ToolOutput; +use crate::tools::context::ToolPayload; +use crate::tools::events::ToolEmitter; +use crate::tools::events::ToolEventCtx; +use crate::tools::events::ToolEventFailure; +use crate::tools::events::ToolEventStage; +use crate::tools::registry::ToolHandler; +use crate::tools::registry::ToolKind; +use codex_protocol::models::FunctionCallOutputBody; + +const ARTIFACTS_TOOL_NAME: &str = "artifacts"; +const ARTIFACTS_PRAGMA_PREFIXES: [&str; 2] = ["// codex-artifacts:", "// codex-artifact-tool:"]; +const PINNED_ARTIFACT_RUNTIME_VERSION: &str = "2.4.0"; +const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30); + +pub struct ArtifactsHandler; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ArtifactsToolArgs { + source: String, + timeout_ms: Option, +} + +#[async_trait] +impl ToolHandler for ArtifactsHandler { + fn kind(&self) -> ToolKind { + ToolKind::Function + } + + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Custom { .. }) + } + + async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool { + true + } + + async fn handle(&self, invocation: ToolInvocation) -> Result { + let ToolInvocation { + session, + turn, + payload, + call_id, + .. + } = invocation; + + if !session.enabled(Feature::Artifact) { + return Err(FunctionCallError::RespondToModel( + "artifacts is disabled by feature flag".to_string(), + )); + } + + let args = match payload { + ToolPayload::Custom { input } => parse_freeform_args(&input)?, + _ => { + return Err(FunctionCallError::RespondToModel( + "artifacts expects freeform JavaScript input authored against the preloaded @oai/artifact-tool surface".to_string(), + )); + } + }; + + let runtime = resolve_preinstalled_runtime(&turn.config.codex_home) + .await + .map_err(artifacts_error)?; + let client = ArtifactsClient::from_installed_runtime(runtime); + + let started_at = Instant::now(); + emit_exec_begin(session.as_ref(), turn.as_ref(), &call_id).await; + + let result = client + .execute_build(ArtifactBuildRequest { + source: args.source, + cwd: turn.cwd.clone(), + timeout: Some(Duration::from_millis( + args.timeout_ms + .unwrap_or(DEFAULT_EXECUTION_TIMEOUT.as_millis() as u64), + )), + env: Default::default(), + }) + .await; + + let (success, output) = match result { + Ok(output) => (output.success(), output), + Err(error) => (false, error_output(&error)), + }; + + emit_exec_end( + session.as_ref(), + turn.as_ref(), + &call_id, + &output, + started_at.elapsed(), + success, + ) + .await; + + Ok(ToolOutput::Function { + body: FunctionCallOutputBody::Text(format_artifact_output(&output)), + success: Some(success), + }) + } +} + +async fn resolve_preinstalled_runtime( + codex_home: &Path, +) -> Result { + let platform = ArtifactRuntimePlatform::detect_current() + .map_err(ArtifactRuntimeError::from) + .map_err(ArtifactsError::Runtime)?; + let install_dir = codex_home + .join("packages") + .join("artifacts") + .join(PINNED_ARTIFACT_RUNTIME_VERSION) + .join(platform.as_str()); + if !install_dir.exists() { + return Err(ArtifactsError::Io { + context: format!( + "artifact runtime {} is not installed at {}", + PINNED_ARTIFACT_RUNTIME_VERSION, + install_dir.display() + ), + source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing artifact runtime"), + }); + } + + InstalledArtifactRuntime::load(install_dir, platform).map_err(ArtifactsError::Runtime) +} + +fn parse_freeform_args(input: &str) -> Result { + if input.trim().is_empty() { + return Err(FunctionCallError::RespondToModel( + "artifacts expects raw JavaScript source text (non-empty) authored against the preloaded @oai/artifact-tool surface. Provide JS only, optionally with first-line `// codex-artifacts: timeout_ms=15000` or `// codex-artifact-tool: timeout_ms=15000`." + .to_string(), + )); + } + + let mut args = ArtifactsToolArgs { + source: input.to_string(), + timeout_ms: None, + }; + + let mut lines = input.splitn(2, '\n'); + let first_line = lines.next().unwrap_or_default(); + let rest = lines.next().unwrap_or_default(); + let trimmed = first_line.trim_start(); + let Some(pragma) = parse_pragma_prefix(trimmed) else { + reject_json_or_quoted_source(&args.source)?; + return Ok(args); + }; + + let mut timeout_ms = None; + let directive = pragma.trim(); + if !directive.is_empty() { + for token in directive.split_whitespace() { + let (key, value) = token.split_once('=').ok_or_else(|| { + FunctionCallError::RespondToModel(format!( + "artifacts pragma expects space-separated key=value pairs (supported keys: timeout_ms); got `{token}`" + )) + })?; + match key { + "timeout_ms" => { + if timeout_ms.is_some() { + return Err(FunctionCallError::RespondToModel( + "artifacts pragma specifies timeout_ms more than once".to_string(), + )); + } + let parsed = value.parse::().map_err(|_| { + FunctionCallError::RespondToModel(format!( + "artifacts pragma timeout_ms must be an integer; got `{value}`" + )) + })?; + timeout_ms = Some(parsed); + } + _ => { + return Err(FunctionCallError::RespondToModel(format!( + "artifacts pragma only supports timeout_ms; got `{key}`" + ))); + } + } + } + } + + if rest.trim().is_empty() { + return Err(FunctionCallError::RespondToModel( + "artifacts pragma must be followed by JavaScript source on subsequent lines" + .to_string(), + )); + } + + reject_json_or_quoted_source(rest)?; + args.source = rest.to_string(); + args.timeout_ms = timeout_ms; + Ok(args) +} + +fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> { + let trimmed = code.trim(); + if trimmed.starts_with("```") { + return Err(FunctionCallError::RespondToModel( + "artifacts expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-artifacts: ...` or `// codex-artifact-tool: ...`)." + .to_string(), + )); + } + let Ok(value) = serde_json::from_str::(trimmed) else { + return Ok(()); + }; + match value { + JsonValue::Object(_) | JsonValue::String(_) => Err(FunctionCallError::RespondToModel( + "artifacts is a freeform tool and expects raw JavaScript source authored against the preloaded @oai/artifact-tool surface. Resend plain JS only (optional first line `// codex-artifacts: ...` or `// codex-artifact-tool: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences." + .to_string(), + )), + _ => Ok(()), + } +} + +fn parse_pragma_prefix(line: &str) -> Option<&str> { + ARTIFACTS_PRAGMA_PREFIXES + .iter() + .find_map(|prefix| line.strip_prefix(prefix)) +} + +fn artifacts_error(error: ArtifactsError) -> FunctionCallError { + FunctionCallError::RespondToModel(error.to_string()) +} + +async fn emit_exec_begin(session: &Session, turn: &TurnContext, call_id: &str) { + let emitter = ToolEmitter::shell( + vec![ARTIFACTS_TOOL_NAME.to_string()], + turn.cwd.clone(), + ExecCommandSource::Agent, + true, + ); + let ctx = ToolEventCtx::new(session, turn, call_id, None); + emitter.emit(ctx, ToolEventStage::Begin).await; +} + +async fn emit_exec_end( + session: &Session, + turn: &TurnContext, + call_id: &str, + output: &ArtifactCommandOutput, + duration: Duration, + success: bool, +) { + let exec_output = ExecToolCallOutput { + exit_code: output.exit_code.unwrap_or(1), + stdout: StreamOutput::new(output.stdout.clone()), + stderr: StreamOutput::new(output.stderr.clone()), + aggregated_output: StreamOutput::new(format_artifact_output(output)), + duration, + timed_out: false, + }; + let emitter = ToolEmitter::shell( + vec![ARTIFACTS_TOOL_NAME.to_string()], + turn.cwd.clone(), + ExecCommandSource::Agent, + true, + ); + let ctx = ToolEventCtx::new(session, turn, call_id, None); + let stage = if success { + ToolEventStage::Success(exec_output) + } else { + ToolEventStage::Failure(ToolEventFailure::Output(exec_output)) + }; + emitter.emit(ctx, stage).await; +} + +fn format_artifact_output(output: &ArtifactCommandOutput) -> String { + let stdout = output.stdout.trim(); + let stderr = output.stderr.trim(); + let mut sections = vec![format!( + "exit_code: {}", + output + .exit_code + .map(|code| code.to_string()) + .unwrap_or_else(|| "null".to_string()) + )]; + if !stdout.is_empty() { + sections.push(format!("stdout:\n{stdout}")); + } + if !stderr.is_empty() { + sections.push(format!("stderr:\n{stderr}")); + } + if stdout.is_empty() && stderr.is_empty() && output.success() { + sections.push("artifact JS completed successfully.".to_string()); + } + sections.join("\n\n") +} + +fn error_output(error: &ArtifactsError) -> ArtifactCommandOutput { + ArtifactCommandOutput { + exit_code: Some(1), + stdout: String::new(), + stderr: error.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_artifacts::RuntimeEntrypoints; + use codex_artifacts::RuntimePathEntry; + use tempfile::TempDir; + + #[test] + fn parse_freeform_args_without_pragma() { + let args = parse_freeform_args("console.log('ok');").expect("parse args"); + assert_eq!(args.source, "console.log('ok');"); + assert_eq!(args.timeout_ms, None); + } + + #[test] + fn parse_freeform_args_with_pragma() { + let args = parse_freeform_args("// codex-artifacts: timeout_ms=45000\nconsole.log('ok');") + .expect("parse args"); + assert_eq!(args.source, "console.log('ok');"); + assert_eq!(args.timeout_ms, Some(45_000)); + } + + #[test] + fn parse_freeform_args_with_artifact_tool_pragma() { + let args = + parse_freeform_args("// codex-artifact-tool: timeout_ms=45000\nconsole.log('ok');") + .expect("parse args"); + assert_eq!(args.source, "console.log('ok');"); + assert_eq!(args.timeout_ms, Some(45_000)); + } + + #[test] + fn parse_freeform_args_rejects_json_wrapped_code() { + let err = + parse_freeform_args("{\"code\":\"console.log('ok')\"}").expect_err("expected error"); + assert!( + err.to_string() + .contains("artifacts is a freeform tool and expects raw JavaScript source") + ); + } + + #[tokio::test] + async fn resolve_preinstalled_runtime_reads_pinned_cache_path() { + let codex_home = TempDir::new().expect("create temp codex home"); + let platform = ArtifactRuntimePlatform::detect_current().expect("detect platform"); + let install_dir = codex_home + .path() + .join("packages") + .join("artifacts") + .join(PINNED_ARTIFACT_RUNTIME_VERSION) + .join(platform.as_str()); + std::fs::create_dir_all(&install_dir).expect("create install dir"); + std::fs::write( + install_dir.join("manifest.json"), + serde_json::json!({ + "schema_version": 1, + "runtime_version": PINNED_ARTIFACT_RUNTIME_VERSION, + "node": { "relative_path": "node/bin/node" }, + "entrypoints": { + "build_js": { "relative_path": "artifact-tool/dist/artifact_tool.mjs" }, + "render_cli": { "relative_path": "granola-render/dist/render_cli.mjs" } + } + }) + .to_string(), + ) + .expect("write manifest"); + + let runtime = resolve_preinstalled_runtime(codex_home.path()) + .await + .expect("resolve runtime"); + assert_eq!(runtime.runtime_version(), PINNED_ARTIFACT_RUNTIME_VERSION); + assert_eq!( + runtime.manifest().entrypoints, + RuntimeEntrypoints { + build_js: RuntimePathEntry { + relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(), + }, + render_cli: RuntimePathEntry { + relative_path: "granola-render/dist/render_cli.mjs".to_string(), + }, + } + ); + } + + #[test] + fn format_artifact_output_includes_success_message_when_silent() { + let formatted = format_artifact_output(&ArtifactCommandOutput { + exit_code: Some(0), + stdout: String::new(), + stderr: String::new(), + }); + assert!(formatted.contains("artifact JS completed successfully.")); + } +} diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 908f3d18d..d98576809 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod agent_jobs; pub mod apply_patch; +mod artifacts; mod dynamic; mod grep_files; mod js_repl; @@ -8,12 +9,10 @@ mod mcp; mod mcp_resource; pub(crate) mod multi_agents; mod plan; -mod presentation_artifact; mod read_file; mod request_user_input; mod search_tool_bm25; mod shell; -mod spreadsheet_artifact; mod test_sync; pub(crate) mod unified_exec; mod view_image; @@ -29,6 +28,7 @@ use crate::function_tool::FunctionCallError; use crate::sandboxing::SandboxPermissions; use crate::sandboxing::normalize_additional_permissions; pub use apply_patch::ApplyPatchHandler; +pub use artifacts::ArtifactsHandler; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; pub use dynamic::DynamicToolHandler; @@ -40,7 +40,6 @@ pub use mcp::McpHandler; pub use mcp_resource::McpResourceHandler; pub use multi_agents::MultiAgentHandler; pub use plan::PlanHandler; -pub use presentation_artifact::PresentationArtifactHandler; pub use read_file::ReadFileHandler; pub use request_user_input::RequestUserInputHandler; pub(crate) use request_user_input::request_user_input_tool_description; @@ -49,7 +48,6 @@ pub(crate) use search_tool_bm25::SEARCH_TOOL_BM25_TOOL_NAME; pub use search_tool_bm25::SearchToolBm25Handler; pub use shell::ShellCommandHandler; pub use shell::ShellHandler; -pub use spreadsheet_artifact::SpreadsheetArtifactHandler; pub use test_sync::TestSyncHandler; pub use unified_exec::UnifiedExecHandler; pub use view_image::ViewImageHandler; diff --git a/codex-rs/core/src/tools/handlers/presentation_artifact.rs b/codex-rs/core/src/tools/handlers/presentation_artifact.rs deleted file mode 100644 index 23b80d691..000000000 --- a/codex-rs/core/src/tools/handlers/presentation_artifact.rs +++ /dev/null @@ -1,250 +0,0 @@ -use async_trait::async_trait; -use codex_artifact_presentation::PathAccessKind; -use codex_artifact_presentation::PathAccessRequirement; -use codex_artifact_presentation::PresentationArtifactError; -use codex_artifact_presentation::PresentationArtifactToolRequest; -use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::ReviewDecision; -use serde_json::to_string; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; - -use crate::codex::Session; -use crate::codex::TurnContext; -use crate::features::Feature; -use crate::function_tool::FunctionCallError; -use crate::path_utils::normalize_for_path_comparison; -use crate::path_utils::resolve_symlink_write_paths; -use crate::tools::context::ToolInvocation; -use crate::tools::context::ToolOutput; -use crate::tools::context::ToolPayload; -use crate::tools::handlers::parse_arguments; -use crate::tools::registry::ToolHandler; -use crate::tools::registry::ToolKind; -use crate::tools::sandboxing::with_cached_approval; -use codex_protocol::models::FunctionCallOutputBody; - -pub struct PresentationArtifactHandler; - -#[async_trait] -impl ToolHandler for PresentationArtifactHandler { - fn kind(&self) -> ToolKind { - ToolKind::Function - } - - async fn is_mutating(&self, invocation: &ToolInvocation) -> bool { - let ToolPayload::Function { arguments } = &invocation.payload else { - return true; - }; - let Ok(request) = parse_arguments::(arguments) else { - return true; - }; - request.is_mutating().unwrap_or(true) - } - - async fn handle(&self, invocation: ToolInvocation) -> Result { - let ToolInvocation { - session, - turn, - payload, - call_id, - .. - } = invocation; - - if !session.enabled(Feature::Artifact) { - return Err(FunctionCallError::RespondToModel( - "presentation_artifact is disabled by feature flag".to_string(), - )); - } - - let arguments = match payload { - ToolPayload::Function { arguments } => arguments, - _ => { - return Err(FunctionCallError::RespondToModel( - "presentation_artifact handler received unsupported payload".to_string(), - )); - } - }; - - let request: PresentationArtifactToolRequest = parse_arguments(&arguments)?; - for access in request - .required_path_accesses(&turn.cwd) - .map_err(presentation_error)? - { - authorize_path_access(session.as_ref(), turn.as_ref(), &call_id, &access).await?; - } - - let response = session - .execute_presentation_artifact( - request - .into_execution_request() - .map_err(presentation_error)?, - &turn.cwd, - ) - .await - .map_err(presentation_error)?; - - Ok(ToolOutput::Function { - body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| { - FunctionCallError::RespondToModel(format!( - "failed to serialize presentation_artifact response: {error}" - )) - })?), - success: Some(true), - }) - } -} - -fn presentation_error(error: PresentationArtifactError) -> FunctionCallError { - FunctionCallError::RespondToModel(error.to_string()) -} - -async fn authorize_path_access( - session: &Session, - turn: &TurnContext, - call_id: &str, - access: &PathAccessRequirement, -) -> Result<(), FunctionCallError> { - let effective_path = match access.kind { - PathAccessKind::Read => effective_read_path(&access.path), - PathAccessKind::Write => effective_write_path(&access.path), - }; - let allowed = match access.kind { - PathAccessKind::Read => path_is_readable(turn, &effective_path), - PathAccessKind::Write => path_is_writable(turn, &effective_path), - }; - if allowed { - return Ok(()); - } - - let approval_policy = turn.approval_policy.value(); - if !matches!( - approval_policy, - AskForApproval::OnRequest | AskForApproval::UnlessTrusted - ) { - return Err(FunctionCallError::RespondToModel(format!( - "{} path `{}` is outside the current sandbox policy", - access_kind_label(access.kind), - access.path.display() - ))); - } - - let key = format!( - "presentation_artifact:{:?}:{}", - access.kind, - effective_path.display() - ); - let path = access.path.clone(); - let action = access.action.clone(); - let decision = with_cached_approval( - &session.services, - "presentation_artifact", - vec![key], - || { - let path = path.clone(); - let action = action.clone(); - async move { - session - .request_command_approval( - turn, - call_id.to_string(), - None, - vec![ - "presentation_artifact".to_string(), - action, - path.display().to_string(), - ], - turn.cwd.clone(), - Some(format!( - "Allow presentation_artifact to {} `{}`?", - access_kind_verb(access.kind), - path.display() - )), - None, - None, - None, - None, - ) - .await - } - }, - ) - .await; - - if matches!( - decision, - ReviewDecision::Approved - | ReviewDecision::ApprovedForSession - | ReviewDecision::ApprovedExecpolicyAmendment { .. } - ) { - return Ok(()); - } - - Err(FunctionCallError::RespondToModel(format!( - "{} path `{}` was not approved", - access_kind_label(access.kind), - access.path.display() - ))) -} - -fn path_is_readable(turn: &TurnContext, path: &Path) -> bool { - if turn.sandbox_policy.has_full_disk_read_access() { - return true; - } - - turn.sandbox_policy - .get_readable_roots_with_cwd(&turn.cwd) - .iter() - .any(|root| path.starts_with(root.as_path())) -} - -fn path_is_writable(turn: &TurnContext, path: &Path) -> bool { - if turn.sandbox_policy.has_full_disk_write_access() { - return true; - } - - turn.sandbox_policy - .get_writable_roots_with_cwd(&turn.cwd) - .iter() - .any(|root| root.is_path_writable(path)) -} - -fn effective_read_path(path: &Path) -> PathBuf { - normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path)) -} - -fn effective_write_path(path: &Path) -> PathBuf { - let write_path = resolve_symlink_write_paths(path) - .map(|paths| paths.write_path) - .unwrap_or_else(|_| path.to_path_buf()); - normalize_for_path_comparison(&write_path).unwrap_or_else(|_| normalize_without_fs(&write_path)) -} - -fn normalize_without_fs(path: &Path) -> PathBuf { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::ParentDir => { - normalized.pop(); - } - Component::CurDir => {} - other => normalized.push(other.as_os_str()), - } - } - normalized -} - -fn access_kind_label(kind: PathAccessKind) -> &'static str { - match kind { - PathAccessKind::Read => "read", - PathAccessKind::Write => "write", - } -} - -fn access_kind_verb(kind: PathAccessKind) -> &'static str { - match kind { - PathAccessKind::Read => "read from", - PathAccessKind::Write => "write to", - } -} diff --git a/codex-rs/core/src/tools/handlers/spreadsheet_artifact.rs b/codex-rs/core/src/tools/handlers/spreadsheet_artifact.rs deleted file mode 100644 index be4eb7fc5..000000000 --- a/codex-rs/core/src/tools/handlers/spreadsheet_artifact.rs +++ /dev/null @@ -1,235 +0,0 @@ -use async_trait::async_trait; -use codex_artifact_spreadsheet::PathAccessKind; -use codex_artifact_spreadsheet::PathAccessRequirement; -use codex_artifact_spreadsheet::SpreadsheetArtifactError; -use codex_artifact_spreadsheet::SpreadsheetArtifactRequest; -use codex_protocol::models::FunctionCallOutputBody; -use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::ReviewDecision; -use serde_json::to_string; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; - -use crate::codex::Session; -use crate::codex::TurnContext; -use crate::features::Feature; -use crate::function_tool::FunctionCallError; -use crate::path_utils::normalize_for_path_comparison; -use crate::path_utils::resolve_symlink_write_paths; -use crate::tools::context::ToolInvocation; -use crate::tools::context::ToolOutput; -use crate::tools::context::ToolPayload; -use crate::tools::handlers::parse_arguments; -use crate::tools::registry::ToolHandler; -use crate::tools::registry::ToolKind; -use crate::tools::sandboxing::with_cached_approval; - -pub struct SpreadsheetArtifactHandler; - -#[async_trait] -impl ToolHandler for SpreadsheetArtifactHandler { - fn kind(&self) -> ToolKind { - ToolKind::Function - } - - async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool { - true - } - - async fn handle(&self, invocation: ToolInvocation) -> Result { - let ToolInvocation { - session, - turn, - payload, - call_id, - .. - } = invocation; - - if !session.enabled(Feature::Artifact) { - return Err(FunctionCallError::RespondToModel( - "spreadsheet_artifact is disabled by feature flag".to_string(), - )); - } - - let arguments = match payload { - ToolPayload::Function { arguments } => arguments, - _ => { - return Err(FunctionCallError::RespondToModel( - "spreadsheet_artifact handler received unsupported payload".to_string(), - )); - } - }; - - let request: SpreadsheetArtifactRequest = parse_arguments(&arguments)?; - for access in request - .required_path_accesses(&turn.cwd) - .map_err(spreadsheet_error)? - { - authorize_path_access(session.as_ref(), turn.as_ref(), &call_id, &access).await?; - } - - let response = session - .execute_spreadsheet_artifact(request, &turn.cwd) - .await - .map_err(spreadsheet_error)?; - - Ok(ToolOutput::Function { - body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| { - FunctionCallError::RespondToModel(format!( - "failed to serialize spreadsheet_artifact response: {error}" - )) - })?), - success: Some(true), - }) - } -} - -fn spreadsheet_error(error: SpreadsheetArtifactError) -> FunctionCallError { - FunctionCallError::RespondToModel(error.to_string()) -} - -async fn authorize_path_access( - session: &Session, - turn: &TurnContext, - call_id: &str, - access: &PathAccessRequirement, -) -> Result<(), FunctionCallError> { - let effective_path = match access.kind { - PathAccessKind::Read => effective_read_path(&access.path), - PathAccessKind::Write => effective_write_path(&access.path), - }; - let allowed = match access.kind { - PathAccessKind::Read => path_is_readable(turn, &effective_path), - PathAccessKind::Write => path_is_writable(turn, &effective_path), - }; - if allowed { - return Ok(()); - } - - let approval_policy = turn.approval_policy.value(); - if !matches!( - approval_policy, - AskForApproval::OnRequest | AskForApproval::UnlessTrusted - ) { - return Err(FunctionCallError::RespondToModel(format!( - "{} path `{}` is outside the current sandbox policy", - access_kind_label(access.kind), - access.path.display() - ))); - } - - let key = format!( - "spreadsheet_artifact:{:?}:{}", - access.kind, - effective_path.display() - ); - let path = access.path.clone(); - let action = access.action.clone(); - let decision = - with_cached_approval(&session.services, "spreadsheet_artifact", vec![key], || { - let path = path.clone(); - let action = action.clone(); - async move { - session - .request_command_approval( - turn, - call_id.to_string(), - None, - vec![ - "spreadsheet_artifact".to_string(), - action, - path.display().to_string(), - ], - turn.cwd.clone(), - Some(format!( - "Allow spreadsheet_artifact to {} `{}`?", - access_kind_verb(access.kind), - path.display() - )), - None, - None, - None, - None, - ) - .await - } - }) - .await; - - if matches!( - decision, - ReviewDecision::Approved - | ReviewDecision::ApprovedForSession - | ReviewDecision::ApprovedExecpolicyAmendment { .. } - ) { - return Ok(()); - } - - Err(FunctionCallError::RespondToModel(format!( - "{} path `{}` was not approved", - access_kind_label(access.kind), - access.path.display() - ))) -} - -fn path_is_readable(turn: &TurnContext, path: &Path) -> bool { - if turn.sandbox_policy.has_full_disk_read_access() { - return true; - } - - turn.sandbox_policy - .get_readable_roots_with_cwd(&turn.cwd) - .iter() - .any(|root| path.starts_with(root.as_path())) -} - -fn path_is_writable(turn: &TurnContext, path: &Path) -> bool { - if turn.sandbox_policy.has_full_disk_write_access() { - return true; - } - - turn.sandbox_policy - .get_writable_roots_with_cwd(&turn.cwd) - .iter() - .any(|root| root.is_path_writable(path)) -} - -fn effective_read_path(path: &Path) -> PathBuf { - normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path)) -} - -fn effective_write_path(path: &Path) -> PathBuf { - let write_path = resolve_symlink_write_paths(path) - .map(|paths| paths.write_path) - .unwrap_or_else(|_| path.to_path_buf()); - normalize_for_path_comparison(&write_path).unwrap_or_else(|_| normalize_without_fs(&write_path)) -} - -fn normalize_without_fs(path: &Path) -> PathBuf { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::ParentDir => { - normalized.pop(); - } - Component::CurDir => {} - other => normalized.push(other.as_os_str()), - } - } - normalized -} - -fn access_kind_label(kind: PathAccessKind) -> &'static str { - match kind { - PathAccessKind::Read => "read", - PathAccessKind::Write => "write", - } -} - -fn access_kind_verb(kind: PathAccessKind) -> &'static str { - match kind { - PathAccessKind::Read => "read from", - PathAccessKind::Write => "write to", - } -} diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index b0b354099..dcdb48500 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -575,97 +575,6 @@ fn create_view_image_tool() -> ToolSpec { }) } -fn create_presentation_artifact_tool() -> ToolSpec { - let action_step_schema = JsonSchema::Object { - properties: BTreeMap::from([ - ( - "action".to_string(), - JsonSchema::String { - description: Some("Action name to run for this step.".to_string()), - }, - ), - ( - "args".to_string(), - JsonSchema::Object { - properties: BTreeMap::new(), - required: None, - additional_properties: Some(true.into()), - }, - ), - ]), - required: Some(vec!["action".to_string(), "args".to_string()]), - additional_properties: Some(false.into()), - }; - let properties = BTreeMap::from([ - ( - "artifact_id".to_string(), - JsonSchema::String { - description: Some( - "Artifact id returned by an earlier presentation_artifact call.".to_string(), - ), - }, - ), - ( - "actions".to_string(), - JsonSchema::Array { - items: Box::new(action_step_schema), - description: Some( - "Array of `(action, args)` steps to execute sequentially.".to_string(), - ), - }, - ), - ]); - - ToolSpec::Function(ResponsesApiTool { - name: "presentation_artifact".to_string(), - description: "Create or edit a presentation artifact for the current thread.".to_string(), - strict: false, - parameters: JsonSchema::Object { - properties, - required: Some(vec!["actions".to_string()]), - additional_properties: Some(false.into()), - }, - }) -} - -fn create_spreadsheet_artifact_tool() -> ToolSpec { - let properties = BTreeMap::from([ - ( - "artifact_id".to_string(), - JsonSchema::String { - description: Some( - "Artifact id returned by an earlier spreadsheet_artifact call.".to_string(), - ), - }, - ), - ( - "action".to_string(), - JsonSchema::String { - description: Some("Action name to run for this request.".to_string()), - }, - ), - ( - "args".to_string(), - JsonSchema::Object { - properties: BTreeMap::new(), - required: None, - additional_properties: Some(true.into()), - }, - ), - ]); - - ToolSpec::Function(ResponsesApiTool { - name: "spreadsheet_artifact".to_string(), - description: "Create or edit a spreadsheet artifact for the current thread.".to_string(), - strict: false, - parameters: JsonSchema::Object { - properties, - required: Some(vec!["action".to_string(), "args".to_string()]), - additional_properties: Some(false.into()), - }, - }) -} - fn create_collab_input_items_schema() -> JsonSchema { let properties = BTreeMap::from([ ( @@ -1461,6 +1370,33 @@ JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/ }) } +fn create_artifacts_tool() -> ToolSpec { + const ARTIFACTS_FREEFORM_GRAMMAR: &str = r#" +start: pragma_source | plain_source + +pragma_source: PRAGMA_LINE NEWLINE js_source +plain_source: PLAIN_JS_SOURCE + +js_source: JS_SOURCE + +PRAGMA_LINE: /[ \t]*\/\/ codex-artifacts:[^\r\n]*/ | /[ \t]*\/\/ codex-artifact-tool:[^\r\n]*/ +NEWLINE: /\r?\n/ +PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/ +JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/ +"#; + + ToolSpec::Freeform(FreeformTool { + name: "artifacts".to_string(), + description: "Runs raw JavaScript against the preinstalled Codex @oai/artifact-tool runtime for creating presentations or spreadsheets. This is plain JavaScript executed by Node with top-level await, not TypeScript: do not use type annotations, `interface`, `type`, or `import type`. Author code the same way you would for `import { Presentation, Workbook, PresentationFile, SpreadsheetFile, FileBlob, ... } from \"@oai/artifact-tool\"`, but omit that import line because the package surface is already preloaded. Named exports are available directly on `globalThis`, and the full module is available as `globalThis.artifactTool` (also aliased as `globalThis.artifacts` and `globalThis.codexArtifacts`). Node built-ins such as `node:fs/promises` may still be imported when needed for saving preview bytes. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-artifacts: timeout_ms=15000` or `// codex-artifact-tool: timeout_ms=15000`; do not send JSON/quotes/markdown fences." + .to_string(), + format: FreeformToolFormat { + r#type: "grammar".to_string(), + syntax: "lark".to_string(), + definition: ARTIFACTS_FREEFORM_GRAMMAR.to_string(), + }, + }) +} + fn create_js_repl_reset_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "js_repl_reset".to_string(), @@ -1781,6 +1717,7 @@ pub(crate) fn build_specs( dynamic_tools: &[DynamicToolSpec], ) -> ToolRegistryBuilder { use crate::tools::handlers::ApplyPatchHandler; + use crate::tools::handlers::ArtifactsHandler; use crate::tools::handlers::DynamicToolHandler; use crate::tools::handlers::GrepFilesHandler; use crate::tools::handlers::JsReplHandler; @@ -1790,13 +1727,11 @@ pub(crate) fn build_specs( use crate::tools::handlers::McpResourceHandler; use crate::tools::handlers::MultiAgentHandler; use crate::tools::handlers::PlanHandler; - use crate::tools::handlers::PresentationArtifactHandler; use crate::tools::handlers::ReadFileHandler; use crate::tools::handlers::RequestUserInputHandler; use crate::tools::handlers::SearchToolBm25Handler; use crate::tools::handlers::ShellCommandHandler; use crate::tools::handlers::ShellHandler; - use crate::tools::handlers::SpreadsheetArtifactHandler; use crate::tools::handlers::TestSyncHandler; use crate::tools::handlers::UnifiedExecHandler; use crate::tools::handlers::ViewImageHandler; @@ -1819,8 +1754,7 @@ pub(crate) fn build_specs( let search_tool_handler = Arc::new(SearchToolBm25Handler); let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); - let presentation_artifact_handler = Arc::new(PresentationArtifactHandler); - let spreadsheet_artifact_handler = Arc::new(SpreadsheetArtifactHandler); + let artifacts_handler = Arc::new(ArtifactsHandler); let request_permission_enabled = config.request_permission_enabled; match &config.shell_type { @@ -1965,10 +1899,8 @@ pub(crate) fn build_specs( builder.register_handler("view_image", view_image_handler); if config.artifact_tools { - builder.push_spec(create_presentation_artifact_tool()); - builder.push_spec(create_spreadsheet_artifact_tool()); - builder.register_handler("presentation_artifact", presentation_artifact_handler); - builder.register_handler("spreadsheet_artifact", spreadsheet_artifact_handler); + builder.push_spec(create_artifacts_tool()); + builder.register_handler("artifacts", artifacts_handler); } if config.collab_tools { @@ -2301,7 +2233,7 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert_contains_tool_names(&tools, &["presentation_artifact", "spreadsheet_artifact"]); + assert_contains_tool_names(&tools, &["artifacts"]); } #[test] diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index d2de50249..29bf6c588 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -78,7 +78,6 @@ ignore = [ # TODO(fcoury): remove this exception when syntect drops yaml-rust and bincode, or updates to versions that have fixed the vulnerabilities. { id = "RUSTSEC-2024-0320", reason = "yaml-rust is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" }, { id = "RUSTSEC-2025-0141", reason = "bincode is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" }, - { id = "RUSTSEC-2025-0134", reason = "rustls-pemfile is unmaintained; pulled in via reqwest -> ppt-rs -> codex-artifact-presentation; no safe upgrade available yet" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. diff --git a/codex-rs/package-manager/BUILD.bazel b/codex-rs/package-manager/BUILD.bazel new file mode 100644 index 000000000..bbbc725cb --- /dev/null +++ b/codex-rs/package-manager/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "package-manager", + crate_name = "codex_package_manager", +) diff --git a/codex-rs/package-manager/Cargo.toml b/codex-rs/package-manager/Cargo.toml new file mode 100644 index 000000000..8a77bfda1 --- /dev/null +++ b/codex-rs/package-manager/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "codex-package-manager" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +flate2 = { workspace = true } +reqwest = { workspace = true, features = ["json", "stream"] } +serde = { workspace = true, features = ["derive"] } +sha2 = { workspace = true } +tar = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs", "rt", "sync"] } +url = { workspace = true } +zip = { workspace = true } + +[lints] +workspace = true + +[dev-dependencies] +pretty_assertions = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["fs", "macros", "rt", "rt-multi-thread"] } +wiremock = { workspace = true } diff --git a/codex-rs/package-manager/src/lib.rs b/codex-rs/package-manager/src/lib.rs new file mode 100644 index 000000000..77e2138d6 --- /dev/null +++ b/codex-rs/package-manager/src/lib.rs @@ -0,0 +1,793 @@ +use flate2::read::GzDecoder; +use reqwest::Client; +use serde::de::DeserializeOwned; +use sha2::Digest; +use sha2::Sha256; +use std::fs::File; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use tar::Archive; +use tempfile::tempdir_in; +use thiserror::Error; +use tokio::fs; +use url::Url; +use zip::ZipArchive; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackageManagerConfig

{ + codex_home: PathBuf, + package: P, + cache_root: Option, +} + +impl

PackageManagerConfig

{ + pub fn new(codex_home: PathBuf, package: P) -> Self { + Self { + codex_home, + package, + cache_root: None, + } + } + + pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self { + self.cache_root = Some(cache_root); + self + } + + pub fn codex_home(&self) -> &Path { + &self.codex_home + } + + pub fn package(&self) -> &P { + &self.package + } +} + +impl PackageManagerConfig

{ + pub fn cache_root(&self) -> PathBuf { + self.cache_root.clone().unwrap_or_else(|| { + self.codex_home.join( + self.package + .default_cache_root_relative() + .replace('/', std::path::MAIN_SEPARATOR_STR), + ) + }) + } +} + +#[derive(Clone, Debug)] +pub struct PackageManager

{ + client: Client, + config: PackageManagerConfig

, +} + +impl

PackageManager

{ + pub fn new(config: PackageManagerConfig

) -> Self { + Self { + client: Client::new(), + config, + } + } + + pub fn with_client(config: PackageManagerConfig

, client: Client) -> Self { + Self { client, config } + } + + pub fn config(&self) -> &PackageManagerConfig

{ + &self.config + } +} + +impl PackageManager

{ + pub async fn resolve_cached(&self) -> Result, P::Error> { + let platform = PackagePlatform::detect_current().map_err(P::Error::from)?; + let install_dir = self + .config + .package() + .install_dir(&self.config.cache_root(), platform); + if !fs::try_exists(&install_dir) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to read {}", install_dir.display()), + source, + }) + .map_err(P::Error::from)? + { + return Ok(None); + } + + let package = self + .config + .package() + .load_installed(install_dir, platform)?; + if self.config.package().installed_version(&package) != self.config.package().version() { + return Ok(None); + } + Ok(Some(package)) + } + + pub async fn ensure_installed(&self) -> Result { + if let Some(package) = self.resolve_cached().await? { + return Ok(package); + } + + let platform = PackagePlatform::detect_current().map_err(P::Error::from)?; + let manifest = self.fetch_release_manifest().await?; + if self.config.package().release_version(&manifest) != self.config.package().version() { + return Err(PackageManagerError::UnexpectedPackageVersion { + expected: self.config.package().version().to_string(), + actual: self.config.package().release_version(&manifest).to_string(), + } + .into()); + } + + let platform_archive = self + .config + .package() + .platform_archive(&manifest, platform)?; + let archive_url = self + .config + .package() + .archive_url(&platform_archive) + .map_err(P::Error::from)?; + let archive_bytes = self.download_bytes(&archive_url).await?; + verify_sha256(&archive_bytes, &platform_archive.sha256).map_err(P::Error::from)?; + + let install_dir = self + .config + .package() + .install_dir(&self.config.cache_root(), platform); + if fs::try_exists(&install_dir) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to read {}", install_dir.display()), + source, + }) + .map_err(P::Error::from)? + { + fs::remove_dir_all(&install_dir) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to remove {}", install_dir.display()), + source, + }) + .map_err(P::Error::from)?; + } + + let cache_root = self.config.cache_root(); + fs::create_dir_all(&cache_root) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", cache_root.display()), + source, + }) + .map_err(P::Error::from)?; + + let staging_dir = tempdir_in(&cache_root) + .map_err(|source| PackageManagerError::Io { + context: format!( + "failed to create staging directory in {}", + cache_root.display() + ), + source, + }) + .map_err(P::Error::from)?; + let archive_path = staging_dir.path().join(&platform_archive.archive); + fs::write(&archive_path, &archive_bytes) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to write {}", archive_path.display()), + source, + }) + .map_err(P::Error::from)?; + let extraction_root = staging_dir.path().join("extract"); + fs::create_dir_all(&extraction_root) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", extraction_root.display()), + source, + }) + .map_err(P::Error::from)?; + + extract_archive(&archive_path, &extraction_root, platform_archive.format) + .map_err(P::Error::from)?; + let extracted_root = self + .config + .package() + .detect_extracted_root(&extraction_root)?; + let package = self + .config + .package() + .load_installed(extracted_root.clone(), platform)?; + if self.config.package().installed_version(&package) != self.config.package().version() { + return Err(PackageManagerError::UnexpectedPackageVersion { + expected: self.config.package().version().to_string(), + actual: self + .config + .package() + .installed_version(&package) + .to_string(), + } + .into()); + } + + if let Some(parent) = install_dir.parent() { + fs::create_dir_all(parent) + .await + .map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", parent.display()), + source, + }) + .map_err(P::Error::from)?; + } + + fs::rename(&extracted_root, &install_dir) + .await + .map_err(|source| PackageManagerError::Io { + context: format!( + "failed to move {} to {}", + extracted_root.display(), + install_dir.display() + ), + source, + }) + .map_err(P::Error::from)?; + + self.config.package().load_installed(install_dir, platform) + } + + async fn fetch_release_manifest(&self) -> Result { + let manifest_url = self + .config + .package() + .manifest_url() + .map_err(P::Error::from)?; + let response = self + .client + .get(manifest_url.clone()) + .send() + .await + .map_err(|source| PackageManagerError::Http { + context: format!("failed to fetch {manifest_url}"), + source, + }) + .map_err(P::Error::from)? + .error_for_status() + .map_err(|source| PackageManagerError::Http { + context: format!("manifest request failed for {manifest_url}"), + source, + }) + .map_err(P::Error::from)?; + + response + .json::() + .await + .map_err(|source| PackageManagerError::Http { + context: format!("failed to decode manifest from {manifest_url}"), + source, + }) + .map_err(P::Error::from) + } + + async fn download_bytes(&self, url: &Url) -> Result, P::Error> { + let response = self + .client + .get(url.clone()) + .send() + .await + .map_err(|source| PackageManagerError::Http { + context: format!("failed to download {url}"), + source, + }) + .map_err(P::Error::from)? + .error_for_status() + .map_err(|source| PackageManagerError::Http { + context: format!("archive request failed for {url}"), + source, + }) + .map_err(P::Error::from)?; + let bytes = response + .bytes() + .await + .map_err(|source| PackageManagerError::Http { + context: format!("failed to read response body for {url}"), + source, + }) + .map_err(P::Error::from)?; + Ok(bytes.to_vec()) + } +} + +pub trait ManagedPackage: Clone { + type Error: From; + type Installed: Clone; + type ReleaseManifest: DeserializeOwned; + + fn default_cache_root_relative(&self) -> &str; + fn version(&self) -> &str; + fn manifest_url(&self) -> Result; + fn archive_url(&self, archive: &PackageReleaseArchive) -> Result; + fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str; + fn platform_archive( + &self, + manifest: &Self::ReleaseManifest, + platform: PackagePlatform, + ) -> Result; + fn install_dir(&self, cache_root: &Path, platform: PackagePlatform) -> PathBuf; + fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str; + fn load_installed( + &self, + root_dir: PathBuf, + platform: PackagePlatform, + ) -> Result; + + fn detect_extracted_root(&self, extraction_root: &Path) -> Result { + detect_single_package_root(extraction_root).map_err(Self::Error::from) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PackagePlatform { + DarwinArm64, + DarwinX64, + LinuxArm64, + LinuxX64, + WindowsArm64, + WindowsX64, +} + +impl PackagePlatform { + pub fn detect_current() -> Result { + match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") | ("macos", "arm64") => Ok(Self::DarwinArm64), + ("macos", "x86_64") => Ok(Self::DarwinX64), + ("linux", "aarch64") | ("linux", "arm64") => Ok(Self::LinuxArm64), + ("linux", "x86_64") => Ok(Self::LinuxX64), + ("windows", "aarch64") | ("windows", "arm64") => Ok(Self::WindowsArm64), + ("windows", "x86_64") => Ok(Self::WindowsX64), + (os, arch) => Err(PackageManagerError::UnsupportedPlatform { + os: os.to_string(), + arch: arch.to_string(), + }), + } + } + + pub fn as_str(self) -> &'static str { + match self { + Self::DarwinArm64 => "darwin-arm64", + Self::DarwinX64 => "darwin-x64", + Self::LinuxArm64 => "linux-arm64", + Self::LinuxX64 => "linux-x64", + Self::WindowsArm64 => "windows-arm64", + Self::WindowsX64 => "windows-x64", + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub struct PackageReleaseArchive { + pub archive: String, + pub sha256: String, + pub format: ArchiveFormat, + pub size_bytes: Option, +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +pub enum ArchiveFormat { + #[serde(rename = "zip")] + Zip, + #[serde(rename = "tar.gz")] + TarGz, +} + +#[derive(Debug, Error)] +pub enum PackageManagerError { + #[error("unsupported platform: {os}-{arch}")] + UnsupportedPlatform { os: String, arch: String }, + #[error("invalid release base url")] + InvalidBaseUrl(#[source] url::ParseError), + #[error("{context}")] + Http { + context: String, + #[source] + source: reqwest::Error, + }, + #[error("{context}")] + Io { + context: String, + #[source] + source: std::io::Error, + }, + #[error("missing platform entry `{0}` in release manifest")] + MissingPlatform(String), + #[error("unexpected package version: expected `{expected}`, got `{actual}`")] + UnexpectedPackageVersion { expected: String, actual: String }, + #[error("checksum mismatch: expected `{expected}`, got `{actual}`")] + ChecksumMismatch { expected: String, actual: String }, + #[error("archive extraction failed: {0}")] + ArchiveExtraction(String), + #[error("archive did not contain a package root with manifest.json under {0}")] + MissingPackageRoot(PathBuf), +} + +pub fn detect_single_package_root(extraction_root: &Path) -> Result { + let direct_manifest = extraction_root.join("manifest.json"); + if direct_manifest.exists() { + return Ok(extraction_root.to_path_buf()); + } + + let mut directory_candidates = Vec::new(); + for entry in std::fs::read_dir(extraction_root).map_err(|source| PackageManagerError::Io { + context: format!("failed to read {}", extraction_root.display()), + source, + })? { + let entry = entry.map_err(|source| PackageManagerError::Io { + context: format!("failed to read entry in {}", extraction_root.display()), + source, + })?; + let path = entry.path(); + if path.is_dir() { + directory_candidates.push(path); + } + } + + if directory_candidates.len() == 1 { + let candidate = &directory_candidates[0]; + if candidate.join("manifest.json").exists() { + return Ok(candidate.clone()); + } + } + + Err(PackageManagerError::MissingPackageRoot( + extraction_root.to_path_buf(), + )) +} + +fn verify_sha256(bytes: &[u8], expected: &str) -> Result<(), PackageManagerError> { + let actual = format!("{:x}", Sha256::digest(bytes)); + if actual == expected.to_ascii_lowercase() { + return Ok(()); + } + Err(PackageManagerError::ChecksumMismatch { + expected: expected.to_string(), + actual, + }) +} + +fn extract_archive( + archive_path: &Path, + destination: &Path, + format: ArchiveFormat, +) -> Result<(), PackageManagerError> { + match format { + ArchiveFormat::Zip => extract_zip_archive(archive_path, destination), + ArchiveFormat::TarGz => extract_tar_gz_archive(archive_path, destination), + } +} + +fn extract_zip_archive(archive_path: &Path, destination: &Path) -> Result<(), PackageManagerError> { + let file = File::open(archive_path).map_err(|source| PackageManagerError::Io { + context: format!("failed to open {}", archive_path.display()), + source, + })?; + let mut archive = ZipArchive::new(file) + .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; + for index in 0..archive.len() { + let mut entry = archive + .by_index(index) + .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; + let Some(relative_path) = entry.enclosed_name() else { + return Err(PackageManagerError::ArchiveExtraction(format!( + "zip entry `{}` escapes extraction root", + entry.name() + ))); + }; + let output_path = destination.join(relative_path); + if entry.is_dir() { + std::fs::create_dir_all(&output_path).map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", output_path.display()), + source, + })?; + continue; + } + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", parent.display()), + source, + })?; + } + let mut output = File::create(&output_path).map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", output_path.display()), + source, + })?; + std::io::copy(&mut entry, &mut output).map_err(|source| PackageManagerError::Io { + context: format!("failed to write {}", output_path.display()), + source, + })?; + } + Ok(()) +} + +fn extract_tar_gz_archive( + archive_path: &Path, + destination: &Path, +) -> Result<(), PackageManagerError> { + let file = File::open(archive_path).map_err(|source| PackageManagerError::Io { + context: format!("failed to open {}", archive_path.display()), + source, + })?; + let decoder = GzDecoder::new(file); + let mut archive = Archive::new(decoder); + for entry in archive + .entries() + .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))? + { + let mut entry = + entry.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; + let path = entry + .path() + .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; + let output_path = safe_extract_path(destination, path.as_ref())?; + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io { + context: format!("failed to create {}", parent.display()), + source, + })?; + } + entry + .unpack(&output_path) + .map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?; + } + Ok(()) +} + +fn safe_extract_path(root: &Path, relative_path: &Path) -> Result { + let mut clean_relative = PathBuf::new(); + for component in relative_path.components() { + match component { + Component::Normal(segment) => clean_relative.push(segment), + Component::CurDir => {} + Component::ParentDir | Component::RootDir | Component::Prefix(_) => { + return Err(PackageManagerError::ArchiveExtraction(format!( + "entry `{}` escapes extraction root", + relative_path.display() + ))); + } + } + } + + if clean_relative.as_os_str().is_empty() { + return Err(PackageManagerError::ArchiveExtraction( + "archive entry had an empty path".to_string(), + )); + } + Ok(root.join(clean_relative)) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use serde::Deserialize; + use std::collections::BTreeMap; + use std::io::Cursor; + use std::io::Write; + use tempfile::TempDir; + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + use zip::ZipWriter; + use zip::write::SimpleFileOptions; + + #[derive(Clone, Debug)] + struct TestPackage { + base_url: Url, + version: String, + } + + #[derive(Clone, Debug, Deserialize)] + struct TestReleaseManifest { + package_version: String, + platforms: BTreeMap, + } + + #[derive(Clone, Debug, PartialEq, Eq)] + struct TestInstalledPackage { + version: String, + platform: PackagePlatform, + root_dir: PathBuf, + } + + impl ManagedPackage for TestPackage { + type Error = PackageManagerError; + type Installed = TestInstalledPackage; + type ReleaseManifest = TestReleaseManifest; + + fn default_cache_root_relative(&self) -> &str { + "packages/test-package" + } + + fn version(&self) -> &str { + &self.version + } + + fn manifest_url(&self) -> Result { + self.base_url + .join(&format!("test-package-v{}-manifest.json", self.version)) + .map_err(PackageManagerError::InvalidBaseUrl) + } + + fn archive_url(&self, archive: &PackageReleaseArchive) -> Result { + self.base_url + .join(&archive.archive) + .map_err(PackageManagerError::InvalidBaseUrl) + } + + fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str { + &manifest.package_version + } + + fn platform_archive( + &self, + manifest: &Self::ReleaseManifest, + platform: PackagePlatform, + ) -> Result { + manifest + .platforms + .get(platform.as_str()) + .cloned() + .ok_or_else(|| PackageManagerError::MissingPlatform(platform.as_str().to_string())) + } + + fn install_dir(&self, cache_root: &Path, platform: PackagePlatform) -> PathBuf { + cache_root.join(self.version()).join(platform.as_str()) + } + + fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str { + &package.version + } + + fn load_installed( + &self, + root_dir: PathBuf, + platform: PackagePlatform, + ) -> Result { + let version = + std::fs::read_to_string(root_dir.join("manifest.json")).map_err(|source| { + PackageManagerError::Io { + context: format!( + "failed to read {}", + root_dir.join("manifest.json").display() + ), + source, + } + })?; + Ok(TestInstalledPackage { + version: version.trim().to_string(), + platform, + root_dir, + }) + } + } + + #[tokio::test] + async fn ensure_installed_downloads_and_extracts_zip_package() { + let server = MockServer::start().await; + let version = "0.1.0"; + let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); + let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str()); + let archive_bytes = build_zip_archive(version); + let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); + let manifest = serde_json::json!({ + "package_version": version, + "platforms": { + platform.as_str(): { + "archive": archive_name, + "sha256": archive_sha, + "format": "zip", + "size_bytes": archive_bytes.len(), + } + } + }); + Mock::given(method("GET")) + .and(path(format!("/test-package-v{version}-manifest.json"))) + .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path(format!("/{archive_name}"))) + .respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes)) + .mount(&server) + .await; + + let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); + let package = TestPackage { + base_url: Url::parse(&format!("{}/", server.uri())) + .unwrap_or_else(|error| panic!("{error}")), + version: version.to_string(), + }; + let manager = PackageManager::new(PackageManagerConfig::new( + codex_home.path().to_path_buf(), + package, + )); + + let installed = manager + .ensure_installed() + .await + .unwrap_or_else(|error| panic!("{error}")); + + assert_eq!( + installed, + TestInstalledPackage { + version: version.to_string(), + platform, + root_dir: codex_home + .path() + .join("packages") + .join("test-package") + .join(version) + .join(platform.as_str()), + } + ); + } + + #[test] + fn tar_gz_extraction_supports_default_package_root_detection() { + let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); + let archive_path = temp.path().join("package.tar.gz"); + let extraction_root = temp.path().join("extract"); + std::fs::create_dir_all(&extraction_root).unwrap_or_else(|error| panic!("{error}")); + write_tar_gz_archive(&archive_path, "0.2.0"); + + extract_archive(&archive_path, &extraction_root, ArchiveFormat::TarGz) + .unwrap_or_else(|error| panic!("{error}")); + let package_root = + detect_single_package_root(&extraction_root).unwrap_or_else(|error| panic!("{error}")); + + assert!(package_root.join("manifest.json").exists()); + } + + fn build_zip_archive(version: &str) -> Vec { + let mut bytes = Cursor::new(Vec::new()); + { + let mut zip = ZipWriter::new(&mut bytes); + let options = SimpleFileOptions::default(); + zip.start_file("test-package/manifest.json", options) + .unwrap_or_else(|error| panic!("{error}")); + zip.write_all(version.as_bytes()) + .unwrap_or_else(|error| panic!("{error}")); + zip.finish().unwrap_or_else(|error| panic!("{error}")); + } + bytes.into_inner() + } + + fn write_tar_gz_archive(archive_path: &Path, version: &str) { + let file = File::create(archive_path).unwrap_or_else(|error| panic!("{error}")); + let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); + let mut builder = tar::Builder::new(encoder); + + append_tar_file( + &mut builder, + "test-package/manifest.json", + version.as_bytes(), + ); + builder.finish().unwrap_or_else(|error| panic!("{error}")); + } + + fn append_tar_file( + builder: &mut tar::Builder>, + path: &str, + contents: &[u8], + ) { + let mut header = tar::Header::new_gnu(); + header.set_size(contents.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + builder + .append_data(&mut header, path, contents) + .unwrap_or_else(|error| panic!("{error}")); + } +}