A typst plugin for unit-aware calculations
typst

Rename to evaltor and use fend backend for now

+62 -85
+8 -1
Cargo.lock
··· 3 version = 4 4 5 [[package]] 6 - name = "numbat-typst" 7 version = "0.1.0" 8 dependencies = [ 9 "typst-wasm-protocol", 10 ] 11 12 [[package]] 13 name = "proc-macro2"
··· 3 version = 4 4 5 [[package]] 6 + name = "evaltor" 7 version = "0.1.0" 8 dependencies = [ 9 + "fend-core", 10 "typst-wasm-protocol", 11 ] 12 + 13 + [[package]] 14 + name = "fend-core" 15 + version = "1.5.7" 16 + source = "registry+https://github.com/rust-lang/crates.io-index" 17 + checksum = "3f775cab5068a34b942b110dcb11f42c96d376b681c45e604884da6059cb9d2c" 18 19 [[package]] 20 name = "proc-macro2"
+2 -1
Cargo.toml
··· 1 [package] 2 - name = "numbat-typst" 3 version = "0.1.0" 4 edition = "2024" 5 ··· 7 crate-type = ["cdylib"] 8 9 [dependencies] 10 typst-wasm-protocol = "0.0.2"
··· 1 [package] 2 + name = "evaltor" 3 version = "0.1.0" 4 edition = "2024" 5 ··· 7 crate-type = ["cdylib"] 8 9 [dependencies] 10 + fend-core = "1.5.7" 11 typst-wasm-protocol = "0.0.2"
+6 -2
README.md
··· 1 - # Typst plugin for [numbat](https://numbat.dev) 2 3 Built using [typst-wasm](https://github.com/sjfhsjfh/typst-wasm). 4 5 First compile the rust code to WASM using: 6 7 ```sh 8 cargo build --target wasm32-unknown-unknown --release 9 ``` 10 ··· 17 Then just load the functions like: 18 19 ```typ 20 - #import "@local/numbat:0.1.0": to_uppercase 21 ```
··· 1 + # Evaltor: Typst plugin for unit-aware calculations 2 + 3 + I initially wanted to use [numbat](https://github.com/sharkdp/numbat) but since it requires access to filesystem I ended up choosing [fend](https://printfn.github.io/fend) as the backend. Maybe one day I will try to implement InMemory module importer where all the nbt files are imported at compilation. 4 + 5 6 Built using [typst-wasm](https://github.com/sjfhsjfh/typst-wasm). 7 8 First compile the rust code to WASM using: 9 10 ```sh 11 + rustup target add wasm32-unknown-unknown 12 cargo build --target wasm32-unknown-unknown --release 13 ``` 14 ··· 21 Then just load the functions like: 22 23 ```typ 24 + #import "@local/evaltor:0.1.0": print_eval 25 ```
+14 -60
src/lib.rs
··· 1 use std::str; 2 use typst_wasm_protocol::wasm_export; 3 4 - // Simple string operation function 5 #[wasm_export] 6 - fn to_uppercase(input: &[u8]) -> Vec<u8> { 7 - let input_str = match str::from_utf8(input) { 8 - Ok(s) => s, 9 - Err(_) => return b"Invalid UTF-8 input".to_vec(), 10 - }; 11 - 12 - input_str.to_uppercase().into_bytes() 13 - } 14 - 15 - // Using custom export name 16 - #[wasm_export(export_rename = "count_chars")] 17 - fn count_characters(input: &[u8]) -> Vec<u8> { 18 - let input_str = match str::from_utf8(input) { 19 - Ok(s) => s, 20 - Err(_) => return b"Invalid UTF-8 input".to_vec(), 21 - }; 22 - 23 - format!("Character count: {}", input_str.chars().count()).into_bytes() 24 - } 25 - 26 - // Function returning Result type 27 - #[wasm_export] 28 - fn divide_numbers(input: &[u8]) -> Result<String, String> { 29 - let input_str = str::from_utf8(input).map_err(|e| format!("UTF-8 error: {}", e))?; 30 - 31 - let numbers: Vec<&str> = input_str.split(',').collect(); 32 - if numbers.len() != 2 { 33 - return Err("Expected two comma-separated numbers".to_string()); 34 - } 35 - 36 - let a: f64 = numbers[0] 37 - .trim() 38 - .parse() 39 - .map_err(|_| "First value is not a valid number".to_string())?; 40 - let b: f64 = numbers[1] 41 - .trim() 42 - .parse() 43 - .map_err(|_| "Second value is not a valid number".to_string())?; 44 - 45 - if b == 0.0 { 46 - return Err("Cannot divide by zero".to_string()); 47 } 48 - 49 - let result = a / b; 50 - Ok(format!("Result: {:.2}", result)) 51 - } 52 - 53 - // Function returning Result<String, String> type 54 - #[wasm_export] 55 - fn validate_email(input: &[u8]) -> Result<String, String> { 56 - let email = str::from_utf8(input) 57 - .map_err(|e| format!("UTF-8 error: {}", e))? 58 - .trim(); 59 - 60 - // Simple email validation 61 - if !email.contains('@') || !email.contains('.') { 62 - return Err("Invalid email format".to_string()); 63 - } 64 - 65 - Ok("Email is valid".to_string()) 66 }
··· 1 use std::str; 2 use typst_wasm_protocol::wasm_export; 3 4 #[wasm_export] 5 + fn fend_evaluate(query: &[u8]) -> Result<String, String> { 6 + let input = str::from_utf8(query).map_err(|e| format!("UTF-8 error: {}", e))?; 7 + let mut ctx = fend_core::Context::new(); 8 + let mut result = String::new(); 9 + match fend_core::evaluate(input, &mut ctx) { 10 + Ok(res) => { 11 + if !res.output_is_empty() { 12 + result.push_str(res.get_main_result()); 13 + } 14 + } 15 + Err(msg) => { 16 + return Err(format!("Error: {msg}")); 17 + } 18 } 19 + Ok(result.to_string()) 20 }
+6 -1
src/lib.typ
··· 1 // Should compile numbat_typst.wasm first 2 // using: 3 - #import plugin("../target/wasm32-unknown-unknown/release/numbat_typst.wasm"): to_uppercase, count_chars, divide_numbers, validate_email
··· 1 // Should compile numbat_typst.wasm first 2 // using: 3 + #import plugin("../target/wasm32-unknown-unknown/release/evaltor.wasm"):fend_evaluate 4 + 5 + #let print_eval(input) = { 6 + let output = str(fend_evaluate(bytes(input))) 7 + [#input = #output] 8 + }
+25 -19
tests/examples.typ
··· 1 - #import "@local/numbat:0.1.0": to_uppercase, count_chars, divide_numbers, validate_email 2 3 - = Tests 4 5 - Hi there! 6 7 - #let input = "Hello, Typst!" 8 9 - // Call the to_uppercase function 10 - #let uppercase = str(to_uppercase(bytes(input))) 11 - Original: #input\ 12 - Uppercase: #uppercase 13 14 - // Call the count_chars function (note this is exported with a custom name) 15 - #let char_count = str(count_chars(bytes(input))) 16 - #char_count 17 18 - // Call a function that returns a Result type 19 - #let division_result = str(divide_numbers(bytes("10,2"))) 20 - #division_result 21 22 - // Handle potential errors, uncomment to see the error handling in action 23 - // #let division_error = divide_numbers(bytes("10,0")) 24 25 - // Use a function with Result<String, String> type 26 - #let email_valid = str(validate_email(bytes("user@example.com"))) 27 - Email validation: #email_valid
··· 1 + #import "@local/evaltor:0.1.0": print_eval 2 3 + #show heading: it => { 4 + it.body 5 + v(1pt) 6 + } 7 + #set text(size:16pt) 8 9 + = `print_eval` with `fend` backend 10 11 + It will print the input along with its result, calculated using `fend` backend, which is unit-aware. 12 13 + Some examples: 14 15 + #print_eval("2 km + 30 m") 16 17 + #print_eval("1 ft to cm") 18 19 + #print_eval("5'7\" to cm") 20 + 21 + #print_eval("cos (pi/4) + i * (sin (pi/4))") 22 23 + #print_eval("0b1001 + 3") 24 + 25 + #print_eval("0xffff to decimal") 26 + 27 + #print_eval("100 C to F") 28 + 29 + #print_eval("temperature = 30 °C; temperature to °F") 30 + 31 + #print_eval("(10um)/(0.334cm) + (0.6mm)/(35.28cm) + (1mm)/(47.57cm) + (0.4mm)/(35.28cm) + (2um)/(3.56cm) to percent") 32 + 33 + *Currently, random numbers are not available.*
+1 -1
typst.toml
··· 1 [package] 2 - name = "numbat" 3 version = "0.1.0" 4 entrypoint = "src/lib.typ"
··· 1 [package] 2 + name = "evaltor" 3 version = "0.1.0" 4 entrypoint = "src/lib.typ"