From cc9b69f13fb1e61b9a635d66b544cc2b15f7196b Mon Sep 17 00:00:00 2001 From: Wesley Yang <48174882+wesleyel@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:45:51 +0800 Subject: [PATCH 1/4] Add `map` function to `Dict` for transforming key-value pairs and update tests (#6006) --- crates/typst-library/src/foundations/dict.rs | 77 +++++++++++++++++++- tests/suite/foundations/dict.typ | 28 +++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index c93670c1d..d8815354b 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -3,15 +3,17 @@ use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign}; use std::sync::Arc; +use comemo::Tracked; use ecow::{eco_format, EcoString}; use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use typst_syntax::is_ident; use typst_utils::ArcExt; -use crate::diag::{Hint, HintedStrResult, StrResult}; +use crate::diag::{Hint, HintedStrResult, SourceResult, StrResult}; +use crate::engine::Engine; use crate::foundations::{ - array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value, + array, cast, func, repr, scope, ty, Array, Context, Func, Module, Repr, Str, Value, }; /// Create a new [`Dict`] from key-value pairs. @@ -254,6 +256,77 @@ impl Dict { .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) .collect() } + + /// Produces a new dictionary or array by transforming each key-value pair with the given function. + /// + /// If the mapper function returns a pair (array of length 2), the result will be a new dictionary. + /// Otherwise, the result will be an array containing all mapped values. + /// + /// ```example + /// #let prices = (apples: 2, oranges: 3, bananas: 1.5) + /// #prices.map(key => key.len()) \ + /// #prices.map((key, price) => (key, price * 1.1)) + /// ``` + #[func] + pub fn map( + self, + engine: &mut Engine, + context: Tracked, + /// The function to apply to each key-value pair. + /// The function can either take a single parameter (receiving a pair as array of length 2), + /// or two parameters (receiving key and value separately). + mapper: Func, + ) -> SourceResult { + let mut dict_result = IndexMap::new(); + let mut array_result = Vec::new(); + let mut is_dict = true; + + // try to check the number of parameters, if not, use array form + let use_two_args = mapper.params().map_or(false, |params| params.len() >= 2); + + for (key, value) in self { + // choose how to pass parameters based on the function signature + let mapped = if use_two_args { + mapper.call(engine, context, [ + Value::Str(key.clone()), + value.clone(), + ])? + } else { + mapper.call(engine, context, [ + Value::Array(array![Value::Str(key.clone()), value]), + ])? + }; + + // check if the result is a dictionary key-value pair + if let Value::Array(arr) = &mapped { + if arr.len() == 2 { + if let Value::Str(k) = &arr.as_slice()[0] { + if is_dict { + dict_result.insert(k.clone(), arr.as_slice()[1].clone()); + continue; + } + } + } + } + + // if the result is not a key-value pair, switch the result type to array + if is_dict { + is_dict = false; + // convert the collected dictionary result to array items + for (k, v) in dict_result.drain(..) { + array_result.push(Value::Array(array![Value::Str(k), v])); + } + } + + array_result.push(mapped); + } + + if is_dict { + Ok(Value::Dict(Dict::from(dict_result))) + } else { + Ok(Value::Array(array_result.into_iter().collect())) + } + } } /// A value that can be cast to dictionary. diff --git a/tests/suite/foundations/dict.typ b/tests/suite/foundations/dict.typ index af9ad5e1a..b9e4c96dd 100644 --- a/tests/suite/foundations/dict.typ +++ b/tests/suite/foundations/dict.typ @@ -23,6 +23,34 @@ test(world, "world") } +--- dict-map --- +// Test the map function +#let dict = (a: 1, b: 2, c: 3) + +// test map return new dict +#test( + dict.map(((key, value)) => (key, value * 2)), + (a: 2, b: 4, c: 6) +) + +// test map empty dict +#test( + (:).map(((key, value)) => (key, value * 2)), + (:) +) + +// test map return array +#test( + dict.map(pair => pair.at(0) + ": " + str(pair.at(1))), + ("a: 1", "b: 2", "c: 3") +) + +// test map return array(different return type) +#test( + dict.map(((key, value)) => if value > 1 { (key, value * 2) } else { "key smaller than 1: " + key }), + ("key smaller than 1: a", ("b", 4), ("c", 6)) +) + --- dict-missing-field --- // Error: 6-13 dictionary does not contain key "invalid" #(:).invalid From 463da08fba401f11f65ca3d0700ff9a78ca56d9a Mon Sep 17 00:00:00 2001 From: Wesley Yang <48174882+wesleyel@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:50:00 +0800 Subject: [PATCH 2/4] Refactor parameter checking in `Dict` to use `is_some_and` for improved clarity --- crates/typst-library/src/foundations/dict.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index d8815354b..f49080203 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -282,7 +282,7 @@ impl Dict { let mut is_dict = true; // try to check the number of parameters, if not, use array form - let use_two_args = mapper.params().map_or(false, |params| params.len() >= 2); + let use_two_args = mapper.params().is_some_and(|params| params.len() >= 2); for (key, value) in self { // choose how to pass parameters based on the function signature From c9c921b8775974c4fe6d8827308e996bc67f45f5 Mon Sep 17 00:00:00 2001 From: Wesley Yang <48174882+wesleyel@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:54:32 +0800 Subject: [PATCH 3/4] Fix formatting --- crates/typst-library/src/foundations/dict.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index f49080203..b0b29d316 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -272,7 +272,7 @@ impl Dict { self, engine: &mut Engine, context: Tracked, - /// The function to apply to each key-value pair. + /// The function to apply to each key-value pair. /// The function can either take a single parameter (receiving a pair as array of length 2), /// or two parameters (receiving key and value separately). mapper: Func, @@ -287,14 +287,13 @@ impl Dict { for (key, value) in self { // choose how to pass parameters based on the function signature let mapped = if use_two_args { - mapper.call(engine, context, [ - Value::Str(key.clone()), - value.clone(), - ])? + mapper.call(engine, context, [Value::Str(key.clone()), value.clone()])? } else { - mapper.call(engine, context, [ - Value::Array(array![Value::Str(key.clone()), value]), - ])? + mapper.call( + engine, + context, + [Value::Array(array![Value::Str(key.clone()), value])], + )? }; // check if the result is a dictionary key-value pair @@ -308,7 +307,7 @@ impl Dict { } } } - + // if the result is not a key-value pair, switch the result type to array if is_dict { is_dict = false; @@ -317,7 +316,7 @@ impl Dict { array_result.push(Value::Array(array![Value::Str(k), v])); } } - + array_result.push(mapped); } From 06bc7bac6694d63459b39844782aca50ddee8d6e Mon Sep 17 00:00:00 2001 From: Wesley Yang <48174882+wesleyel@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:18:11 +0800 Subject: [PATCH 4/4] Refactor `map` function in `Dict` to improve parameter handling and update related tests for consistency --- crates/typst-library/src/foundations/dict.rs | 39 ++++++++++++++++---- tests/suite/foundations/dict.typ | 6 +-- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index b0b29d316..a4afbb759 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -264,8 +264,8 @@ impl Dict { /// /// ```example /// #let prices = (apples: 2, oranges: 3, bananas: 1.5) - /// #prices.map(key => key.len()) \ - /// #prices.map((key, price) => (key, price * 1.1)) + /// #prices.map(pair => pair.at(0).len()) + /// #prices.map((key, value) => (key, value * 1.1)) /// ``` #[func] pub fn map( @@ -275,6 +275,7 @@ impl Dict { /// The function to apply to each key-value pair. /// The function can either take a single parameter (receiving a pair as array of length 2), /// or two parameters (receiving key and value separately). + /// Parameters exceeding two will be ignored. mapper: Func, ) -> SourceResult { let mut dict_result = IndexMap::new(); @@ -282,18 +283,42 @@ impl Dict { let mut is_dict = true; // try to check the number of parameters, if not, use array form - let use_two_args = mapper.params().is_some_and(|params| params.len() >= 2); + let mut first_pair = true; + let mut use_single_arg = false; for (key, value) in self { - // choose how to pass parameters based on the function signature - let mapped = if use_two_args { - mapper.call(engine, context, [Value::Str(key.clone()), value.clone()])? - } else { + let mapped = if first_pair { + // try two calling ways for the first pair + first_pair = false; + + // try to call with two parameters + let result = mapper.call( + engine, + context, + [Value::Str(key.clone()), value.clone()], + ); + + // if failed, try to call with one parameter + if result.is_err() { + use_single_arg = true; + mapper.call( + engine, + context, + [Value::Array(array![Value::Str(key.clone()), value])], + )? + } else { + result? + } + } else if use_single_arg { + // try to call with one parameter mapper.call( engine, context, [Value::Array(array![Value::Str(key.clone()), value])], )? + } else { + // try to call with two parameters + mapper.call(engine, context, [Value::Str(key.clone()), value.clone()])? }; // check if the result is a dictionary key-value pair diff --git a/tests/suite/foundations/dict.typ b/tests/suite/foundations/dict.typ index b9e4c96dd..8a2f5ffe5 100644 --- a/tests/suite/foundations/dict.typ +++ b/tests/suite/foundations/dict.typ @@ -29,13 +29,13 @@ // test map return new dict #test( - dict.map(((key, value)) => (key, value * 2)), + dict.map((key, value) => (key, value * 2)), (a: 2, b: 4, c: 6) ) // test map empty dict #test( - (:).map(((key, value)) => (key, value * 2)), + (:).map((key, value) => (key, value * 2)), (:) ) @@ -47,7 +47,7 @@ // test map return array(different return type) #test( - dict.map(((key, value)) => if value > 1 { (key, value * 2) } else { "key smaller than 1: " + key }), + dict.map((key, value) => if value > 1 { (key, value * 2) } else { "key smaller than 1: " + key }), ("key smaller than 1: a", ("b", 4), ("c", 6)) )