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