jj_cli/
template_builder.rs

1// Copyright 2020-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::io;
18use std::iter;
19
20use itertools::Itertools as _;
21use jj_lib::backend::Signature;
22use jj_lib::backend::Timestamp;
23use jj_lib::config::ConfigNamePathBuf;
24use jj_lib::config::ConfigValue;
25use jj_lib::dsl_util::AliasExpandError as _;
26use jj_lib::settings::UserSettings;
27use jj_lib::time_util::DatePattern;
28use serde::de::IntoDeserializer as _;
29use serde::Deserialize;
30
31use crate::formatter::FormatRecorder;
32use crate::formatter::Formatter;
33use crate::template_parser;
34use crate::template_parser::BinaryOp;
35use crate::template_parser::ExpressionKind;
36use crate::template_parser::ExpressionNode;
37use crate::template_parser::FunctionCallNode;
38use crate::template_parser::LambdaNode;
39use crate::template_parser::TemplateAliasesMap;
40use crate::template_parser::TemplateDiagnostics;
41use crate::template_parser::TemplateParseError;
42use crate::template_parser::TemplateParseErrorKind;
43use crate::template_parser::TemplateParseResult;
44use crate::template_parser::UnaryOp;
45use crate::templater::BoxedTemplateProperty;
46use crate::templater::CoalesceTemplate;
47use crate::templater::ConcatTemplate;
48use crate::templater::ConditionalTemplate;
49use crate::templater::Email;
50use crate::templater::LabelTemplate;
51use crate::templater::ListPropertyTemplate;
52use crate::templater::ListTemplate;
53use crate::templater::Literal;
54use crate::templater::PlainTextFormattedProperty;
55use crate::templater::PropertyPlaceholder;
56use crate::templater::RawEscapeSequenceTemplate;
57use crate::templater::ReformatTemplate;
58use crate::templater::SeparateTemplate;
59use crate::templater::SizeHint;
60use crate::templater::Template;
61use crate::templater::TemplateProperty;
62use crate::templater::TemplatePropertyError;
63use crate::templater::TemplatePropertyExt as _;
64use crate::templater::TemplateRenderer;
65use crate::templater::TimestampRange;
66use crate::text_util;
67use crate::time_util;
68
69/// Callbacks to build language-specific evaluation objects from AST nodes.
70pub trait TemplateLanguage<'a> {
71    type Property: CoreTemplatePropertyVar<'a>;
72
73    fn settings(&self) -> &UserSettings;
74
75    /// Translates the given global `function` call to a property.
76    ///
77    /// This should be delegated to
78    /// `CoreTemplateBuildFnTable::build_function()`.
79    fn build_function(
80        &self,
81        diagnostics: &mut TemplateDiagnostics,
82        build_ctx: &BuildContext<Self::Property>,
83        function: &FunctionCallNode,
84    ) -> TemplateParseResult<Self::Property>;
85
86    fn build_method(
87        &self,
88        diagnostics: &mut TemplateDiagnostics,
89        build_ctx: &BuildContext<Self::Property>,
90        property: Self::Property,
91        function: &FunctionCallNode,
92    ) -> TemplateParseResult<Self::Property>;
93}
94
95/// Implements `CoreTemplatePropertyVar::wrap_<type>()` functions.
96///
97/// - `impl_core_wrap_property_fns('a)` for `CoreTemplatePropertyKind`,
98/// - `impl_core_wrap_property_fns('a, MyKind::Core)` for `MyKind::Core(..)`.
99macro_rules! impl_core_wrap_property_fns {
100    ($a:lifetime) => {
101        $crate::template_builder::impl_core_wrap_property_fns!($a, std::convert::identity);
102    };
103    ($a:lifetime, $outer:path) => {
104        $crate::template_builder::impl_wrap_property_fns!(
105            $a, $crate::template_builder::CoreTemplatePropertyKind, $outer, {
106                wrap_string(String) => String,
107                wrap_string_list(Vec<String>) => StringList,
108                wrap_boolean(bool) => Boolean,
109                wrap_integer(i64) => Integer,
110                wrap_integer_opt(Option<i64>) => IntegerOpt,
111                wrap_config_value(jj_lib::config::ConfigValue) => ConfigValue,
112                wrap_signature(jj_lib::backend::Signature) => Signature,
113                wrap_email($crate::templater::Email) => Email,
114                wrap_size_hint($crate::templater::SizeHint) => SizeHint,
115                wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp,
116                wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange,
117            }
118        );
119        fn wrap_template(
120            template: Box<dyn $crate::templater::Template + $a>,
121        ) -> Self {
122            use $crate::template_builder::CoreTemplatePropertyKind as Kind;
123            $outer(Kind::Template(template))
124        }
125        fn wrap_list_template(
126            template: Box<dyn $crate::templater::ListTemplate + $a>,
127        ) -> Self {
128            use $crate::template_builder::CoreTemplatePropertyKind as Kind;
129            $outer(Kind::ListTemplate(template))
130        }
131    };
132}
133
134macro_rules! impl_wrap_property_fns {
135    ($a:lifetime, $kind:path, { $($body:tt)* }) => {
136        $crate::template_builder::impl_wrap_property_fns!(
137            $a, $kind, std::convert::identity, { $($body)* });
138    };
139    ($a:lifetime, $kind:path, $outer:path, {
140        $( $vis:vis $func:ident($ty:ty) => $var:ident, )+
141    }) => {
142        $(
143            $vis fn $func(
144                property: $crate::templater::BoxedTemplateProperty<$a, $ty>,
145            ) -> Self {
146                use $kind as Kind; // https://github.com/rust-lang/rust/issues/48067
147                $outer(Kind::$var(property))
148            }
149        )+
150    };
151}
152
153pub(crate) use impl_core_wrap_property_fns;
154pub(crate) use impl_wrap_property_fns;
155
156/// Wrapper for the core template property types.
157pub trait CoreTemplatePropertyVar<'a> {
158    fn wrap_string(property: BoxedTemplateProperty<'a, String>) -> Self;
159    fn wrap_string_list(property: BoxedTemplateProperty<'a, Vec<String>>) -> Self;
160    fn wrap_boolean(property: BoxedTemplateProperty<'a, bool>) -> Self;
161    fn wrap_integer(property: BoxedTemplateProperty<'a, i64>) -> Self;
162    fn wrap_integer_opt(property: BoxedTemplateProperty<'a, Option<i64>>) -> Self;
163    fn wrap_config_value(property: BoxedTemplateProperty<'a, ConfigValue>) -> Self;
164    fn wrap_signature(property: BoxedTemplateProperty<'a, Signature>) -> Self;
165    fn wrap_email(property: BoxedTemplateProperty<'a, Email>) -> Self;
166    fn wrap_size_hint(property: BoxedTemplateProperty<'a, SizeHint>) -> Self;
167    fn wrap_timestamp(property: BoxedTemplateProperty<'a, Timestamp>) -> Self;
168    fn wrap_timestamp_range(property: BoxedTemplateProperty<'a, TimestampRange>) -> Self;
169
170    fn wrap_template(template: Box<dyn Template + 'a>) -> Self;
171    fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> Self;
172
173    /// Type name of the property output.
174    fn type_name(&self) -> &'static str;
175
176    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>>;
177    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>>;
178
179    fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'a, String>>;
180    fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
181
182    /// Transforms into a property that will evaluate to `self == other`.
183    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>>;
184
185    /// Transforms into a property that will evaluate to an [`Ordering`].
186    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>>;
187}
188
189pub enum CoreTemplatePropertyKind<'a> {
190    String(BoxedTemplateProperty<'a, String>),
191    StringList(BoxedTemplateProperty<'a, Vec<String>>),
192    Boolean(BoxedTemplateProperty<'a, bool>),
193    Integer(BoxedTemplateProperty<'a, i64>),
194    IntegerOpt(BoxedTemplateProperty<'a, Option<i64>>),
195    ConfigValue(BoxedTemplateProperty<'a, ConfigValue>),
196    Signature(BoxedTemplateProperty<'a, Signature>),
197    Email(BoxedTemplateProperty<'a, Email>),
198    SizeHint(BoxedTemplateProperty<'a, SizeHint>),
199    Timestamp(BoxedTemplateProperty<'a, Timestamp>),
200    TimestampRange(BoxedTemplateProperty<'a, TimestampRange>),
201
202    // Both TemplateProperty and Template can represent a value to be evaluated
203    // dynamically, which suggests that `Box<dyn Template + 'a>` could be
204    // composed as `Box<dyn TemplateProperty<Output = Box<dyn Template ..`.
205    // However, there's a subtle difference: TemplateProperty is strict on
206    // error, whereas Template is usually lax and prints an error inline. If
207    // `concat(x, y)` were a property returning Template, and if `y` failed to
208    // evaluate, the whole expression would fail. In this example, a partial
209    // evaluation output is more useful. That's one reason why Template isn't
210    // wrapped in a TemplateProperty. Another reason is that the outermost
211    // caller expects a Template, not a TemplateProperty of Template output.
212    Template(Box<dyn Template + 'a>),
213    ListTemplate(Box<dyn ListTemplate + 'a>),
214}
215
216impl<'a> CoreTemplatePropertyVar<'a> for CoreTemplatePropertyKind<'a> {
217    impl_core_wrap_property_fns!('a);
218
219    fn type_name(&self) -> &'static str {
220        match self {
221            CoreTemplatePropertyKind::String(_) => "String",
222            CoreTemplatePropertyKind::StringList(_) => "List<String>",
223            CoreTemplatePropertyKind::Boolean(_) => "Boolean",
224            CoreTemplatePropertyKind::Integer(_) => "Integer",
225            CoreTemplatePropertyKind::IntegerOpt(_) => "Option<Integer>",
226            CoreTemplatePropertyKind::ConfigValue(_) => "ConfigValue",
227            CoreTemplatePropertyKind::Signature(_) => "Signature",
228            CoreTemplatePropertyKind::Email(_) => "Email",
229            CoreTemplatePropertyKind::SizeHint(_) => "SizeHint",
230            CoreTemplatePropertyKind::Timestamp(_) => "Timestamp",
231            CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange",
232            CoreTemplatePropertyKind::Template(_) => "Template",
233            CoreTemplatePropertyKind::ListTemplate(_) => "ListTemplate",
234        }
235    }
236
237    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
238        match self {
239            CoreTemplatePropertyKind::String(property) => {
240                Some(property.map(|s| !s.is_empty()).into_dyn())
241            }
242            CoreTemplatePropertyKind::StringList(property) => {
243                Some(property.map(|l| !l.is_empty()).into_dyn())
244            }
245            CoreTemplatePropertyKind::Boolean(property) => Some(property),
246            CoreTemplatePropertyKind::Integer(_) => None,
247            CoreTemplatePropertyKind::IntegerOpt(property) => {
248                Some(property.map(|opt| opt.is_some()).into_dyn())
249            }
250            CoreTemplatePropertyKind::ConfigValue(_) => None,
251            CoreTemplatePropertyKind::Signature(_) => None,
252            CoreTemplatePropertyKind::Email(property) => {
253                Some(property.map(|e| !e.0.is_empty()).into_dyn())
254            }
255            CoreTemplatePropertyKind::SizeHint(_) => None,
256            CoreTemplatePropertyKind::Timestamp(_) => None,
257            CoreTemplatePropertyKind::TimestampRange(_) => None,
258            // Template types could also be evaluated to boolean, but it's less likely
259            // to apply label() or .map() and use the result as conditional. It's also
260            // unclear whether ListTemplate should behave as a "list" or a "template".
261            CoreTemplatePropertyKind::Template(_) => None,
262            CoreTemplatePropertyKind::ListTemplate(_) => None,
263        }
264    }
265
266    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
267        match self {
268            CoreTemplatePropertyKind::Integer(property) => Some(property),
269            CoreTemplatePropertyKind::IntegerOpt(property) => {
270                Some(property.try_unwrap("Integer").into_dyn())
271            }
272            _ => None,
273        }
274    }
275
276    fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'a, String>> {
277        match self {
278            CoreTemplatePropertyKind::String(property) => Some(property),
279            _ => {
280                let template = self.try_into_template()?;
281                Some(PlainTextFormattedProperty::new(template).into_dyn())
282            }
283        }
284    }
285
286    fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
287        match self {
288            CoreTemplatePropertyKind::String(property) => Some(property.into_template()),
289            CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()),
290            CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()),
291            CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()),
292            CoreTemplatePropertyKind::IntegerOpt(property) => Some(property.into_template()),
293            CoreTemplatePropertyKind::ConfigValue(property) => Some(property.into_template()),
294            CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()),
295            CoreTemplatePropertyKind::Email(property) => Some(property.into_template()),
296            CoreTemplatePropertyKind::SizeHint(_) => None,
297            CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()),
298            CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()),
299            CoreTemplatePropertyKind::Template(template) => Some(template),
300            CoreTemplatePropertyKind::ListTemplate(template) => Some(template.into_template()),
301        }
302    }
303
304    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
305        match (self, other) {
306            (CoreTemplatePropertyKind::String(lhs), CoreTemplatePropertyKind::String(rhs)) => {
307                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
308            }
309            (CoreTemplatePropertyKind::String(lhs), CoreTemplatePropertyKind::Email(rhs)) => {
310                Some((lhs, rhs).map(|(l, r)| l == r.0).into_dyn())
311            }
312            (CoreTemplatePropertyKind::Boolean(lhs), CoreTemplatePropertyKind::Boolean(rhs)) => {
313                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
314            }
315            (CoreTemplatePropertyKind::Integer(lhs), CoreTemplatePropertyKind::Integer(rhs)) => {
316                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
317            }
318            (CoreTemplatePropertyKind::Email(lhs), CoreTemplatePropertyKind::Email(rhs)) => {
319                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
320            }
321            (CoreTemplatePropertyKind::Email(lhs), CoreTemplatePropertyKind::String(rhs)) => {
322                Some((lhs, rhs).map(|(l, r)| l.0 == r).into_dyn())
323            }
324            (CoreTemplatePropertyKind::String(_), _) => None,
325            (CoreTemplatePropertyKind::StringList(_), _) => None,
326            (CoreTemplatePropertyKind::Boolean(_), _) => None,
327            (CoreTemplatePropertyKind::Integer(_), _) => None,
328            (CoreTemplatePropertyKind::IntegerOpt(_), _) => None,
329            (CoreTemplatePropertyKind::ConfigValue(_), _) => None,
330            (CoreTemplatePropertyKind::Signature(_), _) => None,
331            (CoreTemplatePropertyKind::Email(_), _) => None,
332            (CoreTemplatePropertyKind::SizeHint(_), _) => None,
333            (CoreTemplatePropertyKind::Timestamp(_), _) => None,
334            (CoreTemplatePropertyKind::TimestampRange(_), _) => None,
335            (CoreTemplatePropertyKind::Template(_), _) => None,
336            (CoreTemplatePropertyKind::ListTemplate(_), _) => None,
337        }
338    }
339
340    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
341        match (self, other) {
342            (CoreTemplatePropertyKind::Integer(lhs), CoreTemplatePropertyKind::Integer(rhs)) => {
343                Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
344            }
345            (CoreTemplatePropertyKind::String(_), _) => None,
346            (CoreTemplatePropertyKind::StringList(_), _) => None,
347            (CoreTemplatePropertyKind::Boolean(_), _) => None,
348            (CoreTemplatePropertyKind::Integer(_), _) => None,
349            (CoreTemplatePropertyKind::IntegerOpt(_), _) => None,
350            (CoreTemplatePropertyKind::ConfigValue(_), _) => None,
351            (CoreTemplatePropertyKind::Signature(_), _) => None,
352            (CoreTemplatePropertyKind::Email(_), _) => None,
353            (CoreTemplatePropertyKind::SizeHint(_), _) => None,
354            (CoreTemplatePropertyKind::Timestamp(_), _) => None,
355            (CoreTemplatePropertyKind::TimestampRange(_), _) => None,
356            (CoreTemplatePropertyKind::Template(_), _) => None,
357            (CoreTemplatePropertyKind::ListTemplate(_), _) => None,
358        }
359    }
360}
361
362/// Function that translates global function call node.
363// The lifetime parameter 'a could be replaced with for<'a> to keep the method
364// table away from a certain lifetime. That's technically more correct, but I
365// couldn't find an easy way to expand that to the core template methods, which
366// are defined for L: TemplateLanguage<'a>. That's why the build fn table is
367// bound to a named lifetime, and therefore can't be cached statically.
368pub type TemplateBuildFunctionFn<'a, L> =
369    fn(
370        &L,
371        &mut TemplateDiagnostics,
372        &BuildContext<<L as TemplateLanguage<'a>>::Property>,
373        &FunctionCallNode,
374    ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
375
376/// Function that translates method call node of self type `T`.
377pub type TemplateBuildMethodFn<'a, L, T> =
378    fn(
379        &L,
380        &mut TemplateDiagnostics,
381        &BuildContext<<L as TemplateLanguage<'a>>::Property>,
382        BoxedTemplateProperty<'a, T>,
383        &FunctionCallNode,
384    ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
385
386/// Table of functions that translate global function call node.
387pub type TemplateBuildFunctionFnMap<'a, L> = HashMap<&'static str, TemplateBuildFunctionFn<'a, L>>;
388
389/// Table of functions that translate method call node of self type `T`.
390pub type TemplateBuildMethodFnMap<'a, L, T> =
391    HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>;
392
393/// Symbol table of functions and methods available in the core template.
394pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> {
395    pub functions: TemplateBuildFunctionFnMap<'a, L>,
396    pub string_methods: TemplateBuildMethodFnMap<'a, L, String>,
397    pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>,
398    pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>,
399    pub config_value_methods: TemplateBuildMethodFnMap<'a, L, ConfigValue>,
400    pub email_methods: TemplateBuildMethodFnMap<'a, L, Email>,
401    pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>,
402    pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint>,
403    pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>,
404    pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>,
405}
406
407pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
408    for (name, function) in extension {
409        if base.insert(name, function).is_some() {
410            panic!("Conflicting template definitions for '{name}' function");
411        }
412    }
413}
414
415impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
416    /// Creates new symbol table containing the builtin functions and methods.
417    pub fn builtin() -> Self {
418        CoreTemplateBuildFnTable {
419            functions: builtin_functions(),
420            string_methods: builtin_string_methods(),
421            boolean_methods: HashMap::new(),
422            integer_methods: HashMap::new(),
423            config_value_methods: builtin_config_value_methods(),
424            signature_methods: builtin_signature_methods(),
425            email_methods: builtin_email_methods(),
426            size_hint_methods: builtin_size_hint_methods(),
427            timestamp_methods: builtin_timestamp_methods(),
428            timestamp_range_methods: builtin_timestamp_range_methods(),
429        }
430    }
431
432    pub fn empty() -> Self {
433        CoreTemplateBuildFnTable {
434            functions: HashMap::new(),
435            string_methods: HashMap::new(),
436            boolean_methods: HashMap::new(),
437            integer_methods: HashMap::new(),
438            config_value_methods: HashMap::new(),
439            signature_methods: HashMap::new(),
440            email_methods: HashMap::new(),
441            size_hint_methods: HashMap::new(),
442            timestamp_methods: HashMap::new(),
443            timestamp_range_methods: HashMap::new(),
444        }
445    }
446
447    pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) {
448        let CoreTemplateBuildFnTable {
449            functions,
450            string_methods,
451            boolean_methods,
452            integer_methods,
453            config_value_methods,
454            signature_methods,
455            email_methods,
456            size_hint_methods,
457            timestamp_methods,
458            timestamp_range_methods,
459        } = extension;
460
461        merge_fn_map(&mut self.functions, functions);
462        merge_fn_map(&mut self.string_methods, string_methods);
463        merge_fn_map(&mut self.boolean_methods, boolean_methods);
464        merge_fn_map(&mut self.integer_methods, integer_methods);
465        merge_fn_map(&mut self.config_value_methods, config_value_methods);
466        merge_fn_map(&mut self.signature_methods, signature_methods);
467        merge_fn_map(&mut self.email_methods, email_methods);
468        merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
469        merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
470        merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
471    }
472
473    /// Translates the function call node `function` by using this symbol table.
474    pub fn build_function(
475        &self,
476        language: &L,
477        diagnostics: &mut TemplateDiagnostics,
478        build_ctx: &BuildContext<L::Property>,
479        function: &FunctionCallNode,
480    ) -> TemplateParseResult<L::Property> {
481        let table = &self.functions;
482        let build = template_parser::lookup_function(table, function)?;
483        build(language, diagnostics, build_ctx, function)
484    }
485
486    /// Applies the method call node `function` to the given `property` by using
487    /// this symbol table.
488    pub fn build_method(
489        &self,
490        language: &L,
491        diagnostics: &mut TemplateDiagnostics,
492        build_ctx: &BuildContext<L::Property>,
493        property: CoreTemplatePropertyKind<'a>,
494        function: &FunctionCallNode,
495    ) -> TemplateParseResult<L::Property> {
496        let type_name = property.type_name();
497        match property {
498            CoreTemplatePropertyKind::String(property) => {
499                let table = &self.string_methods;
500                let build = template_parser::lookup_method(type_name, table, function)?;
501                build(language, diagnostics, build_ctx, property, function)
502            }
503            CoreTemplatePropertyKind::StringList(property) => {
504                // TODO: migrate to table?
505                build_formattable_list_method(
506                    language,
507                    diagnostics,
508                    build_ctx,
509                    property,
510                    function,
511                    L::Property::wrap_string,
512                    L::Property::wrap_string_list,
513                )
514            }
515            CoreTemplatePropertyKind::Boolean(property) => {
516                let table = &self.boolean_methods;
517                let build = template_parser::lookup_method(type_name, table, function)?;
518                build(language, diagnostics, build_ctx, property, function)
519            }
520            CoreTemplatePropertyKind::Integer(property) => {
521                let table = &self.integer_methods;
522                let build = template_parser::lookup_method(type_name, table, function)?;
523                build(language, diagnostics, build_ctx, property, function)
524            }
525            CoreTemplatePropertyKind::IntegerOpt(property) => {
526                let type_name = "Integer";
527                let table = &self.integer_methods;
528                let build = template_parser::lookup_method(type_name, table, function)?;
529                let inner_property = property.try_unwrap(type_name).into_dyn();
530                build(language, diagnostics, build_ctx, inner_property, function)
531            }
532            CoreTemplatePropertyKind::ConfigValue(property) => {
533                let table = &self.config_value_methods;
534                let build = template_parser::lookup_method(type_name, table, function)?;
535                build(language, diagnostics, build_ctx, property, function)
536            }
537            CoreTemplatePropertyKind::Signature(property) => {
538                let table = &self.signature_methods;
539                let build = template_parser::lookup_method(type_name, table, function)?;
540                build(language, diagnostics, build_ctx, property, function)
541            }
542            CoreTemplatePropertyKind::Email(property) => {
543                let table = &self.email_methods;
544                let build = template_parser::lookup_method(type_name, table, function)?;
545                build(language, diagnostics, build_ctx, property, function)
546            }
547            CoreTemplatePropertyKind::SizeHint(property) => {
548                let table = &self.size_hint_methods;
549                let build = template_parser::lookup_method(type_name, table, function)?;
550                build(language, diagnostics, build_ctx, property, function)
551            }
552            CoreTemplatePropertyKind::Timestamp(property) => {
553                let table = &self.timestamp_methods;
554                let build = template_parser::lookup_method(type_name, table, function)?;
555                build(language, diagnostics, build_ctx, property, function)
556            }
557            CoreTemplatePropertyKind::TimestampRange(property) => {
558                let table = &self.timestamp_range_methods;
559                let build = template_parser::lookup_method(type_name, table, function)?;
560                build(language, diagnostics, build_ctx, property, function)
561            }
562            CoreTemplatePropertyKind::Template(_) => {
563                // TODO: migrate to table?
564                Err(TemplateParseError::no_such_method(type_name, function))
565            }
566            CoreTemplatePropertyKind::ListTemplate(template) => {
567                // TODO: migrate to table?
568                build_list_template_method(language, diagnostics, build_ctx, template, function)
569            }
570        }
571    }
572}
573
574/// Opaque struct that represents a template value.
575pub struct Expression<P> {
576    property: P,
577    labels: Vec<String>,
578}
579
580impl<P> Expression<P> {
581    fn unlabeled(property: P) -> Self {
582        let labels = vec![];
583        Expression { property, labels }
584    }
585
586    fn with_label(property: P, label: impl Into<String>) -> Self {
587        let labels = vec![label.into()];
588        Expression { property, labels }
589    }
590}
591
592impl<'a, P: CoreTemplatePropertyVar<'a>> Expression<P> {
593    pub fn type_name(&self) -> &'static str {
594        self.property.type_name()
595    }
596
597    pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
598        self.property.try_into_boolean()
599    }
600
601    pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
602        self.property.try_into_integer()
603    }
604
605    pub fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'a, String>> {
606        self.property.try_into_plain_text()
607    }
608
609    pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
610        let template = self.property.try_into_template()?;
611        if self.labels.is_empty() {
612            Some(template)
613        } else {
614            Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
615        }
616    }
617
618    pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
619        self.property.try_into_eq(other.property)
620    }
621
622    pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
623        self.property.try_into_cmp(other.property)
624    }
625}
626
627pub struct BuildContext<'i, P> {
628    /// Map of functions to create `L::Property`.
629    local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>,
630    /// Function to create `L::Property` representing `self`.
631    ///
632    /// This could be `local_variables["self"]`, but keyword lookup shouldn't be
633    /// overridden by a user-defined `self` variable.
634    self_variable: &'i (dyn Fn() -> P),
635}
636
637fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
638    language: &L,
639    diagnostics: &mut TemplateDiagnostics,
640    build_ctx: &BuildContext<L::Property>,
641    name: &str,
642    name_span: pest::Span<'_>,
643) -> TemplateParseResult<L::Property> {
644    // Keyword is a 0-ary method on the "self" property
645    let self_property = (build_ctx.self_variable)();
646    let function = FunctionCallNode {
647        name,
648        name_span,
649        args: vec![],
650        keyword_args: vec![],
651        args_span: name_span.end_pos().span(&name_span.end_pos()),
652    };
653    language
654        .build_method(diagnostics, build_ctx, self_property, &function)
655        .map_err(|err| match err.kind() {
656            TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
657                let kind = TemplateParseErrorKind::NoSuchKeyword {
658                    name: name.to_owned(),
659                    // TODO: filter methods by arity?
660                    candidates: candidates.clone(),
661                };
662                TemplateParseError::with_span(kind, name_span)
663            }
664            // Since keyword is a 0-ary method, any argument errors mean there's
665            // no such keyword.
666            TemplateParseErrorKind::InvalidArguments { .. } => {
667                let kind = TemplateParseErrorKind::NoSuchKeyword {
668                    name: name.to_owned(),
669                    // TODO: might be better to phrase the error differently
670                    candidates: vec![format!("self.{name}(..)")],
671                };
672                TemplateParseError::with_span(kind, name_span)
673            }
674            // The keyword function may fail with the other reasons.
675            _ => err,
676        })
677}
678
679fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
680    language: &L,
681    diagnostics: &mut TemplateDiagnostics,
682    build_ctx: &BuildContext<L::Property>,
683    op: UnaryOp,
684    arg_node: &ExpressionNode,
685) -> TemplateParseResult<L::Property> {
686    match op {
687        UnaryOp::LogicalNot => {
688            let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
689            Ok(L::Property::wrap_boolean(arg.map(|v| !v).into_dyn()))
690        }
691        UnaryOp::Negate => {
692            let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
693            let out = arg.and_then(|v| {
694                v.checked_neg()
695                    .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
696            });
697            Ok(L::Property::wrap_integer(out.into_dyn()))
698        }
699    }
700}
701
702fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
703    language: &L,
704    diagnostics: &mut TemplateDiagnostics,
705    build_ctx: &BuildContext<L::Property>,
706    op: BinaryOp,
707    lhs_node: &ExpressionNode,
708    rhs_node: &ExpressionNode,
709    span: pest::Span<'_>,
710) -> TemplateParseResult<L::Property> {
711    match op {
712        BinaryOp::LogicalOr => {
713            let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
714            let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
715            let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
716            Ok(L::Property::wrap_boolean(out.into_dyn()))
717        }
718        BinaryOp::LogicalAnd => {
719            let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
720            let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
721            let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
722            Ok(L::Property::wrap_boolean(out.into_dyn()))
723        }
724        BinaryOp::Eq | BinaryOp::Ne => {
725            let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
726            let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
727            let lty = lhs.type_name();
728            let rty = rhs.type_name();
729            let eq = lhs.try_into_eq(rhs).ok_or_else(|| {
730                let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
731                TemplateParseError::expression(message, span)
732            })?;
733            let out = match op {
734                BinaryOp::Eq => eq.into_dyn(),
735                BinaryOp::Ne => eq.map(|eq| !eq).into_dyn(),
736                _ => unreachable!(),
737            };
738            Ok(L::Property::wrap_boolean(out))
739        }
740        BinaryOp::Ge | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Lt => {
741            let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
742            let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
743            let lty = lhs.type_name();
744            let rty = rhs.type_name();
745            let cmp = lhs.try_into_cmp(rhs).ok_or_else(|| {
746                let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
747                TemplateParseError::expression(message, span)
748            })?;
749            let out = match op {
750                BinaryOp::Ge => cmp.map(|ordering| ordering.is_ge()).into_dyn(),
751                BinaryOp::Gt => cmp.map(|ordering| ordering.is_gt()).into_dyn(),
752                BinaryOp::Le => cmp.map(|ordering| ordering.is_le()).into_dyn(),
753                BinaryOp::Lt => cmp.map(|ordering| ordering.is_lt()).into_dyn(),
754                _ => unreachable!(),
755            };
756            Ok(L::Property::wrap_boolean(out))
757        }
758    }
759}
760
761fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
762) -> TemplateBuildMethodFnMap<'a, L, String> {
763    // Not using maplit::hashmap!{} or custom declarative macro here because
764    // code completion inside macro is quite restricted.
765    let mut map = TemplateBuildMethodFnMap::<L, String>::new();
766    map.insert(
767        "len",
768        |_language, _diagnostics, _build_ctx, self_property, function| {
769            function.expect_no_arguments()?;
770            let out_property = self_property.and_then(|s| Ok(s.len().try_into()?));
771            Ok(L::Property::wrap_integer(out_property.into_dyn()))
772        },
773    );
774    map.insert(
775        "contains",
776        |language, diagnostics, build_ctx, self_property, function| {
777            let [needle_node] = function.expect_exact_arguments()?;
778            // TODO: or .try_into_string() to disable implicit type cast?
779            let needle_property =
780                expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
781            let out_property = (self_property, needle_property)
782                .map(|(haystack, needle)| haystack.contains(&needle));
783            Ok(L::Property::wrap_boolean(out_property.into_dyn()))
784        },
785    );
786    map.insert(
787        "starts_with",
788        |language, diagnostics, build_ctx, self_property, function| {
789            let [needle_node] = function.expect_exact_arguments()?;
790            let needle_property =
791                expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
792            let out_property = (self_property, needle_property)
793                .map(|(haystack, needle)| haystack.starts_with(&needle));
794            Ok(L::Property::wrap_boolean(out_property.into_dyn()))
795        },
796    );
797    map.insert(
798        "ends_with",
799        |language, diagnostics, build_ctx, self_property, function| {
800            let [needle_node] = function.expect_exact_arguments()?;
801            let needle_property =
802                expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
803            let out_property = (self_property, needle_property)
804                .map(|(haystack, needle)| haystack.ends_with(&needle));
805            Ok(L::Property::wrap_boolean(out_property.into_dyn()))
806        },
807    );
808    map.insert(
809        "remove_prefix",
810        |language, diagnostics, build_ctx, self_property, function| {
811            let [needle_node] = function.expect_exact_arguments()?;
812            let needle_property =
813                expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
814            let out_property = (self_property, needle_property).map(|(haystack, needle)| {
815                haystack
816                    .strip_prefix(&needle)
817                    .map(ToOwned::to_owned)
818                    .unwrap_or(haystack)
819            });
820            Ok(L::Property::wrap_string(out_property.into_dyn()))
821        },
822    );
823    map.insert(
824        "remove_suffix",
825        |language, diagnostics, build_ctx, self_property, function| {
826            let [needle_node] = function.expect_exact_arguments()?;
827            let needle_property =
828                expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
829            let out_property = (self_property, needle_property).map(|(haystack, needle)| {
830                haystack
831                    .strip_suffix(&needle)
832                    .map(ToOwned::to_owned)
833                    .unwrap_or(haystack)
834            });
835            Ok(L::Property::wrap_string(out_property.into_dyn()))
836        },
837    );
838    map.insert(
839        "trim",
840        |_language, _diagnostics, _build_ctx, self_property, function| {
841            function.expect_no_arguments()?;
842            let out_property = self_property.map(|s| s.trim().to_owned());
843            Ok(L::Property::wrap_string(out_property.into_dyn()))
844        },
845    );
846    map.insert(
847        "trim_start",
848        |_language, _diagnostics, _build_ctx, self_property, function| {
849            function.expect_no_arguments()?;
850            let out_property = self_property.map(|s| s.trim_start().to_owned());
851            Ok(L::Property::wrap_string(out_property.into_dyn()))
852        },
853    );
854    map.insert(
855        "trim_end",
856        |_language, _diagnostics, _build_ctx, self_property, function| {
857            function.expect_no_arguments()?;
858            let out_property = self_property.map(|s| s.trim_end().to_owned());
859            Ok(L::Property::wrap_string(out_property.into_dyn()))
860        },
861    );
862    map.insert(
863        "substr",
864        |language, diagnostics, build_ctx, self_property, function| {
865            let [start_idx, end_idx] = function.expect_exact_arguments()?;
866            let start_idx_property =
867                expect_isize_expression(language, diagnostics, build_ctx, start_idx)?;
868            let end_idx_property =
869                expect_isize_expression(language, diagnostics, build_ctx, end_idx)?;
870            let out_property = (self_property, start_idx_property, end_idx_property).map(
871                |(s, start_idx, end_idx)| {
872                    let start_idx = string_index_to_char_boundary(&s, start_idx);
873                    let end_idx = string_index_to_char_boundary(&s, end_idx);
874                    s.get(start_idx..end_idx).unwrap_or_default().to_owned()
875                },
876            );
877            Ok(L::Property::wrap_string(out_property.into_dyn()))
878        },
879    );
880    map.insert(
881        "first_line",
882        |_language, _diagnostics, _build_ctx, self_property, function| {
883            function.expect_no_arguments()?;
884            let out_property =
885                self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
886            Ok(L::Property::wrap_string(out_property.into_dyn()))
887        },
888    );
889    map.insert(
890        "lines",
891        |_language, _diagnostics, _build_ctx, self_property, function| {
892            function.expect_no_arguments()?;
893            let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect());
894            Ok(L::Property::wrap_string_list(out_property.into_dyn()))
895        },
896    );
897    map.insert(
898        "upper",
899        |_language, _diagnostics, _build_ctx, self_property, function| {
900            function.expect_no_arguments()?;
901            let out_property = self_property.map(|s| s.to_uppercase());
902            Ok(L::Property::wrap_string(out_property.into_dyn()))
903        },
904    );
905    map.insert(
906        "lower",
907        |_language, _diagnostics, _build_ctx, self_property, function| {
908            function.expect_no_arguments()?;
909            let out_property = self_property.map(|s| s.to_lowercase());
910            Ok(L::Property::wrap_string(out_property.into_dyn()))
911        },
912    );
913    map.insert(
914        "escape_json",
915        |_language, _diagnostics, _build_ctx, self_property, function| {
916            function.expect_no_arguments()?;
917            let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
918            Ok(L::Property::wrap_string(out_property.into_dyn()))
919        },
920    );
921    map
922}
923
924/// Clamps and aligns the given index `i` to char boundary.
925///
926/// Negative index counts from the end. If the index isn't at a char boundary,
927/// it will be rounded towards 0 (left or right depending on the sign.)
928fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
929    // TODO: use floor/ceil_char_boundary() if get stabilized
930    let magnitude = i.unsigned_abs();
931    if i < 0 {
932        let p = s.len().saturating_sub(magnitude);
933        (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
934    } else {
935        let p = magnitude.min(s.len());
936        (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
937    }
938}
939
940fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
941) -> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
942    fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
943        T::deserialize(value.into_deserializer())
944            // map to err.message() because TomlError appends newline to it
945            .map_err(|err| TemplatePropertyError(err.message().into()))
946    }
947
948    // Not using maplit::hashmap!{} or custom declarative macro here because
949    // code completion inside macro is quite restricted.
950    let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
951    // These methods are called "as_<type>", not "to_<type>" to clarify that
952    // they'll never convert types (e.g. integer to string.) Since templater
953    // doesn't provide binding syntax, there's no need to distinguish between
954    // reference and consuming access.
955    map.insert(
956        "as_boolean",
957        |_language, _diagnostics, _build_ctx, self_property, function| {
958            function.expect_no_arguments()?;
959            let out_property = self_property.and_then(extract);
960            Ok(L::Property::wrap_boolean(out_property.into_dyn()))
961        },
962    );
963    map.insert(
964        "as_integer",
965        |_language, _diagnostics, _build_ctx, self_property, function| {
966            function.expect_no_arguments()?;
967            let out_property = self_property.and_then(extract);
968            Ok(L::Property::wrap_integer(out_property.into_dyn()))
969        },
970    );
971    map.insert(
972        "as_string",
973        |_language, _diagnostics, _build_ctx, self_property, function| {
974            function.expect_no_arguments()?;
975            let out_property = self_property.and_then(extract);
976            Ok(L::Property::wrap_string(out_property.into_dyn()))
977        },
978    );
979    map.insert(
980        "as_string_list",
981        |_language, _diagnostics, _build_ctx, self_property, function| {
982            function.expect_no_arguments()?;
983            let out_property = self_property.and_then(extract);
984            Ok(L::Property::wrap_string_list(out_property.into_dyn()))
985        },
986    );
987    // TODO: add is_<type>() -> Boolean?
988    // TODO: add .get(key) -> ConfigValue or Option<ConfigValue>?
989    map
990}
991
992fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
993) -> TemplateBuildMethodFnMap<'a, L, Signature> {
994    // Not using maplit::hashmap!{} or custom declarative macro here because
995    // code completion inside macro is quite restricted.
996    let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
997    map.insert(
998        "name",
999        |_language, _diagnostics, _build_ctx, self_property, function| {
1000            function.expect_no_arguments()?;
1001            let out_property = self_property.map(|signature| signature.name);
1002            Ok(L::Property::wrap_string(out_property.into_dyn()))
1003        },
1004    );
1005    map.insert(
1006        "email",
1007        |_language, _diagnostics, _build_ctx, self_property, function| {
1008            function.expect_no_arguments()?;
1009            let out_property = self_property.map(|signature| signature.email.into());
1010            Ok(L::Property::wrap_email(out_property.into_dyn()))
1011        },
1012    );
1013    map.insert(
1014        "username",
1015        |_language, diagnostics, _build_ctx, self_property, function| {
1016            function.expect_no_arguments()?;
1017            // TODO: Remove in jj 0.30+
1018            diagnostics.add_warning(TemplateParseError::expression(
1019                "username() is deprecated; use email().local() instead",
1020                function.name_span,
1021            ));
1022            let out_property = self_property.map(|signature| {
1023                let (username, _) = text_util::split_email(&signature.email);
1024                username.to_owned()
1025            });
1026            Ok(L::Property::wrap_string(out_property.into_dyn()))
1027        },
1028    );
1029    map.insert(
1030        "timestamp",
1031        |_language, _diagnostics, _build_ctx, self_property, function| {
1032            function.expect_no_arguments()?;
1033            let out_property = self_property.map(|signature| signature.timestamp);
1034            Ok(L::Property::wrap_timestamp(out_property.into_dyn()))
1035        },
1036    );
1037    map
1038}
1039
1040fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
1041) -> TemplateBuildMethodFnMap<'a, L, Email> {
1042    // Not using maplit::hashmap!{} or custom declarative macro here because
1043    // code completion inside macro is quite restricted.
1044    let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1045    map.insert(
1046        "local",
1047        |_language, _diagnostics, _build_ctx, self_property, function| {
1048            function.expect_no_arguments()?;
1049            let out_property = self_property.map(|email| {
1050                let (local, _) = text_util::split_email(&email.0);
1051                local.to_owned()
1052            });
1053            Ok(L::Property::wrap_string(out_property.into_dyn()))
1054        },
1055    );
1056    map.insert(
1057        "domain",
1058        |_language, _diagnostics, _build_ctx, self_property, function| {
1059            function.expect_no_arguments()?;
1060            let out_property = self_property.map(|email| {
1061                let (_, domain) = text_util::split_email(&email.0);
1062                domain.unwrap_or_default().to_owned()
1063            });
1064            Ok(L::Property::wrap_string(out_property.into_dyn()))
1065        },
1066    );
1067    map
1068}
1069
1070fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
1071) -> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1072    // Not using maplit::hashmap!{} or custom declarative macro here because
1073    // code completion inside macro is quite restricted.
1074    let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1075    map.insert(
1076        "lower",
1077        |_language, _diagnostics, _build_ctx, self_property, function| {
1078            function.expect_no_arguments()?;
1079            let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1080            Ok(L::Property::wrap_integer(out_property.into_dyn()))
1081        },
1082    );
1083    map.insert(
1084        "upper",
1085        |_language, _diagnostics, _build_ctx, self_property, function| {
1086            function.expect_no_arguments()?;
1087            let out_property =
1088                self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1089            Ok(L::Property::wrap_integer_opt(out_property.into_dyn()))
1090        },
1091    );
1092    map.insert(
1093        "exact",
1094        |_language, _diagnostics, _build_ctx, self_property, function| {
1095            function.expect_no_arguments()?;
1096            let out_property = self_property.and_then(|(lower, upper)| {
1097                let exact = (Some(lower) == upper).then_some(lower);
1098                Ok(exact.map(i64::try_from).transpose()?)
1099            });
1100            Ok(L::Property::wrap_integer_opt(out_property.into_dyn()))
1101        },
1102    );
1103    map.insert(
1104        "zero",
1105        |_language, _diagnostics, _build_ctx, self_property, function| {
1106            function.expect_no_arguments()?;
1107            let out_property = self_property.map(|(_, upper)| upper == Some(0));
1108            Ok(L::Property::wrap_boolean(out_property.into_dyn()))
1109        },
1110    );
1111    map
1112}
1113
1114fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
1115) -> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1116    // Not using maplit::hashmap!{} or custom declarative macro here because
1117    // code completion inside macro is quite restricted.
1118    let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1119    map.insert(
1120        "ago",
1121        |_language, _diagnostics, _build_ctx, self_property, function| {
1122            function.expect_no_arguments()?;
1123            let now = Timestamp::now();
1124            let format = timeago::Formatter::new();
1125            let out_property = self_property.and_then(move |timestamp| {
1126                Ok(time_util::format_duration(&timestamp, &now, &format)?)
1127            });
1128            Ok(L::Property::wrap_string(out_property.into_dyn()))
1129        },
1130    );
1131    map.insert(
1132        "format",
1133        |_language, _diagnostics, _build_ctx, self_property, function| {
1134            // No dynamic string is allowed as the templater has no runtime error type.
1135            let [format_node] = function.expect_exact_arguments()?;
1136            let format =
1137                template_parser::expect_string_literal_with(format_node, |format, span| {
1138                    time_util::FormattingItems::parse(format)
1139                        .ok_or_else(|| TemplateParseError::expression("Invalid time format", span))
1140                })?
1141                .into_owned();
1142            let out_property = self_property.and_then(move |timestamp| {
1143                Ok(time_util::format_absolute_timestamp_with(
1144                    &timestamp, &format,
1145                )?)
1146            });
1147            Ok(L::Property::wrap_string(out_property.into_dyn()))
1148        },
1149    );
1150    map.insert(
1151        "utc",
1152        |_language, _diagnostics, _build_ctx, self_property, function| {
1153            function.expect_no_arguments()?;
1154            let out_property = self_property.map(|mut timestamp| {
1155                timestamp.tz_offset = 0;
1156                timestamp
1157            });
1158            Ok(L::Property::wrap_timestamp(out_property.into_dyn()))
1159        },
1160    );
1161    map.insert(
1162        "local",
1163        |_language, _diagnostics, _build_ctx, self_property, function| {
1164            function.expect_no_arguments()?;
1165            let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1166                .ok()
1167                .and_then(|tz_string| tz_string.parse::<i32>().ok())
1168                .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1169            let out_property = self_property.map(move |mut timestamp| {
1170                timestamp.tz_offset = tz_offset;
1171                timestamp
1172            });
1173            Ok(L::Property::wrap_timestamp(out_property.into_dyn()))
1174        },
1175    );
1176    map.insert(
1177        "after",
1178        |_language, _diagnostics, _build_ctx, self_property, function| {
1179            let [date_pattern_node] = function.expect_exact_arguments()?;
1180            let now = chrono::Local::now();
1181            let date_pattern = template_parser::expect_string_literal_with(
1182                date_pattern_node,
1183                |date_pattern, span| {
1184                    DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1185                        TemplateParseError::expression("Invalid date pattern", span)
1186                            .with_source(err)
1187                    })
1188                },
1189            )?;
1190            let out_property = self_property.map(move |timestamp| date_pattern.matches(&timestamp));
1191            Ok(L::Property::wrap_boolean(out_property.into_dyn()))
1192        },
1193    );
1194    map.insert("before", map["after"]);
1195    map
1196}
1197
1198fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
1199) -> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1200    // Not using maplit::hashmap!{} or custom declarative macro here because
1201    // code completion inside macro is quite restricted.
1202    let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1203    map.insert(
1204        "start",
1205        |_language, _diagnostics, _build_ctx, self_property, function| {
1206            function.expect_no_arguments()?;
1207            let out_property = self_property.map(|time_range| time_range.start);
1208            Ok(L::Property::wrap_timestamp(out_property.into_dyn()))
1209        },
1210    );
1211    map.insert(
1212        "end",
1213        |_language, _diagnostics, _build_ctx, self_property, function| {
1214            function.expect_no_arguments()?;
1215            let out_property = self_property.map(|time_range| time_range.end);
1216            Ok(L::Property::wrap_timestamp(out_property.into_dyn()))
1217        },
1218    );
1219    map.insert(
1220        "duration",
1221        |_language, _diagnostics, _build_ctx, self_property, function| {
1222            function.expect_no_arguments()?;
1223            let out_property = self_property.and_then(|time_range| Ok(time_range.duration()?));
1224            Ok(L::Property::wrap_string(out_property.into_dyn()))
1225        },
1226    );
1227    map
1228}
1229
1230fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>(
1231    language: &L,
1232    diagnostics: &mut TemplateDiagnostics,
1233    build_ctx: &BuildContext<L::Property>,
1234    self_template: Box<dyn ListTemplate + 'a>,
1235    function: &FunctionCallNode,
1236) -> TemplateParseResult<L::Property> {
1237    let property = match function.name {
1238        "join" => {
1239            let [separator_node] = function.expect_exact_arguments()?;
1240            let separator =
1241                expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1242            L::Property::wrap_template(self_template.join(separator))
1243        }
1244        _ => return Err(TemplateParseError::no_such_method("ListTemplate", function)),
1245    };
1246    Ok(property)
1247}
1248
1249/// Builds method call expression for printable list property.
1250pub fn build_formattable_list_method<'a, L, O>(
1251    language: &L,
1252    diagnostics: &mut TemplateDiagnostics,
1253    build_ctx: &BuildContext<L::Property>,
1254    self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
1255    function: &FunctionCallNode,
1256    // TODO: Generic L: WrapProperty<O> trait might be needed to support more
1257    // list operations such as first()/slice().
1258    wrap_item: impl Fn(BoxedTemplateProperty<'a, O>) -> L::Property,
1259    wrap_list: impl Fn(BoxedTemplateProperty<'a, Vec<O>>) -> L::Property,
1260) -> TemplateParseResult<L::Property>
1261where
1262    L: TemplateLanguage<'a> + ?Sized,
1263    O: Template + Clone + 'a,
1264{
1265    let property = match function.name {
1266        "len" => {
1267            function.expect_no_arguments()?;
1268            let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
1269            L::Property::wrap_integer(out_property.into_dyn())
1270        }
1271        "join" => {
1272            let [separator_node] = function.expect_exact_arguments()?;
1273            let separator =
1274                expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1275            let template =
1276                ListPropertyTemplate::new(self_property, separator, |formatter, item| {
1277                    item.format(formatter)
1278                });
1279            L::Property::wrap_template(Box::new(template))
1280        }
1281        "filter" => build_filter_operation(
1282            language,
1283            diagnostics,
1284            build_ctx,
1285            self_property,
1286            function,
1287            wrap_item,
1288            wrap_list,
1289        )?,
1290        "map" => build_map_operation(
1291            language,
1292            diagnostics,
1293            build_ctx,
1294            self_property,
1295            function,
1296            wrap_item,
1297        )?,
1298        _ => return Err(TemplateParseError::no_such_method("List", function)),
1299    };
1300    Ok(property)
1301}
1302
1303pub fn build_unformattable_list_method<'a, L, O>(
1304    language: &L,
1305    diagnostics: &mut TemplateDiagnostics,
1306    build_ctx: &BuildContext<L::Property>,
1307    self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
1308    function: &FunctionCallNode,
1309    wrap_item: impl Fn(BoxedTemplateProperty<'a, O>) -> L::Property,
1310    wrap_list: impl Fn(BoxedTemplateProperty<'a, Vec<O>>) -> L::Property,
1311) -> TemplateParseResult<L::Property>
1312where
1313    L: TemplateLanguage<'a> + ?Sized,
1314    O: Clone + 'a,
1315{
1316    let property = match function.name {
1317        "len" => {
1318            function.expect_no_arguments()?;
1319            let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
1320            L::Property::wrap_integer(out_property.into_dyn())
1321        }
1322        // No "join"
1323        "filter" => build_filter_operation(
1324            language,
1325            diagnostics,
1326            build_ctx,
1327            self_property,
1328            function,
1329            wrap_item,
1330            wrap_list,
1331        )?,
1332        "map" => build_map_operation(
1333            language,
1334            diagnostics,
1335            build_ctx,
1336            self_property,
1337            function,
1338            wrap_item,
1339        )?,
1340        _ => return Err(TemplateParseError::no_such_method("List", function)),
1341    };
1342    Ok(property)
1343}
1344
1345/// Builds expression that extracts iterable property and filters its items.
1346///
1347/// `wrap_item()` is the function to wrap a list item of type `O` as a property.
1348/// `wrap_list()` is the function to wrap filtered list.
1349fn build_filter_operation<'a, L, O, P, B>(
1350    language: &L,
1351    diagnostics: &mut TemplateDiagnostics,
1352    build_ctx: &BuildContext<L::Property>,
1353    self_property: P,
1354    function: &FunctionCallNode,
1355    wrap_item: impl Fn(BoxedTemplateProperty<'a, O>) -> L::Property,
1356    wrap_list: impl Fn(BoxedTemplateProperty<'a, B>) -> L::Property,
1357) -> TemplateParseResult<L::Property>
1358where
1359    L: TemplateLanguage<'a> + ?Sized,
1360    P: TemplateProperty + 'a,
1361    P::Output: IntoIterator<Item = O>,
1362    O: Clone + 'a,
1363    B: FromIterator<O>,
1364{
1365    let [lambda_node] = function.expect_exact_arguments()?;
1366    let item_placeholder = PropertyPlaceholder::new();
1367    let item_predicate = template_parser::expect_lambda_with(lambda_node, |lambda, _span| {
1368        build_lambda_expression(
1369            build_ctx,
1370            lambda,
1371            &[&|| wrap_item(item_placeholder.clone().into_dyn())],
1372            |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1373        )
1374    })?;
1375    let out_property = self_property.and_then(move |items| {
1376        items
1377            .into_iter()
1378            .filter_map(|item| {
1379                // Evaluate predicate with the current item
1380                item_placeholder.set(item);
1381                let result = item_predicate.extract();
1382                let item = item_placeholder.take().unwrap();
1383                result.map(|pred| pred.then_some(item)).transpose()
1384            })
1385            .collect()
1386    });
1387    Ok(wrap_list(out_property.into_dyn()))
1388}
1389
1390/// Builds expression that extracts iterable property and applies template to
1391/// each item.
1392///
1393/// `wrap_item()` is the function to wrap a list item of type `O` as a property.
1394fn build_map_operation<'a, L, O, P>(
1395    language: &L,
1396    diagnostics: &mut TemplateDiagnostics,
1397    build_ctx: &BuildContext<L::Property>,
1398    self_property: P,
1399    function: &FunctionCallNode,
1400    wrap_item: impl Fn(BoxedTemplateProperty<'a, O>) -> L::Property,
1401) -> TemplateParseResult<L::Property>
1402where
1403    L: TemplateLanguage<'a> + ?Sized,
1404    P: TemplateProperty + 'a,
1405    P::Output: IntoIterator<Item = O>,
1406    O: Clone + 'a,
1407{
1408    let [lambda_node] = function.expect_exact_arguments()?;
1409    let item_placeholder = PropertyPlaceholder::new();
1410    let item_template = template_parser::expect_lambda_with(lambda_node, |lambda, _span| {
1411        build_lambda_expression(
1412            build_ctx,
1413            lambda,
1414            &[&|| wrap_item(item_placeholder.clone().into_dyn())],
1415            |build_ctx, body| expect_template_expression(language, diagnostics, build_ctx, body),
1416        )
1417    })?;
1418    let list_template = ListPropertyTemplate::new(
1419        self_property,
1420        Literal(" "), // separator
1421        move |formatter, item| {
1422            item_placeholder.with_value(item, || item_template.format(formatter))
1423        },
1424    );
1425    Ok(L::Property::wrap_list_template(Box::new(list_template)))
1426}
1427
1428/// Builds lambda expression to be evaluated with the provided arguments.
1429/// `arg_fns` is usually an array of wrapped [`PropertyPlaceholder`]s.
1430fn build_lambda_expression<'i, P, T>(
1431    build_ctx: &BuildContext<'i, P>,
1432    lambda: &LambdaNode<'i>,
1433    arg_fns: &[&'i dyn Fn() -> P],
1434    build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
1435) -> TemplateParseResult<T> {
1436    if lambda.params.len() != arg_fns.len() {
1437        return Err(TemplateParseError::expression(
1438            format!("Expected {} lambda parameters", arg_fns.len()),
1439            lambda.params_span,
1440        ));
1441    }
1442    let mut local_variables = build_ctx.local_variables.clone();
1443    local_variables.extend(iter::zip(&lambda.params, arg_fns));
1444    let inner_build_ctx = BuildContext {
1445        local_variables,
1446        self_variable: build_ctx.self_variable,
1447    };
1448    build_body(&inner_build_ctx, &lambda.body)
1449}
1450
1451fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
1452    // Not using maplit::hashmap!{} or custom declarative macro here because
1453    // code completion inside macro is quite restricted.
1454    let mut map = TemplateBuildFunctionFnMap::<L>::new();
1455    map.insert("fill", |language, diagnostics, build_ctx, function| {
1456        let [width_node, content_node] = function.expect_exact_arguments()?;
1457        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1458        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1459        let template =
1460            ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
1461                Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
1462                Err(err) => formatter.handle_error(err),
1463            });
1464        Ok(L::Property::wrap_template(Box::new(template)))
1465    });
1466    map.insert("indent", |language, diagnostics, build_ctx, function| {
1467        let [prefix_node, content_node] = function.expect_exact_arguments()?;
1468        let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1469        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1470        let template = ReformatTemplate::new(content, move |formatter, recorded| {
1471            let rewrap = formatter.rewrap_fn();
1472            text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
1473                prefix.format(&mut rewrap(formatter))
1474            })
1475        });
1476        Ok(L::Property::wrap_template(Box::new(template)))
1477    });
1478    map.insert("pad_start", |language, diagnostics, build_ctx, function| {
1479        let ([width_node, content_node], [fill_char_node]) =
1480            function.expect_named_arguments(&["", "", "fill_char"])?;
1481        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1482        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1483        let fill_char = fill_char_node
1484            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1485            .transpose()?;
1486        let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
1487        Ok(L::Property::wrap_template(template))
1488    });
1489    map.insert("pad_end", |language, diagnostics, build_ctx, function| {
1490        let ([width_node, content_node], [fill_char_node]) =
1491            function.expect_named_arguments(&["", "", "fill_char"])?;
1492        let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1493        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1494        let fill_char = fill_char_node
1495            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1496            .transpose()?;
1497        let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
1498        Ok(L::Property::wrap_template(template))
1499    });
1500    map.insert(
1501        "pad_centered",
1502        |language, diagnostics, build_ctx, function| {
1503            let ([width_node, content_node], [fill_char_node]) =
1504                function.expect_named_arguments(&["", "", "fill_char"])?;
1505            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1506            let content =
1507                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1508            let fill_char = fill_char_node
1509                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1510                .transpose()?;
1511            let template =
1512                new_pad_template(content, fill_char, width, text_util::write_padded_centered);
1513            Ok(L::Property::wrap_template(template))
1514        },
1515    );
1516    map.insert(
1517        "truncate_start",
1518        |language, diagnostics, build_ctx, function| {
1519            let ([width_node, content_node], [ellipsis_node]) =
1520                function.expect_named_arguments(&["", "", "ellipsis"])?;
1521            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1522            let content =
1523                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1524            let ellipsis = ellipsis_node
1525                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1526                .transpose()?;
1527            let template =
1528                new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
1529            Ok(L::Property::wrap_template(template))
1530        },
1531    );
1532    map.insert(
1533        "truncate_end",
1534        |language, diagnostics, build_ctx, function| {
1535            let ([width_node, content_node], [ellipsis_node]) =
1536                function.expect_named_arguments(&["", "", "ellipsis"])?;
1537            let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1538            let content =
1539                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1540            let ellipsis = ellipsis_node
1541                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1542                .transpose()?;
1543            let template =
1544                new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
1545            Ok(L::Property::wrap_template(template))
1546        },
1547    );
1548    map.insert("label", |language, diagnostics, build_ctx, function| {
1549        let [label_node, content_node] = function.expect_exact_arguments()?;
1550        let label_property =
1551            expect_plain_text_expression(language, diagnostics, build_ctx, label_node)?;
1552        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1553        let labels =
1554            label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
1555        Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
1556            content, labels,
1557        ))))
1558    });
1559    map.insert(
1560        "raw_escape_sequence",
1561        |language, diagnostics, build_ctx, function| {
1562            let [content_node] = function.expect_exact_arguments()?;
1563            let content =
1564                expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1565            Ok(L::Property::wrap_template(Box::new(
1566                RawEscapeSequenceTemplate(content),
1567            )))
1568        },
1569    );
1570    map.insert("stringify", |language, diagnostics, build_ctx, function| {
1571        let [content_node] = function.expect_exact_arguments()?;
1572        let content = expect_plain_text_expression(language, diagnostics, build_ctx, content_node)?;
1573        Ok(L::Property::wrap_string(content))
1574    });
1575    map.insert("if", |language, diagnostics, build_ctx, function| {
1576        let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
1577        let condition =
1578            expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
1579        let true_template =
1580            expect_template_expression(language, diagnostics, build_ctx, true_node)?;
1581        let false_template = false_node
1582            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1583            .transpose()?;
1584        let template = ConditionalTemplate::new(condition, true_template, false_template);
1585        Ok(L::Property::wrap_template(Box::new(template)))
1586    });
1587    map.insert("coalesce", |language, diagnostics, build_ctx, function| {
1588        let contents = function
1589            .args
1590            .iter()
1591            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1592            .try_collect()?;
1593        Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
1594            contents,
1595        ))))
1596    });
1597    map.insert("concat", |language, diagnostics, build_ctx, function| {
1598        let contents = function
1599            .args
1600            .iter()
1601            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1602            .try_collect()?;
1603        Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
1604            contents,
1605        ))))
1606    });
1607    map.insert("separate", |language, diagnostics, build_ctx, function| {
1608        let ([separator_node], content_nodes) = function.expect_some_arguments()?;
1609        let separator =
1610            expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1611        let contents = content_nodes
1612            .iter()
1613            .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1614            .try_collect()?;
1615        Ok(L::Property::wrap_template(Box::new(SeparateTemplate::new(
1616            separator, contents,
1617        ))))
1618    });
1619    map.insert("surround", |language, diagnostics, build_ctx, function| {
1620        let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
1621        let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1622        let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
1623        let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1624        let template = ReformatTemplate::new(content, move |formatter, recorded| {
1625            if recorded.data().is_empty() {
1626                return Ok(());
1627            }
1628            prefix.format(formatter)?;
1629            recorded.replay(formatter.as_mut())?;
1630            suffix.format(formatter)?;
1631            Ok(())
1632        });
1633        Ok(L::Property::wrap_template(Box::new(template)))
1634    });
1635    map.insert("config", |language, _diagnostics, _build_ctx, function| {
1636        // Dynamic lookup can be implemented if needed. The name is literal
1637        // string for now so the error can be reported early.
1638        let [name_node] = function.expect_exact_arguments()?;
1639        let name: ConfigNamePathBuf =
1640            template_parser::expect_string_literal_with(name_node, |name, span| {
1641                name.parse().map_err(|err| {
1642                    TemplateParseError::expression("Failed to parse config name", span)
1643                        .with_source(err)
1644                })
1645            })?;
1646        let value = language.settings().get_value(&name).map_err(|err| {
1647            TemplateParseError::expression("Failed to get config value", function.name_span)
1648                .with_source(err)
1649        })?;
1650        // .decorated("", "") to trim leading/trailing whitespace
1651        Ok(L::Property::wrap_config_value(
1652            Literal(value.decorated("", "")).into_dyn(),
1653        ))
1654    });
1655    map
1656}
1657
1658fn new_pad_template<'a, W>(
1659    content: Box<dyn Template + 'a>,
1660    fill_char: Option<Box<dyn Template + 'a>>,
1661    width: BoxedTemplateProperty<'a, usize>,
1662    write_padded: W,
1663) -> Box<dyn Template + 'a>
1664where
1665    W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
1666{
1667    let default_fill_char = FormatRecorder::with_data(" ");
1668    let template = ReformatTemplate::new(content, move |formatter, recorded| {
1669        let width = match width.extract() {
1670            Ok(width) => width,
1671            Err(err) => return formatter.handle_error(err),
1672        };
1673        let mut fill_char_recorder;
1674        let recorded_fill_char = if let Some(fill_char) = &fill_char {
1675            let rewrap = formatter.rewrap_fn();
1676            fill_char_recorder = FormatRecorder::new();
1677            fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
1678            &fill_char_recorder
1679        } else {
1680            &default_fill_char
1681        };
1682        write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
1683    });
1684    Box::new(template)
1685}
1686
1687fn new_truncate_template<'a, W>(
1688    content: Box<dyn Template + 'a>,
1689    ellipsis: Option<Box<dyn Template + 'a>>,
1690    width: BoxedTemplateProperty<'a, usize>,
1691    write_truncated: W,
1692) -> Box<dyn Template + 'a>
1693where
1694    W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
1695{
1696    let default_ellipsis = FormatRecorder::with_data("");
1697    let template = ReformatTemplate::new(content, move |formatter, recorded| {
1698        let width = match width.extract() {
1699            Ok(width) => width,
1700            Err(err) => return formatter.handle_error(err),
1701        };
1702        let mut ellipsis_recorder;
1703        let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
1704            let rewrap = formatter.rewrap_fn();
1705            ellipsis_recorder = FormatRecorder::new();
1706            ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
1707            &ellipsis_recorder
1708        } else {
1709            &default_ellipsis
1710        };
1711        write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
1712        Ok(())
1713    });
1714    Box::new(template)
1715}
1716
1717/// Builds intermediate expression tree from AST nodes.
1718pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1719    language: &L,
1720    diagnostics: &mut TemplateDiagnostics,
1721    build_ctx: &BuildContext<L::Property>,
1722    node: &ExpressionNode,
1723) -> TemplateParseResult<Expression<L::Property>> {
1724    match &node.kind {
1725        ExpressionKind::Identifier(name) => {
1726            if let Some(make) = build_ctx.local_variables.get(name) {
1727                // Don't label a local variable with its name
1728                Ok(Expression::unlabeled(make()))
1729            } else if *name == "self" {
1730                // "self" is a special variable, so don't label it
1731                let make = build_ctx.self_variable;
1732                Ok(Expression::unlabeled(make()))
1733            } else {
1734                let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
1735                    .map_err(|err| {
1736                        err.extend_keyword_candidates(itertools::chain(
1737                            build_ctx.local_variables.keys().copied(),
1738                            ["self"],
1739                        ))
1740                    })?;
1741                Ok(Expression::with_label(property, *name))
1742            }
1743        }
1744        ExpressionKind::Boolean(value) => {
1745            let property = L::Property::wrap_boolean(Literal(*value).into_dyn());
1746            Ok(Expression::unlabeled(property))
1747        }
1748        ExpressionKind::Integer(value) => {
1749            let property = L::Property::wrap_integer(Literal(*value).into_dyn());
1750            Ok(Expression::unlabeled(property))
1751        }
1752        ExpressionKind::String(value) => {
1753            let property = L::Property::wrap_string(Literal(value.clone()).into_dyn());
1754            Ok(Expression::unlabeled(property))
1755        }
1756        ExpressionKind::Unary(op, arg_node) => {
1757            let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
1758            Ok(Expression::unlabeled(property))
1759        }
1760        ExpressionKind::Binary(op, lhs_node, rhs_node) => {
1761            let property = build_binary_operation(
1762                language,
1763                diagnostics,
1764                build_ctx,
1765                *op,
1766                lhs_node,
1767                rhs_node,
1768                node.span,
1769            )?;
1770            Ok(Expression::unlabeled(property))
1771        }
1772        ExpressionKind::Concat(nodes) => {
1773            let templates = nodes
1774                .iter()
1775                .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1776                .try_collect()?;
1777            let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
1778            Ok(Expression::unlabeled(property))
1779        }
1780        ExpressionKind::FunctionCall(function) => {
1781            let property = language.build_function(diagnostics, build_ctx, function)?;
1782            Ok(Expression::unlabeled(property))
1783        }
1784        ExpressionKind::MethodCall(method) => {
1785            let mut expression =
1786                build_expression(language, diagnostics, build_ctx, &method.object)?;
1787            expression.property = language.build_method(
1788                diagnostics,
1789                build_ctx,
1790                expression.property,
1791                &method.function,
1792            )?;
1793            expression.labels.push(method.function.name.to_owned());
1794            Ok(expression)
1795        }
1796        ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
1797            "Lambda cannot be defined here",
1798            node.span,
1799        )),
1800        ExpressionKind::AliasExpanded(id, subst) => {
1801            let mut inner_diagnostics = TemplateDiagnostics::new();
1802            let expression = build_expression(language, &mut inner_diagnostics, build_ctx, subst)
1803                .map_err(|e| e.within_alias_expansion(*id, node.span))?;
1804            diagnostics.extend_with(inner_diagnostics, |diag| {
1805                diag.within_alias_expansion(*id, node.span)
1806            });
1807            Ok(expression)
1808        }
1809    }
1810}
1811
1812/// Builds template evaluation tree from AST nodes, with fresh build context.
1813///
1814/// `wrap_self` specifies the type of the top-level property, which should be
1815/// one of the `L::Property::wrap_*()` functions.
1816pub fn build<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
1817    language: &L,
1818    diagnostics: &mut TemplateDiagnostics,
1819    node: &ExpressionNode,
1820    // TODO: Generic L: WrapProperty<C> trait might be better. See the
1821    // comment in build_formattable_list_method().
1822    wrap_self: impl Fn(BoxedTemplateProperty<'a, C>) -> L::Property,
1823) -> TemplateParseResult<TemplateRenderer<'a, C>> {
1824    let self_placeholder = PropertyPlaceholder::new();
1825    let build_ctx = BuildContext {
1826        local_variables: HashMap::new(),
1827        self_variable: &|| wrap_self(self_placeholder.clone().into_dyn()),
1828    };
1829    let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
1830    Ok(TemplateRenderer::new(template, self_placeholder))
1831}
1832
1833/// Parses text, expands aliases, then builds template evaluation tree.
1834pub fn parse<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
1835    language: &L,
1836    diagnostics: &mut TemplateDiagnostics,
1837    template_text: &str,
1838    aliases_map: &TemplateAliasesMap,
1839    wrap_self: impl Fn(BoxedTemplateProperty<'a, C>) -> L::Property,
1840) -> TemplateParseResult<TemplateRenderer<'a, C>> {
1841    let node = template_parser::parse(template_text, aliases_map)?;
1842    build(language, diagnostics, &node, wrap_self)
1843        .map_err(|err| err.extend_alias_candidates(aliases_map))
1844}
1845
1846pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1847    language: &L,
1848    diagnostics: &mut TemplateDiagnostics,
1849    build_ctx: &BuildContext<L::Property>,
1850    node: &ExpressionNode,
1851) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
1852    expect_expression_of_type(
1853        language,
1854        diagnostics,
1855        build_ctx,
1856        node,
1857        "Boolean",
1858        |expression| expression.try_into_boolean(),
1859    )
1860}
1861
1862pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1863    language: &L,
1864    diagnostics: &mut TemplateDiagnostics,
1865    build_ctx: &BuildContext<L::Property>,
1866    node: &ExpressionNode,
1867) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
1868    expect_expression_of_type(
1869        language,
1870        diagnostics,
1871        build_ctx,
1872        node,
1873        "Integer",
1874        |expression| expression.try_into_integer(),
1875    )
1876}
1877
1878/// If the given expression `node` is of `Integer` type, converts it to `isize`.
1879pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1880    language: &L,
1881    diagnostics: &mut TemplateDiagnostics,
1882    build_ctx: &BuildContext<L::Property>,
1883    node: &ExpressionNode,
1884) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
1885    let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
1886    let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
1887    Ok(isize_property.into_dyn())
1888}
1889
1890/// If the given expression `node` is of `Integer` type, converts it to `usize`.
1891pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1892    language: &L,
1893    diagnostics: &mut TemplateDiagnostics,
1894    build_ctx: &BuildContext<L::Property>,
1895    node: &ExpressionNode,
1896) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
1897    let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
1898    let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
1899    Ok(usize_property.into_dyn())
1900}
1901
1902pub fn expect_plain_text_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1903    language: &L,
1904    diagnostics: &mut TemplateDiagnostics,
1905    build_ctx: &BuildContext<L::Property>,
1906    node: &ExpressionNode,
1907) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
1908    // Since any formattable type can be converted to a string property,
1909    // the expected type is not a String, but a Template.
1910    expect_expression_of_type(
1911        language,
1912        diagnostics,
1913        build_ctx,
1914        node,
1915        "Template",
1916        |expression| expression.try_into_plain_text(),
1917    )
1918}
1919
1920pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1921    language: &L,
1922    diagnostics: &mut TemplateDiagnostics,
1923    build_ctx: &BuildContext<L::Property>,
1924    node: &ExpressionNode,
1925) -> TemplateParseResult<Box<dyn Template + 'a>> {
1926    expect_expression_of_type(
1927        language,
1928        diagnostics,
1929        build_ctx,
1930        node,
1931        "Template",
1932        |expression| expression.try_into_template(),
1933    )
1934}
1935
1936fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
1937    language: &L,
1938    diagnostics: &mut TemplateDiagnostics,
1939    build_ctx: &BuildContext<L::Property>,
1940    node: &ExpressionNode,
1941    expected_type: &str,
1942    f: impl FnOnce(Expression<L::Property>) -> Option<T>,
1943) -> TemplateParseResult<T> {
1944    if let ExpressionKind::AliasExpanded(id, subst) = &node.kind {
1945        let mut inner_diagnostics = TemplateDiagnostics::new();
1946        let expression = expect_expression_of_type(
1947            language,
1948            &mut inner_diagnostics,
1949            build_ctx,
1950            subst,
1951            expected_type,
1952            f,
1953        )
1954        .map_err(|e| e.within_alias_expansion(*id, node.span))?;
1955        diagnostics.extend_with(inner_diagnostics, |diag| {
1956            diag.within_alias_expansion(*id, node.span)
1957        });
1958        Ok(expression)
1959    } else {
1960        let expression = build_expression(language, diagnostics, build_ctx, node)?;
1961        let actual_type = expression.type_name();
1962        f(expression)
1963            .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
1964    }
1965}
1966
1967#[cfg(test)]
1968mod tests {
1969    use std::iter;
1970
1971    use jj_lib::backend::MillisSinceEpoch;
1972    use jj_lib::config::StackedConfig;
1973
1974    use super::*;
1975    use crate::formatter;
1976    use crate::formatter::ColorFormatter;
1977    use crate::generic_templater::GenericTemplateLanguage;
1978
1979    type TestTemplateLanguage = GenericTemplateLanguage<'static, ()>;
1980    type P = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
1981
1982    /// Helper to set up template evaluation environment.
1983    struct TestTemplateEnv {
1984        language: TestTemplateLanguage,
1985        aliases_map: TemplateAliasesMap,
1986        color_rules: Vec<(Vec<String>, formatter::Style)>,
1987    }
1988
1989    impl TestTemplateEnv {
1990        fn new() -> Self {
1991            Self::with_config(StackedConfig::with_defaults())
1992        }
1993
1994        fn with_config(config: StackedConfig) -> Self {
1995            let settings = UserSettings::from_config(config).unwrap();
1996            TestTemplateEnv {
1997                language: TestTemplateLanguage::new(&settings),
1998                aliases_map: TemplateAliasesMap::new(),
1999                color_rules: Vec::new(),
2000            }
2001        }
2002    }
2003
2004    impl TestTemplateEnv {
2005        fn add_keyword<F>(&mut self, name: &'static str, build: F)
2006        where
2007            F: Fn() -> P + 'static,
2008        {
2009            self.language.add_keyword(name, move |_| Ok(build()));
2010        }
2011
2012        fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
2013            self.aliases_map.insert(decl, defn).unwrap();
2014        }
2015
2016        fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
2017            let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
2018            let style = formatter::Style {
2019                fg: Some(fg),
2020                ..Default::default()
2021            };
2022            self.color_rules.push((labels, style));
2023        }
2024
2025        fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, ()>> {
2026            parse(
2027                &self.language,
2028                &mut TemplateDiagnostics::new(),
2029                template,
2030                &self.aliases_map,
2031                P::wrap_self,
2032            )
2033        }
2034
2035        fn parse_err(&self, template: &str) -> String {
2036            let err = self.parse(template).err().unwrap();
2037            iter::successors(Some(&err), |e| e.origin()).join("\n")
2038        }
2039
2040        fn render_ok(&self, template: &str) -> String {
2041            let template = self.parse(template).unwrap();
2042            let mut output = Vec::new();
2043            let mut formatter =
2044                ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
2045            template.format(&(), &mut formatter).unwrap();
2046            drop(formatter);
2047            String::from_utf8(output).unwrap()
2048        }
2049    }
2050
2051    fn literal<'a, O: Clone + 'a>(value: O) -> BoxedTemplateProperty<'a, O> {
2052        Literal(value).into_dyn()
2053    }
2054
2055    fn new_error_property<O>(message: &str) -> BoxedTemplateProperty<'_, O> {
2056        Literal(())
2057            .and_then(|()| Err(TemplatePropertyError(message.into())))
2058            .into_dyn()
2059    }
2060
2061    fn new_signature(name: &str, email: &str) -> Signature {
2062        Signature {
2063            name: name.to_owned(),
2064            email: email.to_owned(),
2065            timestamp: new_timestamp(0, 0),
2066        }
2067    }
2068
2069    fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
2070        Timestamp {
2071            timestamp: MillisSinceEpoch(msec),
2072            tz_offset,
2073        }
2074    }
2075
2076    #[test]
2077    fn test_parsed_tree() {
2078        let mut env = TestTemplateEnv::new();
2079        env.add_keyword("divergent", || P::wrap_boolean(literal(false)));
2080        env.add_keyword("empty", || P::wrap_boolean(literal(true)));
2081        env.add_keyword("hello", || P::wrap_string(literal("Hello".to_owned())));
2082
2083        // Empty
2084        insta::assert_snapshot!(env.render_ok(r#"  "#), @"");
2085
2086        // Single term with whitespace
2087        insta::assert_snapshot!(env.render_ok(r#"  hello.upper()  "#), @"HELLO");
2088
2089        // Multiple terms
2090        insta::assert_snapshot!(env.render_ok(r#"  hello.upper()  ++ true "#), @"HELLOtrue");
2091
2092        // Parenthesized single term
2093        insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
2094
2095        // Parenthesized multiple terms and concatenation
2096        insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
2097
2098        // Parenthesized "if" condition
2099        insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
2100
2101        // Parenthesized method chaining
2102        insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
2103    }
2104
2105    #[test]
2106    fn test_parse_error() {
2107        let mut env = TestTemplateEnv::new();
2108        env.add_keyword("description", || P::wrap_string(literal("".to_owned())));
2109        env.add_keyword("empty", || P::wrap_boolean(literal(true)));
2110
2111        insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r"
2112         --> 1:13
2113          |
2114        1 | description ()
2115          |             ^---
2116          |
2117          = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, or `<`
2118        ");
2119
2120        insta::assert_snapshot!(env.parse_err(r#"foo"#), @r"
2121         --> 1:1
2122          |
2123        1 | foo
2124          | ^-^
2125          |
2126          = Keyword `foo` doesn't exist
2127        ");
2128
2129        insta::assert_snapshot!(env.parse_err(r#"foo()"#), @r"
2130         --> 1:1
2131          |
2132        1 | foo()
2133          | ^-^
2134          |
2135          = Function `foo` doesn't exist
2136        ");
2137        insta::assert_snapshot!(env.parse_err(r#"false()"#), @r"
2138         --> 1:1
2139          |
2140        1 | false()
2141          | ^---^
2142          |
2143          = Expected identifier
2144        ");
2145
2146        insta::assert_snapshot!(env.parse_err(r#"!foo"#), @r"
2147         --> 1:2
2148          |
2149        1 | !foo
2150          |  ^-^
2151          |
2152          = Keyword `foo` doesn't exist
2153        ");
2154        insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @r"
2155         --> 1:9
2156          |
2157        1 | true && 123
2158          |         ^-^
2159          |
2160          = Expected expression of type `Boolean`, but actual type is `Integer`
2161        ");
2162        insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @r"
2163         --> 1:1
2164          |
2165        1 | true == 1
2166          | ^-------^
2167          |
2168          = Cannot compare expressions of type `Boolean` and `Integer`
2169        ");
2170        insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @r"
2171         --> 1:1
2172          |
2173        1 | true != 'a'
2174          | ^---------^
2175          |
2176          = Cannot compare expressions of type `Boolean` and `String`
2177        ");
2178        insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @r"
2179         --> 1:1
2180          |
2181        1 | 1 == true
2182          | ^-------^
2183          |
2184          = Cannot compare expressions of type `Integer` and `Boolean`
2185        ");
2186        insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @r"
2187         --> 1:1
2188          |
2189        1 | 1 != 'a'
2190          | ^------^
2191          |
2192          = Cannot compare expressions of type `Integer` and `String`
2193        ");
2194        insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @r"
2195         --> 1:1
2196          |
2197        1 | 'a' == true
2198          | ^---------^
2199          |
2200          = Cannot compare expressions of type `String` and `Boolean`
2201        ");
2202        insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @r"
2203         --> 1:1
2204          |
2205        1 | 'a' != 1
2206          | ^------^
2207          |
2208          = Cannot compare expressions of type `String` and `Integer`
2209        ");
2210        insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
2211         --> 1:1
2212          |
2213        1 | 'a' == label("", "")
2214          | ^------------------^
2215          |
2216          = Cannot compare expressions of type `String` and `Template`
2217        "#);
2218        insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @r"
2219         --> 1:1
2220          |
2221        1 | 'a' > 1
2222          | ^-----^
2223          |
2224          = Cannot compare expressions of type `String` and `Integer`
2225        ");
2226
2227        insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r"
2228         --> 1:26
2229          |
2230        1 | description.first_line().foo()
2231          |                          ^-^
2232          |
2233          = Method `foo` doesn't exist for type `String`
2234        ");
2235
2236        insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @r"
2237         --> 1:1
2238          |
2239        1 | 10000000000000000000
2240          | ^------------------^
2241          |
2242          = Invalid integer literal
2243        ");
2244        insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @r"
2245         --> 1:4
2246          |
2247        1 | 42.foo()
2248          |    ^-^
2249          |
2250          = Method `foo` doesn't exist for type `Integer`
2251        ");
2252        insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @r"
2253         --> 1:3
2254          |
2255        1 | (-empty)
2256          |   ^---^
2257          |
2258          = Expected expression of type `Integer`, but actual type is `Boolean`
2259        ");
2260
2261        insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
2262         --> 1:18
2263          |
2264        1 | ("foo" ++ "bar").baz()
2265          |                  ^-^
2266          |
2267          = Method `baz` doesn't exist for type `Template`
2268        "#);
2269
2270        insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @r"
2271         --> 1:22
2272          |
2273        1 | description.contains()
2274          |                      ^
2275          |
2276          = Function `contains`: Expected 1 arguments
2277        ");
2278
2279        insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
2280         --> 1:24
2281          |
2282        1 | description.first_line("foo")
2283          |                        ^---^
2284          |
2285          = Function `first_line`: Expected 0 arguments
2286        "#);
2287
2288        insta::assert_snapshot!(env.parse_err(r#"label()"#), @r"
2289         --> 1:7
2290          |
2291        1 | label()
2292          |       ^
2293          |
2294          = Function `label`: Expected 2 arguments
2295        ");
2296        insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
2297         --> 1:7
2298          |
2299        1 | label("foo", "bar", "baz")
2300          |       ^-----------------^
2301          |
2302          = Function `label`: Expected 2 arguments
2303        "#);
2304
2305        insta::assert_snapshot!(env.parse_err(r#"if()"#), @r"
2306         --> 1:4
2307          |
2308        1 | if()
2309          |    ^
2310          |
2311          = Function `if`: Expected 2 to 3 arguments
2312        ");
2313        insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
2314         --> 1:4
2315          |
2316        1 | if("foo", "bar", "baz", "quux")
2317          |    ^-------------------------^
2318          |
2319          = Function `if`: Expected 2 to 3 arguments
2320        "#);
2321
2322        insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
2323         --> 1:37
2324          |
2325        1 | pad_start("foo", fill_char = "bar", "baz")
2326          |                                     ^---^
2327          |
2328          = Function `pad_start`: Positional argument follows keyword argument
2329        "#);
2330
2331        insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
2332         --> 1:4
2333          |
2334        1 | if(label("foo", "bar"), "baz")
2335          |    ^-----------------^
2336          |
2337          = Expected expression of type `Boolean`, but actual type is `Template`
2338        "#);
2339
2340        insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @r"
2341         --> 1:1
2342          |
2343        1 | |x| description
2344          | ^-------------^
2345          |
2346          = Lambda cannot be defined here
2347        ");
2348    }
2349
2350    #[test]
2351    fn test_self_keyword() {
2352        let mut env = TestTemplateEnv::new();
2353        env.add_keyword("say_hello", || P::wrap_string(literal("Hello".to_owned())));
2354
2355        insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
2356        insta::assert_snapshot!(env.parse_err(r#"self"#), @r"
2357         --> 1:1
2358          |
2359        1 | self
2360          | ^--^
2361          |
2362          = Expected expression of type `Template`, but actual type is `Self`
2363        ");
2364    }
2365
2366    #[test]
2367    fn test_boolean_cast() {
2368        let mut env = TestTemplateEnv::new();
2369
2370        insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
2371        insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
2372
2373        env.add_keyword("sl0", || {
2374            P::wrap_string_list(literal::<Vec<String>>(vec![]))
2375        });
2376        env.add_keyword("sl1", || P::wrap_string_list(literal(vec!["".to_owned()])));
2377        insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
2378        insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
2379
2380        // No implicit cast of integer
2381        insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @r"
2382         --> 1:4
2383          |
2384        1 | if(0, true, false)
2385          |    ^
2386          |
2387          = Expected expression of type `Boolean`, but actual type is `Integer`
2388        ");
2389
2390        // Optional integer can be converted to boolean, and Some(0) is truthy.
2391        env.add_keyword("none_i64", || P::wrap_integer_opt(literal(None)));
2392        env.add_keyword("some_i64", || P::wrap_integer_opt(literal(Some(0))));
2393        insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
2394        insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
2395
2396        insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
2397         --> 1:4
2398          |
2399        1 | if(label("", ""), true, false)
2400          |    ^-----------^
2401          |
2402          = Expected expression of type `Boolean`, but actual type is `Template`
2403        "#);
2404        insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @r"
2405         --> 1:4
2406          |
2407        1 | if(sl0.map(|x| x), true, false)
2408          |    ^------------^
2409          |
2410          = Expected expression of type `Boolean`, but actual type is `ListTemplate`
2411        ");
2412
2413        env.add_keyword("empty_email", || {
2414            P::wrap_email(literal(Email("".to_owned())))
2415        });
2416        env.add_keyword("nonempty_email", || {
2417            P::wrap_email(literal(Email("local@domain".to_owned())))
2418        });
2419        insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
2420        insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
2421    }
2422
2423    #[test]
2424    fn test_arithmetic_operation() {
2425        let mut env = TestTemplateEnv::new();
2426        env.add_keyword("none_i64", || P::wrap_integer_opt(literal(None)));
2427        env.add_keyword("some_i64", || P::wrap_integer_opt(literal(Some(1))));
2428        env.add_keyword("i64_min", || P::wrap_integer(literal(i64::MIN)));
2429
2430        insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
2431        insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
2432        insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
2433
2434        // Since methods of the contained value can be invoked, it makes sense
2435        // to apply operators to optional integers as well.
2436        insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
2437        insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
2438
2439        // No panic on integer overflow.
2440        insta::assert_snapshot!(
2441            env.render_ok(r#"-i64_min"#),
2442            @"<Error: Attempt to negate with overflow>");
2443    }
2444
2445    #[test]
2446    fn test_relational_operation() {
2447        let env = TestTemplateEnv::new();
2448
2449        insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
2450        insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
2451        insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
2452        insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
2453        insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
2454        insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
2455        insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
2456        insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
2457    }
2458
2459    #[test]
2460    fn test_logical_operation() {
2461        let mut env = TestTemplateEnv::new();
2462        env.add_keyword("email1", || {
2463            P::wrap_email(literal(Email("local-1@domain".to_owned())))
2464        });
2465        env.add_keyword("email2", || {
2466            P::wrap_email(literal(Email("local-2@domain".to_owned())))
2467        });
2468
2469        insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
2470        insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
2471        insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
2472        insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
2473        insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
2474        insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
2475        insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
2476        insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
2477        insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
2478        insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
2479        insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
2480        insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
2481        insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
2482        insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
2483        insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
2484        insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
2485        insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
2486        insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
2487        insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
2488        insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
2489        insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
2490
2491        insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
2492        insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
2493
2494        // Short-circuiting
2495        env.add_keyword("bad_bool", || P::wrap_boolean(new_error_property("Bad")));
2496        insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
2497        insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
2498        insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
2499        insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
2500    }
2501
2502    #[test]
2503    fn test_list_method() {
2504        let mut env = TestTemplateEnv::new();
2505        env.add_keyword("empty", || P::wrap_boolean(literal(true)));
2506        env.add_keyword("sep", || P::wrap_string(literal("sep".to_owned())));
2507
2508        insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
2509        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
2510
2511        insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
2512        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
2513        // Null separator
2514        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
2515        // Keyword as separator
2516        insta::assert_snapshot!(
2517            env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
2518            @"aSEPbSEPc");
2519
2520        insta::assert_snapshot!(
2521            env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
2522            @"a c");
2523
2524        insta::assert_snapshot!(
2525            env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
2526            @"aa bb cc");
2527        // Global keyword in item template
2528        insta::assert_snapshot!(
2529            env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
2530            @"atrue btrue ctrue");
2531        // Global keyword in item template shadowing 'self'
2532        insta::assert_snapshot!(
2533            env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
2534            @"atrue btrue ctrue");
2535        // Override global keyword 'empty'
2536        insta::assert_snapshot!(
2537            env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
2538            @"a b c");
2539        // Nested map operations
2540        insta::assert_snapshot!(
2541            env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
2542            @"ax ay bx by cx cy");
2543        // Nested map/join operations
2544        insta::assert_snapshot!(
2545            env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
2546            @"ax,ay;bx,by;cx,cy");
2547        // Nested string operations
2548        insta::assert_snapshot!(
2549            env.render_ok(r#""!  a\n!b\nc\n   end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
2550            @"a b c");
2551
2552        // Lambda expression in alias
2553        env.add_alias("identity", "|x| x");
2554        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
2555
2556        // Not a lambda expression
2557        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
2558         --> 1:17
2559          |
2560        1 | "a".lines().map(empty)
2561          |                 ^---^
2562          |
2563          = Expected lambda expression
2564        "#);
2565        // Bad lambda parameter count
2566        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
2567         --> 1:18
2568          |
2569        1 | "a".lines().map(|| "")
2570          |                  ^
2571          |
2572          = Expected 1 lambda parameters
2573        "#);
2574        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
2575         --> 1:18
2576          |
2577        1 | "a".lines().map(|a, b| "")
2578          |                  ^--^
2579          |
2580          = Expected 1 lambda parameters
2581        "#);
2582        // Bad lambda output
2583        insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
2584         --> 1:24
2585          |
2586        1 | "a".lines().filter(|s| s ++ "\n")
2587          |                        ^-------^
2588          |
2589          = Expected expression of type `Boolean`, but actual type is `Template`
2590        "#);
2591        // Error in lambda expression
2592        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
2593         --> 1:23
2594          |
2595        1 | "a".lines().map(|s| s.unknown())
2596          |                       ^-----^
2597          |
2598          = Method `unknown` doesn't exist for type `String`
2599        "#);
2600        // Error in lambda alias
2601        env.add_alias("too_many_params", "|x, y| x");
2602        insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
2603         --> 1:17
2604          |
2605        1 | "a".lines().map(too_many_params)
2606          |                 ^-------------^
2607          |
2608          = In alias `too_many_params`
2609         --> 1:2
2610          |
2611        1 | |x, y| x
2612          |  ^--^
2613          |
2614          = Expected 1 lambda parameters
2615        "#);
2616    }
2617
2618    #[test]
2619    fn test_string_method() {
2620        let mut env = TestTemplateEnv::new();
2621        env.add_keyword("description", || {
2622            P::wrap_string(literal("description 1".to_owned()))
2623        });
2624        env.add_keyword("bad_string", || P::wrap_string(new_error_property("Bad")));
2625
2626        insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
2627        insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
2628        insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
2629
2630        insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
2631        insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
2632        insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
2633        insta::assert_snapshot!(
2634            env.render_ok(r#""description 123".contains(description.first_line())"#),
2635            @"true");
2636
2637        // inner template error should propagate
2638        insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
2639        insta::assert_snapshot!(
2640            env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
2641        insta::assert_snapshot!(
2642            env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
2643
2644        insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
2645        insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
2646
2647        insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
2648        insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
2649
2650        insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
2651        insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
2652        insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
2653        insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
2654        insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
2655        insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
2656
2657        insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
2658        insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
2659        insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
2660        insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
2661        insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
2662        insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
2663
2664        insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
2665        insta::assert_snapshot!(
2666            env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
2667            @"testing");
2668
2669        insta::assert_snapshot!(
2670            env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
2671            @"bar@my.example.com");
2672        insta::assert_snapshot!(
2673            env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
2674            @"bar");
2675
2676        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim()"#), @"");
2677        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim()"#), @"foo  bar");
2678
2679        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim_start()"#), @"");
2680        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim_start()"#), @"foo  bar");
2681
2682        insta::assert_snapshot!(env.render_ok(r#"" \n \r    \t \r ".trim_end()"#), @"");
2683        insta::assert_snapshot!(env.render_ok(r#"" \n \r foo  bar \t \r ".trim_end()"#), @" foo  bar");
2684
2685        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
2686        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
2687        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
2688        insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
2689        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
2690        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
2691        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
2692        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
2693
2694        // non-ascii characters
2695        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
2696        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
2697        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
2698        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
2699        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
2700        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
2701        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
2702        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
2703        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
2704        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
2705        insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
2706
2707        // ranges with end > start are empty
2708        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
2709        insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
2710
2711        insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
2712        insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
2713    }
2714
2715    #[test]
2716    fn test_config_value_method() {
2717        let mut env = TestTemplateEnv::new();
2718        env.add_keyword("boolean", || {
2719            P::wrap_config_value(literal(ConfigValue::from(true)))
2720        });
2721        env.add_keyword("integer", || {
2722            P::wrap_config_value(literal(ConfigValue::from(42)))
2723        });
2724        env.add_keyword("string", || {
2725            P::wrap_config_value(literal(ConfigValue::from("foo")))
2726        });
2727        env.add_keyword("string_list", || {
2728            P::wrap_config_value(literal(ConfigValue::from_iter(["foo", "bar"])))
2729        });
2730
2731        insta::assert_snapshot!(env.render_ok("boolean"), @"true");
2732        insta::assert_snapshot!(env.render_ok("integer"), @"42");
2733        insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
2734        insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
2735
2736        insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
2737        insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
2738        insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
2739        insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
2740
2741        insta::assert_snapshot!(
2742            env.render_ok("boolean.as_integer()"),
2743            @"<Error: invalid type: boolean `true`, expected i64>");
2744        insta::assert_snapshot!(
2745            env.render_ok("integer.as_string()"),
2746            @"<Error: invalid type: integer `42`, expected a string>");
2747        insta::assert_snapshot!(
2748            env.render_ok("string.as_string_list()"),
2749            @r#"<Error: invalid type: string "foo", expected a sequence>"#);
2750        insta::assert_snapshot!(
2751            env.render_ok("string_list.as_boolean()"),
2752            @"<Error: invalid type: sequence, expected a boolean>");
2753    }
2754
2755    #[test]
2756    fn test_signature() {
2757        let mut env = TestTemplateEnv::new();
2758
2759        env.add_keyword("author", || {
2760            P::wrap_signature(literal(new_signature("Test User", "test.user@example.com")))
2761        });
2762        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
2763        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
2764        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
2765        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2766
2767        env.add_keyword("author", || {
2768            P::wrap_signature(literal(new_signature(
2769                "Another Test User",
2770                "test.user@example.com",
2771            )))
2772        });
2773        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
2774        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
2775        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
2776        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2777
2778        env.add_keyword("author", || {
2779            P::wrap_signature(literal(new_signature(
2780                "Test User",
2781                "test.user@invalid@example.com",
2782            )))
2783        });
2784        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
2785        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
2786        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
2787        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2788
2789        env.add_keyword("author", || {
2790            P::wrap_signature(literal(new_signature("Test User", "test.user")))
2791        });
2792        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
2793        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
2794        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2795
2796        env.add_keyword("author", || {
2797            P::wrap_signature(literal(new_signature(
2798                "Test User",
2799                "test.user+tag@example.com",
2800            )))
2801        });
2802        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
2803        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
2804        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user+tag");
2805
2806        env.add_keyword("author", || {
2807            P::wrap_signature(literal(new_signature("Test User", "x@y")))
2808        });
2809        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
2810        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
2811        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"x");
2812
2813        env.add_keyword("author", || {
2814            P::wrap_signature(literal(new_signature("", "test.user@example.com")))
2815        });
2816        insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
2817        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
2818        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
2819        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2820
2821        env.add_keyword("author", || {
2822            P::wrap_signature(literal(new_signature("Test User", "")))
2823        });
2824        insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
2825        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
2826        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
2827        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
2828
2829        env.add_keyword("author", || {
2830            P::wrap_signature(literal(new_signature("", "")))
2831        });
2832        insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
2833        insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
2834        insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
2835        insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
2836    }
2837
2838    #[test]
2839    fn test_size_hint_method() {
2840        let mut env = TestTemplateEnv::new();
2841
2842        env.add_keyword("unbounded", || P::wrap_size_hint(literal((5, None))));
2843        insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
2844        insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
2845        insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
2846        insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
2847
2848        env.add_keyword("bounded", || P::wrap_size_hint(literal((0, Some(10)))));
2849        insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
2850        insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
2851        insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
2852        insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
2853
2854        env.add_keyword("zero", || P::wrap_size_hint(literal((0, Some(0)))));
2855        insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
2856        insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
2857        insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
2858        insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
2859    }
2860
2861    #[test]
2862    fn test_timestamp_method() {
2863        let mut env = TestTemplateEnv::new();
2864        env.add_keyword("t0", || P::wrap_timestamp(literal(new_timestamp(0, 0))));
2865
2866        insta::assert_snapshot!(
2867            env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
2868            @"19700101 00:00:00");
2869
2870        // Invalid format string
2871        insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
2872         --> 1:11
2873          |
2874        1 | t0.format("%_")
2875          |           ^--^
2876          |
2877          = Invalid time format
2878        "#);
2879
2880        // Invalid type
2881        insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @r"
2882         --> 1:11
2883          |
2884        1 | t0.format(0)
2885          |           ^
2886          |
2887          = Expected string literal
2888        ");
2889
2890        // Dynamic string isn't supported yet
2891        insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r#"
2892         --> 1:11
2893          |
2894        1 | t0.format("%Y" ++ "%m")
2895          |           ^----------^
2896          |
2897          = Expected string literal
2898        "#);
2899
2900        // Literal alias expansion
2901        env.add_alias("time_format", r#""%Y-%m-%d""#);
2902        env.add_alias("bad_time_format", r#""%_""#);
2903        insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
2904        insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
2905         --> 1:11
2906          |
2907        1 | t0.format(bad_time_format)
2908          |           ^-------------^
2909          |
2910          = In alias `bad_time_format`
2911         --> 1:1
2912          |
2913        1 | "%_"
2914          | ^--^
2915          |
2916          = Invalid time format
2917        "#);
2918    }
2919
2920    #[test]
2921    fn test_fill_function() {
2922        let mut env = TestTemplateEnv::new();
2923        env.add_color("error", crossterm::style::Color::DarkRed);
2924
2925        insta::assert_snapshot!(
2926            env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
2927                                  label("error", "lazy") ++ " dog\n")"#),
2928            @r"
2929        The quick fox jumps
2930        over the lazy dog
2931        ");
2932
2933        // A low value will not chop words, but can chop a label by words
2934        insta::assert_snapshot!(
2935            env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
2936                                  label("error", "longlonglongword and short words") ++
2937                                  " back out\n")"#),
2938            @r"
2939        Longlonglongword
2940        an some
2941        short
2942        words
2943        longlonglongword
2944        and short
2945        words
2946        back out
2947        ");
2948
2949        // Filling to 0 means breaking at every word
2950        insta::assert_snapshot!(
2951            env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
2952                                  label("error", "lazy") ++ " dog\n")"#),
2953            @r"
2954        The
2955        quick
2956        fox
2957        jumps
2958        over
2959        the
2960        lazy
2961        dog
2962        ");
2963
2964        // Filling to -0 is the same as 0
2965        insta::assert_snapshot!(
2966            env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
2967                                  label("error", "lazy") ++ " dog\n")"#),
2968            @r"
2969        The
2970        quick
2971        fox
2972        jumps
2973        over
2974        the
2975        lazy
2976        dog
2977        ");
2978
2979        // Filling to negative width is an error
2980        insta::assert_snapshot!(
2981            env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
2982                                  label("error", "lazy") ++ " dog\n")"#),
2983            @"<Error: out of range integral type conversion attempted>");
2984
2985        // Word-wrap, then indent
2986        insta::assert_snapshot!(
2987            env.render_ok(r#""START marker to help insta\n" ++
2988                             indent("    ", fill(20, "The quick fox jumps over the " ++
2989                                                 label("error", "lazy") ++ " dog\n"))"#),
2990            @r"
2991        START marker to help insta
2992            The quick fox jumps
2993            over the lazy dog
2994        ");
2995
2996        // Word-wrap indented (no special handling for leading spaces)
2997        insta::assert_snapshot!(
2998            env.render_ok(r#""START marker to help insta\n" ++
2999                             fill(20, indent("    ", "The quick fox jumps over the " ++
3000                                             label("error", "lazy") ++ " dog\n"))"#),
3001            @r"
3002        START marker to help insta
3003            The quick fox
3004        jumps over the lazy
3005        dog
3006        ");
3007    }
3008
3009    #[test]
3010    fn test_indent_function() {
3011        let mut env = TestTemplateEnv::new();
3012        env.add_color("error", crossterm::style::Color::DarkRed);
3013        env.add_color("warning", crossterm::style::Color::DarkYellow);
3014        env.add_color("hint", crossterm::style::Color::DarkCyan);
3015
3016        // Empty line shouldn't be indented. Not using insta here because we test
3017        // whitespace existence.
3018        assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
3019        assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
3020        assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
3021
3022        // "\n" at end of labeled text
3023        insta::assert_snapshot!(
3024            env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
3025            @r"
3026        __a
3027        __b
3028        ");
3029
3030        // "\n" in labeled text
3031        insta::assert_snapshot!(
3032            env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
3033            @r"
3034        __ab
3035        __c
3036        ");
3037
3038        // Labeled prefix + unlabeled content
3039        insta::assert_snapshot!(
3040            env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
3041            @r"
3042        XXa
3043        XXb
3044        ");
3045
3046        // Nested indent, silly but works
3047        insta::assert_snapshot!(
3048            env.render_ok(r#"indent(label("hint", "A"),
3049                                    label("warning", indent(label("hint", "B"),
3050                                                            label("error", "x\n") ++ "y")))"#),
3051            @r"
3052        ABx
3053        ABy
3054        ");
3055    }
3056
3057    #[test]
3058    fn test_pad_function() {
3059        let mut env = TestTemplateEnv::new();
3060        env.add_keyword("bad_string", || P::wrap_string(new_error_property("Bad")));
3061        env.add_color("red", crossterm::style::Color::Red);
3062        env.add_color("cyan", crossterm::style::Color::DarkCyan);
3063
3064        // Default fill_char is ' '
3065        insta::assert_snapshot!(
3066            env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
3067            @"{  foo}");
3068        insta::assert_snapshot!(
3069            env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
3070            @"{foo  }");
3071        insta::assert_snapshot!(
3072            env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
3073            @"{ foo }");
3074
3075        // Labeled fill char
3076        insta::assert_snapshot!(
3077            env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
3078            @"==foo");
3079        insta::assert_snapshot!(
3080            env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
3081            @"foo==");
3082        insta::assert_snapshot!(
3083            env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
3084            @"=foo=");
3085
3086        // Error in fill char: the output looks odd (because the error message
3087        // isn't 1-width character), but is still readable.
3088        insta::assert_snapshot!(
3089            env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
3090            @"foo");
3091        insta::assert_snapshot!(
3092            env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
3093            @"foo<<Error: Error: Bad>Bad>");
3094        insta::assert_snapshot!(
3095            env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
3096            @"<Error: Bad>foo<Error: Bad>");
3097    }
3098
3099    #[test]
3100    fn test_truncate_function() {
3101        let mut env = TestTemplateEnv::new();
3102        env.add_color("red", crossterm::style::Color::Red);
3103
3104        insta::assert_snapshot!(
3105            env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
3106            @"arbaz");
3107        insta::assert_snapshot!(
3108            env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
3109            @"fobaz");
3110    }
3111
3112    #[test]
3113    fn test_label_function() {
3114        let mut env = TestTemplateEnv::new();
3115        env.add_keyword("empty", || P::wrap_boolean(literal(true)));
3116        env.add_color("error", crossterm::style::Color::DarkRed);
3117        env.add_color("warning", crossterm::style::Color::DarkYellow);
3118
3119        // Literal
3120        insta::assert_snapshot!(
3121            env.render_ok(r#"label("error", "text")"#),
3122            @"text");
3123
3124        // Evaluated property
3125        insta::assert_snapshot!(
3126            env.render_ok(r#"label("error".first_line(), "text")"#),
3127            @"text");
3128
3129        // Template
3130        insta::assert_snapshot!(
3131            env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
3132            @"text");
3133    }
3134
3135    #[test]
3136    fn test_raw_escape_sequence_function_strip_labels() {
3137        let mut env = TestTemplateEnv::new();
3138        env.add_color("error", crossterm::style::Color::DarkRed);
3139        env.add_color("warning", crossterm::style::Color::DarkYellow);
3140
3141        insta::assert_snapshot!(
3142            env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
3143            @"text",
3144        );
3145    }
3146
3147    #[test]
3148    fn test_raw_escape_sequence_function_ansi_escape() {
3149        let env = TestTemplateEnv::new();
3150
3151        // Sanitize ANSI escape without raw_escape_sequence
3152        insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
3153        insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
3154        insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
3155        insta::assert_snapshot!(
3156            env.render_ok(r#""]8;;"
3157                ++ "http://example.com"
3158                ++ "\e\\"
3159                ++ "Example"
3160                ++ "\x1b]8;;\x1B\\""#),
3161            @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
3162
3163        // Don't sanitize ANSI escape with raw_escape_sequence
3164        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
3165        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
3166        insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
3167        insta::assert_snapshot!(
3168            env.render_ok(r#"raw_escape_sequence("]8;;"
3169                ++ "http://example.com"
3170                ++ "\e\\"
3171                ++ "Example"
3172                ++ "\x1b]8;;\x1B\\")"#),
3173            @r"]8;;http://example.com\Example]8;;\");
3174    }
3175
3176    #[test]
3177    fn test_stringify_function() {
3178        let mut env = TestTemplateEnv::new();
3179        env.add_color("error", crossterm::style::Color::DarkRed);
3180
3181        insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
3182        insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
3183        insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
3184    }
3185
3186    #[test]
3187    fn test_coalesce_function() {
3188        let mut env = TestTemplateEnv::new();
3189        env.add_keyword("bad_string", || P::wrap_string(new_error_property("Bad")));
3190        env.add_keyword("empty_string", || P::wrap_string(literal("".to_owned())));
3191        env.add_keyword("non_empty_string", || {
3192            P::wrap_string(literal("a".to_owned()))
3193        });
3194
3195        insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
3196        insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
3197        insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
3198        insta::assert_snapshot!(
3199            env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
3200
3201        // "false" is not empty
3202        insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
3203
3204        // Error is not empty
3205        insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
3206        // but can be short-circuited
3207        insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
3208    }
3209
3210    #[test]
3211    fn test_concat_function() {
3212        let mut env = TestTemplateEnv::new();
3213        env.add_keyword("empty", || P::wrap_boolean(literal(true)));
3214        env.add_keyword("hidden", || P::wrap_boolean(literal(false)));
3215        env.add_color("empty", crossterm::style::Color::DarkGreen);
3216        env.add_color("error", crossterm::style::Color::DarkRed);
3217        env.add_color("warning", crossterm::style::Color::DarkYellow);
3218
3219        insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
3220        insta::assert_snapshot!(
3221            env.render_ok(r#"concat(hidden, empty)"#),
3222            @"falsetrue");
3223        insta::assert_snapshot!(
3224            env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
3225            @"ab");
3226    }
3227
3228    #[test]
3229    fn test_separate_function() {
3230        let mut env = TestTemplateEnv::new();
3231        env.add_keyword("description", || P::wrap_string(literal("".to_owned())));
3232        env.add_keyword("empty", || P::wrap_boolean(literal(true)));
3233        env.add_keyword("hidden", || P::wrap_boolean(literal(false)));
3234        env.add_color("empty", crossterm::style::Color::DarkGreen);
3235        env.add_color("error", crossterm::style::Color::DarkRed);
3236        env.add_color("warning", crossterm::style::Color::DarkYellow);
3237
3238        insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
3239        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
3240        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
3241        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
3242        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
3243        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
3244        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
3245
3246        // Labeled
3247        insta::assert_snapshot!(
3248            env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
3249            @"a b");
3250
3251        // List template
3252        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
3253        insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
3254
3255        // Nested separate
3256        insta::assert_snapshot!(
3257            env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
3258        insta::assert_snapshot!(
3259            env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
3260        insta::assert_snapshot!(
3261            env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
3262
3263        // Conditional template
3264        insta::assert_snapshot!(
3265            env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
3266        insta::assert_snapshot!(
3267            env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
3268        insta::assert_snapshot!(
3269            env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
3270        insta::assert_snapshot!(
3271            env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
3272
3273        // Separate keywords
3274        insta::assert_snapshot!(
3275            env.render_ok(r#"separate(" ", hidden, description, empty)"#),
3276            @"false true");
3277
3278        // Keyword as separator
3279        insta::assert_snapshot!(
3280            env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
3281            @"XfalseYfalseZ");
3282    }
3283
3284    #[test]
3285    fn test_surround_function() {
3286        let mut env = TestTemplateEnv::new();
3287        env.add_keyword("lt", || P::wrap_string(literal("<".to_owned())));
3288        env.add_keyword("gt", || P::wrap_string(literal(">".to_owned())));
3289        env.add_keyword("content", || P::wrap_string(literal("content".to_owned())));
3290        env.add_keyword("empty_content", || P::wrap_string(literal("".to_owned())));
3291        env.add_color("error", crossterm::style::Color::DarkRed);
3292        env.add_color("paren", crossterm::style::Color::Cyan);
3293
3294        insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
3295        insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
3296
3297        // Labeled
3298        insta::assert_snapshot!(
3299            env.render_ok(
3300                r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
3301            @"(a)");
3302
3303        // Keyword
3304        insta::assert_snapshot!(
3305            env.render_ok(r#"surround(lt, gt, content)"#),
3306            @"<content>");
3307        insta::assert_snapshot!(
3308            env.render_ok(r#"surround(lt, gt, empty_content)"#),
3309            @"");
3310
3311        // Conditional template as content
3312        insta::assert_snapshot!(
3313            env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
3314            @"<empty>");
3315        insta::assert_snapshot!(
3316            env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
3317            @"");
3318    }
3319}