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