Skip to main content

jj_cli/
template_builder.rs

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