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