diff --git a/src/eval/methods.rs b/src/eval/methods.rs index 420845435..7de3bc20a 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -31,7 +31,11 @@ pub fn call( "len" => Value::Int(string.len()), "first" => Value::Str(string.first().at(span)?), "last" => Value::Str(string.last().at(span)?), - "at" => Value::Str(string.at(args.expect("index")?, None).at(span)?), + "at" => { + let index = args.expect("index")?; + let default = args.named::("default")?; + Value::Str(string.at(index, default.as_deref()).at(span)?) + } "slice" => { let start = args.expect("start")?; let mut end = args.eat()?; @@ -74,7 +78,9 @@ pub fn call( Value::Content(content) => match method { "func" => content.func().into(), "has" => Value::Bool(content.has(&args.expect::("field")?)), - "at" => content.at(&args.expect::("field")?, None).at(span)?, + "at" => content + .at(&args.expect::("field")?, args.named("default")?) + .at(span)?, "location" => content .location() .ok_or("this method can only be called on content returned by query(..)") diff --git a/src/eval/str.rs b/src/eval/str.rs index d7e00bf6a..3c377595a 100644 --- a/src/eval/str.rs +++ b/src/eval/str.rs @@ -71,9 +71,9 @@ impl Str { /// Extract the grapheme cluster at the given index. pub fn at<'a>(&'a self, index: i64, default: Option<&'a str>) -> StrResult { let len = self.len(); - let grapheme = self.0[self.locate(index)?..] - .graphemes(true) - .next() + let grapheme = self + .locate_opt(index)? + .and_then(|i| self.0[i..].graphemes(true).next()) .or(default) .ok_or_else(|| no_default_and_out_of_bounds(index, len))?; Ok(grapheme.into()) @@ -325,22 +325,28 @@ impl Str { Ok(Self(self.0.repeat(n))) } - /// Resolve an index. - fn locate(&self, index: i64) -> StrResult { + /// Resolve an index, if it is within bounds. + /// Errors on invalid char boundaries. + fn locate_opt(&self, index: i64) -> StrResult> { let wrapped = if index >= 0 { Some(index) } else { self.len().checked_add(index) }; let resolved = wrapped .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()) - .ok_or_else(|| out_of_bounds(index, self.len()))?; + .filter(|&v| v <= self.0.len()); - if !self.0.is_char_boundary(resolved) { + if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) { return Err(not_a_char_boundary(index)); } Ok(resolved) } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64) -> StrResult { + self.locate_opt(index)? + .ok_or_else(|| out_of_bounds(index, self.len())) + } } /// The out of bounds access error message. diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ index afcb024fd..8b36dea94 100644 --- a/tests/typ/compiler/methods.typ +++ b/tests/typ/compiler/methods.typ @@ -26,6 +26,10 @@ test(rewritten, "Hello!\n This is a sentence!\n And one more!") } +--- +// Test .at() default values for content. +#test(auto, [a].at("doesn't exist", default: auto)) + --- // Error: 2:2-2:15 type array has no method `fun` #let numbers = () diff --git a/tests/typ/compiler/string.typ b/tests/typ/compiler/string.typ index 9a4b41468..c4c1669e5 100644 --- a/tests/typ/compiler/string.typ +++ b/tests/typ/compiler/string.typ @@ -28,6 +28,10 @@ #test("Hello".at(-2), "l") #test("Hey: 🏳️‍🌈 there!".at(5), "🏳️‍🌈") +--- +// Test `at`'s 'default' parameter. +#test("z", "Hello".at(5, default: "z")) + --- // Error: 2-14 string index 2 is not a character boundary #"🏳️‍🌈".at(2) @@ -36,6 +40,10 @@ // Error: 2-15 no default value was specified and string index out of bounds (index: 5, len: 5) #"Hello".at(5) +--- +// Error: 25-32 expected string, found dictionary +#"Hello".at(5, default: (a: 10)) + --- // Test the `slice` method. #test("abc".slice(1, 2), "b")