ok/jj
1
0
Fork 0
forked from mirrors/jj

templater: add coalesce() function that selects first non-empty content

This can be used to flatten nested "if()"s. It's not exactly the same as "case"
or "switch" expression, but works reasonably well in template. It's not uncommon
to show placeholder text in place of an empty content, and a nullish value
(e.g. empty string, list, option) is usually rendered as an empty text.
This commit is contained in:
Yuya Nishihara 2024-03-25 18:19:36 +09:00
parent 6c5a54a29f
commit a2a9b7decb
6 changed files with 72 additions and 24 deletions

View file

@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Commit templates now support `immutable` keyword.
* New template function `coalesce(content, ..)` is added.
* Timestamps are now shown in local timezone and without milliseconds and
timezone offset by default.

View file

@ -96,7 +96,7 @@ concat(
"Author: " ++ format_detailed_signature(author) ++ "\n",
"Committer: " ++ format_detailed_signature(committer) ++ "\n",
"\n",
indent(" ", if(description, description, description_placeholder ++ "\n")),
indent(" ", coalesce(description, description_placeholder ++ "\n")),
"\n",
)
'''

View file

@ -22,10 +22,10 @@ use crate::template_parser::{
TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp,
};
use crate::templater::{
ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, ListPropertyTemplate,
ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate,
SeparateTemplate, Template, TemplateProperty, TemplatePropertyError, TemplatePropertyExt as _,
TemplateRenderer, TimestampRange,
CoalesceTemplate, ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate,
ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder,
ReformatTemplate, SeparateTemplate, Template, TemplateProperty, TemplatePropertyError,
TemplatePropertyExt as _, TemplateRenderer, TimestampRange,
};
use crate::{text_util, time_util};
@ -921,6 +921,14 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun
let template = ConditionalTemplate::new(condition, true_template, false_template);
Ok(L::wrap_template(Box::new(template)))
});
map.insert("coalesce", |language, build_ctx, function| {
let contents = function
.args
.iter()
.map(|node| expect_template_expression(language, build_ctx, node))
.try_collect()?;
Ok(L::wrap_template(Box::new(CoalesceTemplate(contents))))
});
map.insert("concat", |language, build_ctx, function| {
let contents = function
.args
@ -1993,6 +2001,30 @@ mod tests {
@"text");
}
#[test]
fn test_coalesce_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
env.add_keyword("empty_string", || L::wrap_string(Literal("".to_owned())));
env.add_keyword("non_empty_string", || {
L::wrap_string(Literal("a".to_owned()))
});
insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
// "false" is not empty
insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
// Error is not empty
insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
// but can be short-circuited
insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
}
#[test]
fn test_concat_function() {
let mut env = TestTemplateEnv::new();

View file

@ -184,6 +184,22 @@ where
}
}
/// Renders contents in order, and returns the first non-empty output.
pub struct CoalesceTemplate<T>(pub Vec<T>);
impl<T: Template> Template for CoalesceTemplate<T> {
fn format(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
let Some((last, contents)) = self.0.split_last() else {
return Ok(());
};
if let Some(recorder) = contents.iter().find_map(record_non_empty) {
recorder?.replay(formatter)
} else {
last.format(formatter) // no need to capture the last content
}
}
}
pub struct ConcatTemplate<T>(pub Vec<T>);
impl<T: Template> Template for ConcatTemplate<T> {
@ -248,18 +264,7 @@ where
T: Template,
{
fn format(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
let mut content_recorders = self
.contents
.iter()
.filter_map(|template| {
let mut recorder = FormatRecorder::new();
match template.format(&mut recorder) {
Ok(()) if recorder.data().is_empty() => None, // omit empty content
Ok(()) => Some(Ok(recorder)),
Err(e) => Some(Err(e)),
}
})
.fuse();
let mut content_recorders = self.contents.iter().filter_map(record_non_empty).fuse();
if let Some(recorder) = content_recorders.next() {
recorder?.replay(formatter)?;
}
@ -697,3 +702,12 @@ fn format_error_inline(formatter: &mut dyn Formatter, err: &dyn error::Error) ->
Ok(())
})
}
fn record_non_empty(template: impl Template) -> Option<io::Result<FormatRecorder>> {
let mut recorder = FormatRecorder::new();
match template.format(&mut recorder) {
Ok(()) if recorder.data().is_empty() => None, // omit empty content
Ok(()) => Some(Ok(recorder)),
Err(e) => Some(Err(e)),
}
}

View file

@ -241,13 +241,11 @@ For example:
```toml
[templates]
log_node = '''
if(self,
if(current_working_copy, "@",
if(root, "┴",
if(immutable, "●", "○")
)
),
"🮀",
coalesce(
if(!self, "🮀"),
if(current_working_copy, "@"),
if(root, "┴"),
if(immutable, "●", "○"),
)
'''
op_log_node = 'if(current_operation, "@", "○")'

View file

@ -47,6 +47,8 @@ The following functions are defined.
the content. The `label` is evaluated as a space-separated string.
* `if(condition: Boolean, then: Template[, else: Template]) -> Template`:
Conditionally evaluate `then`/`else` template content.
* `coalesce(content: Template...) -> Template`: Returns the first **non-empty**
content.
* `concat(content: Template...) -> Template`:
Same as `content_1 ++ ... ++ content_n`.
* `separate(separator: Template, content: Template...) -> Template`: