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