jj_cli/
template_builder.rs

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