just playing with tangled
1// Copyright 2020-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16
17use itertools::Itertools as _;
18use jj_lib::backend::{Signature, Timestamp};
19
20use crate::template_parser::{
21 self, BinaryOp, ExpressionKind, ExpressionNode, FunctionCallNode, TemplateAliasesMap,
22 TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp,
23};
24use crate::templater::{
25 CoalesceTemplate, ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate,
26 ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder,
27 ReformatTemplate, SeparateTemplate, Template, TemplateProperty, TemplatePropertyError,
28 TemplatePropertyExt as _, TemplateRenderer, TimestampRange,
29};
30use crate::{text_util, time_util};
31
32/// Callbacks to build language-specific evaluation objects from AST nodes.
33pub trait TemplateLanguage<'a> {
34 type Property: IntoTemplateProperty<'a>;
35
36 fn wrap_string(property: impl TemplateProperty<Output = String> + 'a) -> Self::Property;
37 fn wrap_string_list(
38 property: impl TemplateProperty<Output = Vec<String>> + 'a,
39 ) -> Self::Property;
40 fn wrap_boolean(property: impl TemplateProperty<Output = bool> + 'a) -> Self::Property;
41 fn wrap_integer(property: impl TemplateProperty<Output = i64> + 'a) -> Self::Property;
42 fn wrap_signature(property: impl TemplateProperty<Output = Signature> + 'a) -> Self::Property;
43 fn wrap_timestamp(property: impl TemplateProperty<Output = Timestamp> + 'a) -> Self::Property;
44 fn wrap_timestamp_range(
45 property: impl TemplateProperty<Output = TimestampRange> + 'a,
46 ) -> Self::Property;
47
48 fn wrap_template(template: Box<dyn Template + 'a>) -> Self::Property;
49 fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> Self::Property;
50
51 /// Translates the given global `function` call to a property.
52 ///
53 /// This should be delegated to
54 /// `CoreTemplateBuildFnTable::build_function()`.
55 fn build_function(
56 &self,
57 build_ctx: &BuildContext<Self::Property>,
58 function: &FunctionCallNode,
59 ) -> TemplateParseResult<Self::Property>;
60
61 fn build_method(
62 &self,
63 build_ctx: &BuildContext<Self::Property>,
64 property: Self::Property,
65 function: &FunctionCallNode,
66 ) -> TemplateParseResult<Self::Property>;
67}
68
69/// Implements `TemplateLanguage::wrap_<type>()` functions.
70///
71/// - `impl_core_wrap_property_fns('a)` for `CoreTemplatePropertyKind`,
72/// - `impl_core_wrap_property_fns('a, MyKind::Core)` for `MyKind::Core(..)`.
73macro_rules! impl_core_wrap_property_fns {
74 ($a:lifetime) => {
75 $crate::template_builder::impl_core_wrap_property_fns!($a, std::convert::identity);
76 };
77 ($a:lifetime, $outer:path) => {
78 $crate::template_builder::impl_wrap_property_fns!(
79 $a, $crate::template_builder::CoreTemplatePropertyKind, $outer, {
80 wrap_string(String) => String,
81 wrap_string_list(Vec<String>) => StringList,
82 wrap_boolean(bool) => Boolean,
83 wrap_integer(i64) => Integer,
84 wrap_signature(jj_lib::backend::Signature) => Signature,
85 wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp,
86 wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange,
87 }
88 );
89 fn wrap_template(
90 template: Box<dyn $crate::templater::Template + $a>,
91 ) -> Self::Property {
92 use $crate::template_builder::CoreTemplatePropertyKind as Kind;
93 $outer(Kind::Template(template))
94 }
95 fn wrap_list_template(
96 template: Box<dyn $crate::templater::ListTemplate + $a>,
97 ) -> Self::Property {
98 use $crate::template_builder::CoreTemplatePropertyKind as Kind;
99 $outer(Kind::ListTemplate(template))
100 }
101 };
102}
103
104macro_rules! impl_wrap_property_fns {
105 ($a:lifetime, $kind:path, $outer:path, { $( $func:ident($ty:ty) => $var:ident, )+ }) => {
106 $(
107 fn $func(
108 property: impl $crate::templater::TemplateProperty<Output = $ty> + $a,
109 ) -> Self::Property {
110 use $kind as Kind; // https://github.com/rust-lang/rust/issues/48067
111 $outer(Kind::$var(Box::new(property)))
112 }
113 )+
114 };
115}
116
117pub(crate) use {impl_core_wrap_property_fns, impl_wrap_property_fns};
118
119/// Provides access to basic template property types.
120pub trait IntoTemplateProperty<'a> {
121 /// Type name of the property output.
122 fn type_name(&self) -> &'static str;
123
124 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>>;
125 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>>;
126
127 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>>;
128 fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
129}
130
131pub enum CoreTemplatePropertyKind<'a> {
132 String(Box<dyn TemplateProperty<Output = String> + 'a>),
133 StringList(Box<dyn TemplateProperty<Output = Vec<String>> + 'a>),
134 Boolean(Box<dyn TemplateProperty<Output = bool> + 'a>),
135 Integer(Box<dyn TemplateProperty<Output = i64> + 'a>),
136 Signature(Box<dyn TemplateProperty<Output = Signature> + 'a>),
137 Timestamp(Box<dyn TemplateProperty<Output = Timestamp> + 'a>),
138 TimestampRange(Box<dyn TemplateProperty<Output = TimestampRange> + 'a>),
139
140 // Both TemplateProperty and Template can represent a value to be evaluated
141 // dynamically, which suggests that `Box<dyn Template + 'a>` could be
142 // composed as `Box<dyn TemplateProperty<Output = Box<dyn Template ..`.
143 // However, there's a subtle difference: TemplateProperty is strict on
144 // error, whereas Template is usually lax and prints an error inline. If
145 // `concat(x, y)` were a property returning Template, and if `y` failed to
146 // evaluate, the whole expression would fail. In this example, a partial
147 // evaluation output is more useful. That's one reason why Template isn't
148 // wrapped in a TemplateProperty. Another reason is that the outermost
149 // caller expects a Template, not a TemplateProperty of Template output.
150 Template(Box<dyn Template + 'a>),
151 ListTemplate(Box<dyn ListTemplate + 'a>),
152}
153
154impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
155 fn type_name(&self) -> &'static str {
156 match self {
157 CoreTemplatePropertyKind::String(_) => "String",
158 CoreTemplatePropertyKind::StringList(_) => "List<String>",
159 CoreTemplatePropertyKind::Boolean(_) => "Boolean",
160 CoreTemplatePropertyKind::Integer(_) => "Integer",
161 CoreTemplatePropertyKind::Signature(_) => "Signature",
162 CoreTemplatePropertyKind::Timestamp(_) => "Timestamp",
163 CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange",
164 CoreTemplatePropertyKind::Template(_) => "Template",
165 CoreTemplatePropertyKind::ListTemplate(_) => "ListTemplate",
166 }
167 }
168
169 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
170 match self {
171 CoreTemplatePropertyKind::String(property) => {
172 Some(Box::new(property.map(|s| !s.is_empty())))
173 }
174 CoreTemplatePropertyKind::StringList(property) => {
175 Some(Box::new(property.map(|l| !l.is_empty())))
176 }
177 CoreTemplatePropertyKind::Boolean(property) => Some(property),
178 CoreTemplatePropertyKind::Integer(_) => None,
179 CoreTemplatePropertyKind::Signature(_) => None,
180 CoreTemplatePropertyKind::Timestamp(_) => None,
181 CoreTemplatePropertyKind::TimestampRange(_) => None,
182 // Template types could also be evaluated to boolean, but it's less likely
183 // to apply label() or .map() and use the result as conditional. It's also
184 // unclear whether ListTemplate should behave as a "list" or a "template".
185 CoreTemplatePropertyKind::Template(_) => None,
186 CoreTemplatePropertyKind::ListTemplate(_) => None,
187 }
188 }
189
190 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
191 match self {
192 CoreTemplatePropertyKind::Integer(property) => Some(property),
193 _ => None,
194 }
195 }
196
197 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>> {
198 match self {
199 CoreTemplatePropertyKind::String(property) => Some(property),
200 _ => {
201 let template = self.try_into_template()?;
202 Some(Box::new(PlainTextFormattedProperty::new(template)))
203 }
204 }
205 }
206
207 fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
208 match self {
209 CoreTemplatePropertyKind::String(property) => Some(property.into_template()),
210 CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()),
211 CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()),
212 CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()),
213 CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()),
214 CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()),
215 CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()),
216 CoreTemplatePropertyKind::Template(template) => Some(template),
217 CoreTemplatePropertyKind::ListTemplate(template) => Some(template.into_template()),
218 }
219 }
220}
221
222/// Function that translates global function call node.
223// The lifetime parameter 'a could be replaced with for<'a> to keep the method
224// table away from a certain lifetime. That's technically more correct, but I
225// couldn't find an easy way to expand that to the core template methods, which
226// are defined for L: TemplateLanguage<'a>. That's why the build fn table is
227// bound to a named lifetime, and therefore can't be cached statically.
228pub type TemplateBuildFunctionFn<'a, L> =
229 fn(
230 &L,
231 &BuildContext<<L as TemplateLanguage<'a>>::Property>,
232 &FunctionCallNode,
233 ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
234
235/// Function that translates method call node of self type `T`.
236pub type TemplateBuildMethodFn<'a, L, T> =
237 fn(
238 &L,
239 &BuildContext<<L as TemplateLanguage<'a>>::Property>,
240 Box<dyn TemplateProperty<Output = T> + 'a>,
241 &FunctionCallNode,
242 ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
243
244/// Table of functions that translate global function call node.
245pub type TemplateBuildFunctionFnMap<'a, L> = HashMap<&'static str, TemplateBuildFunctionFn<'a, L>>;
246
247/// Table of functions that translate method call node of self type `T`.
248pub type TemplateBuildMethodFnMap<'a, L, T> =
249 HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>;
250
251/// Symbol table of functions and methods available in the core template.
252pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> {
253 pub functions: TemplateBuildFunctionFnMap<'a, L>,
254 pub string_methods: TemplateBuildMethodFnMap<'a, L, String>,
255 pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>,
256 pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>,
257 pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>,
258 pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>,
259 pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>,
260}
261
262pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
263 for (name, function) in extension {
264 if base.insert(name, function).is_some() {
265 panic!("Conflicting template definitions for '{name}' function");
266 }
267 }
268}
269
270impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
271 /// Creates new symbol table containing the builtin functions and methods.
272 pub fn builtin() -> Self {
273 CoreTemplateBuildFnTable {
274 functions: builtin_functions(),
275 string_methods: builtin_string_methods(),
276 boolean_methods: HashMap::new(),
277 integer_methods: HashMap::new(),
278 signature_methods: builtin_signature_methods(),
279 timestamp_methods: builtin_timestamp_methods(),
280 timestamp_range_methods: builtin_timestamp_range_methods(),
281 }
282 }
283
284 pub fn empty() -> Self {
285 CoreTemplateBuildFnTable {
286 functions: HashMap::new(),
287 string_methods: HashMap::new(),
288 boolean_methods: HashMap::new(),
289 integer_methods: HashMap::new(),
290 signature_methods: HashMap::new(),
291 timestamp_methods: HashMap::new(),
292 timestamp_range_methods: HashMap::new(),
293 }
294 }
295
296 pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) {
297 let CoreTemplateBuildFnTable {
298 functions,
299 string_methods,
300 boolean_methods,
301 integer_methods,
302 signature_methods,
303 timestamp_methods,
304 timestamp_range_methods,
305 } = extension;
306
307 merge_fn_map(&mut self.functions, functions);
308 merge_fn_map(&mut self.string_methods, string_methods);
309 merge_fn_map(&mut self.boolean_methods, boolean_methods);
310 merge_fn_map(&mut self.integer_methods, integer_methods);
311 merge_fn_map(&mut self.signature_methods, signature_methods);
312 merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
313 merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
314 }
315
316 /// Translates the function call node `function` by using this symbol table.
317 pub fn build_function(
318 &self,
319 language: &L,
320 build_ctx: &BuildContext<L::Property>,
321 function: &FunctionCallNode,
322 ) -> TemplateParseResult<L::Property> {
323 let table = &self.functions;
324 let build = template_parser::lookup_function(table, function)?;
325 build(language, build_ctx, function)
326 }
327
328 /// Applies the method call node `function` to the given `property` by using
329 /// this symbol table.
330 pub fn build_method(
331 &self,
332 language: &L,
333 build_ctx: &BuildContext<L::Property>,
334 property: CoreTemplatePropertyKind<'a>,
335 function: &FunctionCallNode,
336 ) -> TemplateParseResult<L::Property> {
337 let type_name = property.type_name();
338 match property {
339 CoreTemplatePropertyKind::String(property) => {
340 let table = &self.string_methods;
341 let build = template_parser::lookup_method(type_name, table, function)?;
342 build(language, build_ctx, property, function)
343 }
344 CoreTemplatePropertyKind::StringList(property) => {
345 // TODO: migrate to table?
346 build_formattable_list_method(language, build_ctx, property, function, |item| {
347 L::wrap_string(item)
348 })
349 }
350 CoreTemplatePropertyKind::Boolean(property) => {
351 let table = &self.boolean_methods;
352 let build = template_parser::lookup_method(type_name, table, function)?;
353 build(language, build_ctx, property, function)
354 }
355 CoreTemplatePropertyKind::Integer(property) => {
356 let table = &self.integer_methods;
357 let build = template_parser::lookup_method(type_name, table, function)?;
358 build(language, build_ctx, property, function)
359 }
360 CoreTemplatePropertyKind::Signature(property) => {
361 let table = &self.signature_methods;
362 let build = template_parser::lookup_method(type_name, table, function)?;
363 build(language, build_ctx, property, function)
364 }
365 CoreTemplatePropertyKind::Timestamp(property) => {
366 let table = &self.timestamp_methods;
367 let build = template_parser::lookup_method(type_name, table, function)?;
368 build(language, build_ctx, property, function)
369 }
370 CoreTemplatePropertyKind::TimestampRange(property) => {
371 let table = &self.timestamp_range_methods;
372 let build = template_parser::lookup_method(type_name, table, function)?;
373 build(language, build_ctx, property, function)
374 }
375 CoreTemplatePropertyKind::Template(_) => {
376 // TODO: migrate to table?
377 Err(TemplateParseError::no_such_method(type_name, function))
378 }
379 CoreTemplatePropertyKind::ListTemplate(template) => {
380 // TODO: migrate to table?
381 build_list_template_method(language, build_ctx, template, function)
382 }
383 }
384 }
385}
386
387/// Opaque struct that represents a template value.
388pub struct Expression<P> {
389 property: P,
390 labels: Vec<String>,
391}
392
393impl<P> Expression<P> {
394 fn unlabeled(property: P) -> Self {
395 let labels = vec![];
396 Expression { property, labels }
397 }
398
399 fn with_label(property: P, label: impl Into<String>) -> Self {
400 let labels = vec![label.into()];
401 Expression { property, labels }
402 }
403}
404
405impl<'a, P: IntoTemplateProperty<'a>> Expression<P> {
406 pub fn type_name(&self) -> &'static str {
407 self.property.type_name()
408 }
409
410 pub fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
411 self.property.try_into_boolean()
412 }
413
414 pub fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
415 self.property.try_into_integer()
416 }
417
418 pub fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>> {
419 self.property.try_into_plain_text()
420 }
421
422 pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
423 let template = self.property.try_into_template()?;
424 if self.labels.is_empty() {
425 Some(template)
426 } else {
427 Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
428 }
429 }
430}
431
432pub struct BuildContext<'i, P> {
433 /// Map of functions to create `L::Property`.
434 local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>,
435 /// Function to create `L::Property` representing `self`.
436 ///
437 /// This could be `local_variables["self"]`, but keyword lookup shouldn't be
438 /// overridden by a user-defined `self` variable.
439 self_variable: &'i (dyn Fn() -> P),
440}
441
442fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
443 language: &L,
444 build_ctx: &BuildContext<L::Property>,
445 name: &str,
446 name_span: pest::Span<'_>,
447) -> TemplateParseResult<L::Property> {
448 // Keyword is a 0-ary method on the "self" property
449 let self_property = (build_ctx.self_variable)();
450 let function = FunctionCallNode {
451 name,
452 name_span,
453 args: vec![],
454 args_span: name_span.end_pos().span(&name_span.end_pos()),
455 };
456 language
457 .build_method(build_ctx, self_property, &function)
458 .map_err(|err| match err.kind() {
459 TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
460 let kind = TemplateParseErrorKind::NoSuchKeyword {
461 name: name.to_owned(),
462 // TODO: filter methods by arity?
463 candidates: candidates.clone(),
464 };
465 TemplateParseError::with_span(kind, name_span)
466 }
467 // Since keyword is a 0-ary method, any argument errors mean there's
468 // no such keyword.
469 TemplateParseErrorKind::InvalidArguments { .. } => {
470 let kind = TemplateParseErrorKind::NoSuchKeyword {
471 name: name.to_owned(),
472 // TODO: might be better to phrase the error differently
473 candidates: vec![format!("self.{name}(..)")],
474 };
475 TemplateParseError::with_span(kind, name_span)
476 }
477 // The keyword function may fail with the other reasons.
478 _ => err,
479 })
480}
481
482fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
483 language: &L,
484 build_ctx: &BuildContext<L::Property>,
485 op: UnaryOp,
486 arg_node: &ExpressionNode,
487) -> TemplateParseResult<L::Property> {
488 match op {
489 UnaryOp::LogicalNot => {
490 let arg = expect_boolean_expression(language, build_ctx, arg_node)?;
491 Ok(L::wrap_boolean(arg.map(|v| !v)))
492 }
493 UnaryOp::Negate => {
494 let arg = expect_integer_expression(language, build_ctx, arg_node)?;
495 Ok(L::wrap_integer(arg.and_then(|v| {
496 v.checked_neg()
497 .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
498 })))
499 }
500 }
501}
502
503fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
504 language: &L,
505 build_ctx: &BuildContext<L::Property>,
506 op: BinaryOp,
507 lhs_node: &ExpressionNode,
508 rhs_node: &ExpressionNode,
509) -> TemplateParseResult<L::Property> {
510 match op {
511 BinaryOp::LogicalOr => {
512 let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?;
513 let rhs = expect_boolean_expression(language, build_ctx, rhs_node)?;
514 let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
515 Ok(L::wrap_boolean(out))
516 }
517 BinaryOp::LogicalAnd => {
518 let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?;
519 let rhs = expect_boolean_expression(language, build_ctx, rhs_node)?;
520 let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
521 Ok(L::wrap_boolean(out))
522 }
523 }
524}
525
526fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
527) -> TemplateBuildMethodFnMap<'a, L, String> {
528 // Not using maplit::hashmap!{} or custom declarative macro here because
529 // code completion inside macro is quite restricted.
530 let mut map = TemplateBuildMethodFnMap::<L, String>::new();
531 map.insert("len", |_language, _build_ctx, self_property, function| {
532 template_parser::expect_no_arguments(function)?;
533 let out_property = self_property.and_then(|s| Ok(s.len().try_into()?));
534 Ok(L::wrap_integer(out_property))
535 });
536 map.insert(
537 "contains",
538 |language, build_ctx, self_property, function| {
539 let [needle_node] = template_parser::expect_exact_arguments(function)?;
540 // TODO: or .try_into_string() to disable implicit type cast?
541 let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
542 let out_property = (self_property, needle_property)
543 .map(|(haystack, needle)| haystack.contains(&needle));
544 Ok(L::wrap_boolean(out_property))
545 },
546 );
547 map.insert(
548 "starts_with",
549 |language, build_ctx, self_property, function| {
550 let [needle_node] = template_parser::expect_exact_arguments(function)?;
551 let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
552 let out_property = (self_property, needle_property)
553 .map(|(haystack, needle)| haystack.starts_with(&needle));
554 Ok(L::wrap_boolean(out_property))
555 },
556 );
557 map.insert(
558 "ends_with",
559 |language, build_ctx, self_property, function| {
560 let [needle_node] = template_parser::expect_exact_arguments(function)?;
561 let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
562 let out_property = (self_property, needle_property)
563 .map(|(haystack, needle)| haystack.ends_with(&needle));
564 Ok(L::wrap_boolean(out_property))
565 },
566 );
567 map.insert(
568 "remove_prefix",
569 |language, build_ctx, self_property, function| {
570 let [needle_node] = template_parser::expect_exact_arguments(function)?;
571 let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
572 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
573 haystack
574 .strip_prefix(&needle)
575 .map(ToOwned::to_owned)
576 .unwrap_or(haystack)
577 });
578 Ok(L::wrap_string(out_property))
579 },
580 );
581 map.insert(
582 "remove_suffix",
583 |language, build_ctx, self_property, function| {
584 let [needle_node] = template_parser::expect_exact_arguments(function)?;
585 let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
586 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
587 haystack
588 .strip_suffix(&needle)
589 .map(ToOwned::to_owned)
590 .unwrap_or(haystack)
591 });
592 Ok(L::wrap_string(out_property))
593 },
594 );
595 map.insert("substr", |language, build_ctx, self_property, function| {
596 let [start_idx, end_idx] = template_parser::expect_exact_arguments(function)?;
597 let start_idx_property = expect_isize_expression(language, build_ctx, start_idx)?;
598 let end_idx_property = expect_isize_expression(language, build_ctx, end_idx)?;
599 let out_property =
600 (self_property, start_idx_property, end_idx_property).map(|(s, start_idx, end_idx)| {
601 let start_idx = string_index_to_char_boundary(&s, start_idx);
602 let end_idx = string_index_to_char_boundary(&s, end_idx);
603 s.get(start_idx..end_idx).unwrap_or_default().to_owned()
604 });
605 Ok(L::wrap_string(out_property))
606 });
607 map.insert(
608 "first_line",
609 |_language, _build_ctx, self_property, function| {
610 template_parser::expect_no_arguments(function)?;
611 let out_property =
612 self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
613 Ok(L::wrap_string(out_property))
614 },
615 );
616 map.insert("lines", |_language, _build_ctx, self_property, function| {
617 template_parser::expect_no_arguments(function)?;
618 let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect());
619 Ok(L::wrap_string_list(out_property))
620 });
621 map.insert("upper", |_language, _build_ctx, self_property, function| {
622 template_parser::expect_no_arguments(function)?;
623 let out_property = self_property.map(|s| s.to_uppercase());
624 Ok(L::wrap_string(out_property))
625 });
626 map.insert("lower", |_language, _build_ctx, self_property, function| {
627 template_parser::expect_no_arguments(function)?;
628 let out_property = self_property.map(|s| s.to_lowercase());
629 Ok(L::wrap_string(out_property))
630 });
631 map
632}
633
634/// Clamps and aligns the given index `i` to char boundary.
635///
636/// Negative index counts from the end. If the index isn't at a char boundary,
637/// it will be rounded towards 0 (left or right depending on the sign.)
638fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
639 // TODO: use floor/ceil_char_boundary() if get stabilized
640 let magnitude = i.unsigned_abs();
641 if i < 0 {
642 let p = s.len().saturating_sub(magnitude);
643 (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
644 } else {
645 let p = magnitude.min(s.len());
646 (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
647 }
648}
649
650fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
651) -> TemplateBuildMethodFnMap<'a, L, Signature> {
652 // Not using maplit::hashmap!{} or custom declarative macro here because
653 // code completion inside macro is quite restricted.
654 let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
655 map.insert("name", |_language, _build_ctx, self_property, function| {
656 template_parser::expect_no_arguments(function)?;
657 let out_property = self_property.map(|signature| signature.name);
658 Ok(L::wrap_string(out_property))
659 });
660 map.insert("email", |_language, _build_ctx, self_property, function| {
661 template_parser::expect_no_arguments(function)?;
662 let out_property = self_property.map(|signature| signature.email);
663 Ok(L::wrap_string(out_property))
664 });
665 map.insert(
666 "username",
667 |_language, _build_ctx, self_property, function| {
668 template_parser::expect_no_arguments(function)?;
669 let out_property = self_property.map(|signature| {
670 let (username, _) = text_util::split_email(&signature.email);
671 username.to_owned()
672 });
673 Ok(L::wrap_string(out_property))
674 },
675 );
676 map.insert(
677 "timestamp",
678 |_language, _build_ctx, self_property, function| {
679 template_parser::expect_no_arguments(function)?;
680 let out_property = self_property.map(|signature| signature.timestamp);
681 Ok(L::wrap_timestamp(out_property))
682 },
683 );
684 map
685}
686
687fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
688) -> TemplateBuildMethodFnMap<'a, L, Timestamp> {
689 // Not using maplit::hashmap!{} or custom declarative macro here because
690 // code completion inside macro is quite restricted.
691 let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
692 map.insert("ago", |_language, _build_ctx, self_property, function| {
693 template_parser::expect_no_arguments(function)?;
694 let now = Timestamp::now();
695 let format = timeago::Formatter::new();
696 let out_property = self_property
697 .and_then(move |timestamp| Ok(time_util::format_duration(×tamp, &now, &format)?));
698 Ok(L::wrap_string(out_property))
699 });
700 map.insert(
701 "format",
702 |_language, _build_ctx, self_property, function| {
703 // No dynamic string is allowed as the templater has no runtime error type.
704 let [format_node] = template_parser::expect_exact_arguments(function)?;
705 let format =
706 template_parser::expect_string_literal_with(format_node, |format, span| {
707 time_util::FormattingItems::parse(format)
708 .ok_or_else(|| TemplateParseError::expression("Invalid time format", span))
709 })?
710 .into_owned();
711 let out_property = self_property.and_then(move |timestamp| {
712 Ok(time_util::format_absolute_timestamp_with(
713 ×tamp, &format,
714 )?)
715 });
716 Ok(L::wrap_string(out_property))
717 },
718 );
719 map.insert("utc", |_language, _build_ctx, self_property, function| {
720 template_parser::expect_no_arguments(function)?;
721 let out_property = self_property.map(|mut timestamp| {
722 timestamp.tz_offset = 0;
723 timestamp
724 });
725 Ok(L::wrap_timestamp(out_property))
726 });
727 map.insert("local", |_language, _build_ctx, self_property, function| {
728 template_parser::expect_no_arguments(function)?;
729 let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
730 .ok()
731 .and_then(|tz_string| tz_string.parse::<i32>().ok())
732 .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
733 let out_property = self_property.map(move |mut timestamp| {
734 timestamp.tz_offset = tz_offset;
735 timestamp
736 });
737 Ok(L::wrap_timestamp(out_property))
738 });
739 map
740}
741
742fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
743) -> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
744 // Not using maplit::hashmap!{} or custom declarative macro here because
745 // code completion inside macro is quite restricted.
746 let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
747 map.insert("start", |_language, _build_ctx, self_property, function| {
748 template_parser::expect_no_arguments(function)?;
749 let out_property = self_property.map(|time_range| time_range.start);
750 Ok(L::wrap_timestamp(out_property))
751 });
752 map.insert("end", |_language, _build_ctx, self_property, function| {
753 template_parser::expect_no_arguments(function)?;
754 let out_property = self_property.map(|time_range| time_range.end);
755 Ok(L::wrap_timestamp(out_property))
756 });
757 map.insert(
758 "duration",
759 |_language, _build_ctx, self_property, function| {
760 template_parser::expect_no_arguments(function)?;
761 let out_property = self_property.and_then(|time_range| Ok(time_range.duration()?));
762 Ok(L::wrap_string(out_property))
763 },
764 );
765 map
766}
767
768fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>(
769 language: &L,
770 build_ctx: &BuildContext<L::Property>,
771 self_template: Box<dyn ListTemplate + 'a>,
772 function: &FunctionCallNode,
773) -> TemplateParseResult<L::Property> {
774 let property = match function.name {
775 "join" => {
776 let [separator_node] = template_parser::expect_exact_arguments(function)?;
777 let separator = expect_template_expression(language, build_ctx, separator_node)?;
778 L::wrap_template(self_template.join(separator))
779 }
780 _ => return Err(TemplateParseError::no_such_method("ListTemplate", function)),
781 };
782 Ok(property)
783}
784
785/// Builds method call expression for printable list property.
786pub fn build_formattable_list_method<'a, L, O>(
787 language: &L,
788 build_ctx: &BuildContext<L::Property>,
789 self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
790 function: &FunctionCallNode,
791 // TODO: Generic L: WrapProperty<O> trait might be needed to support more
792 // list operations such as first()/slice(). For .map(), a simple callback works.
793 wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
794) -> TemplateParseResult<L::Property>
795where
796 L: TemplateLanguage<'a> + ?Sized,
797 O: Template + Clone + 'a,
798{
799 let property = match function.name {
800 "len" => {
801 template_parser::expect_no_arguments(function)?;
802 let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
803 L::wrap_integer(out_property)
804 }
805 "join" => {
806 let [separator_node] = template_parser::expect_exact_arguments(function)?;
807 let separator = expect_template_expression(language, build_ctx, separator_node)?;
808 let template =
809 ListPropertyTemplate::new(self_property, separator, |formatter, item| {
810 item.format(formatter)
811 });
812 L::wrap_template(Box::new(template))
813 }
814 "map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?,
815 _ => return Err(TemplateParseError::no_such_method("List", function)),
816 };
817 Ok(property)
818}
819
820pub fn build_unformattable_list_method<'a, L, O>(
821 language: &L,
822 build_ctx: &BuildContext<L::Property>,
823 self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
824 function: &FunctionCallNode,
825 wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
826) -> TemplateParseResult<L::Property>
827where
828 L: TemplateLanguage<'a> + ?Sized,
829 O: Clone + 'a,
830{
831 let property = match function.name {
832 "len" => {
833 template_parser::expect_no_arguments(function)?;
834 let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
835 L::wrap_integer(out_property)
836 }
837 // No "join"
838 "map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?,
839 _ => return Err(TemplateParseError::no_such_method("List", function)),
840 };
841 Ok(property)
842}
843
844/// Builds expression that extracts iterable property and applies template to
845/// each item.
846///
847/// `wrap_item()` is the function to wrap a list item of type `O` as a property.
848fn build_map_operation<'a, L, O, P>(
849 language: &L,
850 build_ctx: &BuildContext<L::Property>,
851 self_property: P,
852 function: &FunctionCallNode,
853 wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
854) -> TemplateParseResult<L::Property>
855where
856 L: TemplateLanguage<'a> + ?Sized,
857 P: TemplateProperty + 'a,
858 P::Output: IntoIterator<Item = O>,
859 O: Clone + 'a,
860{
861 // Build an item template with placeholder property, then evaluate it
862 // for each item.
863 let [lambda_node] = template_parser::expect_exact_arguments(function)?;
864 let item_placeholder = PropertyPlaceholder::new();
865 let item_template = template_parser::expect_lambda_with(lambda_node, |lambda, _span| {
866 let item_fn = || wrap_item(item_placeholder.clone());
867 let mut local_variables = build_ctx.local_variables.clone();
868 if let [name] = lambda.params.as_slice() {
869 local_variables.insert(name, &item_fn);
870 } else {
871 return Err(TemplateParseError::expression(
872 "Expected 1 lambda parameters",
873 lambda.params_span,
874 ));
875 }
876 let inner_build_ctx = BuildContext {
877 local_variables,
878 self_variable: build_ctx.self_variable,
879 };
880 expect_template_expression(language, &inner_build_ctx, &lambda.body)
881 })?;
882 let list_template = ListPropertyTemplate::new(
883 self_property,
884 Literal(" "), // separator
885 move |formatter, item| {
886 item_placeholder.with_value(item, || item_template.format(formatter))
887 },
888 );
889 Ok(L::wrap_list_template(Box::new(list_template)))
890}
891
892fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
893 // Not using maplit::hashmap!{} or custom declarative macro here because
894 // code completion inside macro is quite restricted.
895 let mut map = TemplateBuildFunctionFnMap::<L>::new();
896 map.insert("fill", |language, build_ctx, function| {
897 let [width_node, content_node] = template_parser::expect_exact_arguments(function)?;
898 let width = expect_usize_expression(language, build_ctx, width_node)?;
899 let content = expect_template_expression(language, build_ctx, content_node)?;
900 let template =
901 ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
902 Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
903 Err(err) => formatter.handle_error(err),
904 });
905 Ok(L::wrap_template(Box::new(template)))
906 });
907 map.insert("indent", |language, build_ctx, function| {
908 let [prefix_node, content_node] = template_parser::expect_exact_arguments(function)?;
909 let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
910 let content = expect_template_expression(language, build_ctx, content_node)?;
911 let template = ReformatTemplate::new(content, move |formatter, recorded| {
912 let rewrap = formatter.rewrap_fn();
913 text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
914 prefix.format(&mut rewrap(formatter))
915 })
916 });
917 Ok(L::wrap_template(Box::new(template)))
918 });
919 map.insert("label", |language, build_ctx, function| {
920 let [label_node, content_node] = template_parser::expect_exact_arguments(function)?;
921 let label_property = expect_plain_text_expression(language, build_ctx, label_node)?;
922 let content = expect_template_expression(language, build_ctx, content_node)?;
923 let labels =
924 label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
925 Ok(L::wrap_template(Box::new(LabelTemplate::new(
926 content, labels,
927 ))))
928 });
929 map.insert("if", |language, build_ctx, function| {
930 let ([condition_node, true_node], [false_node]) =
931 template_parser::expect_arguments(function)?;
932 let condition = expect_boolean_expression(language, build_ctx, condition_node)?;
933 let true_template = expect_template_expression(language, build_ctx, true_node)?;
934 let false_template = false_node
935 .map(|node| expect_template_expression(language, build_ctx, node))
936 .transpose()?;
937 let template = ConditionalTemplate::new(condition, true_template, false_template);
938 Ok(L::wrap_template(Box::new(template)))
939 });
940 map.insert("coalesce", |language, build_ctx, function| {
941 let contents = function
942 .args
943 .iter()
944 .map(|node| expect_template_expression(language, build_ctx, node))
945 .try_collect()?;
946 Ok(L::wrap_template(Box::new(CoalesceTemplate(contents))))
947 });
948 map.insert("concat", |language, build_ctx, function| {
949 let contents = function
950 .args
951 .iter()
952 .map(|node| expect_template_expression(language, build_ctx, node))
953 .try_collect()?;
954 Ok(L::wrap_template(Box::new(ConcatTemplate(contents))))
955 });
956 map.insert("separate", |language, build_ctx, function| {
957 let ([separator_node], content_nodes) = template_parser::expect_some_arguments(function)?;
958 let separator = expect_template_expression(language, build_ctx, separator_node)?;
959 let contents = content_nodes
960 .iter()
961 .map(|node| expect_template_expression(language, build_ctx, node))
962 .try_collect()?;
963 Ok(L::wrap_template(Box::new(SeparateTemplate::new(
964 separator, contents,
965 ))))
966 });
967 map.insert("surround", |language, build_ctx, function| {
968 let [prefix_node, suffix_node, content_node] =
969 template_parser::expect_exact_arguments(function)?;
970 let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
971 let suffix = expect_template_expression(language, build_ctx, suffix_node)?;
972 let content = expect_template_expression(language, build_ctx, content_node)?;
973 let template = ReformatTemplate::new(content, move |formatter, recorded| {
974 if recorded.data().is_empty() {
975 return Ok(());
976 }
977 prefix.format(formatter)?;
978 recorded.replay(formatter.as_mut())?;
979 suffix.format(formatter)?;
980 Ok(())
981 });
982 Ok(L::wrap_template(Box::new(template)))
983 });
984 map
985}
986
987/// Builds intermediate expression tree from AST nodes.
988pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
989 language: &L,
990 build_ctx: &BuildContext<L::Property>,
991 node: &ExpressionNode,
992) -> TemplateParseResult<Expression<L::Property>> {
993 match &node.kind {
994 ExpressionKind::Identifier(name) => {
995 if let Some(make) = build_ctx.local_variables.get(name) {
996 // Don't label a local variable with its name
997 Ok(Expression::unlabeled(make()))
998 } else if *name == "self" {
999 // "self" is a special variable, so don't label it
1000 let make = build_ctx.self_variable;
1001 Ok(Expression::unlabeled(make()))
1002 } else {
1003 let property =
1004 build_keyword(language, build_ctx, name, node.span).map_err(|err| {
1005 err.extend_keyword_candidates(itertools::chain(
1006 build_ctx.local_variables.keys().copied(),
1007 ["self"],
1008 ))
1009 })?;
1010 Ok(Expression::with_label(property, *name))
1011 }
1012 }
1013 ExpressionKind::Boolean(value) => {
1014 let property = L::wrap_boolean(Literal(*value));
1015 Ok(Expression::unlabeled(property))
1016 }
1017 ExpressionKind::Integer(value) => {
1018 let property = L::wrap_integer(Literal(*value));
1019 Ok(Expression::unlabeled(property))
1020 }
1021 ExpressionKind::String(value) => {
1022 let property = L::wrap_string(Literal(value.clone()));
1023 Ok(Expression::unlabeled(property))
1024 }
1025 ExpressionKind::Unary(op, arg_node) => {
1026 let property = build_unary_operation(language, build_ctx, *op, arg_node)?;
1027 Ok(Expression::unlabeled(property))
1028 }
1029 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
1030 let property = build_binary_operation(language, build_ctx, *op, lhs_node, rhs_node)?;
1031 Ok(Expression::unlabeled(property))
1032 }
1033 ExpressionKind::Concat(nodes) => {
1034 let templates = nodes
1035 .iter()
1036 .map(|node| expect_template_expression(language, build_ctx, node))
1037 .try_collect()?;
1038 let property = L::wrap_template(Box::new(ConcatTemplate(templates)));
1039 Ok(Expression::unlabeled(property))
1040 }
1041 ExpressionKind::FunctionCall(function) => {
1042 let property = language.build_function(build_ctx, function)?;
1043 Ok(Expression::unlabeled(property))
1044 }
1045 ExpressionKind::MethodCall(method) => {
1046 let mut expression = build_expression(language, build_ctx, &method.object)?;
1047 expression.property =
1048 language.build_method(build_ctx, expression.property, &method.function)?;
1049 expression.labels.push(method.function.name.to_owned());
1050 Ok(expression)
1051 }
1052 ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
1053 "Lambda cannot be defined here",
1054 node.span,
1055 )),
1056 ExpressionKind::AliasExpanded(id, subst) => build_expression(language, build_ctx, subst)
1057 .map_err(|e| e.within_alias_expansion(*id, node.span)),
1058 }
1059}
1060
1061/// Builds template evaluation tree from AST nodes, with fresh build context.
1062///
1063/// `wrap_self` specifies the type of the top-level property, which should be
1064/// one of the `L::wrap_*()` functions.
1065pub fn build<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
1066 language: &L,
1067 node: &ExpressionNode,
1068 // TODO: Generic L: WrapProperty<C> trait might be better. See the
1069 // comment in build_formattable_list_method().
1070 wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
1071) -> TemplateParseResult<TemplateRenderer<'a, C>> {
1072 let self_placeholder = PropertyPlaceholder::new();
1073 let build_ctx = BuildContext {
1074 local_variables: HashMap::new(),
1075 self_variable: &|| wrap_self(self_placeholder.clone()),
1076 };
1077 let template = expect_template_expression(language, &build_ctx, node)?;
1078 Ok(TemplateRenderer::new(template, self_placeholder))
1079}
1080
1081/// Parses text, expands aliases, then builds template evaluation tree.
1082pub fn parse<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
1083 language: &L,
1084 template_text: &str,
1085 aliases_map: &TemplateAliasesMap,
1086 wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
1087) -> TemplateParseResult<TemplateRenderer<'a, C>> {
1088 let node = template_parser::parse(template_text, aliases_map)?;
1089 build(language, &node, wrap_self).map_err(|err| err.extend_alias_candidates(aliases_map))
1090}
1091
1092pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1093 language: &L,
1094 build_ctx: &BuildContext<L::Property>,
1095 node: &ExpressionNode,
1096) -> TemplateParseResult<Box<dyn TemplateProperty<Output = bool> + 'a>> {
1097 let expression = build_expression(language, build_ctx, node)?;
1098 let actual_type = expression.type_name();
1099 expression
1100 .try_into_boolean()
1101 .ok_or_else(|| TemplateParseError::expected_type("Boolean", actual_type, node.span))
1102}
1103
1104pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1105 language: &L,
1106 build_ctx: &BuildContext<L::Property>,
1107 node: &ExpressionNode,
1108) -> TemplateParseResult<Box<dyn TemplateProperty<Output = i64> + 'a>> {
1109 let expression = build_expression(language, build_ctx, node)?;
1110 let actual_type = expression.type_name();
1111 expression
1112 .try_into_integer()
1113 .ok_or_else(|| TemplateParseError::expected_type("Integer", actual_type, node.span))
1114}
1115
1116/// If the given expression `node` is of `Integer` type, converts it to `isize`.
1117pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1118 language: &L,
1119 build_ctx: &BuildContext<L::Property>,
1120 node: &ExpressionNode,
1121) -> TemplateParseResult<Box<dyn TemplateProperty<Output = isize> + 'a>> {
1122 let i64_property = expect_integer_expression(language, build_ctx, node)?;
1123 let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
1124 Ok(Box::new(isize_property))
1125}
1126
1127/// If the given expression `node` is of `Integer` type, converts it to `usize`.
1128pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1129 language: &L,
1130 build_ctx: &BuildContext<L::Property>,
1131 node: &ExpressionNode,
1132) -> TemplateParseResult<Box<dyn TemplateProperty<Output = usize> + 'a>> {
1133 let i64_property = expect_integer_expression(language, build_ctx, node)?;
1134 let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
1135 Ok(Box::new(usize_property))
1136}
1137
1138pub fn expect_plain_text_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1139 language: &L,
1140 build_ctx: &BuildContext<L::Property>,
1141 node: &ExpressionNode,
1142) -> TemplateParseResult<Box<dyn TemplateProperty<Output = String> + 'a>> {
1143 // Since any formattable type can be converted to a string property,
1144 // the expected type is not a String, but a Template.
1145 let expression = build_expression(language, build_ctx, node)?;
1146 let actual_type = expression.type_name();
1147 expression
1148 .try_into_plain_text()
1149 .ok_or_else(|| TemplateParseError::expected_type("Template", actual_type, node.span))
1150}
1151
1152pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1153 language: &L,
1154 build_ctx: &BuildContext<L::Property>,
1155 node: &ExpressionNode,
1156) -> TemplateParseResult<Box<dyn Template + 'a>> {
1157 let expression = build_expression(language, build_ctx, node)?;
1158 let actual_type = expression.type_name();
1159 expression
1160 .try_into_template()
1161 .ok_or_else(|| TemplateParseError::expected_type("Template", actual_type, node.span))
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use std::iter;
1167
1168 use jj_lib::backend::MillisSinceEpoch;
1169
1170 use super::*;
1171 use crate::formatter::{self, ColorFormatter};
1172 use crate::generic_templater::GenericTemplateLanguage;
1173
1174 type L = GenericTemplateLanguage<'static, ()>;
1175 type TestTemplatePropertyKind = <L as TemplateLanguage<'static>>::Property;
1176
1177 /// Helper to set up template evaluation environment.
1178 struct TestTemplateEnv {
1179 language: L,
1180 aliases_map: TemplateAliasesMap,
1181 color_rules: Vec<(Vec<String>, formatter::Style)>,
1182 }
1183
1184 impl TestTemplateEnv {
1185 fn new() -> Self {
1186 TestTemplateEnv {
1187 language: L::new(),
1188 aliases_map: TemplateAliasesMap::new(),
1189 color_rules: Vec::new(),
1190 }
1191 }
1192 }
1193
1194 impl TestTemplateEnv {
1195 fn add_keyword<F>(&mut self, name: &'static str, build: F)
1196 where
1197 F: Fn() -> TestTemplatePropertyKind + 'static,
1198 {
1199 self.language.add_keyword(name, move |_| Ok(build()));
1200 }
1201
1202 fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
1203 self.aliases_map.insert(decl, defn).unwrap();
1204 }
1205
1206 fn add_color(&mut self, label: &str, fg_color: crossterm::style::Color) {
1207 let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
1208 let style = formatter::Style {
1209 fg_color: Some(fg_color),
1210 ..Default::default()
1211 };
1212 self.color_rules.push((labels, style));
1213 }
1214
1215 fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, ()>> {
1216 parse(&self.language, template, &self.aliases_map, L::wrap_self)
1217 }
1218
1219 fn parse_err(&self, template: &str) -> String {
1220 let err = self.parse(template).err().unwrap();
1221 iter::successors(Some(&err), |e| e.origin()).join("\n")
1222 }
1223
1224 fn render_ok(&self, template: &str) -> String {
1225 let template = self.parse(template).unwrap();
1226 let mut output = Vec::new();
1227 let mut formatter = ColorFormatter::new(&mut output, self.color_rules.clone().into());
1228 template.format(&(), &mut formatter).unwrap();
1229 drop(formatter);
1230 String::from_utf8(output).unwrap()
1231 }
1232 }
1233
1234 fn new_error_property<O>(message: &str) -> impl TemplateProperty<Output = O> + '_ {
1235 Literal(()).and_then(|()| Err(TemplatePropertyError(message.into())))
1236 }
1237
1238 fn new_signature(name: &str, email: &str) -> Signature {
1239 Signature {
1240 name: name.to_owned(),
1241 email: email.to_owned(),
1242 timestamp: new_timestamp(0, 0),
1243 }
1244 }
1245
1246 fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
1247 Timestamp {
1248 timestamp: MillisSinceEpoch(msec),
1249 tz_offset,
1250 }
1251 }
1252
1253 #[test]
1254 fn test_parsed_tree() {
1255 let mut env = TestTemplateEnv::new();
1256 env.add_keyword("divergent", || L::wrap_boolean(Literal(false)));
1257 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
1258 env.add_keyword("hello", || L::wrap_string(Literal("Hello".to_owned())));
1259
1260 // Empty
1261 insta::assert_snapshot!(env.render_ok(r#" "#), @"");
1262
1263 // Single term with whitespace
1264 insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
1265
1266 // Multiple terms
1267 insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
1268
1269 // Parenthesized single term
1270 insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
1271
1272 // Parenthesized multiple terms and concatenation
1273 insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
1274
1275 // Parenthesized "if" condition
1276 insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
1277
1278 // Parenthesized method chaining
1279 insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
1280 }
1281
1282 #[test]
1283 fn test_parse_error() {
1284 let mut env = TestTemplateEnv::new();
1285 env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
1286 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
1287
1288 insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r###"
1289 --> 1:13
1290 |
1291 1 | description ()
1292 | ^---
1293 |
1294 = expected <EOI>, `++`, `||`, or `&&`
1295 "###);
1296
1297 insta::assert_snapshot!(env.parse_err(r#"foo"#), @r###"
1298 --> 1:1
1299 |
1300 1 | foo
1301 | ^-^
1302 |
1303 = Keyword "foo" doesn't exist
1304 "###);
1305
1306 insta::assert_snapshot!(env.parse_err(r#"foo()"#), @r###"
1307 --> 1:1
1308 |
1309 1 | foo()
1310 | ^-^
1311 |
1312 = Function "foo" doesn't exist
1313 "###);
1314 insta::assert_snapshot!(env.parse_err(r#"false()"#), @r###"
1315 --> 1:1
1316 |
1317 1 | false()
1318 | ^---^
1319 |
1320 = Expected identifier
1321 "###);
1322
1323 insta::assert_snapshot!(env.parse_err(r#"!foo"#), @r###"
1324 --> 1:2
1325 |
1326 1 | !foo
1327 | ^-^
1328 |
1329 = Keyword "foo" doesn't exist
1330 "###);
1331 insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @r###"
1332 --> 1:9
1333 |
1334 1 | true && 123
1335 | ^-^
1336 |
1337 = Expected expression of type "Boolean", but actual type is "Integer"
1338 "###);
1339
1340 insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r###"
1341 --> 1:26
1342 |
1343 1 | description.first_line().foo()
1344 | ^-^
1345 |
1346 = Method "foo" doesn't exist for type "String"
1347 "###);
1348
1349 insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @r###"
1350 --> 1:1
1351 |
1352 1 | 10000000000000000000
1353 | ^------------------^
1354 |
1355 = Invalid integer literal
1356 "###);
1357 insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @r###"
1358 --> 1:4
1359 |
1360 1 | 42.foo()
1361 | ^-^
1362 |
1363 = Method "foo" doesn't exist for type "Integer"
1364 "###);
1365 insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @r###"
1366 --> 1:3
1367 |
1368 1 | (-empty)
1369 | ^---^
1370 |
1371 = Expected expression of type "Integer", but actual type is "Boolean"
1372 "###);
1373
1374 insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r###"
1375 --> 1:18
1376 |
1377 1 | ("foo" ++ "bar").baz()
1378 | ^-^
1379 |
1380 = Method "baz" doesn't exist for type "Template"
1381 "###);
1382
1383 insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @r###"
1384 --> 1:22
1385 |
1386 1 | description.contains()
1387 | ^
1388 |
1389 = Function "contains": Expected 1 arguments
1390 "###);
1391
1392 insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r###"
1393 --> 1:24
1394 |
1395 1 | description.first_line("foo")
1396 | ^---^
1397 |
1398 = Function "first_line": Expected 0 arguments
1399 "###);
1400
1401 insta::assert_snapshot!(env.parse_err(r#"label()"#), @r###"
1402 --> 1:7
1403 |
1404 1 | label()
1405 | ^
1406 |
1407 = Function "label": Expected 2 arguments
1408 "###);
1409 insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r###"
1410 --> 1:7
1411 |
1412 1 | label("foo", "bar", "baz")
1413 | ^-----------------^
1414 |
1415 = Function "label": Expected 2 arguments
1416 "###);
1417
1418 insta::assert_snapshot!(env.parse_err(r#"if()"#), @r###"
1419 --> 1:4
1420 |
1421 1 | if()
1422 | ^
1423 |
1424 = Function "if": Expected 2 to 3 arguments
1425 "###);
1426 insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r###"
1427 --> 1:4
1428 |
1429 1 | if("foo", "bar", "baz", "quux")
1430 | ^-------------------------^
1431 |
1432 = Function "if": Expected 2 to 3 arguments
1433 "###);
1434
1435 insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r###"
1436 --> 1:4
1437 |
1438 1 | if(label("foo", "bar"), "baz")
1439 | ^-----------------^
1440 |
1441 = Expected expression of type "Boolean", but actual type is "Template"
1442 "###);
1443
1444 insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @r###"
1445 --> 1:1
1446 |
1447 1 | |x| description
1448 | ^-------------^
1449 |
1450 = Lambda cannot be defined here
1451 "###);
1452 }
1453
1454 #[test]
1455 fn test_self_keyword() {
1456 let mut env = TestTemplateEnv::new();
1457 env.add_keyword("say_hello", || L::wrap_string(Literal("Hello".to_owned())));
1458
1459 insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
1460 insta::assert_snapshot!(env.parse_err(r#"self"#), @r###"
1461 --> 1:1
1462 |
1463 1 | self
1464 | ^--^
1465 |
1466 = Expected expression of type "Template", but actual type is "Self"
1467 "###);
1468 }
1469
1470 #[test]
1471 fn test_boolean_cast() {
1472 let mut env = TestTemplateEnv::new();
1473
1474 insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
1475 insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
1476
1477 env.add_keyword("sl0", || {
1478 L::wrap_string_list(Literal::<Vec<String>>(vec![]))
1479 });
1480 env.add_keyword("sl1", || L::wrap_string_list(Literal(vec!["".to_owned()])));
1481 insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
1482 insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
1483
1484 // No implicit cast of integer
1485 insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @r###"
1486 --> 1:4
1487 |
1488 1 | if(0, true, false)
1489 | ^
1490 |
1491 = Expected expression of type "Boolean", but actual type is "Integer"
1492 "###);
1493
1494 insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r###"
1495 --> 1:4
1496 |
1497 1 | if(label("", ""), true, false)
1498 | ^-----------^
1499 |
1500 = Expected expression of type "Boolean", but actual type is "Template"
1501 "###);
1502 insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @r###"
1503 --> 1:4
1504 |
1505 1 | if(sl0.map(|x| x), true, false)
1506 | ^------------^
1507 |
1508 = Expected expression of type "Boolean", but actual type is "ListTemplate"
1509 "###);
1510 }
1511
1512 #[test]
1513 fn test_arithmetic_operation() {
1514 let mut env = TestTemplateEnv::new();
1515 env.add_keyword("i64_min", || L::wrap_integer(Literal(i64::MIN)));
1516
1517 insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
1518 insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
1519 insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
1520
1521 // No panic on integer overflow.
1522 insta::assert_snapshot!(
1523 env.render_ok(r#"-i64_min"#),
1524 @"<Error: Attempt to negate with overflow>");
1525 }
1526
1527 #[test]
1528 fn test_logical_operation() {
1529 let mut env = TestTemplateEnv::new();
1530
1531 insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
1532 insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
1533 insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
1534
1535 insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
1536 insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
1537
1538 // Short-circuiting
1539 env.add_keyword("bad_bool", || L::wrap_boolean(new_error_property("Bad")));
1540 insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
1541 insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
1542 insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
1543 insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
1544 }
1545
1546 #[test]
1547 fn test_list_method() {
1548 let mut env = TestTemplateEnv::new();
1549 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
1550 env.add_keyword("sep", || L::wrap_string(Literal("sep".to_owned())));
1551
1552 insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
1553 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
1554
1555 insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
1556 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
1557 // Null separator
1558 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
1559 // Keyword as separator
1560 insta::assert_snapshot!(
1561 env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
1562 @"aSEPbSEPc");
1563
1564 insta::assert_snapshot!(
1565 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
1566 @"aa bb cc");
1567 // Global keyword in item template
1568 insta::assert_snapshot!(
1569 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
1570 @"atrue btrue ctrue");
1571 // Global keyword in item template shadowing 'self'
1572 insta::assert_snapshot!(
1573 env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
1574 @"atrue btrue ctrue");
1575 // Override global keyword 'empty'
1576 insta::assert_snapshot!(
1577 env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
1578 @"a b c");
1579 // Nested map operations
1580 insta::assert_snapshot!(
1581 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
1582 @"ax ay bx by cx cy");
1583 // Nested map/join operations
1584 insta::assert_snapshot!(
1585 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
1586 @"ax,ay;bx,by;cx,cy");
1587 // Nested string operations
1588 insta::assert_snapshot!(
1589 env.render_ok(r#""!a\n!b\nc\nend".remove_suffix("end").lines().map(|s| s.remove_prefix("!"))"#),
1590 @"a b c");
1591
1592 // Lambda expression in alias
1593 env.add_alias("identity", "|x| x");
1594 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
1595
1596 // Not a lambda expression
1597 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r###"
1598 --> 1:17
1599 |
1600 1 | "a".lines().map(empty)
1601 | ^---^
1602 |
1603 = Expected lambda expression
1604 "###);
1605 // Bad lambda parameter count
1606 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r###"
1607 --> 1:18
1608 |
1609 1 | "a".lines().map(|| "")
1610 | ^
1611 |
1612 = Expected 1 lambda parameters
1613 "###);
1614 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r###"
1615 --> 1:18
1616 |
1617 1 | "a".lines().map(|a, b| "")
1618 | ^--^
1619 |
1620 = Expected 1 lambda parameters
1621 "###);
1622 // Error in lambda expression
1623 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r###"
1624 --> 1:23
1625 |
1626 1 | "a".lines().map(|s| s.unknown())
1627 | ^-----^
1628 |
1629 = Method "unknown" doesn't exist for type "String"
1630 "###);
1631 // Error in lambda alias
1632 env.add_alias("too_many_params", "|x, y| x");
1633 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r###"
1634 --> 1:17
1635 |
1636 1 | "a".lines().map(too_many_params)
1637 | ^-------------^
1638 |
1639 = Alias "too_many_params" cannot be expanded
1640 --> 1:2
1641 |
1642 1 | |x, y| x
1643 | ^--^
1644 |
1645 = Expected 1 lambda parameters
1646 "###);
1647 }
1648
1649 #[test]
1650 fn test_string_method() {
1651 let mut env = TestTemplateEnv::new();
1652 env.add_keyword("description", || {
1653 L::wrap_string(Literal("description 1".to_owned()))
1654 });
1655 env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
1656
1657 insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
1658 insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
1659 insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
1660
1661 insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
1662 insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
1663 insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
1664 insta::assert_snapshot!(
1665 env.render_ok(r#""description 123".contains(description.first_line())"#),
1666 @"true");
1667
1668 // inner template error should propagate
1669 insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
1670 insta::assert_snapshot!(
1671 env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
1672 insta::assert_snapshot!(
1673 env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
1674
1675 insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
1676 insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
1677
1678 insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
1679 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
1680
1681 insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
1682 insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
1683 insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
1684 insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
1685 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
1686 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
1687
1688 insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
1689 insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
1690 insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
1691 insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
1692 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
1693 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
1694
1695 insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
1696 insta::assert_snapshot!(
1697 env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
1698 @"testing");
1699
1700 insta::assert_snapshot!(
1701 env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
1702 @"bar@my.example.com");
1703 insta::assert_snapshot!(
1704 env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
1705 @"bar");
1706
1707 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
1708 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
1709 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
1710 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
1711 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
1712 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
1713 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
1714 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
1715
1716 // non-ascii characters
1717 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
1718 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
1719 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
1720 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
1721 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
1722 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
1723 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
1724 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
1725 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
1726 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
1727 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
1728
1729 // ranges with end > start are empty
1730 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
1731 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
1732 }
1733
1734 #[test]
1735 fn test_signature() {
1736 let mut env = TestTemplateEnv::new();
1737
1738 env.add_keyword("author", || {
1739 L::wrap_signature(Literal(new_signature("Test User", "test.user@example.com")))
1740 });
1741 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
1742 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
1743 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
1744 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
1745
1746 env.add_keyword("author", || {
1747 L::wrap_signature(Literal(new_signature(
1748 "Another Test User",
1749 "test.user@example.com",
1750 )))
1751 });
1752 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
1753 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
1754 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
1755 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
1756
1757 env.add_keyword("author", || {
1758 L::wrap_signature(Literal(new_signature(
1759 "Test User",
1760 "test.user@invalid@example.com",
1761 )))
1762 });
1763 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
1764 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
1765 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
1766 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
1767
1768 env.add_keyword("author", || {
1769 L::wrap_signature(Literal(new_signature("Test User", "test.user")))
1770 });
1771 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
1772 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
1773 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
1774
1775 env.add_keyword("author", || {
1776 L::wrap_signature(Literal(new_signature(
1777 "Test User",
1778 "test.user+tag@example.com",
1779 )))
1780 });
1781 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
1782 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
1783 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user+tag");
1784
1785 env.add_keyword("author", || {
1786 L::wrap_signature(Literal(new_signature("Test User", "x@y")))
1787 });
1788 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
1789 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
1790 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"x");
1791
1792 env.add_keyword("author", || {
1793 L::wrap_signature(Literal(new_signature("", "test.user@example.com")))
1794 });
1795 insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
1796 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
1797 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
1798 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
1799
1800 env.add_keyword("author", || {
1801 L::wrap_signature(Literal(new_signature("Test User", "")))
1802 });
1803 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
1804 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
1805 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
1806 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
1807
1808 env.add_keyword("author", || {
1809 L::wrap_signature(Literal(new_signature("", "")))
1810 });
1811 insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
1812 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
1813 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
1814 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
1815 }
1816
1817 #[test]
1818 fn test_timestamp_method() {
1819 let mut env = TestTemplateEnv::new();
1820 env.add_keyword("t0", || L::wrap_timestamp(Literal(new_timestamp(0, 0))));
1821
1822 insta::assert_snapshot!(
1823 env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
1824 @"19700101 00:00:00");
1825
1826 // Invalid format string
1827 insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r###"
1828 --> 1:11
1829 |
1830 1 | t0.format("%_")
1831 | ^--^
1832 |
1833 = Invalid time format
1834 "###);
1835
1836 // Invalid type
1837 insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @r###"
1838 --> 1:11
1839 |
1840 1 | t0.format(0)
1841 | ^
1842 |
1843 = Expected string literal
1844 "###);
1845
1846 // Dynamic string isn't supported yet
1847 insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r###"
1848 --> 1:11
1849 |
1850 1 | t0.format("%Y" ++ "%m")
1851 | ^----------^
1852 |
1853 = Expected string literal
1854 "###);
1855
1856 // Literal alias expansion
1857 env.add_alias("time_format", r#""%Y-%m-%d""#);
1858 env.add_alias("bad_time_format", r#""%_""#);
1859 insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
1860 insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r###"
1861 --> 1:11
1862 |
1863 1 | t0.format(bad_time_format)
1864 | ^-------------^
1865 |
1866 = Alias "bad_time_format" cannot be expanded
1867 --> 1:1
1868 |
1869 1 | "%_"
1870 | ^--^
1871 |
1872 = Invalid time format
1873 "###);
1874 }
1875
1876 #[test]
1877 fn test_fill_function() {
1878 let mut env = TestTemplateEnv::new();
1879 env.add_color("error", crossterm::style::Color::DarkRed);
1880
1881 insta::assert_snapshot!(
1882 env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
1883 label("error", "lazy") ++ " dog\n")"#),
1884 @r###"
1885 The quick fox jumps
1886 over the [38;5;1mlazy[39m dog
1887 "###);
1888
1889 // A low value will not chop words, but can chop a label by words
1890 insta::assert_snapshot!(
1891 env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
1892 label("error", "longlonglongword and short words") ++
1893 " back out\n")"#),
1894 @r###"
1895 Longlonglongword
1896 an some
1897 short
1898 words
1899 [38;5;1mlonglonglongword[39m
1900 [38;5;1mand short[39m
1901 [38;5;1mwords[39m
1902 back out
1903 "###);
1904
1905 // Filling to 0 means breaking at every word
1906 insta::assert_snapshot!(
1907 env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
1908 label("error", "lazy") ++ " dog\n")"#),
1909 @r###"
1910 The
1911 quick
1912 fox
1913 jumps
1914 over
1915 the
1916 [38;5;1mlazy[39m
1917 dog
1918 "###);
1919
1920 // Filling to -0 is the same as 0
1921 insta::assert_snapshot!(
1922 env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
1923 label("error", "lazy") ++ " dog\n")"#),
1924 @r###"
1925 The
1926 quick
1927 fox
1928 jumps
1929 over
1930 the
1931 [38;5;1mlazy[39m
1932 dog
1933 "###);
1934
1935 // Filling to negative width is an error
1936 insta::assert_snapshot!(
1937 env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
1938 label("error", "lazy") ++ " dog\n")"#),
1939 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
1940
1941 // Word-wrap, then indent
1942 insta::assert_snapshot!(
1943 env.render_ok(r#""START marker to help insta\n" ++
1944 indent(" ", fill(20, "The quick fox jumps over the " ++
1945 label("error", "lazy") ++ " dog\n"))"#),
1946 @r###"
1947 START marker to help insta
1948 The quick fox jumps
1949 over the [38;5;1mlazy[39m dog
1950 "###);
1951
1952 // Word-wrap indented (no special handling for leading spaces)
1953 insta::assert_snapshot!(
1954 env.render_ok(r#""START marker to help insta\n" ++
1955 fill(20, indent(" ", "The quick fox jumps over the " ++
1956 label("error", "lazy") ++ " dog\n"))"#),
1957 @r###"
1958 START marker to help insta
1959 The quick fox
1960 jumps over the [38;5;1mlazy[39m
1961 dog
1962 "###);
1963 }
1964
1965 #[test]
1966 fn test_indent_function() {
1967 let mut env = TestTemplateEnv::new();
1968 env.add_color("error", crossterm::style::Color::DarkRed);
1969 env.add_color("warning", crossterm::style::Color::DarkYellow);
1970 env.add_color("hint", crossterm::style::Color::DarkCyan);
1971
1972 // Empty line shouldn't be indented. Not using insta here because we test
1973 // whitespace existence.
1974 assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
1975 assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
1976 assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
1977
1978 // "\n" at end of labeled text
1979 insta::assert_snapshot!(
1980 env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
1981 @r###"
1982 [38;5;1m__a[39m
1983 [38;5;3m__b[39m
1984 "###);
1985
1986 // "\n" in labeled text
1987 insta::assert_snapshot!(
1988 env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
1989 @r###"
1990 [38;5;1m__a[39m[38;5;3mb[39m
1991 [38;5;3m__c[39m
1992 "###);
1993
1994 // Labeled prefix + unlabeled content
1995 insta::assert_snapshot!(
1996 env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
1997 @r###"
1998 [38;5;1mXX[39ma
1999 [38;5;1mXX[39mb
2000 "###);
2001
2002 // Nested indent, silly but works
2003 insta::assert_snapshot!(
2004 env.render_ok(r#"indent(label("hint", "A"),
2005 label("warning", indent(label("hint", "B"),
2006 label("error", "x\n") ++ "y")))"#),
2007 @r###"
2008 [38;5;6mAB[38;5;1mx[39m
2009 [38;5;6mAB[38;5;3my[39m
2010 "###);
2011 }
2012
2013 #[test]
2014 fn test_label_function() {
2015 let mut env = TestTemplateEnv::new();
2016 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2017 env.add_color("error", crossterm::style::Color::DarkRed);
2018 env.add_color("warning", crossterm::style::Color::DarkYellow);
2019
2020 // Literal
2021 insta::assert_snapshot!(
2022 env.render_ok(r#"label("error", "text")"#),
2023 @"[38;5;1mtext[39m");
2024
2025 // Evaluated property
2026 insta::assert_snapshot!(
2027 env.render_ok(r#"label("error".first_line(), "text")"#),
2028 @"[38;5;1mtext[39m");
2029
2030 // Template
2031 insta::assert_snapshot!(
2032 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
2033 @"[38;5;1mtext[39m");
2034 }
2035
2036 #[test]
2037 fn test_coalesce_function() {
2038 let mut env = TestTemplateEnv::new();
2039 env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
2040 env.add_keyword("empty_string", || L::wrap_string(Literal("".to_owned())));
2041 env.add_keyword("non_empty_string", || {
2042 L::wrap_string(Literal("a".to_owned()))
2043 });
2044
2045 insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
2046 insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
2047 insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
2048 insta::assert_snapshot!(
2049 env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
2050
2051 // "false" is not empty
2052 insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
2053
2054 // Error is not empty
2055 insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
2056 // but can be short-circuited
2057 insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
2058 }
2059
2060 #[test]
2061 fn test_concat_function() {
2062 let mut env = TestTemplateEnv::new();
2063 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2064 env.add_keyword("hidden", || L::wrap_boolean(Literal(false)));
2065 env.add_color("empty", crossterm::style::Color::DarkGreen);
2066 env.add_color("error", crossterm::style::Color::DarkRed);
2067 env.add_color("warning", crossterm::style::Color::DarkYellow);
2068
2069 insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
2070 insta::assert_snapshot!(
2071 env.render_ok(r#"concat(hidden, empty)"#),
2072 @"false[38;5;2mtrue[39m");
2073 insta::assert_snapshot!(
2074 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
2075 @"[38;5;3ma[39mb");
2076 }
2077
2078 #[test]
2079 fn test_separate_function() {
2080 let mut env = TestTemplateEnv::new();
2081 env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
2082 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2083 env.add_keyword("hidden", || L::wrap_boolean(Literal(false)));
2084 env.add_color("empty", crossterm::style::Color::DarkGreen);
2085 env.add_color("error", crossterm::style::Color::DarkRed);
2086 env.add_color("warning", crossterm::style::Color::DarkYellow);
2087
2088 insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
2089 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
2090 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
2091 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
2092 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
2093 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
2094 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
2095
2096 // Labeled
2097 insta::assert_snapshot!(
2098 env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
2099 @"[38;5;3ma[39m b");
2100
2101 // List template
2102 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
2103 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
2104
2105 // Nested separate
2106 insta::assert_snapshot!(
2107 env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
2108 insta::assert_snapshot!(
2109 env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
2110 insta::assert_snapshot!(
2111 env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
2112
2113 // Conditional template
2114 insta::assert_snapshot!(
2115 env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
2116 insta::assert_snapshot!(
2117 env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
2118 insta::assert_snapshot!(
2119 env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
2120 insta::assert_snapshot!(
2121 env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
2122
2123 // Separate keywords
2124 insta::assert_snapshot!(
2125 env.render_ok(r#"separate(" ", hidden, description, empty)"#),
2126 @"false [38;5;2mtrue[39m");
2127
2128 // Keyword as separator
2129 insta::assert_snapshot!(
2130 env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
2131 @"XfalseYfalseZ");
2132 }
2133
2134 #[test]
2135 fn test_surround_function() {
2136 let mut env = TestTemplateEnv::new();
2137 env.add_keyword("lt", || L::wrap_string(Literal("<".to_owned())));
2138 env.add_keyword("gt", || L::wrap_string(Literal(">".to_owned())));
2139 env.add_keyword("content", || L::wrap_string(Literal("content".to_owned())));
2140 env.add_keyword("empty_content", || L::wrap_string(Literal("".to_owned())));
2141 env.add_color("error", crossterm::style::Color::DarkRed);
2142 env.add_color("paren", crossterm::style::Color::Cyan);
2143
2144 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
2145 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
2146
2147 // Labeled
2148 insta::assert_snapshot!(
2149 env.render_ok(
2150 r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
2151 @"[38;5;14m([39m[38;5;1ma[39m[38;5;14m)[39m");
2152
2153 // Keyword
2154 insta::assert_snapshot!(
2155 env.render_ok(r#"surround(lt, gt, content)"#),
2156 @"<content>");
2157 insta::assert_snapshot!(
2158 env.render_ok(r#"surround(lt, gt, empty_content)"#),
2159 @"");
2160
2161 // Conditional template as content
2162 insta::assert_snapshot!(
2163 env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
2164 @"<empty>");
2165 insta::assert_snapshot!(
2166 env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
2167 @"");
2168 }
2169}