work
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 12 May 2025 15:57:32 +0000 (08:57 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 12 May 2025 15:57:39 +0000 (08:57 -0700)
rust/Cargo.lock
rust/doc/src/spv/structure.md
rust/pspp/Cargo.toml
rust/pspp/src/format/mod.rs
rust/pspp/src/message.rs
rust/pspp/src/output/mod.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/spv.rs [new file with mode: 0644]

index 9e6c6508452aad79751c432328d6f07f3b74fe20..89b5d190b1420c37361c9855660c9369766c1dec 100644 (file)
@@ -17,6 +17,17 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.3"
@@ -96,6 +107,15 @@ version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
+
 [[package]]
 name = "array-init"
 version = "2.1.0"
@@ -192,11 +212,20 @@ version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "bumpalo"
-version = "3.16.0"
+version = "3.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
 
 [[package]]
 name = "bytemuck"
@@ -204,12 +233,37 @@ version = "1.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
 
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
 [[package]]
 name = "bytes"
 version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
 [[package]]
 name = "cairo-rs"
 version = "0.20.7"
@@ -239,6 +293,8 @@ version = "1.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
 dependencies = [
+ "jobserver",
+ "libc",
  "shlex",
 ]
 
@@ -283,6 +339,16 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.16"
@@ -339,12 +405,42 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
 
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -354,6 +450,22 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
 [[package]]
 name = "dashmap"
 version = "5.5.3"
@@ -367,6 +479,32 @@ dependencies = [
  "parking_lot_core",
 ]
 
+[[package]]
+name = "deflate64"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
+
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "derive_more"
 version = "2.0.1"
@@ -394,6 +532,17 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "either"
 version = "1.13.0"
@@ -613,6 +762,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.3.2"
@@ -620,9 +779,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi",
  "wasi 0.14.2+wasi-0.2.4",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -753,6 +914,15 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "httparse"
 version = "1.9.4"
@@ -808,6 +978,15 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
@@ -829,12 +1008,22 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
+[[package]]
+name = "jobserver"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "js-sys"
-version = "0.3.70"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
 dependencies = [
+ "once_cell",
  "wasm-bindgen",
 ]
 
@@ -874,9 +1063,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.22"
+version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
 
 [[package]]
 name = "lsp-types"
@@ -891,6 +1080,27 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "lzma-rs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
+dependencies = [
+ "byteorder",
+ "crc",
+]
+
+[[package]]
+name = "lzma-sys"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
 [[package]]
 name = "matrixmultiply"
 version = "0.3.9"
@@ -976,6 +1186,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-integer"
 version = "0.1.46"
@@ -1119,6 +1335,16 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.1"
@@ -1178,6 +1404,12 @@ dependencies = [
  "portable-atomic",
 ]
 
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.21"
@@ -1249,6 +1481,8 @@ dependencies = [
  "unicode-linebreak",
  "unicode-width",
  "windows-sys 0.48.0",
+ "xmlwriter",
+ "zip",
 ]
 
 [[package]]
@@ -1389,6 +1623,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
 [[package]]
 name = "ryu"
 version = "1.0.18"
@@ -1453,6 +1693,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -1468,6 +1719,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -1508,6 +1765,12 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
 [[package]]
 name = "syn"
 version = "1.0.109"
@@ -1588,6 +1851,25 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
 [[package]]
 name = "tinyvec"
 version = "1.8.0"
@@ -1770,6 +2052,12 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
 [[package]]
 name = "unicase"
 version = "2.7.0"
@@ -1865,24 +2153,24 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.93"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
 dependencies = [
  "cfg-if",
  "once_cell",
+ "rustversion",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.93"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
 dependencies = [
  "bumpalo",
  "log",
- "once_cell",
  "proc-macro2",
  "quote",
  "syn 2.0.87",
@@ -1891,9 +2179,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.93"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -1901,9 +2189,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.93"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1914,9 +2202,12 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.93"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
 
 [[package]]
 name = "winapi"
@@ -2121,6 +2412,21 @@ dependencies = [
  "bitflags 2.6.0",
 ]
 
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
+[[package]]
+name = "xz2"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
+dependencies = [
+ "lzma-sys",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.8.24"
@@ -2140,3 +2446,91 @@ dependencies = [
  "quote",
  "syn 2.0.87",
 ]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "zip"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
+dependencies = [
+ "aes",
+ "arbitrary",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "crossbeam-utils",
+ "deflate64",
+ "flate2",
+ "getrandom",
+ "hmac",
+ "indexmap",
+ "lzma-rs",
+ "memchr",
+ "pbkdf2",
+ "sha1",
+ "time",
+ "xz2",
+ "zeroize",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zopfli"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.15+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
index b915eac6bb683b7675cb8a8379ff7a4dc099d723..85c515ad4f204185d991c8574e60a44fa490b868 100644 (file)
@@ -208,7 +208,7 @@ children of the root `heading` elements in all structure members in an
 SPV file are siblings.  That is, the root `heading` in all of the
 structure members conceptually represent the same node.  The root
 heading's `label` is ignored (see [the `label`
-element)(#the-label-element)).  The root heading in the first
+element](#the-label-element)).  The root heading in the first
 structure member in the Zip file may contain a `pageSetup` element.
 
 The schema implies that any `heading` may contain a sequence of any
@@ -598,7 +598,7 @@ element](#the-graph-element), for more information.
 
 These elements have no attributes.
 
-## The â€˜pageSetup’ Element
+## The `pageSetup` Element
 
 ```
 pageSetup
index ff7d59235cf42dc5a2248b657f620935d4851d7b..66a8d36703ac148fafaab1049f6514d7fba1a137 100644 (file)
@@ -42,6 +42,8 @@ derive_more = { version = "2.0.1", features = ["debug"] }
 cairo-rs = { version = "0.20.7", features = ["ps", "png", "pdf", "svg"] }
 pango = "0.20.9"
 pangocairo = "0.20.7"
+zip = "2.6.1"
+xmlwriter = "0.1.0"
 
 [target.'cfg(windows)'.dependencies]
 windows-sys = { version = "0.48.0", features = ["Win32_Globalization"] }
index e0eedf9d3e61c5021469c51fd41c831d4bcb95cb..e03fd714e97ede4d55cb8f6359dcbcaa19124b3d 100644 (file)
@@ -332,7 +332,7 @@ impl Type {
         }
     }
 
-    pub fn as_string(&self) -> &'static str {
+    pub fn as_str(&self) -> &'static str {
         match self {
             Self::F => "F",
             Self::Comma => "COMMA",
@@ -384,7 +384,7 @@ impl Type {
 
 impl Display for Type {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
-        write!(f, "{}", self.as_string())
+        write!(f, "{}", self.as_str())
     }
 }
 
@@ -393,7 +393,7 @@ impl FromStr for Type {
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         for type_ in all::<Type>() {
-            if type_.as_string().eq_ignore_ascii_case(s) {
+            if type_.as_str().eq_ignore_ascii_case(s) {
                 return Ok(type_);
             }
         }
@@ -655,6 +655,50 @@ impl TryFrom<UncheckedFormat> for Format {
     }
 }
 
+impl From<Type> for u16 {
+    fn from(source: Type) -> Self {
+        match source {
+            Type::A => 1,
+            Type::AHex => 2,
+            Type::Comma => 3,
+            Type::Dollar => 4,
+            Type::F => 5,
+            Type::IB => 6,
+            Type::PIBHex => 7,
+            Type::P => 8,
+            Type::PIB => 9,
+            Type::PK => 10,
+            Type::RB => 11,
+            Type::RBHex => 12,
+            Type::Z => 15,
+            Type::N => 16,
+            Type::E => 17,
+            Type::Date => 20,
+            Type::Time => 21,
+            Type::DateTime => 22,
+            Type::ADate => 23,
+            Type::JDate => 24,
+            Type::DTime => 25,
+            Type::WkDay => 26,
+            Type::Month => 27,
+            Type::MoYr => 28,
+            Type::QYr => 29,
+            Type::WkYr => 30,
+            Type::Pct => 31,
+            Type::Dot => 32,
+            Type::CC(CC::A) => 33,
+            Type::CC(CC::B) => 34,
+            Type::CC(CC::C) => 35,
+            Type::CC(CC::D) => 36,
+            Type::CC(CC::E) => 37,
+            Type::EDate => 38,
+            Type::SDate => 39,
+            Type::MTime => 40,
+            Type::YmdHms => 41,
+        }
+    }
+}
+
 impl TryFrom<u16> for Type {
     type Error = Error;
 
index b128b5ac0d4850c8642c7438c1c98193c5b97a67..abc7f424f2bb2d7362c8f2794dfbe665ac487b0d 100644 (file)
@@ -128,13 +128,21 @@ pub enum Severity {
 }
 
 impl Severity {
-    fn as_str(&self) -> &'static str {
+    pub fn as_str(&self) -> &'static str {
         match self {
             Severity::Error => "error",
             Severity::Warning => "warning",
             Severity::Note => "note",
         }
     }
+
+    pub fn as_title_str(&self) -> &'static str {
+        match self {
+            Severity::Error => "Error",
+            Severity::Warning => "Warning",
+            Severity::Note => "Note",
+        }
+    }
 }
 
 impl Display for Severity {
index 7f23a50cb9b5d58def685519eef6a22e423603e1..ea3b4274eec085034ea62b86d3aa58522e39fbb3 100644 (file)
@@ -1,5 +1,5 @@
 #![allow(dead_code)]
-use std::sync::Arc;
+use std::{borrow::Cow, sync::Arc};
 
 use pivot::PivotTable;
 
@@ -14,6 +14,7 @@ pub mod html;
 pub mod page;
 pub mod pivot;
 pub mod render;
+pub mod spv;
 pub mod table;
 pub mod text;
 pub mod text_line;
@@ -51,6 +52,13 @@ impl Item {
             details,
         }
     }
+
+    pub fn label(&self) -> Cow<'static, str> {
+        match &self.label {
+            Some(label) => Cow::from(label.clone()),
+            None => self.details.label(),
+        }
+    }
 }
 
 pub enum Details {
@@ -82,6 +90,18 @@ impl Details {
             Details::Table(pivot_table) => pivot_table.command_c.as_ref(),
         }
     }
+
+    pub fn label(&self) -> Cow<'static, str> {
+        match self {
+            Details::Chart => todo!(),
+            Details::Image => todo!(),
+            Details::Group(_) => Cow::from("Group"),
+            Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()),
+            Details::PageBreak => Cow::from("Page Break"),
+            Details::Table(pivot_table) => Cow::from(pivot_table.label()),
+            Details::Text(text) => Cow::from(text.type_.as_str()),
+        }
+    }
 }
 
 pub struct Text {
@@ -104,6 +124,17 @@ pub enum TextType {
     Log,
 }
 
+impl TextType {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            TextType::PageTitle => "Page Title",
+            TextType::Title => "Title",
+            TextType::Syntax => "Log",
+            TextType::Log => "Log",
+        }
+    }
+}
+
 pub struct ItemCursor {
     cur: Option<Arc<Item>>,
     stack: Vec<(Arc<Item>, usize)>,
index 14a6e510557d2b12237728c1a61b76e0982d7cbf..2d5d3683cc25bcd80713dbf130d3adef8b52b764 100644 (file)
@@ -885,6 +885,10 @@ impl Color {
         Self { alpha, ..self }
     }
 
+    pub const fn without_alpha(self) -> Self {
+        self.with_alpha(255)
+    }
+
     pub fn display_css(&self) -> DisplayCss {
         DisplayCss(*self)
     }
@@ -1291,34 +1295,34 @@ pub struct PivotTable {
 }
 
 impl PivotTable {
-    fn with_title(mut self, title: impl Into<Value>) -> Self {
+    pub fn with_title(mut self, title: impl Into<Value>) -> Self {
         self.title = Some(Box::new(title.into()));
         self.show_title = true;
         self
     }
 
-    fn with_caption(mut self, caption: Value) -> Self {
+    pub fn with_caption(mut self, caption: Value) -> Self {
         self.caption = Some(Box::new(caption));
         self.show_caption = true;
         self
     }
 
-    fn with_corner_text(mut self, corner_text: Value) -> Self {
+    pub fn with_corner_text(mut self, corner_text: Value) -> Self {
         self.corner_text = Some(Box::new(corner_text));
         self
     }
 
-    fn with_show_title(mut self, show_title: bool) -> Self {
+    pub fn with_show_title(mut self, show_title: bool) -> Self {
         self.show_title = show_title;
         self
     }
 
-    fn with_show_caption(mut self, show_caption: bool) -> Self {
+    pub fn with_show_caption(mut self, show_caption: bool) -> Self {
         self.show_caption = show_caption;
         self
     }
 
-    fn with_layer(mut self, layer: &[usize]) -> Self {
+    pub fn with_layer(mut self, layer: &[usize]) -> Self {
         debug_assert_eq!(layer.len(), self.current_layer.len());
         if self.look.print_all_layers {
             self.look_mut().print_all_layers = false;
@@ -1328,16 +1332,23 @@ impl PivotTable {
         self
     }
 
-    fn with_all_layers(mut self) -> Self {
+    pub fn with_all_layers(mut self) -> Self {
         if !self.look.print_all_layers {
             self.look_mut().print_all_layers = true;
         }
         self
     }
 
-    fn look_mut(&mut self) -> &mut Look {
+    pub fn look_mut(&mut self) -> &mut Look {
         Arc::make_mut(&mut self.look)
     }
+
+    pub fn label(&self) -> String {
+        match &self.title {
+            Some(title) => title.display(self).to_string(),
+            None => String::from("Table"),
+        }
+    }
 }
 
 impl Default for PivotTable {
@@ -1542,14 +1553,17 @@ impl Footnote {
         self.marker = Some(Box::new(marker.into()));
         self
     }
+
     pub fn with_show(mut self, show: bool) -> Self {
         self.show = show;
         self
     }
+
     pub fn with_index(mut self, index: usize) -> Self {
         self.index = index;
         self
     }
+
     pub fn display_marker(&self, options: impl IntoValueOptions) -> DisplayMarker<'_> {
         DisplayMarker {
             footnote: self,
@@ -1560,6 +1574,10 @@ impl Footnote {
     pub fn display_content(&self, options: impl IntoValueOptions) -> DisplayValue<'_> {
         self.content.display(options)
     }
+
+    pub fn index(&self) -> usize {
+        self.index
+    }
 }
 
 pub struct DisplayMarker<'a> {
@@ -2079,6 +2097,12 @@ pub struct ValueStyle {
     pub footnotes: Vec<Arc<Footnote>>,
 }
 
+impl ValueStyle {
+    pub fn is_empty(&self) -> bool {
+        self.style.is_none() && self.subscripts.is_empty() && self.footnotes.is_empty()
+    }
+}
+
 impl ValueInner {
     // Returns an object that will format this value.  Settings on `options`
     // control whether variable and value labels are included.
diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs
new file mode 100644 (file)
index 0000000..e421b97
--- /dev/null
@@ -0,0 +1,700 @@
+use core::f64;
+use std::{
+    borrow::Cow,
+    fmt::Write as _,
+    io::{Cursor, Result as IoResult, Seek, Write},
+    sync::Arc,
+};
+
+use binrw::{BinWrite, Endian};
+use quick_xml::{
+    events::{attributes::Attribute, BytesText},
+    writer::Writer as XmlWriter,
+    ElementWriter,
+};
+use serde::Serialize;
+use smallstr::SmallString;
+use zip::{result::ZipResult, write::SimpleFileOptions, ZipWriter};
+
+use crate::{
+    format::{Format, Type},
+    output::{
+        driver::Driver,
+        pivot::{
+            Axis2, CellStyle, Color, FontStyle, HeadingRegion, HorzAlign, PivotTable, Value,
+            ValueInner, ValueStyle, VertAlign,
+        },
+        Item,
+    },
+    settings::Show,
+};
+
+fn light_table_name(table_id: u64) -> String {
+    format!("{:010}_lightTableData.bin", table_id)
+}
+
+pub struct SpvWriter<W>
+where
+    W: Write + Seek,
+{
+    writer: ZipWriter<W>,
+    needs_page_break: bool,
+    next_table_id: u64,
+}
+
+impl<W> SpvWriter<W>
+where
+    W: Write + Seek,
+{
+    pub fn new(writer: W) -> Self {
+        Self {
+            writer: ZipWriter::new(writer),
+            needs_page_break: false,
+            next_table_id: 1,
+        }
+    }
+
+    pub fn close(mut self) -> ZipResult<W> {
+        self.writer
+            .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())?;
+        write!(&mut self.writer, "allowPivoting=true")?;
+        self.writer.finish()
+    }
+
+    fn page_break_before(&mut self) -> bool {
+        let page_break_before = self.needs_page_break;
+        self.needs_page_break = false;
+        page_break_before
+    }
+
+    fn write_table(&mut self, item: &Item, pivot_table: &PivotTable) -> Container {
+        let table_id = self.next_table_id;
+        self.next_table_id += 1;
+
+        let mut content = Vec::new();
+        let mut cursor = Cursor::new(&mut content);
+        Header::new(pivot_table).write_le(&mut cursor).unwrap();
+
+        self.writer
+            .start_file(light_table_name(table_id), SimpleFileOptions::default())
+            .unwrap(); // XXX
+        self.writer.write_all(&content).unwrap(); // XXX
+
+        Container {
+            page_break_before: self.page_break_before(),
+            label: Label(item.label().into_owned()),
+            show: item.show,
+            command_name: item.command_name.clone(),
+            content: Content::Table(Table {
+                table_properties: None,
+                table_structure: TableStructure,
+                table_id,
+                subtype: match &pivot_table.subtype {
+                    Some(subtype) => subtype.display(pivot_table).to_string(),
+                    None => String::from("unknown"),
+                },
+            }),
+        }
+    }
+}
+
+impl<W> Driver for SpvWriter<W>
+where
+    W: Write + Seek,
+{
+    fn name(&self) -> Cow<'static, str> {
+        Cow::from("spv")
+    }
+
+    fn write(&mut self, item: &Arc<Item>) {
+        match &item.details {
+            super::Details::Chart => todo!(),
+            super::Details::Image => todo!(),
+            super::Details::Group(items) => todo!(),
+            super::Details::Message(diagnostic) => todo!(),
+            super::Details::PageBreak => {
+                self.needs_page_break = true;
+                return;
+            }
+            super::Details::Table(pivot_table) => self.write_table(&*item, pivot_table),
+            super::Details::Text(text) => todo!(),
+        };
+        todo!()
+    }
+}
+
+struct Heading {
+    command_name: Option<String>,
+    show: bool,
+    label: Label,
+    children: Vec<Child>,
+}
+
+impl Heading {
+    fn emit<W>(&self, writer: &mut XmlWriter<W>) -> IoResult<()>
+    where
+        W: Write,
+    {
+        let mut element = writer.create_element("heading");
+        if let Some(command_name) = &self.command_name {
+            element = element.with_attribute(("commandName", command_name.as_str()));
+        }
+        if !self.show {
+            element = element.with_attribute(("visibility", "collapsed"));
+        }
+        element.write_inner_content(|writer| {
+            self.label.emit(writer)?;
+            Ok(())
+        })?;
+        Ok(())
+    }
+}
+
+enum Child {
+    Container(Container),
+    Heading(Box<Heading>),
+}
+
+impl Child {
+    fn emit<W>(&self, writer: &mut XmlWriter<W>) -> IoResult<()>
+    where
+        W: Write,
+    {
+        match self {
+            Child::Container(container) => container.emit(writer),
+            Child::Heading(heading) => heading.emit(writer),
+        }
+    }
+}
+
+fn maybe_with_attribute<'a, 'b, W, I>(
+    element: ElementWriter<'a, W>,
+    attr: Option<I>,
+) -> ElementWriter<'a, W>
+where
+    I: Into<Attribute<'b>>,
+{
+    if let Some(attr) = attr {
+        element.with_attribute(attr)
+    } else {
+        element
+    }
+}
+
+struct Container {
+    page_break_before: bool,
+    label: Label,
+    show: bool,
+    command_name: Option<String>,
+    content: Content,
+}
+
+impl Container {
+    fn emit<W>(&self, writer: &mut XmlWriter<W>) -> IoResult<()>
+    where
+        W: Write,
+    {
+        let mut element = writer
+            .create_element("container")
+            .with_attribute(("visibility", if self.show { "visible" } else { "hidden" }));
+        if self.page_break_before {
+            element = element.with_attribute(("page-break-before", "always"));
+        }
+        element.write_inner_content(|writer| {
+            self.label.emit(writer)?;
+            self.content
+                .emit(writer, self.command_name.as_ref().map(|name| name.as_str()))?;
+            Ok(())
+        })?;
+        Ok(())
+    }
+}
+
+struct Label(String);
+
+impl Label {
+    fn emit<W>(&self, writer: &mut XmlWriter<W>) -> IoResult<()>
+    where
+        W: Write,
+    {
+        writer
+            .create_element("label")
+            .write_text_content(BytesText::new(&self.0))?;
+        Ok(())
+    }
+}
+
+enum Content {
+    Table(Table),
+}
+
+impl Content {
+    fn emit<W>(&self, writer: &mut XmlWriter<W>, command_name: Option<&str>) -> IoResult<()>
+    where
+        W: Write,
+    {
+        match self {
+            Content::Table(table) => table.emit(writer, command_name),
+        }
+    }
+
+    fn element<'a, W>(
+        writer: &'a mut XmlWriter<W>,
+        name: &'static str,
+        command_name: Option<&str>,
+    ) -> ElementWriter<'a, W> {
+        let element = writer.create_element(name);
+        let element = maybe_with_attribute(
+            element,
+            command_name.map(|command_name| ("commandName", command_name)),
+        );
+        element
+    }
+}
+
+struct Table {
+    table_properties: Option<()>,
+
+    table_structure: TableStructure,
+    table_id: u64,
+    subtype: String,
+}
+
+impl Table {
+    fn emit<W>(&self, writer: &mut XmlWriter<W>, command_name: Option<&str>) -> IoResult<()>
+    where
+        W: Write,
+    {
+        Content::element(writer, "vtb:table", command_name)
+            .with_attribute(("type", "table"))
+            .with_attribute(("tableId", Cow::from(format!("{}", self.table_id))))
+            .with_attribute(("subtype", self.subtype.as_str()))
+            .write_inner_content(|w| {
+                w.create_element("vtb:TableStructure")
+                    .write_inner_content(|w| {
+                        w.create_element("vtb:dataPath")
+                            .write_text_content(BytesText::new(&light_table_name(self.table_id)))?;
+                        Ok(())
+                    })?;
+                Ok(())
+            })?;
+        Ok(())
+    }
+}
+
+#[derive(Serialize)]
+struct TableStructure;
+
+struct Bool(bool);
+impl BinWrite for Bool {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: binrw::Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        (self.0 as u8).write_options(writer, endian, args)
+    }
+}
+
+struct SpvString<'a>(&'a str);
+impl<'a> SpvString<'a> {
+    fn optional(s: &'a Option<String>) -> Self {
+        Self(s.as_ref().map_or("", |s| s.as_str()))
+    }
+}
+impl BinWrite for SpvString<'_> {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: binrw::Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        let length = self.0.len() as u32;
+        (length, self.0.as_bytes()).write_options(writer, endian, args)
+    }
+}
+
+#[derive(BinWrite)]
+#[bw(little)]
+struct Header {
+    #[bw(magic(1u16))]
+    version: u8,
+
+    x0: Bool,
+    x1: Bool,
+    rotate_inner_column_labels: Bool,
+    rotate_outer_row_labels: Bool,
+    x2: Bool,
+    x3: u32,
+    min_col_heading_width: i32,
+    max_col_heading_width: i32,
+    min_row_heading_width: i32,
+    max_row_heading_width: i32,
+}
+
+impl Header {
+    fn new(pivot_table: &PivotTable) -> Self {
+        Self {
+            version: 3,
+            x0: Bool(true),
+            x1: Bool(false),
+            rotate_inner_column_labels: Bool(pivot_table.rotate_inner_column_labels),
+            rotate_outer_row_labels: Bool(pivot_table.rotate_outer_row_labels),
+            x2: Bool(true),
+            x3: 0x15,
+            min_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].start()
+                as i32,
+            max_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].end()
+                as i32,
+            min_row_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Rows].start()
+                as i32,
+            max_row_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Rows].end()
+                as i32,
+        }
+    }
+}
+
+impl Show {
+    fn as_spv(this: &Option<Show>) -> u8 {
+        match this {
+            None => 0,
+            Some(Show::Value) => 1,
+            Some(Show::Label) => 2,
+            Some(Show::Both) => 3,
+        }
+    }
+}
+
+struct Count(u64);
+
+impl Count {
+    fn new<W>(writer: &mut W) -> binrw::BinResult<Self>
+    where
+        W: Write + Seek,
+    {
+        0u32.write_le(writer)?;
+        Ok(Self(writer.stream_position()?))
+    }
+
+    fn finish<W>(self, writer: &mut W, endian: Endian) -> binrw::BinResult<()>
+    where
+        W: Write + Seek,
+    {
+        let saved_position = writer.stream_position()?;
+        let n_bytes = saved_position - self.0;
+        writer.seek(std::io::SeekFrom::Start(self.0 - 4))?;
+        (n_bytes as u32).write_options(writer, endian, ())?;
+        writer.seek(std::io::SeekFrom::Start(saved_position))?;
+        Ok(())
+    }
+
+    fn finish_le32<W>(self, writer: &mut W) -> binrw::BinResult<()>
+    where
+        W: Write + Seek,
+    {
+        self.finish(writer, Endian::Little)
+    }
+
+    fn finish_be32<W>(self, writer: &mut W) -> binrw::BinResult<()>
+    where
+        W: Write + Seek,
+    {
+        self.finish(writer, Endian::Big)
+    }
+}
+
+#[derive(Default)]
+struct StylePair<'a> {
+    font_style: Option<&'a FontStyle>,
+    cell_style: Option<&'a CellStyle>,
+}
+
+impl BinWrite for Color {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        let mut s = SmallString::<[u8; 16]>::new();
+        write!(&mut s, "{}", self.without_alpha().display_css()).unwrap();
+        SpvString(&s).write_options(writer, endian, args)
+    }
+}
+
+impl BinWrite for FontStyle {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        let typeface = if self.font.is_empty() {
+            "SansSerif"
+        } else {
+            self.font.as_str()
+        };
+        (
+            Bool(self.bold),
+            Bool(self.italic),
+            Bool(self.underline),
+            Bool(true),
+            self.fg[0],
+            self.bg[0],
+            SpvString(typeface),
+            (self.size as f64 * 1.33).ceil() as u8,
+        )
+            .write_options(writer, endian, args)
+    }
+}
+
+impl HorzAlign {
+    fn as_spv(&self, decimal: u32) -> u32 {
+        match self {
+            HorzAlign::Right => 4,
+            HorzAlign::Left => 2,
+            HorzAlign::Center => 0,
+            HorzAlign::Decimal { .. } => decimal,
+        }
+    }
+
+    fn decimal_offset(&self) -> Option<f64> {
+        match *self {
+            HorzAlign::Decimal { offset, .. } => Some(offset),
+            _ => None,
+        }
+    }
+}
+
+impl VertAlign {
+    fn as_spv(&self) -> u32 {
+        match self {
+            VertAlign::Top => 1,
+            VertAlign::Middle => 0,
+            VertAlign::Bottom => 3,
+        }
+    }
+}
+
+impl BinWrite for CellStyle {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        (
+            self.horz_align
+                .map_or(0xffffffad, |horz_align| horz_align.as_spv(6)),
+            self.vert_align.as_spv(),
+            self.horz_align
+                .map(|horz_align| horz_align.decimal_offset())
+                .unwrap_or_default(),
+            u16::try_from(self.margins[Axis2::X][0]).unwrap_or_default(),
+            u16::try_from(self.margins[Axis2::X][1]).unwrap_or_default(),
+            u16::try_from(self.margins[Axis2::Y][0]).unwrap_or_default(),
+            u16::try_from(self.margins[Axis2::Y][1]).unwrap_or_default(),
+        )
+            .write_options(writer, endian, args)
+    }
+}
+
+impl<'a> BinWrite for StylePair<'a> {
+    type Args<'b> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        if let Some(font_style) = self.font_style {
+            (0x31u8, font_style).write_options(writer, endian, args)?;
+        } else {
+            0x58u8.write_options(writer, endian, args)?;
+        }
+
+        if let Some(cell_style) = self.cell_style {
+            (0x31u8, cell_style).write_options(writer, endian, args)?;
+        } else {
+            0x58u8.write_options(writer, endian, args)?;
+        }
+
+        Ok(())
+    }
+}
+
+struct OptionalStyle<'a> {
+    style: &'a Option<Box<ValueStyle>>,
+    template: Option<&'a str>,
+}
+
+impl<'a> BinWrite for OptionalStyle<'a> {
+    type Args<'b> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: binrw::Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        if self.style.as_ref().is_some_and(|style| !style.is_empty()) || self.template.is_some() {
+            0x31u8.write_options(writer, endian, args)?;
+            let default_style = Default::default();
+            let style = self.style.as_ref().unwrap_or(&default_style);
+
+            (style.footnotes.len() as u32).write_options(writer, endian, args)?;
+            for footnote in &style.footnotes {
+                (footnote.index() as u16).write_options(writer, endian, args)?;
+            }
+
+            (style.subscripts.len() as u32).write_options(writer, endian, args)?;
+            for subscript in &style.subscripts {
+                SpvString(subscript.as_str()).write_options(writer, endian, args)?;
+            }
+            let v3_start = Count::new(writer)?;
+            let template_string_start = Count::new(writer)?;
+            if let Some(template) = self.template {
+                Count::new(writer)?.finish_le32(writer)?;
+                (0x31u8, SpvString(template)).write_options(writer, endian, args)?;
+            }
+            template_string_start.finish_le32(writer)?;
+            style
+                .style
+                .as_ref()
+                .map_or_else(
+                    || StylePair::default(),
+                    |area_style| StylePair {
+                        font_style: Some(&area_style.font_style),
+                        cell_style: Some(&area_style.cell_style),
+                    },
+                )
+                .write_options(writer, endian, args)?;
+            v3_start.finish_le32(writer)
+        } else {
+            0x58u8.write_options(writer, endian, args)
+        }
+    }
+}
+
+struct SpvFormat {
+    format: Format,
+    honor_small: bool,
+}
+
+impl BinWrite for SpvFormat {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: binrw::Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        let type_ = if self.format.type_() == Type::F && self.honor_small {
+            40
+        } else {
+            self.format.type_().into()
+        };
+        (((type_ as u32) << 16) | ((self.format.w() as u32) << 8) | (self.format.d() as u32))
+            .write_options(writer, endian, args)
+    }
+}
+
+impl BinWrite for Value {
+    type Args<'a> = ();
+
+    fn write_options<W: Write + Seek>(
+        &self,
+        writer: &mut W,
+        endian: binrw::Endian,
+        args: Self::Args<'_>,
+    ) -> binrw::BinResult<()> {
+        match &self.inner {
+            ValueInner::Number {
+                show,
+                format,
+                honor_small,
+                value,
+                var_name,
+                value_label,
+            } => {
+                if var_name.is_some() || value_label.is_some() {
+                    2u8.write_options(writer, endian, args)?;
+                    //write_optional_style(self.styling.as_ref(),writer, endian, args)?;
+                    (
+                        SpvFormat {
+                            format: *format,
+                            honor_small: *honor_small,
+                        },
+                        value.unwrap_or(-f64::MAX),
+                        SpvString::optional(var_name),
+                        SpvString::optional(value_label),
+                        Show::as_spv(show),
+                    )
+                        .write_options(writer, endian, args)?;
+                } else {
+                    1u8.write_options(writer, endian, args)?;
+                    //write_optional_style(self.styling.as_ref(),writer, endian, args)?;
+                    value
+                        .unwrap_or(-f64::MAX)
+                        .write_options(writer, endian, args)?;
+                    Show::as_spv(show).write_options(writer, endian, args)?;
+                }
+            }
+            ValueInner::String {
+                show,
+                hex,
+                s,
+                var_name,
+                value_label,
+            } => todo!(),
+            ValueInner::Variable {
+                show,
+                var_name,
+                variable_label,
+            } => todo!(),
+            ValueInner::Text {
+                user_provided,
+                local,
+                c,
+                id,
+            } => todo!(),
+            ValueInner::Template { args, local, id } => todo!(),
+            ValueInner::Empty => todo!(),
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::output::spv::Heading;
+
+    #[test]
+    fn serialize() {
+        let heading = Heading {
+            command_name: Some("foo".into()),
+            show: false,
+            label: super::Label("bar".into()),
+            children: Vec::new(),
+        };
+        let mut output = Vec::new();
+        let mut writer = quick_xml::writer::Writer::new(&mut output);
+        heading.emit(&mut writer).unwrap();
+        drop(writer);
+        let output = String::from_utf8(output).unwrap();
+        println!("{output}");
+    }
+}