From fb933946108315fac2e218adc14c958226941433 Mon Sep 17 00:00:00 2001 From: Vamsi Avula Date: Sun, 6 Oct 2024 09:14:53 +0530 Subject: [PATCH] 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 --- CHANGELOG.md | 2 ++ cli/src/template_builder.rs | 53 +++++++++++++++++++++++++++++++++++++ cli/src/templater.rs | 16 +++++++++++ docs/templates.md | 3 +++ 4 files changed, 74 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91fd87c76..dc1acab14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index bc152fcd2..321ce776b 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -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(); diff --git a/cli/src/templater.rs b/cli/src/templater.rs index 2dd28e27c..b393013bf 100644 --- a/cli/src/templater.rs +++ b/cli/src/templater.rs @@ -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(pub T); + +impl Template for RawEscapeSequenceTemplate { + 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(pub Vec); @@ -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>( &mut self, label: S, diff --git a/docs/templates.md b/docs/templates.md index 3809479e1..84a7ca0eb 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -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**