templates: add raw_escape_sequence

Templates can be formatted (using labels) and are usually sanitized
(unless for plain text output).
`raw_escape_sequence(content)` bypasses both.

```toml
	'hyperlink(url, text)' = '''
		raw_escape_sequence("\e]8;;" ++ url ++ "\e\\") ++
		text ++
		raw_escape_sequence("\e]8;;\e\\")
	'''
```

In this example, `raw_escape_sequence` not only outputs the intended
escape codes, it also strips away any escape codes that might otherwise
be part of the `url` (from any labels attached to the `url` content).

Not all formatters (namely FormatRecorder) are supported yet.

Change-Id: Id00000004492dbf39e50f3b7090706839d1d8d45
This commit is contained in:
Vamsi Avula 2024-10-06 09:14:53 +05:30
parent a6aa25c9eb
commit fb93394610
4 changed files with 74 additions and 0 deletions

View file

@ -35,6 +35,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* String literals in filesets, revsets and templates now support hex bytes
(with `\e` as escape / shorthand for `\x1b`).
* New template function `raw_escape_sequence(...)` preserves escape sequences.
### Fixed bugs
* Error on `trunk()` revset resolution is now handled gracefully.

View file

@ -39,6 +39,7 @@ use crate::templater::ListTemplate;
use crate::templater::Literal;
use crate::templater::PlainTextFormattedProperty;
use crate::templater::PropertyPlaceholder;
use crate::templater::RawEscapeSequenceTemplate;
use crate::templater::ReformatTemplate;
use crate::templater::SeparateTemplate;
use crate::templater::SizeHint;
@ -1116,6 +1117,17 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun
content, labels,
))))
});
map.insert(
"raw_escape_sequence",
|language, diagnostics, build_ctx, function| {
let [content_node] = function.expect_exact_arguments()?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
Ok(L::wrap_template(Box::new(RawEscapeSequenceTemplate(
content,
))))
},
);
map.insert("if", |language, diagnostics, build_ctx, function| {
let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
let condition =
@ -2334,6 +2346,47 @@ mod tests {
@"text");
}
#[test]
fn test_raw_escape_sequence_function_strip_labels() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(
env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
@"text",
);
}
#[test]
fn test_raw_escape_sequence_function_ansi_escape() {
let env = TestTemplateEnv::new();
// Sanitize ANSI escape without raw_escape_sequence
insta::assert_snapshot!(env.render_ok(r#""\e""#), @"");
insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"");
insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"");
insta::assert_snapshot!(
env.render_ok(r#""]8;;"
++ "http://example.com"
++ "\e\\"
++ "Example"
++ "\x1b]8;;\x1B\\""#),
@r#"␛]8;;http://example.com␛\Example␛]8;;␛\"#);
// Don't sanitize ANSI escape with raw_escape_sequence
insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
insta::assert_snapshot!(
env.render_ok(r#"raw_escape_sequence("]8;;"
++ "http://example.com"
++ "\e\\"
++ "Example"
++ "\x1b]8;;\x1B\\")"#),
@r#"]8;;http://example.com\Example]8;;\"#);
}
#[test]
fn test_coalesce_function() {
let mut env = TestTemplateEnv::new();

View file

@ -16,6 +16,7 @@ use std::cell::RefCell;
use std::error;
use std::fmt;
use std::io;
use std::io::Write;
use std::iter;
use std::rc::Rc;
@ -184,6 +185,17 @@ where
}
}
pub struct RawEscapeSequenceTemplate<T>(pub T);
impl<T: Template> Template for RawEscapeSequenceTemplate<T> {
fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
let rewrap = formatter.rewrap_fn();
let mut raw_formatter = PlainTextFormatter::new(formatter.raw());
// TODO(#4631): process "buffered" labels.
self.0.format(&mut rewrap(&mut raw_formatter))
}
}
/// Renders contents in order, and returns the first non-empty output.
pub struct CoalesceTemplate<T>(pub Vec<T>);
@ -697,6 +709,10 @@ impl<'a> TemplateFormatter<'a> {
move |formatter| TemplateFormatter::new(formatter, error_handler)
}
pub fn raw(&mut self) -> &mut dyn Write {
self.formatter.raw()
}
pub fn labeled<S: AsRef<str>>(
&mut self,
label: S,

View file

@ -47,6 +47,9 @@ The following functions are defined.
non-empty lines by the given `prefix`.
* `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
sequences in `content` (i.e., bypasses sanitization) and strips labels.
Note: Doesn't yet work with wrapped output / `fill(...)` / `indent(...)`.
* `if(condition: Boolean, then: Template[, else: Template]) -> Template`:
Conditionally evaluate `then`/`else` template content.
* `coalesce(content: Template...) -> Template`: Returns the first **non-empty**