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