templates: add tail argument to truncate_*

When truncation occurs, the tail will be placed once before the start
(for `truncate_start`) or after the end (for `truncate_end`) of the
truncated content.
This commit is contained in:
Bryce Berger 2024-12-30 15:06:21 -05:00
parent dbd0174ee8
commit fbf8c2f82a
No known key found for this signature in database
GPG key ID: 58CA4F9FEF8F4296
4 changed files with 122 additions and 114 deletions

View file

@ -8,6 +8,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
* `truncate_start` and `truncate_end` now accept an optional `tail` named
argument, to indicate when truncation has occurred.
### Release highlights
### Breaking changes

View file

@ -1318,22 +1318,32 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun
map.insert(
"truncate_start",
|language, diagnostics, build_ctx, function| {
let [width_node, content_node] = function.expect_exact_arguments()?;
let ([width_node, content_node], [tail_node]) =
function.expect_named_arguments(&["", "", "tail"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let template = new_truncate_template(content, width, text_util::write_truncated_start);
let tail = tail_node
.map(|node| expect_plain_text_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template =
new_truncate_template(content, width, tail, text_util::write_truncated_start);
Ok(L::wrap_template(template))
},
);
map.insert(
"truncate_end",
|language, diagnostics, build_ctx, function| {
let [width_node, content_node] = function.expect_exact_arguments()?;
let ([width_node, content_node], [tail_node]) =
function.expect_named_arguments(&["", "", "tail"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let template = new_truncate_template(content, width, text_util::write_truncated_end);
let tail = tail_node
.map(|node| expect_plain_text_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template =
new_truncate_template(content, width, tail, text_util::write_truncated_end);
Ok(L::wrap_template(template))
},
);
@ -1450,17 +1460,28 @@ where
fn new_truncate_template<'a, W>(
content: Box<dyn Template + 'a>,
width: Box<dyn TemplateProperty<Output = usize> + 'a>,
tail: Option<Box<dyn TemplateProperty<Output = String> + 'a>>,
write_truncated: W,
) -> Box<dyn Template + 'a>
where
W: Fn(&mut dyn Formatter, &FormatRecorder, usize) -> io::Result<usize> + 'a,
W: Fn(&mut dyn Formatter, &FormatRecorder, usize, &str) -> io::Result<usize> + 'a,
{
let template = ReformatTemplate::new(content, move |formatter, recorded| {
let width = match width.extract() {
Ok(width) => width,
Err(err) => return formatter.handle_error(err),
};
write_truncated(formatter.as_mut(), recorded, width)?;
let tail = match tail.as_ref().map(|t| t.extract()) {
Some(Err(err)) => return formatter.handle_error(err),
Some(Ok(tail)) => Some(tail),
None => None,
};
write_truncated(
formatter.as_mut(),
recorded,
width,
tail.as_deref().unwrap_or_default(),
)?;
Ok(())
});
Box::new(template)
@ -2770,9 +2791,15 @@ mod tests {
insta::assert_snapshot!(
env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
@"arbaz");
insta::assert_snapshot!(
env.render_ok(r"truncate_start(2, label('red', 'foobar'), tail='**') ++ 'baz'"),
@"**arbaz");
insta::assert_snapshot!(
env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
@"fobaz");
insta::assert_snapshot!(
env.render_ok(r"truncate_end(2, label('red', 'foobar'), tail='**') ++ 'baz'"),
@"fo**baz");
}
#[test]

View file

@ -231,10 +231,14 @@ pub fn write_truncated_start(
formatter: &mut dyn Formatter,
recorded_content: &FormatRecorder,
max_width: usize,
tail: &str,
) -> io::Result<usize> {
let data = recorded_content.data();
let (start, truncated_width) = truncate_start_pos_bytes(data, max_width);
let truncated_start = start + count_start_zero_width_chars_bytes(&data[start..]);
if truncated_start != 0 {
formatter.write_all(tail.as_bytes())?;
}
recorded_content.replay_with(formatter, |formatter, range| {
let start = cmp::max(range.start, truncated_start);
if start < range.end {
@ -253,6 +257,7 @@ pub fn write_truncated_end(
formatter: &mut dyn Formatter,
recorded_content: &FormatRecorder,
max_width: usize,
tail: &str,
) -> io::Result<usize> {
let data = recorded_content.data();
let (truncated_end, truncated_width) = truncate_end_pos_bytes(data, max_width);
@ -263,6 +268,9 @@ pub fn write_truncated_end(
}
Ok(())
})?;
if truncated_end != data.len() {
formatter.write_all(tail.as_bytes())?;
}
Ok(truncated_width)
}
@ -679,48 +687,34 @@ mod tests {
}
// Truncate start
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 6).map(|_| ())),
@"foobar"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 5).map(|_| ())),
@"oobar"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 3).map(|_| ())),
@"bar"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 2).map(|_| ())),
@"ar"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 0).map(|_| ())),
@""
);
let render = |max_width, tail| {
format_colored(|fmt| write_truncated_start(fmt, &recorder, max_width, tail).map(|_| ()))
};
insta::assert_snapshot!(render(6, "" ), @"foobar");
insta::assert_snapshot!(render(6, "..."), @"foobar");
insta::assert_snapshot!(render(5, "" ), @"oobar");
insta::assert_snapshot!(render(5, "..."), @"...oobar");
insta::assert_snapshot!(render(3, "" ), @"bar");
insta::assert_snapshot!(render(3, "..."), @"...bar");
insta::assert_snapshot!(render(2, "" ), @"ar");
insta::assert_snapshot!(render(2, "..."), @"...ar");
insta::assert_snapshot!(render(0, "" ), @"");
insta::assert_snapshot!(render(0, "..."), @"...");
// Truncate end
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 6).map(|_| ())),
@"foobar"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 5).map(|_| ())),
@"fooba"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 3).map(|_| ())),
@"foo"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 2).map(|_| ())),
@"fo"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 0).map(|_| ())),
@""
);
let render = |max_width, tail| {
format_colored(|fmt| write_truncated_end(fmt, &recorder, max_width, tail).map(|_| ()))
};
insta::assert_snapshot!(render(6, "" ), @"foobar");
insta::assert_snapshot!(render(6, ""), @"foobar");
insta::assert_snapshot!(render(5, "" ), @"fooba");
insta::assert_snapshot!(render(5, ""), @"fooba…");
insta::assert_snapshot!(render(3, "" ), @"foo");
insta::assert_snapshot!(render(3, ""), @"foo…");
insta::assert_snapshot!(render(2, "" ), @"fo");
insta::assert_snapshot!(render(2, ""), @"fo…");
insta::assert_snapshot!(render(0, "" ), @"");
insta::assert_snapshot!(render(0, ""), @"");
}
#[test]
@ -729,56 +723,38 @@ mod tests {
write!(recorder, "a\u{300}bc\u{300}一二三").unwrap();
// Truncate start
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 1).map(|_| ())),
@""
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 2).map(|_| ())),
@""
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 3).map(|_| ())),
@""
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 6).map(|_| ())),
@"一二三"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 7).map(|_| ())),
@"c̀一二三"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 9).map(|_| ())),
@"àbc̀一二三"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 10).map(|_| ())),
@"àbc̀一二三"
);
let render = |max_width, tail| {
format_colored(|fmt| write_truncated_start(fmt, &recorder, max_width, tail).map(|_| ()))
};
insta::assert_snapshot!(render(1, "" ), @"");
insta::assert_snapshot!(render(1, "123"), @"123");
insta::assert_snapshot!(render(2, "" ), @"");
insta::assert_snapshot!(render(2, "123"), @"123三");
insta::assert_snapshot!(render(3, "" ), @"");
insta::assert_snapshot!(render(3, "123"), @"123三");
insta::assert_snapshot!(render(6, "" ), @"一二三");
insta::assert_snapshot!(render(6, "123"), @"123一二三");
insta::assert_snapshot!(render(7, "" ), @"c̀一二三");
insta::assert_snapshot!(render(7, "123"), @"123c̀一二三");
insta::assert_snapshot!(render(9, "" ), @"àbc̀一二三");
insta::assert_snapshot!(render(9, "123"), @"àbc̀一二三");
insta::assert_snapshot!(render(10, "" ), @"àbc̀一二三");
insta::assert_snapshot!(render(10, "123"), @"àbc̀一二三");
// Truncate end
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 1).map(|_| ())),
@""
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 4).map(|_| ())),
@"àbc̀"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 5).map(|_| ())),
@"àbc̀一"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 9).map(|_| ())),
@"àbc̀一二三"
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 10).map(|_| ())),
@"àbc̀一二三"
);
let render = |max_width, tail| {
format_colored(|fmt| write_truncated_end(fmt, &recorder, max_width, tail).map(|_| ()))
};
insta::assert_snapshot!(render(1, "" ), @"");
insta::assert_snapshot!(render(1, "__"), @"à__");
insta::assert_snapshot!(render(4, "" ), @"àbc̀");
insta::assert_snapshot!(render(4, "__"), @"àbc̀__");
insta::assert_snapshot!(render(5, "" ), @"àbc̀一");
insta::assert_snapshot!(render(5, "__"), @"àbc̀一__");
insta::assert_snapshot!(render(9, "" ), @"àbc̀一二三");
insta::assert_snapshot!(render(9, "__"), @"àbc̀一二三");
insta::assert_snapshot!(render(10, "" ), @"àbc̀一二三");
insta::assert_snapshot!(render(10, "__"), @"àbc̀一二三");
}
#[test]
@ -786,24 +762,22 @@ mod tests {
let recorder = FormatRecorder::new();
// Truncate start
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 0).map(|_| ())),
@""
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_start(formatter, &recorder, 1).map(|_| ())),
@""
);
let render = |max_width, tail| {
format_colored(|fmt| write_truncated_start(fmt, &recorder, max_width, tail).map(|_| ()))
};
insta::assert_snapshot!(render(0, "" ), @"");
insta::assert_snapshot!(render(0, "[]"), @"");
insta::assert_snapshot!(render(1, "" ), @"");
insta::assert_snapshot!(render(1, "[]"), @"");
// Truncate end
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 0).map(|_| ())),
@""
);
insta::assert_snapshot!(
format_colored(|formatter| write_truncated_end(formatter, &recorder, 1).map(|_| ())),
@""
);
let render = |max_width, tail| {
format_colored(|fmt| write_truncated_end(fmt, &recorder, max_width, tail).map(|_| ()))
};
insta::assert_snapshot!(render(0, "" ), @"");
insta::assert_snapshot!(render(0, "[]"), @"");
insta::assert_snapshot!(render(1, "" ), @"");
insta::assert_snapshot!(render(1, "[]"), @"");
}
#[test]

View file

@ -55,10 +55,14 @@ The following functions are defined.
* `pad_end(width: Integer, content: Template[, fill_char: Template])`: Pad (or
left-justify) content by adding trailing fill characters. The `content`
shouldn't have newline character.
* `truncate_start(width: Integer, content: Template)`: Truncate `content` by
removing leading characters. The `content` shouldn't have newline character.
* `truncate_end(width: Integer, content: Template)`: Truncate `content` by
removing trailing characters. The `content` shouldn't have newline character.
* `truncate_start(width: Integer, content: Template[, tail: Template])`:
Truncate `content` by removing leading characters. The `content` shouldn't
have newline character. If the content is truncated and `tail` was provided,
`tail` will be placed before `content`.
* `truncate_end(width: Integer, content: Template[, tail: Template])`: Truncate
`content` by removing trailing characters. The `content` shouldn't have
newline character. If the content is truncated and `tail` was provided, `tail`
will be place after `content`.
* `label(label: Template, content: Template) -> Template`: Apply label to
the content. The `label` is evaluated as a space-separated string.
* `raw_escape_sequence(content: Template) -> Template`: Preserves any escape