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