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