just playing with tangled
at diffofmerge 2816 lines 116 kB view raw
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(&timestamp, &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 &timestamp, &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(&timestamp)); 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 lazy 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 longlonglongword 2460 and short 2461 words 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 lazy 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 lazy 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 @"<Error: out of range integral type conversion attempted>"); 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 lazy 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 lazy 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 __a 2543 __b 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 __ab 2551 __c 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 XXa 2559 XXb 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 ABx 2569 ABy 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 @"{ foo}"); 2584 insta::assert_snapshot!( 2585 env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"), 2586 @"{foo }"); 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 @"==foo"); 2592 insta::assert_snapshot!( 2593 env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"), 2594 @"foo=="); 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 @"arbaz"); 2614 insta::assert_snapshot!( 2615 env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"), 2616 @"fobaz"); 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 @"text"); 2630 2631 // Evaluated property 2632 insta::assert_snapshot!( 2633 env.render_ok(r#"label("error".first_line(), "text")"#), 2634 @"text"); 2635 2636 // Template 2637 insta::assert_snapshot!( 2638 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#), 2639 @"text"); 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 @"falsetrue"); 2720 insta::assert_snapshot!( 2721 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#), 2722 @"ab"); 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 @"a 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 true"); 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 @"(a)"); 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}