templates: Add more string methods

Add starts_with/ends_with/remove_prefix/remove_suffix/substr methods to string when templating.
This commit is contained in:
Zachary Dremann 2023-08-22 14:40:51 -04:00
parent 9702a425e5
commit ac448202da
4 changed files with 110 additions and 0 deletions

View File

@ -105,6 +105,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Revsets gained a new function `mine()` that aliases `author(exact:"your_email")`.
* `jj log` timestamp format now accepts `.utc()` to convert a timestamp to UTC.
* templates now support additional string methods `.starts_with(x)`, `.ends_with(x)`
`.remove_prefix(x)`, `.remove_suffix(x)`, and `.substr(start, end)`.
### Fixed bugs

View File

@ -329,6 +329,76 @@ fn build_string_method<'a, L: TemplateLanguage<'a>>(
|(haystack, needle)| haystack.contains(&needle),
))
}
"starts_with" => {
let [needle_node] = template_parser::expect_exact_arguments(function)?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
language.wrap_boolean(TemplateFunction::new(
(self_property, needle_property),
move |(haystack, needle)| haystack.starts_with(&needle),
))
}
"ends_with" => {
let [needle_node] = template_parser::expect_exact_arguments(function)?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
language.wrap_boolean(TemplateFunction::new(
(self_property, needle_property),
move |(haystack, needle)| haystack.ends_with(&needle),
))
}
"remove_prefix" => {
let [needle_node] = template_parser::expect_exact_arguments(function)?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
language.wrap_string(TemplateFunction::new(
(self_property, needle_property),
move |(haystack, needle)| {
haystack
.strip_prefix(&needle)
.map(ToOwned::to_owned)
.unwrap_or(haystack)
},
))
}
"remove_suffix" => {
let [needle_node] = template_parser::expect_exact_arguments(function)?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
language.wrap_string(TemplateFunction::new(
(self_property, needle_property),
move |(haystack, needle)| {
haystack
.strip_suffix(&needle)
.map(ToOwned::to_owned)
.unwrap_or(haystack)
},
))
}
"substr" => {
let [start_idx, end_idx] = template_parser::expect_exact_arguments(function)?;
let start_idx_property = expect_integer_expression(language, build_ctx, start_idx)?;
let end_idx_property = expect_integer_expression(language, build_ctx, end_idx)?;
language.wrap_string(TemplateFunction::new(
(self_property, start_idx_property, end_idx_property),
|(s, start_idx, end_idx)| {
let to_idx = |i: i64| -> usize {
let magnitude = usize::try_from(i.unsigned_abs()).unwrap_or(usize::MAX);
if i < 0 {
s.len().saturating_sub(magnitude)
} else {
magnitude
}
};
let start_idx = to_idx(start_idx);
let end_idx = to_idx(end_idx);
if start_idx >= end_idx {
String::new()
} else {
s.chars()
.skip(start_idx)
.take(end_idx - start_idx)
.collect()
}
},
))
}
"first_line" => {
template_parser::expect_no_arguments(function)?;
language.wrap_string(TemplateFunction::new(self_property, |s| {

View File

@ -289,6 +289,8 @@ fn test_templater_list_method() {
insta::assert_snapshot!(
render(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
@"ax,ay;bx,by;cx,cy");
// Nested string operations
insta::assert_snapshot!(render(r#""!a\n!b\nc\nend".remove_suffix("end").lines().map(|s| s.remove_prefix("!"))"#), @"a b c");
// Lambda expression in alias
insta::assert_snapshot!(render(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
@ -364,6 +366,36 @@ fn test_templater_string_method() {
insta::assert_snapshot!(render(r#""".lines()"#), @"");
insta::assert_snapshot!(render(r#""a\nb\nc\n".lines()"#), @"a b c");
insta::assert_snapshot!(render(r#""".starts_with("")"#), @"true");
insta::assert_snapshot!(render(r#""everything".starts_with("")"#), @"true");
insta::assert_snapshot!(render(r#""".starts_with("foo")"#), @"false");
insta::assert_snapshot!(render(r#""foo".starts_with("foo")"#), @"true");
insta::assert_snapshot!(render(r#""foobar".starts_with("foo")"#), @"true");
insta::assert_snapshot!(render(r#""foobar".starts_with("bar")"#), @"false");
insta::assert_snapshot!(render(r#""".ends_with("")"#), @"true");
insta::assert_snapshot!(render(r#""everything".ends_with("")"#), @"true");
insta::assert_snapshot!(render(r#""".ends_with("foo")"#), @"false");
insta::assert_snapshot!(render(r#""foo".ends_with("foo")"#), @"true");
insta::assert_snapshot!(render(r#""foobar".ends_with("foo")"#), @"false");
insta::assert_snapshot!(render(r#""foobar".ends_with("bar")"#), @"true");
insta::assert_snapshot!(render(r#""".remove_prefix("wip: ")"#), @"");
insta::assert_snapshot!(render(r#""wip: testing".remove_prefix("wip: ")"#), @"testing");
insta::assert_snapshot!(render(r#""bar@my.example.com".remove_suffix("@other.example.com")"#), @"bar@my.example.com");
insta::assert_snapshot!(render(r#""bar@other.example.com".remove_suffix("@other.example.com")"#), @"bar");
insta::assert_snapshot!(render(r#""foo".substr(0, 0)"#), @"");
insta::assert_snapshot!(render(r#""foo".substr(0, 1)"#), @"f");
insta::assert_snapshot!(render(r#""foo".substr(0, 99)"#), @"foo");
insta::assert_snapshot!(render(r#""abcdef".substr(2, -1)"#), @"cde");
insta::assert_snapshot!(render(r#""abcdef".substr(-3, 99)"#), @"def");
// ranges with end > start are empty
insta::assert_snapshot!(render(r#""abcdef".substr(4, 2)"#), @"");
insta::assert_snapshot!(render(r#""abcdef".substr(-2, -4)"#), @"");
}
#[test]

View File

@ -142,6 +142,11 @@ defined.
* `.lines() -> List<String>`: Split into lines excluding newline characters.
* `.upper() -> String`
* `.lower() -> String`
* `.starts_with(needle: Template) -> Boolean`
* `.ends_with(needle: Template) -> Boolean`
* `.remove_prefix(needle: Template) -> String`: Removes the passed prefix, if present
* `.remove_suffix(needle: Template) -> String`: Removes the passed suffix, if present
* `.substr(start: Integer, end: Integer) -> String`: Extract substring. Negative values count from the end.
### Template type