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