diff --git a/.github/workflows/rust-build-test.yaml b/.github/workflows/rust-build-test.yaml index 6de247a0..08422432 100644 --- a/.github/workflows/rust-build-test.yaml +++ b/.github/workflows/rust-build-test.yaml @@ -18,6 +18,12 @@ jobs: - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Install wasm-bindgen-cli + run: cargo install -f wasm-bindgen-cli --version 0.2.100 + + - name: Add wasm32 target + run: rustup target add wasm32-unknown-unknown - name: Build run: cargo build --release --verbose diff --git a/.gitignore b/.gitignore index 52a2a589..682e5812 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/* **/node_modules/** .DS_Store +bindings/wasm/pkg/ diff --git a/Cargo.lock b/Cargo.lock index 677cbf79..dadda1de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,17 +8,6 @@ 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" @@ -55,12 +44,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - [[package]] name = "bitcode" version = "0.6.3" @@ -91,15 +74,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" -[[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.15.4" @@ -118,36 +92,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[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-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" -dependencies = [ - "jobserver", - "libc", -] [[package]] name = "cfg-if" @@ -190,16 +139,6 @@ dependencies = [ "phf_codegen", ] -[[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 = "console_error_panic_hook" version = "0.1.7" @@ -210,12 +149,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "convert_case" version = "0.6.0" @@ -231,15 +164,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.4.0" @@ -255,16 +179,6 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" -[[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 = "csv" version = "1.3.0" @@ -305,17 +219,6 @@ dependencies = [ "powerfmt", ] -[[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.10.0" @@ -332,16 +235,6 @@ dependencies = [ "miniz_oxide", ] -[[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.2.13" @@ -365,15 +258,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "iana-time-zone" version = "0.1.60" @@ -403,27 +287,20 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - [[package]] name = "ironcalc" version = "0.5.0" dependencies = [ "bitcode", "chrono", + "flate2", "ironcalc_base", "itertools", "roxmltree", "serde", "serde_json", "thiserror", + "time", "uuid", "zip", ] @@ -471,15 +348,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "jobserver" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.69" @@ -624,29 +492,6 @@ dependencies = [ "regex", ] -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[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 = "phf" version = "0.11.2" @@ -685,12 +530,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - [[package]] name = "portable-atomic" version = "1.7.0" @@ -906,9 +745,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.4.5" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ "js-sys", "serde", @@ -937,40 +776,12 @@ 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 = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "2.0.77" @@ -1015,6 +826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde", @@ -1027,12 +839,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1061,12 +867,6 @@ dependencies = [ "serde", ] -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1077,6 +877,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" name = "wasm" version = "0.5.0" dependencies = [ + "ironcalc", "ironcalc_base", "serde", "serde-wasm-bindgen", @@ -1261,45 +1062,9 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes", "byteorder", - "bzip2", - "constant_time_eq", "crc32fast", "crossbeam-utils", "flate2", - "hmac", - "pbkdf2", - "sha1", "time", - "zstd", -] - -[[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", -] - -[[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", -] - -[[package]] -name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" -dependencies = [ - "cc", - "pkg-config", ] diff --git a/Makefile b/Makefile index 5f781d17..5885894f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ .PHONY: lint lint: cargo fmt -- --check - cargo clippy --all-targets --all-features -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings + cargo clippy --workspace --exclude wasm --all-targets --all-features -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings + cd bindings/wasm/ && cargo clippy --target wasm32-unknown-unknown -- -W clippy::unwrap_used -W clippy::expect_used -W clippy::panic -D warnings cd webapp/IronCalc/ && npm install && npm run check cd webapp/app.ironcalc.com/frontend/ && npm install && npm run check @@ -15,7 +16,12 @@ tests: lint make remove-artifacts # Regretabbly we need to build the wasm twice, once for the nodejs tests # and a second one for the vitest. - cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs && make + cd bindings/wasm/ && \ + wasm-pack build --target nodejs --out-name ironcalc && \ + cargo build --release --target wasm32-unknown-unknown --bin xlsx_wasm && \ + wasm-bindgen --target nodejs --typescript --out-dir pkg --out-name xlsx ../../target/wasm32-unknown-unknown/release/xlsx_wasm.wasm && \ + node tests/test.mjs && \ + make cd webapp/IronCalc/ && npm run test cd bindings/python && ./run_tests.sh && ./run_examples.sh diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 30455a4a..99f5c419 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -8,16 +8,19 @@ repository = "https://github.com/ironcalc/ironcalc" edition = "2021" [lib] +name = "ironcalc_wasm" crate-type = ["cdylib"] +[[bin]] +name = "xlsx_wasm" +path = "src/xlsx.rs" + [dependencies] -# Uses `../ironcalc/base` when used locally, and uses -# the inicated version from crates.io when published. -# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations +ironcalc = { path = "../../xlsx", version = "0.5.0" } ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] } serde = { version = "1.0", features = ["derive"] } wasm-bindgen = "0.2.100" -serde-wasm-bindgen = "0.4" +serde-wasm-bindgen = "0.6" [dev-dependencies] -wasm-bindgen-test = "0.3.38" +wasm-bindgen-test = "0.3.38" \ No newline at end of file diff --git a/bindings/wasm/Makefile b/bindings/wasm/Makefile index 67c9ffa6..399b2335 100644 --- a/bindings/wasm/Makefile +++ b/bindings/wasm/Makefile @@ -1,3 +1,20 @@ +# --------------------------------------------------------------------------- +# Build strategy +# --------------------------------------------------------------------------- +# We need two separate WASM bundles in the same NPM package: +# 1) Core engine → @ironcalc/wasm (small, always shipped) +# 2) XLSX helpers → @ironcalc/wasm/xlsx (large, optional) +# +# The natural way would be two wasm-pack invocations – one default lib and one +# wasm-pack build --bin xlsx_wasm … +# Unfortunately, on the stable Rust tool-chain wasm-pack's `--bin` flag relies +# on Cargo's unstable `--out-dir`/`--artifact-dir` feature and therefore fails. +# +# Until that becomes stable (or we switch to a nightly tool-chain), we compile +# the second binary with plain `cargo build` and then run `wasm-bindgen` +# ourselves. As soon as the flag is stabilised we can collapse back to the +# two simple wasm-pack commands shown above and delete the hand-rolled steps. +# --------------------------------------------------------------------------- # In some platforms, python is called python3 PYTHON := $(shell command -v python 2>/dev/null || command -v python3 2>/dev/null) @@ -6,15 +23,35 @@ ifeq ($(PYTHON),) $(error No python found. Please install python.) endif +TEMP_DIR := pkg_xlsx + all: - wasm-pack build --target web --scope ironcalc --release + wasm-pack build --target web --scope ironcalc --out-dir pkg --out-name ironcalc --release + + # Build XLSX helper separately (cargo + wasm-bindgen) + cargo build --release --target wasm32-unknown-unknown --bin xlsx_wasm + wasm-bindgen --target web --typescript --out-dir $(TEMP_DIR) --out-name xlsx ../../target/wasm32-unknown-unknown/release/xlsx_wasm.wasm + + # Move generated files into main pkg directory + mv $(TEMP_DIR)/xlsx_bg.wasm pkg/xlsx_bg.wasm + mv $(TEMP_DIR)/xlsx.js pkg/xlsx.js + mv $(TEMP_DIR)/xlsx.d.ts pkg/xlsx.d.ts + cp README.pkg.md pkg/README.md npx tsc types.ts --target esnext --module esnext $(PYTHON) fix_types.py rm -f types.js + rm -rf $(TEMP_DIR) tests: - wasm-pack build --target nodejs && node tests/test.mjs + wasm-pack build --target nodejs --out-dir pkg --out-name ironcalc + cargo build --release --target wasm32-unknown-unknown --bin xlsx_wasm + wasm-bindgen --target nodejs --typescript --out-dir $(TEMP_DIR) --out-name xlsx ../../target/wasm32-unknown-unknown/release/xlsx_wasm.wasm + mv $(TEMP_DIR)/xlsx_bg.wasm pkg/xlsx_bg.wasm || true + mv $(TEMP_DIR)/xlsx.js pkg/xlsx.js || true + mv $(TEMP_DIR)/xlsx.d.ts pkg/xlsx.d.ts || true + rm -rf $(TEMP_DIR) + node tests/test.mjs lint: cargo check diff --git a/bindings/wasm/README.pkg.md b/bindings/wasm/README.pkg.md index 8e62d0ec..916ed902 100644 --- a/bindings/wasm/README.pkg.md +++ b/bindings/wasm/README.pkg.md @@ -1,7 +1,6 @@ # IronCalc Web bindings -This package contains web bindings for IronCalc. Note that it does not contain the xlsx writer and reader, only the engine. - +This crate is used to build the web bindings for IronCalc. ## Usage @@ -15,8 +14,11 @@ And then in your TypeScript ```TypeScript import init, { Model } from "@ironcalc/wasm"; +import initXLSX, { toXLSXBytes, fromXLSXBytes } from "@ironcalc/wasm/xlsx"; + await init(); +await initXLSX(); function compute() { const model = new Model('en', 'UTC'); @@ -30,4 +32,15 @@ function compute() { } compute(); + +// create a new workbook and export as XLSX bytes +const model = new Model('Workbook1', 'en', 'UTC'); +model.setUserInput(0, 1, 1, '42'); +const xlsxBytes = toXLSXBytes(model.toBytes()); + +// load from those bytes +const roundTrippedBytes = fromXLSXBytes(xlsxBytes, 'Workbook1', 'en', 'UTC'); +const roundTripped = Model.fromBytes(roundTrippedBytes); + ``` + diff --git a/bindings/wasm/fix_types.py b/bindings/wasm/fix_types.py index 02285989..35942ddb 100644 --- a/bindings/wasm/fix_types.py +++ b/bindings/wasm/fix_types.py @@ -21,19 +21,20 @@ def fix_types(text: str): return text if __name__ == "__main__": - types_file = "pkg/wasm.d.ts" - with open(types_file) as f: - text = f.read() - text = fix_types(text) - with open(types_file, "wb") as f: - f.write(bytes(text, "utf8")) + dts_files = ["pkg/ironcalc.d.ts", "pkg/xlsx.d.ts"] + for types_file in dts_files: + with open(types_file) as f: + text = f.read() + text = fix_types(text) + with open(types_file, "wb") as f: + f.write(bytes(text, "utf8")) - js_file = "pkg/wasm.js" + js_files = ["pkg/ironcalc.js", "pkg/xlsx.js"] with open("types.js") as f: text_js = f.read() - with open(js_file) as f: - text = f.read() - with open(js_file, "wb") as f: - f.write(bytes("{}\n{}".format(text_js, text), "utf8")) - \ No newline at end of file + for js_file in js_files: + with open(js_file) as f: + text = f.read() + with open(js_file, "wb") as f: + f.write(bytes("{}\n{}".format(text_js, text), "utf8")) diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json new file mode 100644 index 00000000..1cad81fa --- /dev/null +++ b/bindings/wasm/package.json @@ -0,0 +1,28 @@ +{ + "name": "@ironcalc/wasm", + "version": "0.3.2", + "description": "IronCalc WebAssembly bindings (core engine + optional XLSX helpers)", + "private": false, + "main": "./pkg/ironcalc.js", + "types": "./pkg/ironcalc.d.ts", + "exports": { + ".": { + "require": "./pkg/ironcalc.js", + "import": "./pkg/ironcalc.js", + "types": "./pkg/ironcalc.d.ts" + }, + "./xlsx": { + "require": "./pkg/xlsx.js", + "import": "./pkg/xlsx.js", + "types": "./pkg/xlsx.d.ts" + } + }, + "files": [ + "pkg" + ], + "repository": { + "type": "git", + "url": "https://github.com/ironcalc/ironcalc" + }, + "license": "MIT OR Apache-2.0" +} \ No newline at end of file diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 8517b0af..532de67e 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -51,6 +51,7 @@ impl Model { Ok(Model { model }) } + #[wasm_bindgen(js_name = "fromBytes")] pub fn from_bytes(bytes: &[u8]) -> Result { let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?; Ok(Model { model }) diff --git a/bindings/wasm/src/xlsx.rs b/bindings/wasm/src/xlsx.rs new file mode 100644 index 00000000..1f85fdfb --- /dev/null +++ b/bindings/wasm/src/xlsx.rs @@ -0,0 +1,39 @@ +use ironcalc::{ + base::Model as BaseWorkbookModel, export::save_xlsx_to_writer, import::load_from_xlsx_bytes, +}; +use std::io::{BufWriter, Cursor, Write}; +use wasm_bindgen::prelude::{wasm_bindgen, JsError}; + +fn to_js_error(error: String) -> JsError { + JsError::new(&error.to_string()) +} + +#[wasm_bindgen(js_name = fromXLSXBytes)] +pub fn from_xlsx_bytes( + bytes: &[u8], + name: &str, + locale: &str, + timezone: &str, +) -> Result, JsError> { + let workbook = load_from_xlsx_bytes(bytes, name, locale, timezone) + .map_err(|e| to_js_error(e.to_string()))?; + let base_model = + BaseWorkbookModel::from_workbook(workbook).map_err(|e| to_js_error(e.to_string()))?; + Ok(base_model.to_bytes()) +} + +#[wasm_bindgen(js_name = toXLSXBytes)] +pub fn to_xlsx_bytes(bytes: &[u8]) -> Result, JsError> { + let workbook = BaseWorkbookModel::from_bytes(bytes).map_err(to_js_error)?; + let mut writer = BufWriter::new(Cursor::new(Vec::new())); + save_xlsx_to_writer(&workbook, &mut writer).map_err(|e| to_js_error(e.to_string()))?; + writer.flush().map_err(|e| to_js_error(e.to_string()))?; + Ok(writer + .into_inner() + .map_err(|e| to_js_error(e.to_string()))? + .into_inner()) +} + +fn main() { + // This is required for cargo to compile this as a binary +} diff --git a/bindings/wasm/tests/test.mjs b/bindings/wasm/tests/test.mjs index 03d0c1b8..1f3a7692 100644 --- a/bindings/wasm/tests/test.mjs +++ b/bindings/wasm/tests/test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert' -import { Model } from "../pkg/wasm.js"; +import { Model } from "../pkg/ironcalc.js"; +import { fromXLSXBytes, toXLSXBytes } from "../pkg/xlsx.js"; const DEFAULT_ROW_HEIGHT = 28; @@ -130,6 +131,58 @@ test("autofill", () => { assert.strictEqual(result, "23"); }); +test('toXLSXBytes returns data', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const bytes = toXLSXBytes(model.toBytes()); + assert.ok(bytes instanceof Uint8Array); + assert.ok(bytes.length > 0); +}); + +test('toBytes returns data', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + const bytes = model.toBytes(); + assert.ok(bytes instanceof Uint8Array); + assert.ok(bytes.length > 0); +}); + +test('fromBytes loads model', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + model.setUserInput(0, 1, 1, '42'); + const bytes = model.toBytes(); + const m2 = Model.fromBytes(bytes); + assert.strictEqual(m2.getCellContent(0, 1, 1), '42'); +}); + +test('fromXLSXBytes loads model', () => { + const model = new Model('Workbook1', 'en', 'UTC'); + model.setUserInput(0, 1, 1, '5'); + const xlsxBytes = toXLSXBytes(model.toBytes()); + const modelBytes = fromXLSXBytes(xlsxBytes, 'Workbook1', 'en', 'UTC'); + const m2 = Model.fromBytes(modelBytes); + assert.strictEqual(m2.getCellContent(0, 1, 1), '5'); +}); + +test('roundtrip via xlsx bytes', () => { + const m1 = new Model('Workbook1', 'en', 'UTC'); + m1.setUserInput(0, 1, 1, '7'); + m1.setUserInput(0, 1, 2, '=A1*3'); + const xlsxBytes = toXLSXBytes(m1.toBytes()); + const m2Bytes = fromXLSXBytes(xlsxBytes, 'Workbook1', 'en', 'UTC'); + const m2 = Model.fromBytes(m2Bytes); + m2.evaluate(); + assert.strictEqual(m2.getFormattedCellValue(0, 1, 2), '21'); +}); + +test('roundtrip via bytes', () => { + const m1 = new Model('Workbook1', 'en', 'UTC'); + m1.setUserInput(0, 1, 1, '9'); + m1.setUserInput(0, 1, 2, '=A1*4'); + const bytes = m1.toBytes(); + const m2 = Model.fromBytes(bytes); + m2.evaluate(); + assert.strictEqual(m2.getFormattedCellValue(0, 1, 2), '36'); +}); + test('insertRows shifts cells', () => { const model = new Model('Workbook1', 'en', 'UTC'); model.setUserInput(0, 1, 1, '42'); diff --git a/webapp/IronCalc/src/components/tests/model.test.ts b/webapp/IronCalc/src/components/tests/model.test.ts index 0d030c8c..4ff17ca0 100644 --- a/webapp/IronCalc/src/components/tests/model.test.ts +++ b/webapp/IronCalc/src/components/tests/model.test.ts @@ -5,7 +5,7 @@ import { expect, test } from "vitest"; // This is a simple test that showcases how to load the wasm module in the tests test("simple calculation", async () => { - const buffer = await readFile("node_modules/@ironcalc/wasm/wasm_bg.wasm"); + const buffer = await readFile("node_modules/@ironcalc/wasm/ironcalc_bg.wasm"); initSync(buffer); const model = new Model("workbook", "en", "UTC"); model.setUserInput(0, 1, 1, "=21*2"); diff --git a/webapp/IronCalc/src/components/tests/util.test.ts b/webapp/IronCalc/src/components/tests/util.test.ts index 370efaa5..3811ad88 100644 --- a/webapp/IronCalc/src/components/tests/util.test.ts +++ b/webapp/IronCalc/src/components/tests/util.test.ts @@ -31,7 +31,7 @@ test("decrease decimals", () => { }); test("format range to get the full formula", async () => { - const buffer = await readFile("node_modules/@ironcalc/wasm/wasm_bg.wasm"); + const buffer = await readFile("node_modules/@ironcalc/wasm/ironcalc_bg.wasm"); initSync(buffer); const selectedView: SelectedView = { diff --git a/xlsx/Cargo.toml b/xlsx/Cargo.toml index 713d3bbe..79d7549c 100644 --- a/xlsx/Cargo.toml +++ b/xlsx/Cargo.toml @@ -12,7 +12,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zip = "0.6" +zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } +flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } +time = { version = "0.3", features = ["wasm-bindgen"] } roxmltree = "0.19" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/xlsx/src/import/mod.rs b/xlsx/src/import/mod.rs index c1065fcc..35928ca8 100644 --- a/xlsx/src/import/mod.rs +++ b/xlsx/src/import/mod.rs @@ -153,3 +153,10 @@ pub fn load_from_icalc(file_name: &str) -> Result { .map_err(|e| XlsxError::IO(format!("Failed to decode file: {e}")))?; Model::from_workbook(workbook).map_err(XlsxError::Workbook) } + +/// Loads a [`Model`] from the bytes of an `ic` file. +pub fn load_from_icalc_bytes(bytes: &[u8]) -> Result { + let workbook: Workbook = bitcode::decode(bytes) + .map_err(|e| XlsxError::IO(format!("Failed to decode file: {}", e)))?; + Model::from_workbook(workbook).map_err(XlsxError::Workbook) +} diff --git a/xlsx/tests/test.rs b/xlsx/tests/test.rs index f86ec91a..5095bcc3 100644 --- a/xlsx/tests/test.rs +++ b/xlsx/tests/test.rs @@ -7,7 +7,9 @@ use uuid::Uuid; use ironcalc::compare::{test_file, test_load_and_saving}; use ironcalc::export::save_to_xlsx; -use ironcalc::import::{load_from_icalc, load_from_xlsx, load_from_xlsx_bytes}; +use ironcalc::import::{ + load_from_icalc, load_from_icalc_bytes, load_from_xlsx, load_from_xlsx_bytes, +}; use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment}; use ironcalc_base::{Model, UserModel}; @@ -62,6 +64,16 @@ fn test_load_from_xlsx_bytes() { assert_eq!(workbook.views[&0].sheet, 7); } +#[test] +fn test_load_from_icalc_bytes() { + let mut file = fs::File::open("tests/example.ic").unwrap(); + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes).unwrap(); + let model = load_from_icalc_bytes(&bytes).unwrap(); + let model_from_file = load_from_icalc("tests/example.ic").unwrap(); + assert_eq!(model.workbook, model_from_file.workbook); +} + #[test] fn no_grid() { let model = load_from_xlsx("tests/NoGrid.xlsx", "en", "UTC").unwrap();