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