···11-# Typst plugin for [numbat](https://numbat.dev)
11+# Evaltor: Typst plugin for unit-aware calculations
22+33+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.
44+2536Built using [typst-wasm](https://github.com/sjfhsjfh/typst-wasm).
4758First compile the rust code to WASM using:
69710```sh
1111+rustup target add wasm32-unknown-unknown
812cargo build --target wasm32-unknown-unknown --release
913```
1014···1721Then just load the functions like:
18221923```typ
2020-#import "@local/numbat:0.1.0": to_uppercase
2424+#import "@local/evaltor:0.1.0": print_eval
2125```
+14-60
src/lib.rs
···11use std::str;
22use typst_wasm_protocol::wasm_export;
3344-// Simple string operation function
54#[wasm_export]
66-fn to_uppercase(input: &[u8]) -> Vec<u8> {
77- let input_str = match str::from_utf8(input) {
88- Ok(s) => s,
99- Err(_) => return b"Invalid UTF-8 input".to_vec(),
1010- };
1111-1212- input_str.to_uppercase().into_bytes()
1313-}
1414-1515-// Using custom export name
1616-#[wasm_export(export_rename = "count_chars")]
1717-fn count_characters(input: &[u8]) -> Vec<u8> {
1818- let input_str = match str::from_utf8(input) {
1919- Ok(s) => s,
2020- Err(_) => return b"Invalid UTF-8 input".to_vec(),
2121- };
2222-2323- format!("Character count: {}", input_str.chars().count()).into_bytes()
2424-}
2525-2626-// Function returning Result type
2727-#[wasm_export]
2828-fn divide_numbers(input: &[u8]) -> Result<String, String> {
2929- let input_str = str::from_utf8(input).map_err(|e| format!("UTF-8 error: {}", e))?;
3030-3131- let numbers: Vec<&str> = input_str.split(',').collect();
3232- if numbers.len() != 2 {
3333- return Err("Expected two comma-separated numbers".to_string());
3434- }
3535-3636- let a: f64 = numbers[0]
3737- .trim()
3838- .parse()
3939- .map_err(|_| "First value is not a valid number".to_string())?;
4040- let b: f64 = numbers[1]
4141- .trim()
4242- .parse()
4343- .map_err(|_| "Second value is not a valid number".to_string())?;
4444-4545- if b == 0.0 {
4646- return Err("Cannot divide by zero".to_string());
55+fn fend_evaluate(query: &[u8]) -> Result<String, String> {
66+ let input = str::from_utf8(query).map_err(|e| format!("UTF-8 error: {}", e))?;
77+ let mut ctx = fend_core::Context::new();
88+ let mut result = String::new();
99+ match fend_core::evaluate(input, &mut ctx) {
1010+ Ok(res) => {
1111+ if !res.output_is_empty() {
1212+ result.push_str(res.get_main_result());
1313+ }
1414+ }
1515+ Err(msg) => {
1616+ return Err(format!("Error: {msg}"));
1717+ }
4718 }
4848-4949- let result = a / b;
5050- Ok(format!("Result: {:.2}", result))
5151-}
5252-5353-// Function returning Result<String, String> type
5454-#[wasm_export]
5555-fn validate_email(input: &[u8]) -> Result<String, String> {
5656- let email = str::from_utf8(input)
5757- .map_err(|e| format!("UTF-8 error: {}", e))?
5858- .trim();
5959-6060- // Simple email validation
6161- if !email.contains('@') || !email.contains('.') {
6262- return Err("Invalid email format".to_string());
6363- }
6464-6565- Ok("Email is valid".to_string())
1919+ Ok(result.to_string())
6620}
+6-1
src/lib.typ
···11// Should compile numbat_typst.wasm first
22// using:
33-#import plugin("../target/wasm32-unknown-unknown/release/numbat_typst.wasm"): to_uppercase, count_chars, divide_numbers, validate_email
33+#import plugin("../target/wasm32-unknown-unknown/release/evaltor.wasm"):fend_evaluate
44+55+#let print_eval(input) = {
66+ let output = str(fend_evaluate(bytes(input)))
77+ [#input = #output]
88+}
+25-19
tests/examples.typ
···11-#import "@local/numbat:0.1.0": to_uppercase, count_chars, divide_numbers, validate_email
11+#import "@local/evaltor:0.1.0": print_eval
2233-= Tests
33+#show heading: it => {
44+ it.body
55+ v(1pt)
66+}
77+#set text(size:16pt)
4855-Hi there!
99+= `print_eval` with `fend` backend
61077-#let input = "Hello, Typst!"
1111+It will print the input along with its result, calculated using `fend` backend, which is unit-aware.
81299-// Call the to_uppercase function
1010-#let uppercase = str(to_uppercase(bytes(input)))
1111-Original: #input\
1212-Uppercase: #uppercase
1313+Some examples:
13141414-// Call the count_chars function (note this is exported with a custom name)
1515-#let char_count = str(count_chars(bytes(input)))
1616-#char_count
1515+#print_eval("2 km + 30 m")
17161818-// Call a function that returns a Result type
1919-#let division_result = str(divide_numbers(bytes("10,2")))
2020-#division_result
1717+#print_eval("1 ft to cm")
21182222-// Handle potential errors, uncomment to see the error handling in action
2323-// #let division_error = divide_numbers(bytes("10,0"))
1919+#print_eval("5'7\" to cm")
2020+2121+#print_eval("cos (pi/4) + i * (sin (pi/4))")
24222525-// Use a function with Result<String, String> type
2626-#let email_valid = str(validate_email(bytes("user@example.com")))
2727-Email validation: #email_valid
2323+#print_eval("0b1001 + 3")
2424+2525+#print_eval("0xffff to decimal")
2626+2727+#print_eval("100 C to F")
2828+2929+#print_eval("temperature = 30 °C; temperature to °F")
3030+3131+#print_eval("(10um)/(0.334cm) + (0.6mm)/(35.28cm) + (1mm)/(47.57cm) + (0.4mm)/(35.28cm) + (2um)/(3.56cm) to percent")
3232+3333+*Currently, random numbers are not available.*