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;
16use std::io;
17
18use itertools::Itertools as _;
19use jj_lib::backend::Signature;
20use jj_lib::backend::Timestamp;
21use jj_lib::dsl_util::AliasExpandError as _;
22use jj_lib::time_util::DatePattern;
23
24use crate::formatter::FormatRecorder;
25use crate::formatter::Formatter;
26use crate::template_parser;
27use crate::template_parser::BinaryOp;
28use crate::template_parser::ExpressionKind;
29use crate::template_parser::ExpressionNode;
30use crate::template_parser::FunctionCallNode;
31use crate::template_parser::TemplateAliasesMap;
32use crate::template_parser::TemplateDiagnostics;
33use crate::template_parser::TemplateParseError;
34use crate::template_parser::TemplateParseErrorKind;
35use crate::template_parser::TemplateParseResult;
36use crate::template_parser::UnaryOp;
37use crate::templater::CoalesceTemplate;
38use crate::templater::ConcatTemplate;
39use crate::templater::ConditionalTemplate;
40use crate::templater::LabelTemplate;
41use crate::templater::ListPropertyTemplate;
42use crate::templater::ListTemplate;
43use crate::templater::Literal;
44use crate::templater::PlainTextFormattedProperty;
45use crate::templater::PropertyPlaceholder;
46use crate::templater::RawEscapeSequenceTemplate;
47use crate::templater::ReformatTemplate;
48use crate::templater::SeparateTemplate;
49use crate::templater::SizeHint;
50use crate::templater::Template;
51use crate::templater::TemplateProperty;
52use crate::templater::TemplatePropertyError;
53use crate::templater::TemplatePropertyExt as _;
54use crate::templater::TemplateRenderer;
55use crate::templater::TimestampRange;
56use crate::text_util;
57use crate::time_util;
58
59/// Callbacks to build language-specific evaluation objects from AST nodes.
60pub trait TemplateLanguage<'a> {
61 type Property: IntoTemplateProperty<'a>;
62
63 fn wrap_string(property: impl TemplateProperty<Output = String> + 'a) -> Self::Property;
64 fn wrap_string_list(
65 property: impl TemplateProperty<Output = Vec<String>> + 'a,
66 ) -> Self::Property;
67 fn wrap_boolean(property: impl TemplateProperty<Output = bool> + 'a) -> Self::Property;
68 fn wrap_integer(property: impl TemplateProperty<Output = i64> + 'a) -> Self::Property;
69 fn wrap_integer_opt(
70 property: impl TemplateProperty<Output = Option<i64>> + 'a,
71 ) -> Self::Property;
72 fn wrap_signature(property: impl TemplateProperty<Output = Signature> + 'a) -> Self::Property;
73 fn wrap_size_hint(property: impl TemplateProperty<Output = SizeHint> + 'a) -> Self::Property;
74 fn wrap_timestamp(property: impl TemplateProperty<Output = Timestamp> + 'a) -> Self::Property;
75 fn wrap_timestamp_range(
76 property: impl TemplateProperty<Output = TimestampRange> + 'a,
77 ) -> Self::Property;
78
79 fn wrap_template(template: Box<dyn Template + 'a>) -> Self::Property;
80 fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> Self::Property;
81
82 /// Translates the given global `function` call to a property.
83 ///
84 /// This should be delegated to
85 /// `CoreTemplateBuildFnTable::build_function()`.
86 fn build_function(
87 &self,
88 diagnostics: &mut TemplateDiagnostics,
89 build_ctx: &BuildContext<Self::Property>,
90 function: &FunctionCallNode,
91 ) -> TemplateParseResult<Self::Property>;
92
93 fn build_method(
94 &self,
95 diagnostics: &mut TemplateDiagnostics,
96 build_ctx: &BuildContext<Self::Property>,
97 property: Self::Property,
98 function: &FunctionCallNode,
99 ) -> TemplateParseResult<Self::Property>;
100}
101
102/// Implements `TemplateLanguage::wrap_<type>()` functions.
103///
104/// - `impl_core_wrap_property_fns('a)` for `CoreTemplatePropertyKind`,
105/// - `impl_core_wrap_property_fns('a, MyKind::Core)` for `MyKind::Core(..)`.
106macro_rules! impl_core_wrap_property_fns {
107 ($a:lifetime) => {
108 $crate::template_builder::impl_core_wrap_property_fns!($a, std::convert::identity);
109 };
110 ($a:lifetime, $outer:path) => {
111 $crate::template_builder::impl_wrap_property_fns!(
112 $a, $crate::template_builder::CoreTemplatePropertyKind, $outer, {
113 wrap_string(String) => String,
114 wrap_string_list(Vec<String>) => StringList,
115 wrap_boolean(bool) => Boolean,
116 wrap_integer(i64) => Integer,
117 wrap_integer_opt(Option<i64>) => IntegerOpt,
118 wrap_signature(jj_lib::backend::Signature) => Signature,
119 wrap_size_hint($crate::templater::SizeHint) => SizeHint,
120 wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp,
121 wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange,
122 }
123 );
124 fn wrap_template(
125 template: Box<dyn $crate::templater::Template + $a>,
126 ) -> Self::Property {
127 use $crate::template_builder::CoreTemplatePropertyKind as Kind;
128 $outer(Kind::Template(template))
129 }
130 fn wrap_list_template(
131 template: Box<dyn $crate::templater::ListTemplate + $a>,
132 ) -> Self::Property {
133 use $crate::template_builder::CoreTemplatePropertyKind as Kind;
134 $outer(Kind::ListTemplate(template))
135 }
136 };
137}
138
139macro_rules! impl_wrap_property_fns {
140 ($a:lifetime, $kind:path, $outer:path, { $( $func:ident($ty:ty) => $var:ident, )+ }) => {
141 $(
142 fn $func(
143 property: impl $crate::templater::TemplateProperty<Output = $ty> + $a,
144 ) -> Self::Property {
145 use $kind as Kind; // https://github.com/rust-lang/rust/issues/48067
146 $outer(Kind::$var(Box::new(property)))
147 }
148 )+
149 };
150}
151
152pub(crate) use impl_core_wrap_property_fns;
153pub(crate) use impl_wrap_property_fns;
154
155/// Provides access to basic template property types.
156pub trait IntoTemplateProperty<'a> {
157 /// Type name of the property output.
158 fn type_name(&self) -> &'static str;
159
160 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>>;
161 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>>;
162
163 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>>;
164 fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
165
166 /// Transforms into a property that will evaluate to `self == other`.
167 fn try_into_eq(self, other: Self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>>;
168}
169
170pub enum CoreTemplatePropertyKind<'a> {
171 String(Box<dyn TemplateProperty<Output = String> + 'a>),
172 StringList(Box<dyn TemplateProperty<Output = Vec<String>> + 'a>),
173 Boolean(Box<dyn TemplateProperty<Output = bool> + 'a>),
174 Integer(Box<dyn TemplateProperty<Output = i64> + 'a>),
175 IntegerOpt(Box<dyn TemplateProperty<Output = Option<i64>> + 'a>),
176 Signature(Box<dyn TemplateProperty<Output = Signature> + 'a>),
177 SizeHint(Box<dyn TemplateProperty<Output = SizeHint> + 'a>),
178 Timestamp(Box<dyn TemplateProperty<Output = Timestamp> + 'a>),
179 TimestampRange(Box<dyn TemplateProperty<Output = TimestampRange> + 'a>),
180
181 // Both TemplateProperty and Template can represent a value to be evaluated
182 // dynamically, which suggests that `Box<dyn Template + 'a>` could be
183 // composed as `Box<dyn TemplateProperty<Output = Box<dyn Template ..`.
184 // However, there's a subtle difference: TemplateProperty is strict on
185 // error, whereas Template is usually lax and prints an error inline. If
186 // `concat(x, y)` were a property returning Template, and if `y` failed to
187 // evaluate, the whole expression would fail. In this example, a partial
188 // evaluation output is more useful. That's one reason why Template isn't
189 // wrapped in a TemplateProperty. Another reason is that the outermost
190 // caller expects a Template, not a TemplateProperty of Template output.
191 Template(Box<dyn Template + 'a>),
192 ListTemplate(Box<dyn ListTemplate + 'a>),
193}
194
195impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
196 fn type_name(&self) -> &'static str {
197 match self {
198 CoreTemplatePropertyKind::String(_) => "String",
199 CoreTemplatePropertyKind::StringList(_) => "List<String>",
200 CoreTemplatePropertyKind::Boolean(_) => "Boolean",
201 CoreTemplatePropertyKind::Integer(_) => "Integer",
202 CoreTemplatePropertyKind::IntegerOpt(_) => "Option<Integer>",
203 CoreTemplatePropertyKind::Signature(_) => "Signature",
204 CoreTemplatePropertyKind::SizeHint(_) => "SizeHint",
205 CoreTemplatePropertyKind::Timestamp(_) => "Timestamp",
206 CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange",
207 CoreTemplatePropertyKind::Template(_) => "Template",
208 CoreTemplatePropertyKind::ListTemplate(_) => "ListTemplate",
209 }
210 }
211
212 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
213 match self {
214 CoreTemplatePropertyKind::String(property) => {
215 Some(Box::new(property.map(|s| !s.is_empty())))
216 }
217 CoreTemplatePropertyKind::StringList(property) => {
218 Some(Box::new(property.map(|l| !l.is_empty())))
219 }
220 CoreTemplatePropertyKind::Boolean(property) => Some(property),
221 CoreTemplatePropertyKind::Integer(_) => None,
222 CoreTemplatePropertyKind::IntegerOpt(property) => {
223 Some(Box::new(property.map(|opt| opt.is_some())))
224 }
225 CoreTemplatePropertyKind::Signature(_) => None,
226 CoreTemplatePropertyKind::SizeHint(_) => None,
227 CoreTemplatePropertyKind::Timestamp(_) => None,
228 CoreTemplatePropertyKind::TimestampRange(_) => None,
229 // Template types could also be evaluated to boolean, but it's less likely
230 // to apply label() or .map() and use the result as conditional. It's also
231 // unclear whether ListTemplate should behave as a "list" or a "template".
232 CoreTemplatePropertyKind::Template(_) => None,
233 CoreTemplatePropertyKind::ListTemplate(_) => None,
234 }
235 }
236
237 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
238 match self {
239 CoreTemplatePropertyKind::Integer(property) => Some(property),
240 CoreTemplatePropertyKind::IntegerOpt(property) => {
241 Some(Box::new(property.try_unwrap("Integer")))
242 }
243 _ => None,
244 }
245 }
246
247 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>> {
248 match self {
249 CoreTemplatePropertyKind::String(property) => Some(property),
250 _ => {
251 let template = self.try_into_template()?;
252 Some(Box::new(PlainTextFormattedProperty::new(template)))
253 }
254 }
255 }
256
257 fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
258 match self {
259 CoreTemplatePropertyKind::String(property) => Some(property.into_template()),
260 CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()),
261 CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()),
262 CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()),
263 CoreTemplatePropertyKind::IntegerOpt(property) => Some(property.into_template()),
264 CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()),
265 CoreTemplatePropertyKind::SizeHint(_) => None,
266 CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()),
267 CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()),
268 CoreTemplatePropertyKind::Template(template) => Some(template),
269 CoreTemplatePropertyKind::ListTemplate(template) => Some(template.into_template()),
270 }
271 }
272
273 fn try_into_eq(self, other: Self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
274 match (self, other) {
275 (CoreTemplatePropertyKind::String(lhs), CoreTemplatePropertyKind::String(rhs)) => {
276 Some(Box::new((lhs, rhs).map(|(l, r)| l == r)))
277 }
278 (CoreTemplatePropertyKind::Boolean(lhs), CoreTemplatePropertyKind::Boolean(rhs)) => {
279 Some(Box::new((lhs, rhs).map(|(l, r)| l == r)))
280 }
281 (CoreTemplatePropertyKind::Integer(lhs), CoreTemplatePropertyKind::Integer(rhs)) => {
282 Some(Box::new((lhs, rhs).map(|(l, r)| l == r)))
283 }
284 (CoreTemplatePropertyKind::String(_), _) => None,
285 (CoreTemplatePropertyKind::StringList(_), _) => None,
286 (CoreTemplatePropertyKind::Boolean(_), _) => None,
287 (CoreTemplatePropertyKind::Integer(_), _) => None,
288 (CoreTemplatePropertyKind::IntegerOpt(_), _) => None,
289 (CoreTemplatePropertyKind::Signature(_), _) => None,
290 (CoreTemplatePropertyKind::SizeHint(_), _) => None,
291 (CoreTemplatePropertyKind::Timestamp(_), _) => None,
292 (CoreTemplatePropertyKind::TimestampRange(_), _) => None,
293 (CoreTemplatePropertyKind::Template(_), _) => None,
294 (CoreTemplatePropertyKind::ListTemplate(_), _) => None,
295 }
296 }
297}
298
299/// Function that translates global function call node.
300// The lifetime parameter 'a could be replaced with for<'a> to keep the method
301// table away from a certain lifetime. That's technically more correct, but I
302// couldn't find an easy way to expand that to the core template methods, which
303// are defined for L: TemplateLanguage<'a>. That's why the build fn table is
304// bound to a named lifetime, and therefore can't be cached statically.
305pub type TemplateBuildFunctionFn<'a, L> =
306 fn(
307 &L,
308 &mut TemplateDiagnostics,
309 &BuildContext<<L as TemplateLanguage<'a>>::Property>,
310 &FunctionCallNode,
311 ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
312
313/// Function that translates method call node of self type `T`.
314pub type TemplateBuildMethodFn<'a, L, T> =
315 fn(
316 &L,
317 &mut TemplateDiagnostics,
318 &BuildContext<<L as TemplateLanguage<'a>>::Property>,
319 Box<dyn TemplateProperty<Output = T> + 'a>,
320 &FunctionCallNode,
321 ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
322
323/// Table of functions that translate global function call node.
324pub type TemplateBuildFunctionFnMap<'a, L> = HashMap<&'static str, TemplateBuildFunctionFn<'a, L>>;
325
326/// Table of functions that translate method call node of self type `T`.
327pub type TemplateBuildMethodFnMap<'a, L, T> =
328 HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>;
329
330/// Symbol table of functions and methods available in the core template.
331pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> {
332 pub functions: TemplateBuildFunctionFnMap<'a, L>,
333 pub string_methods: TemplateBuildMethodFnMap<'a, L, String>,
334 pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>,
335 pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>,
336 pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>,
337 pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint>,
338 pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>,
339 pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>,
340}
341
342pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
343 for (name, function) in extension {
344 if base.insert(name, function).is_some() {
345 panic!("Conflicting template definitions for '{name}' function");
346 }
347 }
348}
349
350impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
351 /// Creates new symbol table containing the builtin functions and methods.
352 pub fn builtin() -> Self {
353 CoreTemplateBuildFnTable {
354 functions: builtin_functions(),
355 string_methods: builtin_string_methods(),
356 boolean_methods: HashMap::new(),
357 integer_methods: HashMap::new(),
358 signature_methods: builtin_signature_methods(),
359 size_hint_methods: builtin_size_hint_methods(),
360 timestamp_methods: builtin_timestamp_methods(),
361 timestamp_range_methods: builtin_timestamp_range_methods(),
362 }
363 }
364
365 pub fn empty() -> Self {
366 CoreTemplateBuildFnTable {
367 functions: HashMap::new(),
368 string_methods: HashMap::new(),
369 boolean_methods: HashMap::new(),
370 integer_methods: HashMap::new(),
371 signature_methods: HashMap::new(),
372 size_hint_methods: HashMap::new(),
373 timestamp_methods: HashMap::new(),
374 timestamp_range_methods: HashMap::new(),
375 }
376 }
377
378 pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) {
379 let CoreTemplateBuildFnTable {
380 functions,
381 string_methods,
382 boolean_methods,
383 integer_methods,
384 signature_methods,
385 size_hint_methods,
386 timestamp_methods,
387 timestamp_range_methods,
388 } = extension;
389
390 merge_fn_map(&mut self.functions, functions);
391 merge_fn_map(&mut self.string_methods, string_methods);
392 merge_fn_map(&mut self.boolean_methods, boolean_methods);
393 merge_fn_map(&mut self.integer_methods, integer_methods);
394 merge_fn_map(&mut self.signature_methods, signature_methods);
395 merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
396 merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
397 merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
398 }
399
400 /// Translates the function call node `function` by using this symbol table.
401 pub fn build_function(
402 &self,
403 language: &L,
404 diagnostics: &mut TemplateDiagnostics,
405 build_ctx: &BuildContext<L::Property>,
406 function: &FunctionCallNode,
407 ) -> TemplateParseResult<L::Property> {
408 let table = &self.functions;
409 let build = template_parser::lookup_function(table, function)?;
410 build(language, diagnostics, build_ctx, function)
411 }
412
413 /// Applies the method call node `function` to the given `property` by using
414 /// this symbol table.
415 pub fn build_method(
416 &self,
417 language: &L,
418 diagnostics: &mut TemplateDiagnostics,
419 build_ctx: &BuildContext<L::Property>,
420 property: CoreTemplatePropertyKind<'a>,
421 function: &FunctionCallNode,
422 ) -> TemplateParseResult<L::Property> {
423 let type_name = property.type_name();
424 match property {
425 CoreTemplatePropertyKind::String(property) => {
426 let table = &self.string_methods;
427 let build = template_parser::lookup_method(type_name, table, function)?;
428 build(language, diagnostics, build_ctx, property, function)
429 }
430 CoreTemplatePropertyKind::StringList(property) => {
431 // TODO: migrate to table?
432 build_formattable_list_method(
433 language,
434 diagnostics,
435 build_ctx,
436 property,
437 function,
438 L::wrap_string,
439 )
440 }
441 CoreTemplatePropertyKind::Boolean(property) => {
442 let table = &self.boolean_methods;
443 let build = template_parser::lookup_method(type_name, table, function)?;
444 build(language, diagnostics, build_ctx, property, function)
445 }
446 CoreTemplatePropertyKind::Integer(property) => {
447 let table = &self.integer_methods;
448 let build = template_parser::lookup_method(type_name, table, function)?;
449 build(language, diagnostics, build_ctx, property, function)
450 }
451 CoreTemplatePropertyKind::IntegerOpt(property) => {
452 let type_name = "Integer";
453 let table = &self.integer_methods;
454 let build = template_parser::lookup_method(type_name, table, function)?;
455 let inner_property = property.try_unwrap(type_name);
456 build(
457 language,
458 diagnostics,
459 build_ctx,
460 Box::new(inner_property),
461 function,
462 )
463 }
464 CoreTemplatePropertyKind::Signature(property) => {
465 let table = &self.signature_methods;
466 let build = template_parser::lookup_method(type_name, table, function)?;
467 build(language, diagnostics, build_ctx, property, function)
468 }
469 CoreTemplatePropertyKind::SizeHint(property) => {
470 let table = &self.size_hint_methods;
471 let build = template_parser::lookup_method(type_name, table, function)?;
472 build(language, diagnostics, build_ctx, property, function)
473 }
474 CoreTemplatePropertyKind::Timestamp(property) => {
475 let table = &self.timestamp_methods;
476 let build = template_parser::lookup_method(type_name, table, function)?;
477 build(language, diagnostics, build_ctx, property, function)
478 }
479 CoreTemplatePropertyKind::TimestampRange(property) => {
480 let table = &self.timestamp_range_methods;
481 let build = template_parser::lookup_method(type_name, table, function)?;
482 build(language, diagnostics, build_ctx, property, function)
483 }
484 CoreTemplatePropertyKind::Template(_) => {
485 // TODO: migrate to table?
486 Err(TemplateParseError::no_such_method(type_name, function))
487 }
488 CoreTemplatePropertyKind::ListTemplate(template) => {
489 // TODO: migrate to table?
490 build_list_template_method(language, diagnostics, build_ctx, template, function)
491 }
492 }
493 }
494}
495
496/// Opaque struct that represents a template value.
497pub struct Expression<P> {
498 property: P,
499 labels: Vec<String>,
500}
501
502impl<P> Expression<P> {
503 fn unlabeled(property: P) -> Self {
504 let labels = vec![];
505 Expression { property, labels }
506 }
507
508 fn with_label(property: P, label: impl Into<String>) -> Self {
509 let labels = vec![label.into()];
510 Expression { property, labels }
511 }
512}
513
514impl<'a, P: IntoTemplateProperty<'a>> Expression<P> {
515 pub fn type_name(&self) -> &'static str {
516 self.property.type_name()
517 }
518
519 pub fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
520 self.property.try_into_boolean()
521 }
522
523 pub fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
524 self.property.try_into_integer()
525 }
526
527 pub fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>> {
528 self.property.try_into_plain_text()
529 }
530
531 pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
532 let template = self.property.try_into_template()?;
533 if self.labels.is_empty() {
534 Some(template)
535 } else {
536 Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
537 }
538 }
539
540 pub fn try_into_eq(self, other: Self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
541 self.property.try_into_eq(other.property)
542 }
543}
544
545pub struct BuildContext<'i, P> {
546 /// Map of functions to create `L::Property`.
547 local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>,
548 /// Function to create `L::Property` representing `self`.
549 ///
550 /// This could be `local_variables["self"]`, but keyword lookup shouldn't be
551 /// overridden by a user-defined `self` variable.
552 self_variable: &'i (dyn Fn() -> P),
553}
554
555fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
556 language: &L,
557 diagnostics: &mut TemplateDiagnostics,
558 build_ctx: &BuildContext<L::Property>,
559 name: &str,
560 name_span: pest::Span<'_>,
561) -> TemplateParseResult<L::Property> {
562 // Keyword is a 0-ary method on the "self" property
563 let self_property = (build_ctx.self_variable)();
564 let function = FunctionCallNode {
565 name,
566 name_span,
567 args: vec![],
568 keyword_args: vec![],
569 args_span: name_span.end_pos().span(&name_span.end_pos()),
570 };
571 language
572 .build_method(diagnostics, build_ctx, self_property, &function)
573 .map_err(|err| match err.kind() {
574 TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
575 let kind = TemplateParseErrorKind::NoSuchKeyword {
576 name: name.to_owned(),
577 // TODO: filter methods by arity?
578 candidates: candidates.clone(),
579 };
580 TemplateParseError::with_span(kind, name_span)
581 }
582 // Since keyword is a 0-ary method, any argument errors mean there's
583 // no such keyword.
584 TemplateParseErrorKind::InvalidArguments { .. } => {
585 let kind = TemplateParseErrorKind::NoSuchKeyword {
586 name: name.to_owned(),
587 // TODO: might be better to phrase the error differently
588 candidates: vec![format!("self.{name}(..)")],
589 };
590 TemplateParseError::with_span(kind, name_span)
591 }
592 // The keyword function may fail with the other reasons.
593 _ => err,
594 })
595}
596
597fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
598 language: &L,
599 diagnostics: &mut TemplateDiagnostics,
600 build_ctx: &BuildContext<L::Property>,
601 op: UnaryOp,
602 arg_node: &ExpressionNode,
603) -> TemplateParseResult<L::Property> {
604 match op {
605 UnaryOp::LogicalNot => {
606 let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
607 Ok(L::wrap_boolean(arg.map(|v| !v)))
608 }
609 UnaryOp::Negate => {
610 let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
611 Ok(L::wrap_integer(arg.and_then(|v| {
612 v.checked_neg()
613 .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
614 })))
615 }
616 }
617}
618
619fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
620 language: &L,
621 diagnostics: &mut TemplateDiagnostics,
622 build_ctx: &BuildContext<L::Property>,
623 op: BinaryOp,
624 lhs_node: &ExpressionNode,
625 rhs_node: &ExpressionNode,
626 span: pest::Span<'_>,
627) -> TemplateParseResult<L::Property> {
628 match op {
629 BinaryOp::LogicalOr => {
630 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
631 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
632 let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
633 Ok(L::wrap_boolean(out))
634 }
635 BinaryOp::LogicalAnd => {
636 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
637 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
638 let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
639 Ok(L::wrap_boolean(out))
640 }
641 BinaryOp::LogicalEq | BinaryOp::LogicalNe => {
642 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
643 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
644 let lty = lhs.type_name();
645 let rty = rhs.type_name();
646 let out = lhs.try_into_eq(rhs).ok_or_else(|| {
647 let message = format!(r#"Cannot compare expressions of type "{lty}" and "{rty}""#);
648 TemplateParseError::expression(message, span)
649 })?;
650 match op {
651 BinaryOp::LogicalEq => Ok(L::wrap_boolean(out)),
652 BinaryOp::LogicalNe => Ok(L::wrap_boolean(out.map(|eq| !eq))),
653 _ => unreachable!(),
654 }
655 }
656 }
657}
658
659fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
660) -> TemplateBuildMethodFnMap<'a, L, String> {
661 // Not using maplit::hashmap!{} or custom declarative macro here because
662 // code completion inside macro is quite restricted.
663 let mut map = TemplateBuildMethodFnMap::<L, String>::new();
664 map.insert(
665 "len",
666 |_language, _diagnostics, _build_ctx, self_property, function| {
667 function.expect_no_arguments()?;
668 let out_property = self_property.and_then(|s| Ok(s.len().try_into()?));
669 Ok(L::wrap_integer(out_property))
670 },
671 );
672 map.insert(
673 "contains",
674 |language, diagnostics, build_ctx, self_property, function| {
675 let [needle_node] = function.expect_exact_arguments()?;
676 // TODO: or .try_into_string() to disable implicit type cast?
677 let needle_property =
678 expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
679 let out_property = (self_property, needle_property)
680 .map(|(haystack, needle)| haystack.contains(&needle));
681 Ok(L::wrap_boolean(out_property))
682 },
683 );
684 map.insert(
685 "starts_with",
686 |language, diagnostics, build_ctx, self_property, function| {
687 let [needle_node] = function.expect_exact_arguments()?;
688 let needle_property =
689 expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
690 let out_property = (self_property, needle_property)
691 .map(|(haystack, needle)| haystack.starts_with(&needle));
692 Ok(L::wrap_boolean(out_property))
693 },
694 );
695 map.insert(
696 "ends_with",
697 |language, diagnostics, build_ctx, self_property, function| {
698 let [needle_node] = function.expect_exact_arguments()?;
699 let needle_property =
700 expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
701 let out_property = (self_property, needle_property)
702 .map(|(haystack, needle)| haystack.ends_with(&needle));
703 Ok(L::wrap_boolean(out_property))
704 },
705 );
706 map.insert(
707 "remove_prefix",
708 |language, diagnostics, build_ctx, self_property, function| {
709 let [needle_node] = function.expect_exact_arguments()?;
710 let needle_property =
711 expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
712 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
713 haystack
714 .strip_prefix(&needle)
715 .map(ToOwned::to_owned)
716 .unwrap_or(haystack)
717 });
718 Ok(L::wrap_string(out_property))
719 },
720 );
721 map.insert(
722 "remove_suffix",
723 |language, diagnostics, build_ctx, self_property, function| {
724 let [needle_node] = function.expect_exact_arguments()?;
725 let needle_property =
726 expect_plain_text_expression(language, diagnostics, build_ctx, needle_node)?;
727 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
728 haystack
729 .strip_suffix(&needle)
730 .map(ToOwned::to_owned)
731 .unwrap_or(haystack)
732 });
733 Ok(L::wrap_string(out_property))
734 },
735 );
736 map.insert(
737 "substr",
738 |language, diagnostics, build_ctx, self_property, function| {
739 let [start_idx, end_idx] = function.expect_exact_arguments()?;
740 let start_idx_property =
741 expect_isize_expression(language, diagnostics, build_ctx, start_idx)?;
742 let end_idx_property =
743 expect_isize_expression(language, diagnostics, build_ctx, end_idx)?;
744 let out_property = (self_property, start_idx_property, end_idx_property).map(
745 |(s, start_idx, end_idx)| {
746 let start_idx = string_index_to_char_boundary(&s, start_idx);
747 let end_idx = string_index_to_char_boundary(&s, end_idx);
748 s.get(start_idx..end_idx).unwrap_or_default().to_owned()
749 },
750 );
751 Ok(L::wrap_string(out_property))
752 },
753 );
754 map.insert(
755 "first_line",
756 |_language, _diagnostics, _build_ctx, self_property, function| {
757 function.expect_no_arguments()?;
758 let out_property =
759 self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
760 Ok(L::wrap_string(out_property))
761 },
762 );
763 map.insert(
764 "lines",
765 |_language, _diagnostics, _build_ctx, self_property, function| {
766 function.expect_no_arguments()?;
767 let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect());
768 Ok(L::wrap_string_list(out_property))
769 },
770 );
771 map.insert(
772 "upper",
773 |_language, _diagnostics, _build_ctx, self_property, function| {
774 function.expect_no_arguments()?;
775 let out_property = self_property.map(|s| s.to_uppercase());
776 Ok(L::wrap_string(out_property))
777 },
778 );
779 map.insert(
780 "lower",
781 |_language, _diagnostics, _build_ctx, self_property, function| {
782 function.expect_no_arguments()?;
783 let out_property = self_property.map(|s| s.to_lowercase());
784 Ok(L::wrap_string(out_property))
785 },
786 );
787 map
788}
789
790/// Clamps and aligns the given index `i` to char boundary.
791///
792/// Negative index counts from the end. If the index isn't at a char boundary,
793/// it will be rounded towards 0 (left or right depending on the sign.)
794fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
795 // TODO: use floor/ceil_char_boundary() if get stabilized
796 let magnitude = i.unsigned_abs();
797 if i < 0 {
798 let p = s.len().saturating_sub(magnitude);
799 (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
800 } else {
801 let p = magnitude.min(s.len());
802 (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
803 }
804}
805
806fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
807) -> TemplateBuildMethodFnMap<'a, L, Signature> {
808 // Not using maplit::hashmap!{} or custom declarative macro here because
809 // code completion inside macro is quite restricted.
810 let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
811 map.insert(
812 "name",
813 |_language, _diagnostics, _build_ctx, self_property, function| {
814 function.expect_no_arguments()?;
815 let out_property = self_property.map(|signature| signature.name);
816 Ok(L::wrap_string(out_property))
817 },
818 );
819 map.insert(
820 "email",
821 |_language, _diagnostics, _build_ctx, self_property, function| {
822 function.expect_no_arguments()?;
823 let out_property = self_property.map(|signature| signature.email);
824 Ok(L::wrap_string(out_property))
825 },
826 );
827 map.insert(
828 "username",
829 |_language, _diagnostics, _build_ctx, self_property, function| {
830 function.expect_no_arguments()?;
831 let out_property = self_property.map(|signature| {
832 let (username, _) = text_util::split_email(&signature.email);
833 username.to_owned()
834 });
835 Ok(L::wrap_string(out_property))
836 },
837 );
838 map.insert(
839 "timestamp",
840 |_language, _diagnostics, _build_ctx, self_property, function| {
841 function.expect_no_arguments()?;
842 let out_property = self_property.map(|signature| signature.timestamp);
843 Ok(L::wrap_timestamp(out_property))
844 },
845 );
846 map
847}
848
849fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
850) -> TemplateBuildMethodFnMap<'a, L, SizeHint> {
851 // Not using maplit::hashmap!{} or custom declarative macro here because
852 // code completion inside macro is quite restricted.
853 let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
854 map.insert(
855 "lower",
856 |_language, _diagnostics, _build_ctx, self_property, function| {
857 function.expect_no_arguments()?;
858 let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
859 Ok(L::wrap_integer(out_property))
860 },
861 );
862 map.insert(
863 "upper",
864 |_language, _diagnostics, _build_ctx, self_property, function| {
865 function.expect_no_arguments()?;
866 let out_property =
867 self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
868 Ok(L::wrap_integer_opt(out_property))
869 },
870 );
871 map.insert(
872 "exact",
873 |_language, _diagnostics, _build_ctx, self_property, function| {
874 function.expect_no_arguments()?;
875 let out_property = self_property.and_then(|(lower, upper)| {
876 let exact = (Some(lower) == upper).then_some(lower);
877 Ok(exact.map(i64::try_from).transpose()?)
878 });
879 Ok(L::wrap_integer_opt(out_property))
880 },
881 );
882 map.insert(
883 "zero",
884 |_language, _diagnostics, _build_ctx, self_property, function| {
885 function.expect_no_arguments()?;
886 let out_property = self_property.map(|(_, upper)| upper == Some(0));
887 Ok(L::wrap_boolean(out_property))
888 },
889 );
890 map
891}
892
893fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
894) -> TemplateBuildMethodFnMap<'a, L, Timestamp> {
895 // Not using maplit::hashmap!{} or custom declarative macro here because
896 // code completion inside macro is quite restricted.
897 let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
898 map.insert(
899 "ago",
900 |_language, _diagnostics, _build_ctx, self_property, function| {
901 function.expect_no_arguments()?;
902 let now = Timestamp::now();
903 let format = timeago::Formatter::new();
904 let out_property = self_property.and_then(move |timestamp| {
905 Ok(time_util::format_duration(×tamp, &now, &format)?)
906 });
907 Ok(L::wrap_string(out_property))
908 },
909 );
910 map.insert(
911 "format",
912 |_language, _diagnostics, _build_ctx, self_property, function| {
913 // No dynamic string is allowed as the templater has no runtime error type.
914 let [format_node] = function.expect_exact_arguments()?;
915 let format =
916 template_parser::expect_string_literal_with(format_node, |format, span| {
917 time_util::FormattingItems::parse(format)
918 .ok_or_else(|| TemplateParseError::expression("Invalid time format", span))
919 })?
920 .into_owned();
921 let out_property = self_property.and_then(move |timestamp| {
922 Ok(time_util::format_absolute_timestamp_with(
923 ×tamp, &format,
924 )?)
925 });
926 Ok(L::wrap_string(out_property))
927 },
928 );
929 map.insert(
930 "utc",
931 |_language, _diagnostics, _build_ctx, self_property, function| {
932 function.expect_no_arguments()?;
933 let out_property = self_property.map(|mut timestamp| {
934 timestamp.tz_offset = 0;
935 timestamp
936 });
937 Ok(L::wrap_timestamp(out_property))
938 },
939 );
940 map.insert(
941 "local",
942 |_language, _diagnostics, _build_ctx, self_property, function| {
943 function.expect_no_arguments()?;
944 let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
945 .ok()
946 .and_then(|tz_string| tz_string.parse::<i32>().ok())
947 .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
948 let out_property = self_property.map(move |mut timestamp| {
949 timestamp.tz_offset = tz_offset;
950 timestamp
951 });
952 Ok(L::wrap_timestamp(out_property))
953 },
954 );
955 map.insert(
956 "after",
957 |_language, _diagnostics, _build_ctx, self_property, function| {
958 let [date_pattern_node] = function.expect_exact_arguments()?;
959 let now = chrono::Local::now();
960 let date_pattern = template_parser::expect_string_literal_with(
961 date_pattern_node,
962 |date_pattern, span| {
963 DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
964 TemplateParseError::expression("Invalid date pattern", span)
965 .with_source(err)
966 })
967 },
968 )?;
969 let out_property = self_property.map(move |timestamp| date_pattern.matches(×tamp));
970 Ok(L::wrap_boolean(out_property))
971 },
972 );
973 map.insert("before", map["after"]);
974 map
975}
976
977fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
978) -> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
979 // Not using maplit::hashmap!{} or custom declarative macro here because
980 // code completion inside macro is quite restricted.
981 let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
982 map.insert(
983 "start",
984 |_language, _diagnostics, _build_ctx, self_property, function| {
985 function.expect_no_arguments()?;
986 let out_property = self_property.map(|time_range| time_range.start);
987 Ok(L::wrap_timestamp(out_property))
988 },
989 );
990 map.insert(
991 "end",
992 |_language, _diagnostics, _build_ctx, self_property, function| {
993 function.expect_no_arguments()?;
994 let out_property = self_property.map(|time_range| time_range.end);
995 Ok(L::wrap_timestamp(out_property))
996 },
997 );
998 map.insert(
999 "duration",
1000 |_language, _diagnostics, _build_ctx, self_property, function| {
1001 function.expect_no_arguments()?;
1002 let out_property = self_property.and_then(|time_range| Ok(time_range.duration()?));
1003 Ok(L::wrap_string(out_property))
1004 },
1005 );
1006 map
1007}
1008
1009fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>(
1010 language: &L,
1011 diagnostics: &mut TemplateDiagnostics,
1012 build_ctx: &BuildContext<L::Property>,
1013 self_template: Box<dyn ListTemplate + 'a>,
1014 function: &FunctionCallNode,
1015) -> TemplateParseResult<L::Property> {
1016 let property = match function.name {
1017 "join" => {
1018 let [separator_node] = function.expect_exact_arguments()?;
1019 let separator =
1020 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1021 L::wrap_template(self_template.join(separator))
1022 }
1023 _ => return Err(TemplateParseError::no_such_method("ListTemplate", function)),
1024 };
1025 Ok(property)
1026}
1027
1028/// Builds method call expression for printable list property.
1029pub fn build_formattable_list_method<'a, L, O>(
1030 language: &L,
1031 diagnostics: &mut TemplateDiagnostics,
1032 build_ctx: &BuildContext<L::Property>,
1033 self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
1034 function: &FunctionCallNode,
1035 // TODO: Generic L: WrapProperty<O> trait might be needed to support more
1036 // list operations such as first()/slice(). For .map(), a simple callback works.
1037 wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
1038) -> TemplateParseResult<L::Property>
1039where
1040 L: TemplateLanguage<'a> + ?Sized,
1041 O: Template + Clone + 'a,
1042{
1043 let property = match function.name {
1044 "len" => {
1045 function.expect_no_arguments()?;
1046 let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
1047 L::wrap_integer(out_property)
1048 }
1049 "join" => {
1050 let [separator_node] = function.expect_exact_arguments()?;
1051 let separator =
1052 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1053 let template =
1054 ListPropertyTemplate::new(self_property, separator, |formatter, item| {
1055 item.format(formatter)
1056 });
1057 L::wrap_template(Box::new(template))
1058 }
1059 "map" => build_map_operation(
1060 language,
1061 diagnostics,
1062 build_ctx,
1063 self_property,
1064 function,
1065 wrap_item,
1066 )?,
1067 _ => return Err(TemplateParseError::no_such_method("List", function)),
1068 };
1069 Ok(property)
1070}
1071
1072pub fn build_unformattable_list_method<'a, L, O>(
1073 language: &L,
1074 diagnostics: &mut TemplateDiagnostics,
1075 build_ctx: &BuildContext<L::Property>,
1076 self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
1077 function: &FunctionCallNode,
1078 wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
1079) -> TemplateParseResult<L::Property>
1080where
1081 L: TemplateLanguage<'a> + ?Sized,
1082 O: Clone + 'a,
1083{
1084 let property = match function.name {
1085 "len" => {
1086 function.expect_no_arguments()?;
1087 let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
1088 L::wrap_integer(out_property)
1089 }
1090 // No "join"
1091 "map" => build_map_operation(
1092 language,
1093 diagnostics,
1094 build_ctx,
1095 self_property,
1096 function,
1097 wrap_item,
1098 )?,
1099 _ => return Err(TemplateParseError::no_such_method("List", function)),
1100 };
1101 Ok(property)
1102}
1103
1104/// Builds expression that extracts iterable property and applies template to
1105/// each item.
1106///
1107/// `wrap_item()` is the function to wrap a list item of type `O` as a property.
1108fn build_map_operation<'a, L, O, P>(
1109 language: &L,
1110 diagnostics: &mut TemplateDiagnostics,
1111 build_ctx: &BuildContext<L::Property>,
1112 self_property: P,
1113 function: &FunctionCallNode,
1114 wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
1115) -> TemplateParseResult<L::Property>
1116where
1117 L: TemplateLanguage<'a> + ?Sized,
1118 P: TemplateProperty + 'a,
1119 P::Output: IntoIterator<Item = O>,
1120 O: Clone + 'a,
1121{
1122 // Build an item template with placeholder property, then evaluate it
1123 // for each item.
1124 let [lambda_node] = function.expect_exact_arguments()?;
1125 let item_placeholder = PropertyPlaceholder::new();
1126 let item_template = template_parser::expect_lambda_with(lambda_node, |lambda, _span| {
1127 let item_fn = || wrap_item(item_placeholder.clone());
1128 let mut local_variables = build_ctx.local_variables.clone();
1129 if let [name] = lambda.params.as_slice() {
1130 local_variables.insert(name, &item_fn);
1131 } else {
1132 return Err(TemplateParseError::expression(
1133 "Expected 1 lambda parameters",
1134 lambda.params_span,
1135 ));
1136 }
1137 let inner_build_ctx = BuildContext {
1138 local_variables,
1139 self_variable: build_ctx.self_variable,
1140 };
1141 expect_template_expression(language, diagnostics, &inner_build_ctx, &lambda.body)
1142 })?;
1143 let list_template = ListPropertyTemplate::new(
1144 self_property,
1145 Literal(" "), // separator
1146 move |formatter, item| {
1147 item_placeholder.with_value(item, || item_template.format(formatter))
1148 },
1149 );
1150 Ok(L::wrap_list_template(Box::new(list_template)))
1151}
1152
1153fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
1154 // Not using maplit::hashmap!{} or custom declarative macro here because
1155 // code completion inside macro is quite restricted.
1156 let mut map = TemplateBuildFunctionFnMap::<L>::new();
1157 map.insert("fill", |language, diagnostics, build_ctx, function| {
1158 let [width_node, content_node] = function.expect_exact_arguments()?;
1159 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1160 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1161 let template =
1162 ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
1163 Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
1164 Err(err) => formatter.handle_error(err),
1165 });
1166 Ok(L::wrap_template(Box::new(template)))
1167 });
1168 map.insert("indent", |language, diagnostics, build_ctx, function| {
1169 let [prefix_node, content_node] = function.expect_exact_arguments()?;
1170 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1171 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1172 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1173 let rewrap = formatter.rewrap_fn();
1174 text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
1175 prefix.format(&mut rewrap(formatter))
1176 })
1177 });
1178 Ok(L::wrap_template(Box::new(template)))
1179 });
1180 map.insert("pad_start", |language, diagnostics, build_ctx, function| {
1181 let ([width_node, content_node], [fill_char_node]) =
1182 function.expect_named_arguments(&["", "", "fill_char"])?;
1183 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1184 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1185 let fill_char = fill_char_node
1186 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1187 .transpose()?;
1188 let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
1189 Ok(L::wrap_template(template))
1190 });
1191 map.insert("pad_end", |language, diagnostics, build_ctx, function| {
1192 let ([width_node, content_node], [fill_char_node]) =
1193 function.expect_named_arguments(&["", "", "fill_char"])?;
1194 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1195 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1196 let fill_char = fill_char_node
1197 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1198 .transpose()?;
1199 let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
1200 Ok(L::wrap_template(template))
1201 });
1202 map.insert(
1203 "truncate_start",
1204 |language, diagnostics, build_ctx, function| {
1205 let [width_node, content_node] = function.expect_exact_arguments()?;
1206 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1207 let content =
1208 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1209 let template = new_truncate_template(content, width, text_util::write_truncated_start);
1210 Ok(L::wrap_template(template))
1211 },
1212 );
1213 map.insert(
1214 "truncate_end",
1215 |language, diagnostics, build_ctx, function| {
1216 let [width_node, content_node] = function.expect_exact_arguments()?;
1217 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1218 let content =
1219 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1220 let template = new_truncate_template(content, width, text_util::write_truncated_end);
1221 Ok(L::wrap_template(template))
1222 },
1223 );
1224 map.insert("label", |language, diagnostics, build_ctx, function| {
1225 let [label_node, content_node] = function.expect_exact_arguments()?;
1226 let label_property =
1227 expect_plain_text_expression(language, diagnostics, build_ctx, label_node)?;
1228 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1229 let labels =
1230 label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
1231 Ok(L::wrap_template(Box::new(LabelTemplate::new(
1232 content, labels,
1233 ))))
1234 });
1235 map.insert(
1236 "raw_escape_sequence",
1237 |language, diagnostics, build_ctx, function| {
1238 let [content_node] = function.expect_exact_arguments()?;
1239 let content =
1240 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1241 Ok(L::wrap_template(Box::new(RawEscapeSequenceTemplate(
1242 content,
1243 ))))
1244 },
1245 );
1246 map.insert("if", |language, diagnostics, build_ctx, function| {
1247 let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
1248 let condition =
1249 expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
1250 let true_template =
1251 expect_template_expression(language, diagnostics, build_ctx, true_node)?;
1252 let false_template = false_node
1253 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1254 .transpose()?;
1255 let template = ConditionalTemplate::new(condition, true_template, false_template);
1256 Ok(L::wrap_template(Box::new(template)))
1257 });
1258 map.insert("coalesce", |language, diagnostics, build_ctx, function| {
1259 let contents = function
1260 .args
1261 .iter()
1262 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1263 .try_collect()?;
1264 Ok(L::wrap_template(Box::new(CoalesceTemplate(contents))))
1265 });
1266 map.insert("concat", |language, diagnostics, build_ctx, function| {
1267 let contents = function
1268 .args
1269 .iter()
1270 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1271 .try_collect()?;
1272 Ok(L::wrap_template(Box::new(ConcatTemplate(contents))))
1273 });
1274 map.insert("separate", |language, diagnostics, build_ctx, function| {
1275 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
1276 let separator =
1277 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1278 let contents = content_nodes
1279 .iter()
1280 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1281 .try_collect()?;
1282 Ok(L::wrap_template(Box::new(SeparateTemplate::new(
1283 separator, contents,
1284 ))))
1285 });
1286 map.insert("surround", |language, diagnostics, build_ctx, function| {
1287 let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
1288 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1289 let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
1290 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1291 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1292 if recorded.data().is_empty() {
1293 return Ok(());
1294 }
1295 prefix.format(formatter)?;
1296 recorded.replay(formatter.as_mut())?;
1297 suffix.format(formatter)?;
1298 Ok(())
1299 });
1300 Ok(L::wrap_template(Box::new(template)))
1301 });
1302 map
1303}
1304
1305fn new_pad_template<'a, W>(
1306 content: Box<dyn Template + 'a>,
1307 fill_char: Option<Box<dyn Template + 'a>>,
1308 width: Box<dyn TemplateProperty<Output = usize> + 'a>,
1309 write_padded: W,
1310) -> Box<dyn Template + 'a>
1311where
1312 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
1313{
1314 let default_fill_char = FormatRecorder::with_data(" ");
1315 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1316 let width = match width.extract() {
1317 Ok(width) => width,
1318 Err(err) => return formatter.handle_error(err),
1319 };
1320 let mut fill_char_recorder;
1321 let recorded_fill_char = if let Some(fill_char) = &fill_char {
1322 let rewrap = formatter.rewrap_fn();
1323 fill_char_recorder = FormatRecorder::new();
1324 fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
1325 &fill_char_recorder
1326 } else {
1327 &default_fill_char
1328 };
1329 write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
1330 });
1331 Box::new(template)
1332}
1333
1334fn new_truncate_template<'a, W>(
1335 content: Box<dyn Template + 'a>,
1336 width: Box<dyn TemplateProperty<Output = usize> + 'a>,
1337 write_truncated: W,
1338) -> Box<dyn Template + 'a>
1339where
1340 W: Fn(&mut dyn Formatter, &FormatRecorder, usize) -> io::Result<usize> + 'a,
1341{
1342 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1343 let width = match width.extract() {
1344 Ok(width) => width,
1345 Err(err) => return formatter.handle_error(err),
1346 };
1347 write_truncated(formatter.as_mut(), recorded, width)?;
1348 Ok(())
1349 });
1350 Box::new(template)
1351}
1352
1353/// Builds intermediate expression tree from AST nodes.
1354pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1355 language: &L,
1356 diagnostics: &mut TemplateDiagnostics,
1357 build_ctx: &BuildContext<L::Property>,
1358 node: &ExpressionNode,
1359) -> TemplateParseResult<Expression<L::Property>> {
1360 match &node.kind {
1361 ExpressionKind::Identifier(name) => {
1362 if let Some(make) = build_ctx.local_variables.get(name) {
1363 // Don't label a local variable with its name
1364 Ok(Expression::unlabeled(make()))
1365 } else if *name == "self" {
1366 // "self" is a special variable, so don't label it
1367 let make = build_ctx.self_variable;
1368 Ok(Expression::unlabeled(make()))
1369 } else {
1370 let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
1371 .map_err(|err| {
1372 err.extend_keyword_candidates(itertools::chain(
1373 build_ctx.local_variables.keys().copied(),
1374 ["self"],
1375 ))
1376 })?;
1377 Ok(Expression::with_label(property, *name))
1378 }
1379 }
1380 ExpressionKind::Boolean(value) => {
1381 let property = L::wrap_boolean(Literal(*value));
1382 Ok(Expression::unlabeled(property))
1383 }
1384 ExpressionKind::Integer(value) => {
1385 let property = L::wrap_integer(Literal(*value));
1386 Ok(Expression::unlabeled(property))
1387 }
1388 ExpressionKind::String(value) => {
1389 let property = L::wrap_string(Literal(value.clone()));
1390 Ok(Expression::unlabeled(property))
1391 }
1392 ExpressionKind::Unary(op, arg_node) => {
1393 let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
1394 Ok(Expression::unlabeled(property))
1395 }
1396 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
1397 let property = build_binary_operation(
1398 language,
1399 diagnostics,
1400 build_ctx,
1401 *op,
1402 lhs_node,
1403 rhs_node,
1404 node.span,
1405 )?;
1406 Ok(Expression::unlabeled(property))
1407 }
1408 ExpressionKind::Concat(nodes) => {
1409 let templates = nodes
1410 .iter()
1411 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1412 .try_collect()?;
1413 let property = L::wrap_template(Box::new(ConcatTemplate(templates)));
1414 Ok(Expression::unlabeled(property))
1415 }
1416 ExpressionKind::FunctionCall(function) => {
1417 let property = language.build_function(diagnostics, build_ctx, function)?;
1418 Ok(Expression::unlabeled(property))
1419 }
1420 ExpressionKind::MethodCall(method) => {
1421 let mut expression =
1422 build_expression(language, diagnostics, build_ctx, &method.object)?;
1423 expression.property = language.build_method(
1424 diagnostics,
1425 build_ctx,
1426 expression.property,
1427 &method.function,
1428 )?;
1429 expression.labels.push(method.function.name.to_owned());
1430 Ok(expression)
1431 }
1432 ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
1433 "Lambda cannot be defined here",
1434 node.span,
1435 )),
1436 ExpressionKind::AliasExpanded(id, subst) => {
1437 let mut inner_diagnostics = TemplateDiagnostics::new();
1438 let expression = build_expression(language, &mut inner_diagnostics, build_ctx, subst)
1439 .map_err(|e| e.within_alias_expansion(*id, node.span))?;
1440 diagnostics.extend_with(inner_diagnostics, |diag| {
1441 diag.within_alias_expansion(*id, node.span)
1442 });
1443 Ok(expression)
1444 }
1445 }
1446}
1447
1448/// Builds template evaluation tree from AST nodes, with fresh build context.
1449///
1450/// `wrap_self` specifies the type of the top-level property, which should be
1451/// one of the `L::wrap_*()` functions.
1452pub fn build<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
1453 language: &L,
1454 diagnostics: &mut TemplateDiagnostics,
1455 node: &ExpressionNode,
1456 // TODO: Generic L: WrapProperty<C> trait might be better. See the
1457 // comment in build_formattable_list_method().
1458 wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
1459) -> TemplateParseResult<TemplateRenderer<'a, C>> {
1460 let self_placeholder = PropertyPlaceholder::new();
1461 let build_ctx = BuildContext {
1462 local_variables: HashMap::new(),
1463 self_variable: &|| wrap_self(self_placeholder.clone()),
1464 };
1465 let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
1466 Ok(TemplateRenderer::new(template, self_placeholder))
1467}
1468
1469/// Parses text, expands aliases, then builds template evaluation tree.
1470pub fn parse<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
1471 language: &L,
1472 diagnostics: &mut TemplateDiagnostics,
1473 template_text: &str,
1474 aliases_map: &TemplateAliasesMap,
1475 wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
1476) -> TemplateParseResult<TemplateRenderer<'a, C>> {
1477 let node = template_parser::parse(template_text, aliases_map)?;
1478 build(language, diagnostics, &node, wrap_self)
1479 .map_err(|err| err.extend_alias_candidates(aliases_map))
1480}
1481
1482pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1483 language: &L,
1484 diagnostics: &mut TemplateDiagnostics,
1485 build_ctx: &BuildContext<L::Property>,
1486 node: &ExpressionNode,
1487) -> TemplateParseResult<Box<dyn TemplateProperty<Output = bool> + 'a>> {
1488 expect_expression_of_type(
1489 language,
1490 diagnostics,
1491 build_ctx,
1492 node,
1493 "Boolean",
1494 |expression| expression.try_into_boolean(),
1495 )
1496}
1497
1498pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1499 language: &L,
1500 diagnostics: &mut TemplateDiagnostics,
1501 build_ctx: &BuildContext<L::Property>,
1502 node: &ExpressionNode,
1503) -> TemplateParseResult<Box<dyn TemplateProperty<Output = i64> + 'a>> {
1504 expect_expression_of_type(
1505 language,
1506 diagnostics,
1507 build_ctx,
1508 node,
1509 "Integer",
1510 |expression| expression.try_into_integer(),
1511 )
1512}
1513
1514/// If the given expression `node` is of `Integer` type, converts it to `isize`.
1515pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1516 language: &L,
1517 diagnostics: &mut TemplateDiagnostics,
1518 build_ctx: &BuildContext<L::Property>,
1519 node: &ExpressionNode,
1520) -> TemplateParseResult<Box<dyn TemplateProperty<Output = isize> + 'a>> {
1521 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
1522 let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
1523 Ok(Box::new(isize_property))
1524}
1525
1526/// If the given expression `node` is of `Integer` type, converts it to `usize`.
1527pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1528 language: &L,
1529 diagnostics: &mut TemplateDiagnostics,
1530 build_ctx: &BuildContext<L::Property>,
1531 node: &ExpressionNode,
1532) -> TemplateParseResult<Box<dyn TemplateProperty<Output = usize> + 'a>> {
1533 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
1534 let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
1535 Ok(Box::new(usize_property))
1536}
1537
1538pub fn expect_plain_text_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1539 language: &L,
1540 diagnostics: &mut TemplateDiagnostics,
1541 build_ctx: &BuildContext<L::Property>,
1542 node: &ExpressionNode,
1543) -> TemplateParseResult<Box<dyn TemplateProperty<Output = String> + 'a>> {
1544 // Since any formattable type can be converted to a string property,
1545 // the expected type is not a String, but a Template.
1546 expect_expression_of_type(
1547 language,
1548 diagnostics,
1549 build_ctx,
1550 node,
1551 "Template",
1552 |expression| expression.try_into_plain_text(),
1553 )
1554}
1555
1556pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
1557 language: &L,
1558 diagnostics: &mut TemplateDiagnostics,
1559 build_ctx: &BuildContext<L::Property>,
1560 node: &ExpressionNode,
1561) -> TemplateParseResult<Box<dyn Template + 'a>> {
1562 expect_expression_of_type(
1563 language,
1564 diagnostics,
1565 build_ctx,
1566 node,
1567 "Template",
1568 |expression| expression.try_into_template(),
1569 )
1570}
1571
1572fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
1573 language: &L,
1574 diagnostics: &mut TemplateDiagnostics,
1575 build_ctx: &BuildContext<L::Property>,
1576 node: &ExpressionNode,
1577 expected_type: &str,
1578 f: impl FnOnce(Expression<L::Property>) -> Option<T>,
1579) -> TemplateParseResult<T> {
1580 if let ExpressionKind::AliasExpanded(id, subst) = &node.kind {
1581 let mut inner_diagnostics = TemplateDiagnostics::new();
1582 let expression = expect_expression_of_type(
1583 language,
1584 &mut inner_diagnostics,
1585 build_ctx,
1586 subst,
1587 expected_type,
1588 f,
1589 )
1590 .map_err(|e| e.within_alias_expansion(*id, node.span))?;
1591 diagnostics.extend_with(inner_diagnostics, |diag| {
1592 diag.within_alias_expansion(*id, node.span)
1593 });
1594 Ok(expression)
1595 } else {
1596 let expression = build_expression(language, diagnostics, build_ctx, node)?;
1597 let actual_type = expression.type_name();
1598 f(expression)
1599 .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
1600 }
1601}
1602
1603#[cfg(test)]
1604mod tests {
1605 use std::iter;
1606
1607 use jj_lib::backend::MillisSinceEpoch;
1608
1609 use super::*;
1610 use crate::formatter;
1611 use crate::formatter::ColorFormatter;
1612 use crate::generic_templater::GenericTemplateLanguage;
1613
1614 type L = GenericTemplateLanguage<'static, ()>;
1615 type TestTemplatePropertyKind = <L as TemplateLanguage<'static>>::Property;
1616
1617 /// Helper to set up template evaluation environment.
1618 struct TestTemplateEnv {
1619 language: L,
1620 aliases_map: TemplateAliasesMap,
1621 color_rules: Vec<(Vec<String>, formatter::Style)>,
1622 }
1623
1624 impl TestTemplateEnv {
1625 fn new() -> Self {
1626 TestTemplateEnv {
1627 language: L::new(),
1628 aliases_map: TemplateAliasesMap::new(),
1629 color_rules: Vec::new(),
1630 }
1631 }
1632 }
1633
1634 impl TestTemplateEnv {
1635 fn add_keyword<F>(&mut self, name: &'static str, build: F)
1636 where
1637 F: Fn() -> TestTemplatePropertyKind + 'static,
1638 {
1639 self.language.add_keyword(name, move |_| Ok(build()));
1640 }
1641
1642 fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
1643 self.aliases_map.insert(decl, defn).unwrap();
1644 }
1645
1646 fn add_color(&mut self, label: &str, fg_color: crossterm::style::Color) {
1647 let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
1648 let style = formatter::Style {
1649 fg_color: Some(fg_color),
1650 ..Default::default()
1651 };
1652 self.color_rules.push((labels, style));
1653 }
1654
1655 fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, ()>> {
1656 parse(
1657 &self.language,
1658 &mut TemplateDiagnostics::new(),
1659 template,
1660 &self.aliases_map,
1661 L::wrap_self,
1662 )
1663 }
1664
1665 fn parse_err(&self, template: &str) -> String {
1666 let err = self.parse(template).err().unwrap();
1667 iter::successors(Some(&err), |e| e.origin()).join("\n")
1668 }
1669
1670 fn render_ok(&self, template: &str) -> String {
1671 let template = self.parse(template).unwrap();
1672 let mut output = Vec::new();
1673 let mut formatter =
1674 ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
1675 template.format(&(), &mut formatter).unwrap();
1676 drop(formatter);
1677 String::from_utf8(output).unwrap()
1678 }
1679 }
1680
1681 fn new_error_property<O>(message: &str) -> impl TemplateProperty<Output = O> + '_ {
1682 Literal(()).and_then(|()| Err(TemplatePropertyError(message.into())))
1683 }
1684
1685 fn new_signature(name: &str, email: &str) -> Signature {
1686 Signature {
1687 name: name.to_owned(),
1688 email: email.to_owned(),
1689 timestamp: new_timestamp(0, 0),
1690 }
1691 }
1692
1693 fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
1694 Timestamp {
1695 timestamp: MillisSinceEpoch(msec),
1696 tz_offset,
1697 }
1698 }
1699
1700 #[test]
1701 fn test_parsed_tree() {
1702 let mut env = TestTemplateEnv::new();
1703 env.add_keyword("divergent", || L::wrap_boolean(Literal(false)));
1704 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
1705 env.add_keyword("hello", || L::wrap_string(Literal("Hello".to_owned())));
1706
1707 // Empty
1708 insta::assert_snapshot!(env.render_ok(r#" "#), @"");
1709
1710 // Single term with whitespace
1711 insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
1712
1713 // Multiple terms
1714 insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
1715
1716 // Parenthesized single term
1717 insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
1718
1719 // Parenthesized multiple terms and concatenation
1720 insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
1721
1722 // Parenthesized "if" condition
1723 insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
1724
1725 // Parenthesized method chaining
1726 insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
1727 }
1728
1729 #[test]
1730 fn test_parse_error() {
1731 let mut env = TestTemplateEnv::new();
1732 env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
1733 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
1734
1735 insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r"
1736 --> 1:13
1737 |
1738 1 | description ()
1739 | ^---
1740 |
1741 = expected <EOI>, `++`, `||`, `&&`, `==`, or `!=`
1742 ");
1743
1744 insta::assert_snapshot!(env.parse_err(r#"foo"#), @r###"
1745 --> 1:1
1746 |
1747 1 | foo
1748 | ^-^
1749 |
1750 = Keyword "foo" doesn't exist
1751 "###);
1752
1753 insta::assert_snapshot!(env.parse_err(r#"foo()"#), @r###"
1754 --> 1:1
1755 |
1756 1 | foo()
1757 | ^-^
1758 |
1759 = Function "foo" doesn't exist
1760 "###);
1761 insta::assert_snapshot!(env.parse_err(r#"false()"#), @r###"
1762 --> 1:1
1763 |
1764 1 | false()
1765 | ^---^
1766 |
1767 = Expected identifier
1768 "###);
1769
1770 insta::assert_snapshot!(env.parse_err(r#"!foo"#), @r###"
1771 --> 1:2
1772 |
1773 1 | !foo
1774 | ^-^
1775 |
1776 = Keyword "foo" doesn't exist
1777 "###);
1778 insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @r###"
1779 --> 1:9
1780 |
1781 1 | true && 123
1782 | ^-^
1783 |
1784 = Expected expression of type "Boolean", but actual type is "Integer"
1785 "###);
1786 insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @r#"
1787 --> 1:1
1788 |
1789 1 | true == 1
1790 | ^-------^
1791 |
1792 = Cannot compare expressions of type "Boolean" and "Integer"
1793 "#);
1794 insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @r#"
1795 --> 1:1
1796 |
1797 1 | true != 'a'
1798 | ^---------^
1799 |
1800 = Cannot compare expressions of type "Boolean" and "String"
1801 "#);
1802 insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @r#"
1803 --> 1:1
1804 |
1805 1 | 1 == true
1806 | ^-------^
1807 |
1808 = Cannot compare expressions of type "Integer" and "Boolean"
1809 "#);
1810 insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @r#"
1811 --> 1:1
1812 |
1813 1 | 1 != 'a'
1814 | ^------^
1815 |
1816 = Cannot compare expressions of type "Integer" and "String"
1817 "#);
1818 insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @r#"
1819 --> 1:1
1820 |
1821 1 | 'a' == true
1822 | ^---------^
1823 |
1824 = Cannot compare expressions of type "String" and "Boolean"
1825 "#);
1826 insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @r#"
1827 --> 1:1
1828 |
1829 1 | 'a' != 1
1830 | ^------^
1831 |
1832 = Cannot compare expressions of type "String" and "Integer"
1833 "#);
1834 insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
1835 --> 1:1
1836 |
1837 1 | 'a' == label("", "")
1838 | ^------------------^
1839 |
1840 = Cannot compare expressions of type "String" and "Template"
1841 "#);
1842
1843 insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r###"
1844 --> 1:26
1845 |
1846 1 | description.first_line().foo()
1847 | ^-^
1848 |
1849 = Method "foo" doesn't exist for type "String"
1850 "###);
1851
1852 insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @r###"
1853 --> 1:1
1854 |
1855 1 | 10000000000000000000
1856 | ^------------------^
1857 |
1858 = Invalid integer literal
1859 "###);
1860 insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @r###"
1861 --> 1:4
1862 |
1863 1 | 42.foo()
1864 | ^-^
1865 |
1866 = Method "foo" doesn't exist for type "Integer"
1867 "###);
1868 insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @r###"
1869 --> 1:3
1870 |
1871 1 | (-empty)
1872 | ^---^
1873 |
1874 = Expected expression of type "Integer", but actual type is "Boolean"
1875 "###);
1876
1877 insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r###"
1878 --> 1:18
1879 |
1880 1 | ("foo" ++ "bar").baz()
1881 | ^-^
1882 |
1883 = Method "baz" doesn't exist for type "Template"
1884 "###);
1885
1886 insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @r###"
1887 --> 1:22
1888 |
1889 1 | description.contains()
1890 | ^
1891 |
1892 = Function "contains": Expected 1 arguments
1893 "###);
1894
1895 insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r###"
1896 --> 1:24
1897 |
1898 1 | description.first_line("foo")
1899 | ^---^
1900 |
1901 = Function "first_line": Expected 0 arguments
1902 "###);
1903
1904 insta::assert_snapshot!(env.parse_err(r#"label()"#), @r###"
1905 --> 1:7
1906 |
1907 1 | label()
1908 | ^
1909 |
1910 = Function "label": Expected 2 arguments
1911 "###);
1912 insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r###"
1913 --> 1:7
1914 |
1915 1 | label("foo", "bar", "baz")
1916 | ^-----------------^
1917 |
1918 = Function "label": Expected 2 arguments
1919 "###);
1920
1921 insta::assert_snapshot!(env.parse_err(r#"if()"#), @r###"
1922 --> 1:4
1923 |
1924 1 | if()
1925 | ^
1926 |
1927 = Function "if": Expected 2 to 3 arguments
1928 "###);
1929 insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r###"
1930 --> 1:4
1931 |
1932 1 | if("foo", "bar", "baz", "quux")
1933 | ^-------------------------^
1934 |
1935 = Function "if": Expected 2 to 3 arguments
1936 "###);
1937
1938 insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
1939 --> 1:37
1940 |
1941 1 | pad_start("foo", fill_char = "bar", "baz")
1942 | ^---^
1943 |
1944 = Function "pad_start": Positional argument follows keyword argument
1945 "#);
1946
1947 insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r###"
1948 --> 1:4
1949 |
1950 1 | if(label("foo", "bar"), "baz")
1951 | ^-----------------^
1952 |
1953 = Expected expression of type "Boolean", but actual type is "Template"
1954 "###);
1955
1956 insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @r###"
1957 --> 1:1
1958 |
1959 1 | |x| description
1960 | ^-------------^
1961 |
1962 = Lambda cannot be defined here
1963 "###);
1964 }
1965
1966 #[test]
1967 fn test_self_keyword() {
1968 let mut env = TestTemplateEnv::new();
1969 env.add_keyword("say_hello", || L::wrap_string(Literal("Hello".to_owned())));
1970
1971 insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
1972 insta::assert_snapshot!(env.parse_err(r#"self"#), @r###"
1973 --> 1:1
1974 |
1975 1 | self
1976 | ^--^
1977 |
1978 = Expected expression of type "Template", but actual type is "Self"
1979 "###);
1980 }
1981
1982 #[test]
1983 fn test_boolean_cast() {
1984 let mut env = TestTemplateEnv::new();
1985
1986 insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
1987 insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
1988
1989 env.add_keyword("sl0", || {
1990 L::wrap_string_list(Literal::<Vec<String>>(vec![]))
1991 });
1992 env.add_keyword("sl1", || L::wrap_string_list(Literal(vec!["".to_owned()])));
1993 insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
1994 insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
1995
1996 // No implicit cast of integer
1997 insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @r###"
1998 --> 1:4
1999 |
2000 1 | if(0, true, false)
2001 | ^
2002 |
2003 = Expected expression of type "Boolean", but actual type is "Integer"
2004 "###);
2005
2006 // Optional integer can be converted to boolean, and Some(0) is truthy.
2007 env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None)));
2008 env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(0))));
2009 insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
2010 insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
2011
2012 insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r###"
2013 --> 1:4
2014 |
2015 1 | if(label("", ""), true, false)
2016 | ^-----------^
2017 |
2018 = Expected expression of type "Boolean", but actual type is "Template"
2019 "###);
2020 insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @r###"
2021 --> 1:4
2022 |
2023 1 | if(sl0.map(|x| x), true, false)
2024 | ^------------^
2025 |
2026 = Expected expression of type "Boolean", but actual type is "ListTemplate"
2027 "###);
2028 }
2029
2030 #[test]
2031 fn test_arithmetic_operation() {
2032 let mut env = TestTemplateEnv::new();
2033 env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None)));
2034 env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(1))));
2035 env.add_keyword("i64_min", || L::wrap_integer(Literal(i64::MIN)));
2036
2037 insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
2038 insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
2039 insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
2040
2041 // Since methods of the contained value can be invoked, it makes sense
2042 // to apply operators to optional integers as well.
2043 insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
2044 insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
2045
2046 // No panic on integer overflow.
2047 insta::assert_snapshot!(
2048 env.render_ok(r#"-i64_min"#),
2049 @"<Error: Attempt to negate with overflow>");
2050 }
2051
2052 #[test]
2053 fn test_logical_operation() {
2054 let mut env = TestTemplateEnv::new();
2055
2056 insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
2057 insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
2058 insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
2059 insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
2060 insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
2061 insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
2062 insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
2063 insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
2064 insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
2065 insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
2066 insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
2067 insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
2068 insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
2069 insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
2070 insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
2071
2072 insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
2073 insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
2074
2075 // Short-circuiting
2076 env.add_keyword("bad_bool", || L::wrap_boolean(new_error_property("Bad")));
2077 insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
2078 insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
2079 insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
2080 insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
2081 }
2082
2083 #[test]
2084 fn test_list_method() {
2085 let mut env = TestTemplateEnv::new();
2086 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2087 env.add_keyword("sep", || L::wrap_string(Literal("sep".to_owned())));
2088
2089 insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
2090 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
2091
2092 insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
2093 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
2094 // Null separator
2095 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
2096 // Keyword as separator
2097 insta::assert_snapshot!(
2098 env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
2099 @"aSEPbSEPc");
2100
2101 insta::assert_snapshot!(
2102 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
2103 @"aa bb cc");
2104 // Global keyword in item template
2105 insta::assert_snapshot!(
2106 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
2107 @"atrue btrue ctrue");
2108 // Global keyword in item template shadowing 'self'
2109 insta::assert_snapshot!(
2110 env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
2111 @"atrue btrue ctrue");
2112 // Override global keyword 'empty'
2113 insta::assert_snapshot!(
2114 env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
2115 @"a b c");
2116 // Nested map operations
2117 insta::assert_snapshot!(
2118 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
2119 @"ax ay bx by cx cy");
2120 // Nested map/join operations
2121 insta::assert_snapshot!(
2122 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
2123 @"ax,ay;bx,by;cx,cy");
2124 // Nested string operations
2125 insta::assert_snapshot!(
2126 env.render_ok(r#""!a\n!b\nc\nend".remove_suffix("end").lines().map(|s| s.remove_prefix("!"))"#),
2127 @"a b c");
2128
2129 // Lambda expression in alias
2130 env.add_alias("identity", "|x| x");
2131 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
2132
2133 // Not a lambda expression
2134 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r###"
2135 --> 1:17
2136 |
2137 1 | "a".lines().map(empty)
2138 | ^---^
2139 |
2140 = Expected lambda expression
2141 "###);
2142 // Bad lambda parameter count
2143 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r###"
2144 --> 1:18
2145 |
2146 1 | "a".lines().map(|| "")
2147 | ^
2148 |
2149 = Expected 1 lambda parameters
2150 "###);
2151 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r###"
2152 --> 1:18
2153 |
2154 1 | "a".lines().map(|a, b| "")
2155 | ^--^
2156 |
2157 = Expected 1 lambda parameters
2158 "###);
2159 // Error in lambda expression
2160 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r###"
2161 --> 1:23
2162 |
2163 1 | "a".lines().map(|s| s.unknown())
2164 | ^-----^
2165 |
2166 = Method "unknown" doesn't exist for type "String"
2167 "###);
2168 // Error in lambda alias
2169 env.add_alias("too_many_params", "|x, y| x");
2170 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
2171 --> 1:17
2172 |
2173 1 | "a".lines().map(too_many_params)
2174 | ^-------------^
2175 |
2176 = In alias "too_many_params"
2177 --> 1:2
2178 |
2179 1 | |x, y| x
2180 | ^--^
2181 |
2182 = Expected 1 lambda parameters
2183 "#);
2184 }
2185
2186 #[test]
2187 fn test_string_method() {
2188 let mut env = TestTemplateEnv::new();
2189 env.add_keyword("description", || {
2190 L::wrap_string(Literal("description 1".to_owned()))
2191 });
2192 env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
2193
2194 insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
2195 insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
2196 insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
2197
2198 insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
2199 insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
2200 insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
2201 insta::assert_snapshot!(
2202 env.render_ok(r#""description 123".contains(description.first_line())"#),
2203 @"true");
2204
2205 // inner template error should propagate
2206 insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
2207 insta::assert_snapshot!(
2208 env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
2209 insta::assert_snapshot!(
2210 env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
2211
2212 insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
2213 insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
2214
2215 insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
2216 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
2217
2218 insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
2219 insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
2220 insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
2221 insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
2222 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
2223 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
2224
2225 insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
2226 insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
2227 insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
2228 insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
2229 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
2230 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
2231
2232 insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
2233 insta::assert_snapshot!(
2234 env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
2235 @"testing");
2236
2237 insta::assert_snapshot!(
2238 env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
2239 @"bar@my.example.com");
2240 insta::assert_snapshot!(
2241 env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
2242 @"bar");
2243
2244 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
2245 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
2246 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
2247 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
2248 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
2249 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
2250 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
2251 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
2252
2253 // non-ascii characters
2254 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
2255 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
2256 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
2257 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
2258 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
2259 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
2260 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
2261 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
2262 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
2263 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
2264 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
2265
2266 // ranges with end > start are empty
2267 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
2268 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
2269 }
2270
2271 #[test]
2272 fn test_signature() {
2273 let mut env = TestTemplateEnv::new();
2274
2275 env.add_keyword("author", || {
2276 L::wrap_signature(Literal(new_signature("Test User", "test.user@example.com")))
2277 });
2278 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
2279 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
2280 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
2281 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2282
2283 env.add_keyword("author", || {
2284 L::wrap_signature(Literal(new_signature(
2285 "Another Test User",
2286 "test.user@example.com",
2287 )))
2288 });
2289 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
2290 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
2291 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
2292 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2293
2294 env.add_keyword("author", || {
2295 L::wrap_signature(Literal(new_signature(
2296 "Test User",
2297 "test.user@invalid@example.com",
2298 )))
2299 });
2300 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
2301 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
2302 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
2303 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2304
2305 env.add_keyword("author", || {
2306 L::wrap_signature(Literal(new_signature("Test User", "test.user")))
2307 });
2308 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
2309 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
2310 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2311
2312 env.add_keyword("author", || {
2313 L::wrap_signature(Literal(new_signature(
2314 "Test User",
2315 "test.user+tag@example.com",
2316 )))
2317 });
2318 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
2319 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
2320 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user+tag");
2321
2322 env.add_keyword("author", || {
2323 L::wrap_signature(Literal(new_signature("Test User", "x@y")))
2324 });
2325 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
2326 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
2327 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"x");
2328
2329 env.add_keyword("author", || {
2330 L::wrap_signature(Literal(new_signature("", "test.user@example.com")))
2331 });
2332 insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
2333 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
2334 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
2335 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
2336
2337 env.add_keyword("author", || {
2338 L::wrap_signature(Literal(new_signature("Test User", "")))
2339 });
2340 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
2341 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
2342 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
2343 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
2344
2345 env.add_keyword("author", || {
2346 L::wrap_signature(Literal(new_signature("", "")))
2347 });
2348 insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
2349 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
2350 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
2351 insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
2352 }
2353
2354 #[test]
2355 fn test_size_hint_method() {
2356 let mut env = TestTemplateEnv::new();
2357
2358 env.add_keyword("unbounded", || L::wrap_size_hint(Literal((5, None))));
2359 insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
2360 insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
2361 insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
2362 insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
2363
2364 env.add_keyword("bounded", || L::wrap_size_hint(Literal((0, Some(10)))));
2365 insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
2366 insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
2367 insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
2368 insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
2369
2370 env.add_keyword("zero", || L::wrap_size_hint(Literal((0, Some(0)))));
2371 insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
2372 insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
2373 insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
2374 insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
2375 }
2376
2377 #[test]
2378 fn test_timestamp_method() {
2379 let mut env = TestTemplateEnv::new();
2380 env.add_keyword("t0", || L::wrap_timestamp(Literal(new_timestamp(0, 0))));
2381
2382 insta::assert_snapshot!(
2383 env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
2384 @"19700101 00:00:00");
2385
2386 // Invalid format string
2387 insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r###"
2388 --> 1:11
2389 |
2390 1 | t0.format("%_")
2391 | ^--^
2392 |
2393 = Invalid time format
2394 "###);
2395
2396 // Invalid type
2397 insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @r###"
2398 --> 1:11
2399 |
2400 1 | t0.format(0)
2401 | ^
2402 |
2403 = Expected string literal
2404 "###);
2405
2406 // Dynamic string isn't supported yet
2407 insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r###"
2408 --> 1:11
2409 |
2410 1 | t0.format("%Y" ++ "%m")
2411 | ^----------^
2412 |
2413 = Expected string literal
2414 "###);
2415
2416 // Literal alias expansion
2417 env.add_alias("time_format", r#""%Y-%m-%d""#);
2418 env.add_alias("bad_time_format", r#""%_""#);
2419 insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
2420 insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
2421 --> 1:11
2422 |
2423 1 | t0.format(bad_time_format)
2424 | ^-------------^
2425 |
2426 = In alias "bad_time_format"
2427 --> 1:1
2428 |
2429 1 | "%_"
2430 | ^--^
2431 |
2432 = Invalid time format
2433 "#);
2434 }
2435
2436 #[test]
2437 fn test_fill_function() {
2438 let mut env = TestTemplateEnv::new();
2439 env.add_color("error", crossterm::style::Color::DarkRed);
2440
2441 insta::assert_snapshot!(
2442 env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
2443 label("error", "lazy") ++ " dog\n")"#),
2444 @r###"
2445 The quick fox jumps
2446 over the [38;5;1mlazy[39m dog
2447 "###);
2448
2449 // A low value will not chop words, but can chop a label by words
2450 insta::assert_snapshot!(
2451 env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
2452 label("error", "longlonglongword and short words") ++
2453 " back out\n")"#),
2454 @r###"
2455 Longlonglongword
2456 an some
2457 short
2458 words
2459 [38;5;1mlonglonglongword[39m
2460 [38;5;1mand short[39m
2461 [38;5;1mwords[39m
2462 back out
2463 "###);
2464
2465 // Filling to 0 means breaking at every word
2466 insta::assert_snapshot!(
2467 env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
2468 label("error", "lazy") ++ " dog\n")"#),
2469 @r###"
2470 The
2471 quick
2472 fox
2473 jumps
2474 over
2475 the
2476 [38;5;1mlazy[39m
2477 dog
2478 "###);
2479
2480 // Filling to -0 is the same as 0
2481 insta::assert_snapshot!(
2482 env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
2483 label("error", "lazy") ++ " dog\n")"#),
2484 @r###"
2485 The
2486 quick
2487 fox
2488 jumps
2489 over
2490 the
2491 [38;5;1mlazy[39m
2492 dog
2493 "###);
2494
2495 // Filling to negative width is an error
2496 insta::assert_snapshot!(
2497 env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
2498 label("error", "lazy") ++ " dog\n")"#),
2499 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
2500
2501 // Word-wrap, then indent
2502 insta::assert_snapshot!(
2503 env.render_ok(r#""START marker to help insta\n" ++
2504 indent(" ", fill(20, "The quick fox jumps over the " ++
2505 label("error", "lazy") ++ " dog\n"))"#),
2506 @r###"
2507 START marker to help insta
2508 The quick fox jumps
2509 over the [38;5;1mlazy[39m dog
2510 "###);
2511
2512 // Word-wrap indented (no special handling for leading spaces)
2513 insta::assert_snapshot!(
2514 env.render_ok(r#""START marker to help insta\n" ++
2515 fill(20, indent(" ", "The quick fox jumps over the " ++
2516 label("error", "lazy") ++ " dog\n"))"#),
2517 @r###"
2518 START marker to help insta
2519 The quick fox
2520 jumps over the [38;5;1mlazy[39m
2521 dog
2522 "###);
2523 }
2524
2525 #[test]
2526 fn test_indent_function() {
2527 let mut env = TestTemplateEnv::new();
2528 env.add_color("error", crossterm::style::Color::DarkRed);
2529 env.add_color("warning", crossterm::style::Color::DarkYellow);
2530 env.add_color("hint", crossterm::style::Color::DarkCyan);
2531
2532 // Empty line shouldn't be indented. Not using insta here because we test
2533 // whitespace existence.
2534 assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
2535 assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
2536 assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
2537
2538 // "\n" at end of labeled text
2539 insta::assert_snapshot!(
2540 env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
2541 @r###"
2542 [38;5;1m__a[39m
2543 [38;5;3m__b[39m
2544 "###);
2545
2546 // "\n" in labeled text
2547 insta::assert_snapshot!(
2548 env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
2549 @r###"
2550 [38;5;1m__a[39m[38;5;3mb[39m
2551 [38;5;3m__c[39m
2552 "###);
2553
2554 // Labeled prefix + unlabeled content
2555 insta::assert_snapshot!(
2556 env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
2557 @r###"
2558 [38;5;1mXX[39ma
2559 [38;5;1mXX[39mb
2560 "###);
2561
2562 // Nested indent, silly but works
2563 insta::assert_snapshot!(
2564 env.render_ok(r#"indent(label("hint", "A"),
2565 label("warning", indent(label("hint", "B"),
2566 label("error", "x\n") ++ "y")))"#),
2567 @r###"
2568 [38;5;6mAB[38;5;1mx[39m
2569 [38;5;6mAB[38;5;3my[39m
2570 "###);
2571 }
2572
2573 #[test]
2574 fn test_pad_function() {
2575 let mut env = TestTemplateEnv::new();
2576 env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
2577 env.add_color("red", crossterm::style::Color::Red);
2578 env.add_color("cyan", crossterm::style::Color::DarkCyan);
2579
2580 // Default fill_char is ' '
2581 insta::assert_snapshot!(
2582 env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
2583 @"{ [38;5;9mfoo[39m}");
2584 insta::assert_snapshot!(
2585 env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
2586 @"{[38;5;9mfoo[39m }");
2587
2588 // Labeled fill char
2589 insta::assert_snapshot!(
2590 env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
2591 @"[38;5;6m==[39m[38;5;9mfoo[39m");
2592 insta::assert_snapshot!(
2593 env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
2594 @"[38;5;9mfoo[39m[38;5;6m==[39m");
2595
2596 // Error in fill char: the output looks odd (because the error message
2597 // isn't 1-width character), but is still readable.
2598 insta::assert_snapshot!(
2599 env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
2600 @"foo");
2601 insta::assert_snapshot!(
2602 env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
2603 @"foo<<Error: Error: Bad>Bad>");
2604 }
2605
2606 #[test]
2607 fn test_truncate_function() {
2608 let mut env = TestTemplateEnv::new();
2609 env.add_color("red", crossterm::style::Color::Red);
2610
2611 insta::assert_snapshot!(
2612 env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
2613 @"[38;5;9mar[39mbaz");
2614 insta::assert_snapshot!(
2615 env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
2616 @"[38;5;9mfo[39mbaz");
2617 }
2618
2619 #[test]
2620 fn test_label_function() {
2621 let mut env = TestTemplateEnv::new();
2622 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2623 env.add_color("error", crossterm::style::Color::DarkRed);
2624 env.add_color("warning", crossterm::style::Color::DarkYellow);
2625
2626 // Literal
2627 insta::assert_snapshot!(
2628 env.render_ok(r#"label("error", "text")"#),
2629 @"[38;5;1mtext[39m");
2630
2631 // Evaluated property
2632 insta::assert_snapshot!(
2633 env.render_ok(r#"label("error".first_line(), "text")"#),
2634 @"[38;5;1mtext[39m");
2635
2636 // Template
2637 insta::assert_snapshot!(
2638 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
2639 @"[38;5;1mtext[39m");
2640 }
2641
2642 #[test]
2643 fn test_raw_escape_sequence_function_strip_labels() {
2644 let mut env = TestTemplateEnv::new();
2645 env.add_color("error", crossterm::style::Color::DarkRed);
2646 env.add_color("warning", crossterm::style::Color::DarkYellow);
2647
2648 insta::assert_snapshot!(
2649 env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
2650 @"text",
2651 );
2652 }
2653
2654 #[test]
2655 fn test_raw_escape_sequence_function_ansi_escape() {
2656 let env = TestTemplateEnv::new();
2657
2658 // Sanitize ANSI escape without raw_escape_sequence
2659 insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
2660 insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
2661 insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
2662 insta::assert_snapshot!(
2663 env.render_ok(r#""]8;;"
2664 ++ "http://example.com"
2665 ++ "\e\\"
2666 ++ "Example"
2667 ++ "\x1b]8;;\x1B\\""#),
2668 @r#"␛]8;;http://example.com␛\Example␛]8;;␛\"#);
2669
2670 // Don't sanitize ANSI escape with raw_escape_sequence
2671 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
2672 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
2673 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
2674 insta::assert_snapshot!(
2675 env.render_ok(r#"raw_escape_sequence("]8;;"
2676 ++ "http://example.com"
2677 ++ "\e\\"
2678 ++ "Example"
2679 ++ "\x1b]8;;\x1B\\")"#),
2680 @r#"]8;;http://example.com\Example]8;;\"#);
2681 }
2682
2683 #[test]
2684 fn test_coalesce_function() {
2685 let mut env = TestTemplateEnv::new();
2686 env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
2687 env.add_keyword("empty_string", || L::wrap_string(Literal("".to_owned())));
2688 env.add_keyword("non_empty_string", || {
2689 L::wrap_string(Literal("a".to_owned()))
2690 });
2691
2692 insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
2693 insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
2694 insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
2695 insta::assert_snapshot!(
2696 env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
2697
2698 // "false" is not empty
2699 insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
2700
2701 // Error is not empty
2702 insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
2703 // but can be short-circuited
2704 insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
2705 }
2706
2707 #[test]
2708 fn test_concat_function() {
2709 let mut env = TestTemplateEnv::new();
2710 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2711 env.add_keyword("hidden", || L::wrap_boolean(Literal(false)));
2712 env.add_color("empty", crossterm::style::Color::DarkGreen);
2713 env.add_color("error", crossterm::style::Color::DarkRed);
2714 env.add_color("warning", crossterm::style::Color::DarkYellow);
2715
2716 insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
2717 insta::assert_snapshot!(
2718 env.render_ok(r#"concat(hidden, empty)"#),
2719 @"false[38;5;2mtrue[39m");
2720 insta::assert_snapshot!(
2721 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
2722 @"[38;5;3ma[39mb");
2723 }
2724
2725 #[test]
2726 fn test_separate_function() {
2727 let mut env = TestTemplateEnv::new();
2728 env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
2729 env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
2730 env.add_keyword("hidden", || L::wrap_boolean(Literal(false)));
2731 env.add_color("empty", crossterm::style::Color::DarkGreen);
2732 env.add_color("error", crossterm::style::Color::DarkRed);
2733 env.add_color("warning", crossterm::style::Color::DarkYellow);
2734
2735 insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
2736 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
2737 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
2738 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
2739 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
2740 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
2741 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
2742
2743 // Labeled
2744 insta::assert_snapshot!(
2745 env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
2746 @"[38;5;3ma[39m b");
2747
2748 // List template
2749 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
2750 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
2751
2752 // Nested separate
2753 insta::assert_snapshot!(
2754 env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
2755 insta::assert_snapshot!(
2756 env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
2757 insta::assert_snapshot!(
2758 env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
2759
2760 // Conditional template
2761 insta::assert_snapshot!(
2762 env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
2763 insta::assert_snapshot!(
2764 env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
2765 insta::assert_snapshot!(
2766 env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
2767 insta::assert_snapshot!(
2768 env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
2769
2770 // Separate keywords
2771 insta::assert_snapshot!(
2772 env.render_ok(r#"separate(" ", hidden, description, empty)"#),
2773 @"false [38;5;2mtrue[39m");
2774
2775 // Keyword as separator
2776 insta::assert_snapshot!(
2777 env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
2778 @"XfalseYfalseZ");
2779 }
2780
2781 #[test]
2782 fn test_surround_function() {
2783 let mut env = TestTemplateEnv::new();
2784 env.add_keyword("lt", || L::wrap_string(Literal("<".to_owned())));
2785 env.add_keyword("gt", || L::wrap_string(Literal(">".to_owned())));
2786 env.add_keyword("content", || L::wrap_string(Literal("content".to_owned())));
2787 env.add_keyword("empty_content", || L::wrap_string(Literal("".to_owned())));
2788 env.add_color("error", crossterm::style::Color::DarkRed);
2789 env.add_color("paren", crossterm::style::Color::Cyan);
2790
2791 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
2792 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
2793
2794 // Labeled
2795 insta::assert_snapshot!(
2796 env.render_ok(
2797 r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
2798 @"[38;5;14m([39m[38;5;1ma[39m[38;5;14m)[39m");
2799
2800 // Keyword
2801 insta::assert_snapshot!(
2802 env.render_ok(r#"surround(lt, gt, content)"#),
2803 @"<content>");
2804 insta::assert_snapshot!(
2805 env.render_ok(r#"surround(lt, gt, empty_content)"#),
2806 @"");
2807
2808 // Conditional template as content
2809 insta::assert_snapshot!(
2810 env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
2811 @"<empty>");
2812 insta::assert_snapshot!(
2813 env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
2814 @"");
2815 }
2816}