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