Skip to main content

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