1use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::io;
18use std::iter;
19use std::path::Path;
20use std::path::PathBuf;
21
22use bstr::BString;
23use bstr::ByteSlice as _;
24use itertools::Itertools as _;
25use jj_lib::backend::Signature;
26use jj_lib::backend::Timestamp;
27use jj_lib::config::ConfigGetResultExt as _;
28use jj_lib::config::ConfigNamePathBuf;
29use jj_lib::config::ConfigValue;
30use jj_lib::content_hash::blake2b_hash;
31use jj_lib::file_util;
32use jj_lib::hex_util;
33use jj_lib::op_store::TimestampRange;
34use jj_lib::settings::UserSettings;
35use jj_lib::str_util::StringPattern;
36use jj_lib::time_util::DatePattern;
37use serde::Deserialize;
38use serde::de::IntoDeserializer as _;
39
40use crate::config;
41use crate::formatter::FormatRecorder;
42use crate::formatter::Formatter;
43use crate::template_parser;
44use crate::template_parser::BinaryOp;
45use crate::template_parser::ExpressionKind;
46use crate::template_parser::ExpressionNode;
47use crate::template_parser::FunctionCallNode;
48use crate::template_parser::LambdaNode;
49use crate::template_parser::TemplateAliasesMap;
50use crate::template_parser::TemplateDiagnostics;
51use crate::template_parser::TemplateParseError;
52use crate::template_parser::TemplateParseErrorKind;
53use crate::template_parser::TemplateParseResult;
54use crate::template_parser::UnaryOp;
55use crate::templater::AnyTemplateProperty;
56use crate::templater::BoxedAnyProperty;
57use crate::templater::BoxedSerializeProperty;
58use crate::templater::BoxedTemplateProperty;
59use crate::templater::CoalesceTemplate;
60use crate::templater::ConcatTemplate;
61use crate::templater::ConditionalProperty;
62use crate::templater::Email;
63use crate::templater::HyperlinkTemplate;
64use crate::templater::JoinTemplate;
65use crate::templater::LabelTemplate;
66use crate::templater::ListMapProperty;
67use crate::templater::ListPropertyTemplate;
68use crate::templater::Literal;
69use crate::templater::PlainTextFormattedProperty;
70use crate::templater::PropertyPlaceholder;
71use crate::templater::RawEscapeSequenceTemplate;
72use crate::templater::ReformatTemplate;
73use crate::templater::RegexCaptures;
74use crate::templater::SeparateTemplate;
75use crate::templater::SizeHint;
76use crate::templater::Template;
77use crate::templater::TemplateFormatter;
78use crate::templater::TemplateProperty;
79use crate::templater::TemplatePropertyError;
80use crate::templater::TemplatePropertyExt as _;
81use crate::templater::TemplateRenderer;
82use crate::templater::TryList;
83use crate::templater::WrapTemplateProperty;
84use crate::text_util;
85use crate::text_util::write_replaced;
86use crate::time_util;
87
88pub trait TemplateLanguage<'a> {
94 type Property: CoreTemplatePropertyVar<'a> + 'a;
95
96 fn settings(&self) -> &UserSettings;
97
98 fn current_dir(&self) -> &Path;
100
101 fn build_function(
106 &self,
107 diagnostics: &mut TemplateDiagnostics,
108 build_ctx: &BuildContext<Self::Property>,
109 function: &FunctionCallNode,
110 ) -> TemplateParseResult<Self::Property>;
111
112 fn build_method(
115 &self,
116 diagnostics: &mut TemplateDiagnostics,
117 build_ctx: &BuildContext<Self::Property>,
118 property: Self::Property,
119 function: &FunctionCallNode,
120 ) -> TemplateParseResult<Self::Property>;
121}
122
123macro_rules! impl_property_wrappers {
131 ($kind:path $(=> $var:ident)? { $($body:tt)* }) => {
132 $crate::template_builder::_impl_property_wrappers_many!(
133 [], 'static, $kind $(=> $var)?, { $($body)* });
134 };
135 (<$a:lifetime $(, $p:lifetime)* $(, $q:ident)*>
137 $kind:path $(=> $var:ident)? { $($body:tt)* }) => {
138 $crate::template_builder::_impl_property_wrappers_many!(
139 [$a, $($p,)* $($q,)*], $a, $kind $(=> $var)?, { $($body)* });
140 };
141}
142
143macro_rules! _impl_property_wrappers_many {
144 ($ps:tt, $a:lifetime, $kind:path, { $( $var:ident($ty:ty), )* }) => {
147 $(
148 $crate::template_builder::_impl_property_wrappers_one!(
149 $ps, $a, $kind, $var, $ty, std::convert::identity);
150 )*
151 };
152 ($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $ignored_var:ident($ty:ty), )* }) => {
155 $(
156 $crate::template_builder::_impl_property_wrappers_one!(
157 $ps, $a, $kind, $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property);
158 )*
159 };
160}
161
162macro_rules! _impl_property_wrappers_one {
163 ([$($p:tt)*], $a:lifetime, $kind:path, $var:ident, $ty:ty, $inner:path) => {
164 impl<$($p)*> $crate::templater::WrapTemplateProperty<$a, $ty> for $kind {
165 fn wrap_property(property: $crate::templater::BoxedTemplateProperty<$a, $ty>) -> Self {
166 Self::$var($inner(property))
167 }
168 }
169 };
170}
171
172pub(crate) use _impl_property_wrappers_many;
173pub(crate) use _impl_property_wrappers_one;
174pub(crate) use impl_property_wrappers;
175
176pub trait CoreTemplatePropertyVar<'a>
178where
179 Self: WrapTemplateProperty<'a, BString>,
180 Self: WrapTemplateProperty<'a, Vec<BString>>,
181 Self: WrapTemplateProperty<'a, String>,
182 Self: WrapTemplateProperty<'a, Vec<String>>,
183 Self: WrapTemplateProperty<'a, bool>,
184 Self: WrapTemplateProperty<'a, i64>,
185 Self: WrapTemplateProperty<'a, Option<i64>>,
186 Self: WrapTemplateProperty<'a, ConfigValue>,
187 Self: WrapTemplateProperty<'a, Option<ConfigValue>>,
188 Self: WrapTemplateProperty<'a, PathBuf>,
189 Self: WrapTemplateProperty<'a, Option<PathBuf>>,
190 Self: WrapTemplateProperty<'a, Signature>,
191 Self: WrapTemplateProperty<'a, Email>,
192 Self: WrapTemplateProperty<'a, SizeHint>,
193 Self: WrapTemplateProperty<'a, RegexCaptures>,
194 Self: WrapTemplateProperty<'a, Timestamp>,
195 Self: WrapTemplateProperty<'a, TimestampRange>,
196{
197 fn wrap_template(template: Box<dyn Template + 'a>) -> Self;
198 fn wrap_any(property: BoxedAnyProperty<'a>) -> Self;
199 fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self;
200
201 fn type_name(&self) -> &'static str;
203
204 fn try_into_byte_string(self) -> Result<BoxedTemplateProperty<'a, BString>, Self>;
206 fn try_into_string(self) -> Result<BoxedTemplateProperty<'a, String>, Self>;
208 fn try_into_boolean(self) -> Result<BoxedTemplateProperty<'a, bool>, Self>;
210 fn try_into_integer(self) -> Result<BoxedTemplateProperty<'a, i64>, Self>;
211 fn try_into_timestamp(self) -> Result<BoxedTemplateProperty<'a, Timestamp>, Self>;
212
213 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>>;
214 fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
215
216 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>>;
218
219 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>>;
221}
222
223pub enum CoreTemplatePropertyKind<'a> {
224 ByteString(BoxedTemplateProperty<'a, BString>),
225 ByteStringList(BoxedTemplateProperty<'a, Vec<BString>>),
226 String(BoxedTemplateProperty<'a, String>),
227 StringList(BoxedTemplateProperty<'a, Vec<String>>),
228 Boolean(BoxedTemplateProperty<'a, bool>),
229 Integer(BoxedTemplateProperty<'a, i64>),
230 IntegerOpt(BoxedTemplateProperty<'a, Option<i64>>),
231 ConfigValue(BoxedTemplateProperty<'a, ConfigValue>),
232 ConfigValueOpt(BoxedTemplateProperty<'a, Option<ConfigValue>>),
233 FsPath(BoxedTemplateProperty<'a, PathBuf>),
234 FsPathOpt(BoxedTemplateProperty<'a, Option<PathBuf>>),
235 Signature(BoxedTemplateProperty<'a, Signature>),
236 Email(BoxedTemplateProperty<'a, Email>),
237 SizeHint(BoxedTemplateProperty<'a, SizeHint>),
238 RegexCaptures(BoxedTemplateProperty<'a, RegexCaptures>),
239 Timestamp(BoxedTemplateProperty<'a, Timestamp>),
240 TimestampRange(BoxedTemplateProperty<'a, TimestampRange>),
241
242 Template(Box<dyn Template + 'a>),
253 Any(BoxedAnyProperty<'a>),
254 AnyList(BoxedAnyProperty<'a>),
255}
256
257macro_rules! impl_core_property_wrappers {
262 ($($head:tt)+) => {
263 $crate::template_builder::impl_property_wrappers!($($head)+ {
264 ByteString(bstr::BString),
265 ByteStringList(Vec<bstr::BString>),
266 String(String),
267 StringList(Vec<String>),
268 Boolean(bool),
269 Integer(i64),
270 IntegerOpt(Option<i64>),
271 ConfigValue(jj_lib::config::ConfigValue),
272 ConfigValueOpt(Option<jj_lib::config::ConfigValue>),
273 FsPath(std::path::PathBuf),
274 FsPathOpt(Option<std::path::PathBuf>),
275 Signature(jj_lib::backend::Signature),
276 Email($crate::templater::Email),
277 SizeHint($crate::templater::SizeHint),
278 RegexCaptures($crate::templater::RegexCaptures),
279 Timestamp(jj_lib::backend::Timestamp),
280 TimestampRange(jj_lib::op_store::TimestampRange),
281 });
282 };
283}
284
285pub(crate) use impl_core_property_wrappers;
286
287impl_core_property_wrappers!(<'a> CoreTemplatePropertyKind<'a>);
288
289impl<'a> CoreTemplatePropertyVar<'a> for CoreTemplatePropertyKind<'a> {
290 fn wrap_template(template: Box<dyn Template + 'a>) -> Self {
291 Self::Template(template)
292 }
293
294 fn wrap_any(property: BoxedAnyProperty<'a>) -> Self {
295 Self::Any(property)
296 }
297
298 fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self {
299 Self::AnyList(property)
300 }
301
302 fn type_name(&self) -> &'static str {
303 match self {
304 Self::ByteString(_) => "ByteString",
305 Self::ByteStringList(_) => "List<ByteString>",
306 Self::String(_) => "String",
307 Self::StringList(_) => "List<String>",
308 Self::Boolean(_) => "Boolean",
309 Self::Integer(_) => "Integer",
310 Self::IntegerOpt(_) => "Option<Integer>",
311 Self::ConfigValue(_) => "ConfigValue",
312 Self::ConfigValueOpt(_) => "Option<ConfigValue>",
313 Self::FsPath(_) => "FsPath",
314 Self::FsPathOpt(_) => "Option<FsPath>",
315 Self::Signature(_) => "Signature",
316 Self::Email(_) => "Email",
317 Self::SizeHint(_) => "SizeHint",
318 Self::RegexCaptures(_) => "RegexCaptures",
319 Self::Timestamp(_) => "Timestamp",
320 Self::TimestampRange(_) => "TimestampRange",
321 Self::Template(_) => "Template",
322 Self::Any(_) => "Any",
323 Self::AnyList(_) => "AnyList",
324 }
325 }
326
327 fn try_into_byte_string(self) -> Result<BoxedTemplateProperty<'a, BString>, Self> {
328 match self {
329 Self::ByteString(property) => Ok(property),
330 _ => Err(self),
331 }
332 }
333
334 fn try_into_string(self) -> Result<BoxedTemplateProperty<'a, String>, Self> {
335 match self {
336 Self::String(property) => Ok(property),
337 _ => Err(self),
338 }
339 }
340
341 fn try_into_boolean(self) -> Result<BoxedTemplateProperty<'a, bool>, Self> {
342 match self {
343 Self::ByteString(property) => Ok(property.map(|s| !s.is_empty()).into_dyn()),
344 Self::ByteStringList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()),
345 Self::String(property) => Ok(property.map(|s| !s.is_empty()).into_dyn()),
346 Self::StringList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()),
347 Self::Boolean(property) => Ok(property),
348 Self::Integer(_) => Err(self),
349 Self::IntegerOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
350 Self::ConfigValue(_) => Err(self),
351 Self::ConfigValueOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
352 Self::FsPath(_) => Err(self),
353 Self::FsPathOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
354 Self::Signature(_) => Err(self),
355 Self::Email(property) => Ok(property.map(|e| !e.0.is_empty()).into_dyn()),
356 Self::SizeHint(_) => Err(self),
357 Self::RegexCaptures(_) => Err(self),
358 Self::Timestamp(_) => Err(self),
359 Self::TimestampRange(_) => Err(self),
360 Self::Template(_) => Err(self),
364 Self::Any(_) => Err(self),
365 Self::AnyList(_) => Err(self),
366 }
367 }
368
369 fn try_into_integer(self) -> Result<BoxedTemplateProperty<'a, i64>, Self> {
370 match self {
371 Self::Integer(property) => Ok(property),
372 Self::IntegerOpt(property) => Ok(property.try_unwrap("Integer").into_dyn()),
373 _ => Err(self),
374 }
375 }
376
377 fn try_into_timestamp(self) -> Result<BoxedTemplateProperty<'a, Timestamp>, Self> {
378 match self {
379 Self::Timestamp(property) => Ok(property),
380 _ => Err(self),
381 }
382 }
383
384 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
385 match self {
386 Self::ByteString(property) => Some(property.into_serialize()),
387 Self::ByteStringList(property) => Some(property.into_serialize()),
388 Self::String(property) => Some(property.into_serialize()),
389 Self::StringList(property) => Some(property.into_serialize()),
390 Self::Boolean(property) => Some(property.into_serialize()),
391 Self::Integer(property) => Some(property.into_serialize()),
392 Self::IntegerOpt(property) => Some(property.into_serialize()),
393 Self::ConfigValue(property) => {
394 Some(property.map(config::to_serializable_value).into_serialize())
395 }
396 Self::ConfigValueOpt(property) => Some(
397 property
398 .map(|opt| opt.map(config::to_serializable_value))
399 .into_serialize(),
400 ),
401 Self::FsPath(property) => Some(property.into_serialize()),
402 Self::FsPathOpt(property) => Some(property.into_serialize()),
403 Self::Signature(property) => Some(property.into_serialize()),
404 Self::Email(property) => Some(property.into_serialize()),
405 Self::SizeHint(property) => Some(property.into_serialize()),
406 Self::RegexCaptures(_) => None,
407 Self::Timestamp(property) => Some(property.into_serialize()),
408 Self::TimestampRange(property) => Some(property.into_serialize()),
409 Self::Template(_) => None,
410 Self::Any(property) => property.try_into_serialize(),
411 Self::AnyList(property) => property.try_into_serialize(),
412 }
413 }
414
415 fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
416 match self {
417 Self::ByteString(property) => Some(property.into_template()),
418 Self::ByteStringList(property) => Some(property.into_template()),
419 Self::String(property) => Some(property.into_template()),
420 Self::StringList(property) => Some(property.into_template()),
421 Self::Boolean(property) => Some(property.into_template()),
422 Self::Integer(property) => Some(property.into_template()),
423 Self::IntegerOpt(property) => Some(property.into_template()),
424 Self::ConfigValue(property) => Some(property.into_template()),
425 Self::ConfigValueOpt(property) => Some(property.into_template()),
426 Self::FsPath(property) => Some(property.into_template()),
427 Self::FsPathOpt(property) => Some(property.into_template()),
428 Self::Signature(property) => Some(property.into_template()),
429 Self::Email(property) => Some(property.into_template()),
430 Self::SizeHint(_) => None,
431 Self::RegexCaptures(_) => None,
432 Self::Timestamp(property) => Some(property.into_template()),
433 Self::TimestampRange(property) => Some(property.into_template()),
434 Self::Template(template) => Some(template),
435 Self::Any(property) => property.try_into_template(),
436 Self::AnyList(property) => property.try_into_template(),
437 }
438 }
439
440 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
441 match (self, other) {
442 (Self::ByteString(lhs), Self::ByteString(rhs)) => {
443 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
444 }
445 (Self::ByteString(lhs), Self::String(rhs)) => {
446 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
447 }
448 (Self::String(lhs), Self::ByteString(rhs)) => {
449 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
450 }
451 (Self::String(lhs), Self::String(rhs)) => {
452 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
453 }
454 (Self::String(lhs), Self::Email(rhs)) => {
455 Some((lhs, rhs).map(|(l, r)| l == r.0).into_dyn())
456 }
457 (Self::Boolean(lhs), Self::Boolean(rhs)) => {
458 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
459 }
460 (Self::Integer(lhs), Self::Integer(rhs)) => {
461 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
462 }
463 (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
464 Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
465 }
466 (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
467 Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
468 }
469 (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
470 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
471 }
472 (Self::Email(lhs), Self::Email(rhs)) => {
473 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
474 }
475 (Self::Email(lhs), Self::String(rhs)) => {
476 Some((lhs, rhs).map(|(l, r)| l.0 == r).into_dyn())
477 }
478 (Self::ByteString(_), _) => None,
479 (Self::ByteStringList(_), _) => None,
480 (Self::String(_), _) => None,
481 (Self::StringList(_), _) => None,
482 (Self::Boolean(_), _) => None,
483 (Self::Integer(_), _) => None,
484 (Self::IntegerOpt(_), _) => None,
485 (Self::ConfigValue(_), _) => None,
486 (Self::ConfigValueOpt(_), _) => None,
487 (Self::FsPath(_), _) => None,
488 (Self::FsPathOpt(_), _) => None,
489 (Self::Signature(_), _) => None,
490 (Self::Email(_), _) => None,
491 (Self::SizeHint(_), _) => None,
492 (Self::RegexCaptures(_), _) => None,
493 (Self::Timestamp(_), _) => None,
494 (Self::TimestampRange(_), _) => None,
495 (Self::Template(_), _) => None,
496 (Self::Any(_), _) => None,
497 (Self::AnyList(_), _) => None,
498 }
499 }
500
501 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
502 match (self, other) {
503 (Self::Integer(lhs), Self::Integer(rhs)) => {
504 Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
505 }
506 (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
507 Some((lhs, rhs).map(|(l, r)| Some(l).cmp(&r)).into_dyn())
508 }
509 (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
510 Some((lhs, rhs).map(|(l, r)| l.cmp(&Some(r))).into_dyn())
511 }
512 (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
513 Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
514 }
515 (Self::ByteString(_), _) => None,
516 (Self::ByteStringList(_), _) => None,
517 (Self::String(_), _) => None,
518 (Self::StringList(_), _) => None,
519 (Self::Boolean(_), _) => None,
520 (Self::Integer(_), _) => None,
521 (Self::IntegerOpt(_), _) => None,
522 (Self::ConfigValue(_), _) => None,
523 (Self::ConfigValueOpt(_), _) => None,
524 (Self::FsPath(_), _) => None,
525 (Self::FsPathOpt(_), _) => None,
526 (Self::Signature(_), _) => None,
527 (Self::Email(_), _) => None,
528 (Self::SizeHint(_), _) => None,
529 (Self::RegexCaptures(_), _) => None,
530 (Self::Timestamp(_), _) => None,
531 (Self::TimestampRange(_), _) => None,
532 (Self::Template(_), _) => None,
533 (Self::Any(_), _) => None,
534 (Self::AnyList(_), _) => None,
535 }
536 }
537}
538
539pub type TemplateBuildFunctionFn<'a, L, P> =
546 fn(&L, &mut TemplateDiagnostics, &BuildContext<P>, &FunctionCallNode) -> TemplateParseResult<P>;
547
548type BuildMethodFn<'a, L, T, P> = fn(
549 &L,
550 &mut TemplateDiagnostics,
551 &BuildContext<P>,
552 T,
553 &FunctionCallNode,
554) -> TemplateParseResult<P>;
555
556pub type TemplateBuildMethodFn<'a, L, T, P> = BuildMethodFn<'a, L, BoxedTemplateProperty<'a, T>, P>;
558
559pub type BuildTemplateMethodFn<'a, L, P> = BuildMethodFn<'a, L, Box<dyn Template + 'a>, P>;
561
562pub type BuildAnyMethodFn<'a, L, P> = BuildMethodFn<'a, L, BoxedAnyProperty<'a>, P>;
564
565pub type TemplateBuildFunctionFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
567 HashMap<&'static str, TemplateBuildFunctionFn<'a, L, P>>;
568
569pub type TemplateBuildMethodFnMap<'a, L, T, P = <L as TemplateLanguage<'a>>::Property> =
571 HashMap<&'static str, TemplateBuildMethodFn<'a, L, T, P>>;
572
573pub type BuildTemplateMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
575 HashMap<&'static str, BuildTemplateMethodFn<'a, L, P>>;
576
577pub type BuildAnyMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
579 HashMap<&'static str, BuildAnyMethodFn<'a, L, P>>;
580
581pub struct CoreTemplateBuildFnTable<'a, L: ?Sized, P = <L as TemplateLanguage<'a>>::Property> {
583 pub functions: TemplateBuildFunctionFnMap<'a, L, P>,
584 pub byte_string_methods: TemplateBuildMethodFnMap<'a, L, BString, P>,
585 pub byte_string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<BString>, P>,
586 pub string_methods: TemplateBuildMethodFnMap<'a, L, String, P>,
587 pub string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<String>, P>,
588 pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool, P>,
589 pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64, P>,
590 pub config_value_methods: TemplateBuildMethodFnMap<'a, L, ConfigValue, P>,
591 pub fs_path_methods: TemplateBuildMethodFnMap<'a, L, PathBuf, P>,
592 pub email_methods: TemplateBuildMethodFnMap<'a, L, Email, P>,
593 pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature, P>,
594 pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint, P>,
595 pub regex_captures_methods: TemplateBuildMethodFnMap<'a, L, RegexCaptures, P>,
596 pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp, P>,
597 pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange, P>,
598 pub template_methods: BuildTemplateMethodFnMap<'a, L, P>,
599 pub any_methods: BuildAnyMethodFnMap<'a, L, P>,
600 pub any_list_methods: BuildAnyMethodFnMap<'a, L, P>,
601}
602
603pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
604 for (name, function) in extension {
605 if base.insert(name, function).is_some() {
606 panic!("Conflicting template definitions for '{name}' function");
607 }
608 }
609}
610
611impl<L: ?Sized, P> CoreTemplateBuildFnTable<'_, L, P> {
612 pub fn empty() -> Self {
613 Self {
614 functions: HashMap::new(),
615 byte_string_methods: HashMap::new(),
616 byte_string_list_methods: HashMap::new(),
617 string_methods: HashMap::new(),
618 string_list_methods: HashMap::new(),
619 boolean_methods: HashMap::new(),
620 integer_methods: HashMap::new(),
621 config_value_methods: HashMap::new(),
622 fs_path_methods: HashMap::new(),
623 signature_methods: HashMap::new(),
624 email_methods: HashMap::new(),
625 size_hint_methods: HashMap::new(),
626 regex_captures_methods: HashMap::new(),
627 timestamp_methods: HashMap::new(),
628 timestamp_range_methods: HashMap::new(),
629 template_methods: HashMap::new(),
630 any_methods: HashMap::new(),
631 any_list_methods: HashMap::new(),
632 }
633 }
634
635 pub fn merge(&mut self, other: Self) {
636 let Self {
637 functions,
638 byte_string_methods,
639 byte_string_list_methods,
640 string_methods,
641 string_list_methods,
642 boolean_methods,
643 integer_methods,
644 config_value_methods,
645 fs_path_methods,
646 signature_methods,
647 email_methods,
648 size_hint_methods,
649 regex_captures_methods,
650 timestamp_methods,
651 timestamp_range_methods,
652 template_methods,
653 any_methods,
654 any_list_methods,
655 } = other;
656
657 merge_fn_map(&mut self.functions, functions);
658 merge_fn_map(&mut self.byte_string_methods, byte_string_methods);
659 merge_fn_map(&mut self.byte_string_list_methods, byte_string_list_methods);
660 merge_fn_map(&mut self.string_methods, string_methods);
661 merge_fn_map(&mut self.string_list_methods, string_list_methods);
662 merge_fn_map(&mut self.boolean_methods, boolean_methods);
663 merge_fn_map(&mut self.integer_methods, integer_methods);
664 merge_fn_map(&mut self.config_value_methods, config_value_methods);
665 merge_fn_map(&mut self.fs_path_methods, fs_path_methods);
666 merge_fn_map(&mut self.signature_methods, signature_methods);
667 merge_fn_map(&mut self.email_methods, email_methods);
668 merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
669 merge_fn_map(&mut self.regex_captures_methods, regex_captures_methods);
670 merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
671 merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
672 merge_fn_map(&mut self.template_methods, template_methods);
673 merge_fn_map(&mut self.any_methods, any_methods);
674 merge_fn_map(&mut self.any_list_methods, any_list_methods);
675 }
676}
677
678impl<'a, L> CoreTemplateBuildFnTable<'a, L, L::Property>
679where
680 L: TemplateLanguage<'a> + ?Sized,
681{
682 pub fn builtin() -> Self {
684 Self {
685 functions: builtin_functions(),
686 byte_string_methods: builtin_byte_string_methods(),
687 byte_string_list_methods: builtin_formattable_list_methods(),
688 string_methods: builtin_string_methods(),
689 string_list_methods: builtin_formattable_list_methods(),
690 boolean_methods: HashMap::new(),
691 integer_methods: HashMap::new(),
692 config_value_methods: builtin_config_value_methods(),
693 fs_path_methods: builtin_fs_path_methods(),
694 signature_methods: builtin_signature_methods(),
695 email_methods: builtin_email_methods(),
696 size_hint_methods: builtin_size_hint_methods(),
697 regex_captures_methods: builtin_regex_captures_methods(),
698 timestamp_methods: builtin_timestamp_methods(),
699 timestamp_range_methods: builtin_timestamp_range_methods(),
700 template_methods: HashMap::new(),
701 any_methods: HashMap::new(),
702 any_list_methods: builtin_any_list_methods(),
703 }
704 }
705
706 pub fn build_function(
708 &self,
709 language: &L,
710 diagnostics: &mut TemplateDiagnostics,
711 build_ctx: &BuildContext<L::Property>,
712 function: &FunctionCallNode,
713 ) -> TemplateParseResult<L::Property> {
714 let table = &self.functions;
715 let build = template_parser::lookup_function(table, function)?;
716 build(language, diagnostics, build_ctx, function)
717 }
718
719 pub fn build_method(
722 &self,
723 language: &L,
724 diagnostics: &mut TemplateDiagnostics,
725 build_ctx: &BuildContext<L::Property>,
726 property: CoreTemplatePropertyKind<'a>,
727 function: &FunctionCallNode,
728 ) -> TemplateParseResult<L::Property> {
729 let type_name = property.type_name();
730 match property {
731 CoreTemplatePropertyKind::ByteString(property) => {
732 let table = &self.byte_string_methods;
733 let build = template_parser::lookup_method(type_name, table, function)?;
734 build(language, diagnostics, build_ctx, property, function)
735 }
736 CoreTemplatePropertyKind::ByteStringList(property) => {
737 let table = &self.byte_string_list_methods;
738 let build = template_parser::lookup_method(type_name, table, function)?;
739 build(language, diagnostics, build_ctx, property, function)
740 }
741 CoreTemplatePropertyKind::String(property) => {
742 let table = &self.string_methods;
743 let build = template_parser::lookup_method(type_name, table, function)?;
744 build(language, diagnostics, build_ctx, property, function)
745 }
746 CoreTemplatePropertyKind::StringList(property) => {
747 let table = &self.string_list_methods;
748 let build = template_parser::lookup_method(type_name, table, function)?;
749 build(language, diagnostics, build_ctx, property, function)
750 }
751 CoreTemplatePropertyKind::Boolean(property) => {
752 let table = &self.boolean_methods;
753 let build = template_parser::lookup_method(type_name, table, function)?;
754 build(language, diagnostics, build_ctx, property, function)
755 }
756 CoreTemplatePropertyKind::Integer(property) => {
757 let table = &self.integer_methods;
758 let build = template_parser::lookup_method(type_name, table, function)?;
759 build(language, diagnostics, build_ctx, property, function)
760 }
761 CoreTemplatePropertyKind::IntegerOpt(property) => {
762 let type_name = "Integer";
763 let table = &self.integer_methods;
764 let build = template_parser::lookup_method(type_name, table, function)?;
765 let inner_property = property.try_unwrap(type_name).into_dyn();
766 build(language, diagnostics, build_ctx, inner_property, function)
767 }
768 CoreTemplatePropertyKind::ConfigValue(property) => {
769 let table = &self.config_value_methods;
770 let build = template_parser::lookup_method(type_name, table, function)?;
771 build(language, diagnostics, build_ctx, property, function)
772 }
773 CoreTemplatePropertyKind::ConfigValueOpt(property) => {
774 let type_name = "ConfigValue";
775 let table = &self.config_value_methods;
776 let build = template_parser::lookup_method(type_name, table, function)?;
777 let inner_property = property.try_unwrap(type_name).into_dyn();
778 build(language, diagnostics, build_ctx, inner_property, function)
779 }
780 CoreTemplatePropertyKind::FsPath(property) => {
781 let table = &self.fs_path_methods;
782 let build = template_parser::lookup_method(type_name, table, function)?;
783 build(language, diagnostics, build_ctx, property, function)
784 }
785 CoreTemplatePropertyKind::FsPathOpt(property) => {
786 let type_name = "FsPath";
787 let table = &self.fs_path_methods;
788 let build = template_parser::lookup_method(type_name, table, function)?;
789 let inner_property = property.try_unwrap(type_name).into_dyn();
790 build(language, diagnostics, build_ctx, inner_property, function)
791 }
792 CoreTemplatePropertyKind::Signature(property) => {
793 let table = &self.signature_methods;
794 let build = template_parser::lookup_method(type_name, table, function)?;
795 build(language, diagnostics, build_ctx, property, function)
796 }
797 CoreTemplatePropertyKind::Email(property) => {
798 let table = &self.email_methods;
799 let build = template_parser::lookup_method(type_name, table, function)?;
800 build(language, diagnostics, build_ctx, property, function)
801 }
802 CoreTemplatePropertyKind::SizeHint(property) => {
803 let table = &self.size_hint_methods;
804 let build = template_parser::lookup_method(type_name, table, function)?;
805 build(language, diagnostics, build_ctx, property, function)
806 }
807 CoreTemplatePropertyKind::RegexCaptures(property) => {
808 let table = &self.regex_captures_methods;
809 let build = template_parser::lookup_method(type_name, table, function)?;
810 build(language, diagnostics, build_ctx, property, function)
811 }
812 CoreTemplatePropertyKind::Timestamp(property) => {
813 let table = &self.timestamp_methods;
814 let build = template_parser::lookup_method(type_name, table, function)?;
815 build(language, diagnostics, build_ctx, property, function)
816 }
817 CoreTemplatePropertyKind::TimestampRange(property) => {
818 let table = &self.timestamp_range_methods;
819 let build = template_parser::lookup_method(type_name, table, function)?;
820 build(language, diagnostics, build_ctx, property, function)
821 }
822 CoreTemplatePropertyKind::Template(template) => {
823 let table = &self.template_methods;
824 let build = template_parser::lookup_method(type_name, table, function)?;
825 build(language, diagnostics, build_ctx, template, function)
826 }
827 CoreTemplatePropertyKind::Any(property) => {
828 let table = &self.any_methods;
829 let build = template_parser::lookup_method(type_name, table, function)?;
830 build(language, diagnostics, build_ctx, property, function)
831 }
832 CoreTemplatePropertyKind::AnyList(property) => {
833 let table = &self.any_list_methods;
834 let build = template_parser::lookup_method(type_name, table, function)?;
835 build(language, diagnostics, build_ctx, property, function)
836 }
837 }
838 }
839}
840
841pub struct Expression<P> {
843 property: P,
844 labels: Vec<String>,
845}
846
847impl<P> Expression<P> {
848 fn unlabeled(property: P) -> Self {
849 let labels = vec![];
850 Self { property, labels }
851 }
852
853 fn with_label(property: P, label: impl Into<String>) -> Self {
854 let labels = vec![label.into()];
855 Self { property, labels }
856 }
857}
858
859impl<'a, P: CoreTemplatePropertyVar<'a>> Expression<P> {
860 pub fn type_name(&self) -> &'static str {
861 self.property.type_name()
862 }
863
864 pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
865 self.property.try_into_boolean().ok()
866 }
867
868 pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
869 self.property.try_into_integer().ok()
870 }
871
872 pub fn try_into_timestamp(self) -> Option<BoxedTemplateProperty<'a, Timestamp>> {
873 self.property.try_into_timestamp().ok()
874 }
875
876 pub fn try_into_byte_stringify(self) -> Option<BoxedTemplateProperty<'a, BString>> {
879 let property = match self.property.try_into_byte_string() {
880 Ok(bytes_property) => return Some(bytes_property),
881 Err(property) => property,
882 };
883 let property = match property.try_into_string() {
884 Ok(string_property) => return Some(string_property.map(BString::from).into_dyn()),
885 Err(property) => property,
886 };
887 let template = property.try_into_template()?;
888 Some(PlainTextFormattedProperty::new(template).into_dyn())
889 }
890
891 pub fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
893 let from_bytes =
894 |s: BString| Ok(String::from_utf8(s.into()).map_err(|err| err.utf8_error())?);
895 let property = match self.property.try_into_string() {
896 Ok(string_property) => return Some(string_property),
897 Err(property) => property,
898 };
899 let property = match property.try_into_byte_string() {
900 Ok(bytes_property) => return Some(bytes_property.and_then(from_bytes).into_dyn()),
901 Err(property) => property,
902 };
903 let template = property.try_into_template()?;
904 Some(
905 PlainTextFormattedProperty::new(template)
906 .and_then(from_bytes)
907 .into_dyn(),
908 )
909 }
910
911 pub fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
912 self.property.try_into_serialize()
913 }
914
915 pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
916 let template = self.property.try_into_template()?;
917 if self.labels.is_empty() {
918 Some(template)
919 } else {
920 Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
921 }
922 }
923
924 pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
925 self.property.try_into_eq(other.property)
926 }
927
928 pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
929 self.property.try_into_cmp(other.property)
930 }
931}
932
933impl<'a, P: CoreTemplatePropertyVar<'a>> AnyTemplateProperty<'a> for Expression<P> {
934 fn try_into_serialize(self: Box<Self>) -> Option<BoxedSerializeProperty<'a>> {
935 (*self).try_into_serialize()
936 }
937
938 fn try_into_template(self: Box<Self>) -> Option<Box<dyn Template + 'a>> {
939 (*self).try_into_template()
940 }
941
942 fn try_join(self: Box<Self>, _: Box<dyn Template + 'a>) -> Option<Box<dyn Template + 'a>> {
943 None
944 }
945}
946
947pub struct BuildContext<'i, P> {
949 local_variables: HashMap<&'i str, &'i dyn Fn() -> P>,
951 self_variable: &'i dyn Fn() -> P,
956}
957
958fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
959 language: &L,
960 diagnostics: &mut TemplateDiagnostics,
961 build_ctx: &BuildContext<L::Property>,
962 name: &str,
963 name_span: pest::Span<'_>,
964) -> TemplateParseResult<L::Property> {
965 let self_property = (build_ctx.self_variable)();
967 let function = FunctionCallNode {
968 name,
969 name_span,
970 args: vec![],
971 keyword_args: vec![],
972 args_span: name_span.end_pos().span(&name_span.end_pos()),
973 };
974 language
975 .build_method(diagnostics, build_ctx, self_property, &function)
976 .map_err(|err| match err.kind() {
977 TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
978 let kind = TemplateParseErrorKind::NoSuchKeyword {
979 name: name.to_owned(),
980 candidates: candidates.clone(),
982 };
983 TemplateParseError::with_span(kind, name_span)
984 }
985 TemplateParseErrorKind::InvalidArguments { .. } => {
988 let kind = TemplateParseErrorKind::NoSuchKeyword {
989 name: name.to_owned(),
990 candidates: vec![format!("self.{name}(..)")],
992 };
993 TemplateParseError::with_span(kind, name_span)
994 }
995 _ => err,
997 })
998}
999
1000fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
1001 language: &L,
1002 diagnostics: &mut TemplateDiagnostics,
1003 build_ctx: &BuildContext<L::Property>,
1004 op: UnaryOp,
1005 arg_node: &ExpressionNode,
1006) -> TemplateParseResult<L::Property> {
1007 match op {
1008 UnaryOp::LogicalNot => {
1009 let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
1010 Ok(arg.map(|v| !v).into_dyn_wrapped())
1011 }
1012 UnaryOp::Negate => {
1013 let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
1014 let out = arg.and_then(|v| {
1015 v.checked_neg()
1016 .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
1017 });
1018 Ok(out.into_dyn_wrapped())
1019 }
1020 }
1021}
1022
1023fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
1024 language: &L,
1025 diagnostics: &mut TemplateDiagnostics,
1026 build_ctx: &BuildContext<L::Property>,
1027 op: BinaryOp,
1028 lhs_node: &ExpressionNode,
1029 rhs_node: &ExpressionNode,
1030 span: pest::Span<'_>,
1031) -> TemplateParseResult<L::Property> {
1032 match op {
1033 BinaryOp::LogicalOr => {
1034 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
1035 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
1036 let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
1037 Ok(out.into_dyn_wrapped())
1038 }
1039 BinaryOp::LogicalAnd => {
1040 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
1041 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
1042 let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
1043 Ok(out.into_dyn_wrapped())
1044 }
1045 BinaryOp::Eq | BinaryOp::Ne => {
1046 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
1047 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
1048 let lty = lhs.type_name();
1049 let rty = rhs.type_name();
1050 let eq = lhs.try_into_eq(rhs).ok_or_else(|| {
1051 let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
1052 TemplateParseError::expression(message, span)
1053 })?;
1054 let out = match op {
1055 BinaryOp::Eq => eq.into_dyn(),
1056 BinaryOp::Ne => eq.map(|eq| !eq).into_dyn(),
1057 _ => unreachable!(),
1058 };
1059 Ok(L::Property::wrap_property(out))
1060 }
1061 BinaryOp::Ge | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Lt => {
1062 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
1063 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
1064 let lty = lhs.type_name();
1065 let rty = rhs.type_name();
1066 let cmp = lhs.try_into_cmp(rhs).ok_or_else(|| {
1067 let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
1068 TemplateParseError::expression(message, span)
1069 })?;
1070 let out = match op {
1071 BinaryOp::Ge => cmp.map(|ordering| ordering.is_ge()).into_dyn(),
1072 BinaryOp::Gt => cmp.map(|ordering| ordering.is_gt()).into_dyn(),
1073 BinaryOp::Le => cmp.map(|ordering| ordering.is_le()).into_dyn(),
1074 BinaryOp::Lt => cmp.map(|ordering| ordering.is_lt()).into_dyn(),
1075 _ => unreachable!(),
1076 };
1077 Ok(L::Property::wrap_property(out))
1078 }
1079 BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
1080 let lhs = expect_integer_expression(language, diagnostics, build_ctx, lhs_node)?;
1081 let rhs = expect_integer_expression(language, diagnostics, build_ctx, rhs_node)?;
1082 let build = |op: fn(i64, i64) -> Option<i64>, msg: fn(i64) -> &'static str| {
1083 (lhs, rhs).and_then(move |(l, r)| {
1084 op(l, r).ok_or_else(|| TemplatePropertyError(msg(r).into()))
1085 })
1086 };
1087 let out = match op {
1088 BinaryOp::Add => build(i64::checked_add, |_| "Attempt to add with overflow"),
1089 BinaryOp::Sub => build(i64::checked_sub, |_| "Attempt to subtract with overflow"),
1090 BinaryOp::Mul => build(i64::checked_mul, |_| "Attempt to multiply with overflow"),
1091 BinaryOp::Div => build(i64::checked_div, |r| {
1092 if r == 0 {
1093 "Attempt to divide by zero"
1094 } else {
1095 "Attempt to divide with overflow"
1096 }
1097 }),
1098 BinaryOp::Rem => build(i64::checked_rem, |r| {
1099 if r == 0 {
1100 "Attempt to divide by zero"
1101 } else {
1102 "Attempt to divide with overflow"
1103 }
1104 }),
1105 _ => unreachable!(),
1106 };
1107 Ok(out.into_dyn_wrapped())
1108 }
1109 }
1110}
1111
1112fn builtin_byte_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1113-> TemplateBuildMethodFnMap<'a, L, BString> {
1114 let mut map = TemplateBuildMethodFnMap::<L, BString>::new();
1117 map.insert(
1118 "len",
1119 |_language, _diagnostics, _build_ctx, self_property, function| {
1120 function.expect_no_arguments()?;
1121 let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
1122 Ok(out_property.into_dyn_wrapped())
1123 },
1124 );
1125 map.insert(
1126 "contains",
1127 |language, diagnostics, build_ctx, self_property, function| {
1128 let [needle_node] = function.expect_exact_arguments()?;
1129 let needle_property =
1131 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1132 let out_property = (self_property, needle_property)
1133 .map(|(haystack, needle)| haystack.contains_str(&needle));
1134 Ok(out_property.into_dyn_wrapped())
1135 },
1136 );
1137 map.insert(
1138 "match",
1139 |_language, _diagnostics, _build_ctx, self_property, function| {
1140 let [needle_node] = function.expect_exact_arguments()?;
1141 let needle = template_parser::expect_string_pattern(needle_node)?;
1142 let out_property = match_string_like(self_property, needle);
1143 Ok(out_property.into_dyn_wrapped())
1144 },
1145 );
1146 map.insert(
1147 "starts_with",
1148 |language, diagnostics, build_ctx, self_property, function| {
1149 let [needle_node] = function.expect_exact_arguments()?;
1150 let needle_property =
1151 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1152 let out_property = (self_property, needle_property)
1153 .map(|(haystack, needle)| haystack.starts_with(&needle));
1154 Ok(out_property.into_dyn_wrapped())
1155 },
1156 );
1157 map.insert(
1158 "ends_with",
1159 |language, diagnostics, build_ctx, self_property, function| {
1160 let [needle_node] = function.expect_exact_arguments()?;
1161 let needle_property =
1162 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1163 let out_property = (self_property, needle_property)
1164 .map(|(haystack, needle)| haystack.ends_with(&needle));
1165 Ok(out_property.into_dyn_wrapped())
1166 },
1167 );
1168 map.insert(
1169 "remove_prefix",
1170 |language, diagnostics, build_ctx, self_property, function| {
1171 let [needle_node] = function.expect_exact_arguments()?;
1172 let needle_property =
1173 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1174 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1175 haystack
1176 .strip_prefix(&**needle)
1177 .map(BString::from)
1178 .unwrap_or(haystack)
1179 });
1180 Ok(out_property.into_dyn_wrapped())
1181 },
1182 );
1183 map.insert(
1184 "remove_suffix",
1185 |language, diagnostics, build_ctx, self_property, function| {
1186 let [needle_node] = function.expect_exact_arguments()?;
1187 let needle_property =
1188 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1189 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1190 haystack
1191 .strip_suffix(&**needle)
1192 .map(BString::from)
1193 .unwrap_or(haystack)
1194 });
1195 Ok(out_property.into_dyn_wrapped())
1196 },
1197 );
1198 map.insert(
1199 "trim",
1200 |_language, _diagnostics, _build_ctx, self_property, function| {
1201 function.expect_no_arguments()?;
1202 let out_property = self_property.map(|s| BString::from(s.trim_ascii()));
1203 Ok(out_property.into_dyn_wrapped())
1204 },
1205 );
1206 map.insert(
1207 "trim_start",
1208 |_language, _diagnostics, _build_ctx, self_property, function| {
1209 function.expect_no_arguments()?;
1210 let out_property = self_property.map(|s| BString::from(s.trim_ascii_start()));
1211 Ok(out_property.into_dyn_wrapped())
1212 },
1213 );
1214 map.insert(
1215 "trim_end",
1216 |_language, _diagnostics, _build_ctx, self_property, function| {
1217 function.expect_no_arguments()?;
1218 let out_property = self_property.map(|s| BString::from(s.trim_ascii_end()));
1219 Ok(out_property.into_dyn_wrapped())
1220 },
1221 );
1222 map.insert(
1223 "substr",
1224 |language, diagnostics, build_ctx, self_property, function| {
1225 let ([start_idx_node], [end_idx_node]) = function.expect_arguments()?;
1226 let start_idx_property =
1227 expect_isize_expression(language, diagnostics, build_ctx, start_idx_node)?;
1228 let end_idx_property = end_idx_node
1229 .map(|node| expect_isize_expression(language, diagnostics, build_ctx, node))
1230 .transpose()?;
1231 let out_property = (self_property, start_idx_property, end_idx_property).map(
1232 |(s, start_idx, end_idx)| {
1233 let start_idx = clamp_signed_bytes_index(&s, start_idx);
1234 let end_idx = end_idx.map_or(s.len(), |idx| clamp_signed_bytes_index(&s, idx));
1235 BString::from(s.get(start_idx..end_idx).unwrap_or_default())
1236 },
1237 );
1238 Ok(out_property.into_dyn_wrapped())
1239 },
1240 );
1241 map.insert(
1242 "first_line",
1243 |_language, _diagnostics, _build_ctx, self_property, function| {
1244 function.expect_no_arguments()?;
1245 let out_property =
1246 self_property.map(|s| s.lines().next().map(BString::from).unwrap_or_default());
1247 Ok(out_property.into_dyn_wrapped())
1248 },
1249 );
1250 map.insert(
1251 "lines",
1252 |_language, _diagnostics, _build_ctx, self_property, function| {
1253 function.expect_no_arguments()?;
1254 let out_property = self_property.map(|s| s.lines().map(BString::from).collect_vec());
1255 Ok(out_property.into_dyn_wrapped())
1256 },
1257 );
1258 map.insert(
1259 "split",
1260 |language, diagnostics, build_ctx, self_property, function| {
1261 let ([separator_node], [limit_node]) = function.expect_arguments()?;
1262 let pattern = template_parser::expect_string_pattern(separator_node)?;
1263 if let Some(limit_node) = limit_node {
1264 let limit_property =
1265 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1266 let out_property = splitn_string_like(self_property, pattern, limit_property);
1267 Ok(out_property.into_dyn_wrapped())
1268 } else {
1269 let out_property = split_string_like(self_property, pattern);
1270 Ok(out_property.into_dyn_wrapped())
1271 }
1272 },
1273 );
1274 map.insert(
1275 "replace",
1276 |language, diagnostics, build_ctx, self_property, function| {
1277 let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1278 let pattern = template_parser::expect_string_pattern(pattern_node)?;
1279 let replacement_property = expect_byte_stringify_expression(
1280 language,
1281 diagnostics,
1282 build_ctx,
1283 replacement_node,
1284 )?;
1285 if let Some(limit_node) = limit_node {
1286 let limit_property =
1287 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1288 let out_property = replacen_string_like(
1289 self_property,
1290 pattern,
1291 replacement_property,
1292 limit_property,
1293 );
1294 Ok(out_property.into_dyn_wrapped())
1295 } else {
1296 let out_property =
1297 replace_all_string_like(self_property, pattern, replacement_property);
1298 Ok(out_property.into_dyn_wrapped())
1299 }
1300 },
1301 );
1302 map.insert(
1303 "upper",
1304 |_language, _diagnostics, _build_ctx, self_property, function| {
1305 function.expect_no_arguments()?;
1306 let out_property = self_property.map(|s| BString::from(s.to_ascii_uppercase()));
1307 Ok(out_property.into_dyn_wrapped())
1308 },
1309 );
1310 map.insert(
1311 "lower",
1312 |_language, _diagnostics, _build_ctx, self_property, function| {
1313 function.expect_no_arguments()?;
1314 let out_property = self_property.map(|s| BString::from(s.to_ascii_lowercase()));
1315 Ok(out_property.into_dyn_wrapped())
1316 },
1317 );
1318 map
1319}
1320
1321fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1322-> TemplateBuildMethodFnMap<'a, L, String> {
1323 let mut map = TemplateBuildMethodFnMap::<L, String>::new();
1326 map.insert(
1327 "len",
1328 |_language, _diagnostics, _build_ctx, self_property, function| {
1329 function.expect_no_arguments()?;
1330 let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
1331 Ok(out_property.into_dyn_wrapped())
1332 },
1333 );
1334 map.insert(
1335 "contains",
1336 |language, diagnostics, build_ctx, self_property, function| {
1337 let [needle_node] = function.expect_exact_arguments()?;
1338 let needle_property =
1340 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1341 let out_property = (self_property, needle_property)
1342 .map(|(haystack, needle)| haystack.contains(&needle));
1343 Ok(out_property.into_dyn_wrapped())
1344 },
1345 );
1346 map.insert(
1347 "match",
1348 |_language, _diagnostics, _build_ctx, self_property, function| {
1349 let [needle_node] = function.expect_exact_arguments()?;
1350 let needle = template_parser::expect_string_pattern(needle_node)?;
1351 let out_property = match_string_like(self_property, needle);
1352 Ok(out_property.into_dyn_wrapped())
1353 },
1354 );
1355 map.insert(
1356 "starts_with",
1357 |language, diagnostics, build_ctx, self_property, function| {
1358 let [needle_node] = function.expect_exact_arguments()?;
1359 let needle_property =
1360 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1361 let out_property = (self_property, needle_property)
1362 .map(|(haystack, needle)| haystack.starts_with(&needle));
1363 Ok(out_property.into_dyn_wrapped())
1364 },
1365 );
1366 map.insert(
1367 "ends_with",
1368 |language, diagnostics, build_ctx, self_property, function| {
1369 let [needle_node] = function.expect_exact_arguments()?;
1370 let needle_property =
1371 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1372 let out_property = (self_property, needle_property)
1373 .map(|(haystack, needle)| haystack.ends_with(&needle));
1374 Ok(out_property.into_dyn_wrapped())
1375 },
1376 );
1377 map.insert(
1378 "remove_prefix",
1379 |language, diagnostics, build_ctx, self_property, function| {
1380 let [needle_node] = function.expect_exact_arguments()?;
1381 let needle_property =
1382 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1383 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1384 haystack
1385 .strip_prefix(&needle)
1386 .map(ToOwned::to_owned)
1387 .unwrap_or(haystack)
1388 });
1389 Ok(out_property.into_dyn_wrapped())
1390 },
1391 );
1392 map.insert(
1393 "remove_suffix",
1394 |language, diagnostics, build_ctx, self_property, function| {
1395 let [needle_node] = function.expect_exact_arguments()?;
1396 let needle_property =
1397 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1398 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1399 haystack
1400 .strip_suffix(&needle)
1401 .map(ToOwned::to_owned)
1402 .unwrap_or(haystack)
1403 });
1404 Ok(out_property.into_dyn_wrapped())
1405 },
1406 );
1407 map.insert(
1408 "trim",
1409 |_language, _diagnostics, _build_ctx, self_property, function| {
1410 function.expect_no_arguments()?;
1411 let out_property = self_property.map(|s| s.trim().to_owned());
1412 Ok(out_property.into_dyn_wrapped())
1413 },
1414 );
1415 map.insert(
1416 "trim_start",
1417 |_language, _diagnostics, _build_ctx, self_property, function| {
1418 function.expect_no_arguments()?;
1419 let out_property = self_property.map(|s| s.trim_start().to_owned());
1420 Ok(out_property.into_dyn_wrapped())
1421 },
1422 );
1423 map.insert(
1424 "trim_end",
1425 |_language, _diagnostics, _build_ctx, self_property, function| {
1426 function.expect_no_arguments()?;
1427 let out_property = self_property.map(|s| s.trim_end().to_owned());
1428 Ok(out_property.into_dyn_wrapped())
1429 },
1430 );
1431 map.insert(
1432 "substr",
1433 |language, diagnostics, build_ctx, self_property, function| {
1434 let ([start_idx_node], [end_idx_node]) = function.expect_arguments()?;
1435 let start_idx_property =
1436 expect_isize_expression(language, diagnostics, build_ctx, start_idx_node)?;
1437 let end_idx_property = end_idx_node
1438 .map(|node| expect_isize_expression(language, diagnostics, build_ctx, node))
1439 .transpose()?;
1440 let out_property = (self_property, start_idx_property, end_idx_property).map(
1441 |(s, start_idx, end_idx)| {
1442 let start_idx = string_index_to_char_boundary(&s, start_idx);
1443 let end_idx =
1444 end_idx.map_or(s.len(), |idx| string_index_to_char_boundary(&s, idx));
1445 s.get(start_idx..end_idx).unwrap_or_default().to_owned()
1446 },
1447 );
1448 Ok(out_property.into_dyn_wrapped())
1449 },
1450 );
1451 map.insert(
1452 "first_line",
1453 |_language, _diagnostics, _build_ctx, self_property, function| {
1454 function.expect_no_arguments()?;
1455 let out_property =
1456 self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
1457 Ok(out_property.into_dyn_wrapped())
1458 },
1459 );
1460 map.insert(
1461 "lines",
1462 |_language, _diagnostics, _build_ctx, self_property, function| {
1463 function.expect_no_arguments()?;
1464 let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
1465 Ok(out_property.into_dyn_wrapped())
1466 },
1467 );
1468 map.insert(
1469 "split",
1470 |language, diagnostics, build_ctx, self_property, function| {
1471 let ([separator_node], [limit_node]) = function.expect_arguments()?;
1472 let pattern = template_parser::expect_string_pattern(separator_node)?;
1473 if let Some(limit_node) = limit_node {
1474 let limit_property =
1475 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1476 let out_property = splitn_string_like(self_property, pattern, limit_property);
1477 Ok(out_property.into_dyn_wrapped())
1478 } else {
1479 let out_property = split_string_like(self_property, pattern);
1480 Ok(out_property.into_dyn_wrapped())
1481 }
1482 },
1483 );
1484 map.insert(
1485 "replace",
1486 |language, diagnostics, build_ctx, self_property, function| {
1487 let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1488 let pattern = template_parser::expect_string_pattern(pattern_node)?;
1489 let replacement_property =
1490 expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
1491 if let Some(limit_node) = limit_node {
1492 let limit_property =
1493 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1494 let out_property = replacen_string_like(
1495 self_property,
1496 pattern,
1497 replacement_property,
1498 limit_property,
1499 );
1500 Ok(out_property.into_dyn_wrapped())
1501 } else {
1502 let out_property =
1503 replace_all_string_like(self_property, pattern, replacement_property);
1504 Ok(out_property.into_dyn_wrapped())
1505 }
1506 },
1507 );
1508 map.insert(
1509 "upper",
1510 |_language, _diagnostics, _build_ctx, self_property, function| {
1511 function.expect_no_arguments()?;
1512 let out_property = self_property.map(|s| s.to_uppercase());
1513 Ok(out_property.into_dyn_wrapped())
1514 },
1515 );
1516 map.insert(
1517 "lower",
1518 |_language, _diagnostics, _build_ctx, self_property, function| {
1519 function.expect_no_arguments()?;
1520 let out_property = self_property.map(|s| s.to_lowercase());
1521 Ok(out_property.into_dyn_wrapped())
1522 },
1523 );
1524 map.insert(
1525 "escape_json",
1526 |_language, _diagnostics, _build_ctx, self_property, function| {
1527 function.expect_no_arguments()?;
1528 let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
1529 Ok(out_property.into_dyn_wrapped())
1530 },
1531 );
1532 map
1533}
1534
1535trait StringLike: AsRef<[u8]> + Sized {
1536 fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError>;
1537}
1538
1539impl StringLike for BString {
1540 fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError> {
1541 Ok(bytes.into())
1542 }
1543}
1544
1545impl StringLike for String {
1546 fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError> {
1547 Ok(str::from_utf8(bytes)?.to_owned())
1548 }
1549}
1550
1551fn match_string_like<S: StringLike>(
1552 self_property: impl TemplateProperty<Output = S>,
1553 needle: StringPattern,
1554) -> impl TemplateProperty<Output = S> {
1555 let regex = needle.to_regex();
1556 self_property.and_then(move |haystack| {
1557 S::from_bytes(regex.find(haystack.as_ref()).map_or(b"", |m| m.as_bytes()))
1559 })
1560}
1561
1562fn split_string_like<S: StringLike>(
1563 self_property: impl TemplateProperty<Output = S>,
1564 pattern: StringPattern,
1565) -> impl TemplateProperty<Output = Vec<S>> {
1566 let regex = pattern.to_regex();
1567 self_property
1568 .and_then(move |haystack| regex.split(haystack.as_ref()).map(S::from_bytes).collect())
1569}
1570
1571fn splitn_string_like<S: StringLike>(
1572 self_property: impl TemplateProperty<Output = S>,
1573 pattern: StringPattern,
1574 limit_property: impl TemplateProperty<Output = usize>,
1575) -> impl TemplateProperty<Output = Vec<S>> {
1576 let regex = pattern.to_regex();
1577 (self_property, limit_property).and_then(move |(haystack, limit)| {
1578 regex
1579 .splitn(haystack.as_ref(), limit)
1580 .map(S::from_bytes)
1581 .collect()
1582 })
1583}
1584
1585fn replace_all_string_like<S: StringLike>(
1586 self_property: impl TemplateProperty<Output = S>,
1587 pattern: StringPattern,
1588 replacement_property: impl TemplateProperty<Output = S>,
1589) -> impl TemplateProperty<Output = S> {
1590 let regex = pattern.to_regex();
1591 (self_property, replacement_property).and_then(move |(haystack, replacement)| {
1592 S::from_bytes(®ex.replace_all(haystack.as_ref(), replacement.as_ref()))
1593 })
1594}
1595
1596fn replacen_string_like<S: StringLike>(
1597 self_property: impl TemplateProperty<Output = S>,
1598 pattern: StringPattern,
1599 replacement_property: impl TemplateProperty<Output = S>,
1600 limit_property: impl TemplateProperty<Output = usize>,
1601) -> impl TemplateProperty<Output = S> {
1602 let regex = pattern.to_regex();
1603 (self_property, replacement_property, limit_property).and_then(
1604 move |(haystack, replacement, limit)| {
1605 if limit == 0 {
1606 Ok(haystack)
1610 } else {
1611 S::from_bytes(®ex.replacen(haystack.as_ref(), limit, replacement.as_ref()))
1612 }
1613 },
1614 )
1615}
1616
1617fn clamp_signed_bytes_index(s: &[u8], i: isize) -> usize {
1619 let magnitude = i.unsigned_abs();
1620 if i < 0 {
1621 s.len().saturating_sub(magnitude)
1622 } else {
1623 magnitude.min(s.len())
1624 }
1625}
1626
1627fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
1632 let p = clamp_signed_bytes_index(s.as_bytes(), i);
1634 if i < 0 {
1635 (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
1636 } else {
1637 (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
1638 }
1639}
1640
1641fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1642-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
1643 fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
1644 T::deserialize(value.into_deserializer())
1645 .map_err(|err| TemplatePropertyError(err.message().into()))
1647 }
1648
1649 let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
1652 map.insert(
1657 "as_boolean",
1658 |_language, _diagnostics, _build_ctx, self_property, function| {
1659 function.expect_no_arguments()?;
1660 let out_property = self_property.and_then(extract::<bool>);
1661 Ok(out_property.into_dyn_wrapped())
1662 },
1663 );
1664 map.insert(
1665 "as_integer",
1666 |_language, _diagnostics, _build_ctx, self_property, function| {
1667 function.expect_no_arguments()?;
1668 let out_property = self_property.and_then(extract::<i64>);
1669 Ok(out_property.into_dyn_wrapped())
1670 },
1671 );
1672 map.insert(
1673 "as_string",
1674 |_language, _diagnostics, _build_ctx, self_property, function| {
1675 function.expect_no_arguments()?;
1676 let out_property = self_property.and_then(extract::<String>);
1677 Ok(out_property.into_dyn_wrapped())
1678 },
1679 );
1680 map.insert(
1681 "as_string_list",
1682 |_language, _diagnostics, _build_ctx, self_property, function| {
1683 function.expect_no_arguments()?;
1684 let out_property = self_property.and_then(extract::<Vec<String>>);
1685 Ok(out_property.into_dyn_wrapped())
1686 },
1687 );
1688 map
1691}
1692
1693fn builtin_fs_path_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1694-> TemplateBuildMethodFnMap<'a, L, PathBuf> {
1695 let mut map = TemplateBuildMethodFnMap::<L, PathBuf>::new();
1698 map.insert(
1699 "absolute",
1700 |language, _diagnostics, _build_ctx, self_property, function| {
1701 function.expect_no_arguments()?;
1702 let cwd = language.current_dir().to_owned();
1703 let out_property = self_property.map(move |path| cwd.join(path));
1704 Ok(out_property.into_dyn_wrapped())
1705 },
1706 );
1707 map.insert(
1708 "relative",
1709 |language, _diagnostics, _build_ctx, self_property, function| {
1710 function.expect_no_arguments()?;
1711 let cwd = language.current_dir().to_owned();
1712 let out_property = self_property.map(move |path| file_util::relative_path(&cwd, &path));
1713 Ok(out_property.into_dyn_wrapped())
1714 },
1715 );
1716 map
1717}
1718
1719fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1720-> TemplateBuildMethodFnMap<'a, L, Signature> {
1721 let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
1724 map.insert(
1725 "name",
1726 |_language, _diagnostics, _build_ctx, self_property, function| {
1727 function.expect_no_arguments()?;
1728 let out_property = self_property.map(|signature| signature.name);
1729 Ok(out_property.into_dyn_wrapped())
1730 },
1731 );
1732 map.insert(
1733 "email",
1734 |_language, _diagnostics, _build_ctx, self_property, function| {
1735 function.expect_no_arguments()?;
1736 let out_property = self_property.map(|signature| Email(signature.email));
1737 Ok(out_property.into_dyn_wrapped())
1738 },
1739 );
1740 map.insert(
1741 "timestamp",
1742 |_language, _diagnostics, _build_ctx, self_property, function| {
1743 function.expect_no_arguments()?;
1744 let out_property = self_property.map(|signature| signature.timestamp);
1745 Ok(out_property.into_dyn_wrapped())
1746 },
1747 );
1748 map
1749}
1750
1751fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1752-> TemplateBuildMethodFnMap<'a, L, Email> {
1753 let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1756 map.insert(
1757 "local",
1758 |_language, _diagnostics, _build_ctx, self_property, function| {
1759 function.expect_no_arguments()?;
1760 let out_property = self_property.map(|email| {
1761 let (local, _) = text_util::split_email(&email.0);
1762 local.to_owned()
1763 });
1764 Ok(out_property.into_dyn_wrapped())
1765 },
1766 );
1767 map.insert(
1768 "domain",
1769 |_language, _diagnostics, _build_ctx, self_property, function| {
1770 function.expect_no_arguments()?;
1771 let out_property = self_property.map(|email| {
1772 let (_, domain) = text_util::split_email(&email.0);
1773 domain.unwrap_or_default().to_owned()
1774 });
1775 Ok(out_property.into_dyn_wrapped())
1776 },
1777 );
1778 map
1779}
1780
1781fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1782-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1783 let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1786 map.insert(
1787 "lower",
1788 |_language, _diagnostics, _build_ctx, self_property, function| {
1789 function.expect_no_arguments()?;
1790 let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1791 Ok(out_property.into_dyn_wrapped())
1792 },
1793 );
1794 map.insert(
1795 "upper",
1796 |_language, _diagnostics, _build_ctx, self_property, function| {
1797 function.expect_no_arguments()?;
1798 let out_property =
1799 self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1800 Ok(out_property.into_dyn_wrapped())
1801 },
1802 );
1803 map.insert(
1804 "exact",
1805 |_language, _diagnostics, _build_ctx, self_property, function| {
1806 function.expect_no_arguments()?;
1807 let out_property = self_property.and_then(|(lower, upper)| {
1808 let exact = (Some(lower) == upper).then_some(lower);
1809 Ok(exact.map(i64::try_from).transpose()?)
1810 });
1811 Ok(out_property.into_dyn_wrapped())
1812 },
1813 );
1814 map.insert(
1815 "zero",
1816 |_language, _diagnostics, _build_ctx, self_property, function| {
1817 function.expect_no_arguments()?;
1818 let out_property = self_property.map(|(_, upper)| upper == Some(0));
1819 Ok(out_property.into_dyn_wrapped())
1820 },
1821 );
1822 map
1823}
1824
1825fn builtin_regex_captures_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1826-> TemplateBuildMethodFnMap<'a, L, RegexCaptures> {
1827 let mut map = TemplateBuildMethodFnMap::<L, RegexCaptures>::new();
1830 map.insert(
1831 "len",
1832 |_language, _diagnostics, _build_ctx, self_property, function| {
1833 function.expect_no_arguments()?;
1834 let out_property =
1835 self_property.and_then(|captures| Ok(i64::try_from(captures.len())?));
1836 Ok(out_property.into_dyn_wrapped())
1837 },
1838 );
1839 map.insert(
1840 "get",
1841 |language, diagnostics, build_ctx, self_property, function| {
1842 let [index_node] = function.expect_exact_arguments()?;
1843 let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
1844 let out_property = (self_property, index).and_then(|(captures, index)| {
1845 captures.get(index).ok_or_else(|| {
1846 TemplatePropertyError(
1847 format!("Could not get capture group with index {index}").into(),
1848 )
1849 })
1850 });
1851 Ok(out_property.into_dyn_wrapped())
1852 },
1853 );
1854 map.insert(
1855 "name",
1856 |language, diagnostics, build_ctx, self_property, function| {
1857 let [name_node] = function.expect_exact_arguments()?;
1858 let name = expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
1859 let out_property = (self_property, name).and_then(move |(c, name)| {
1860 c.name(&name).ok_or_else(|| {
1861 TemplatePropertyError(
1862 format!("Could not get capture group with name {name}").into(),
1863 )
1864 })
1865 });
1866 Ok(out_property.into_dyn_wrapped())
1867 },
1868 );
1869 map
1870}
1871
1872fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1873-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1874 let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1877 map.insert(
1878 "ago",
1879 |_language, _diagnostics, _build_ctx, self_property, function| {
1880 function.expect_no_arguments()?;
1881 let now = Timestamp::now();
1882 let format = timeago::Formatter::new();
1883 let out_property = self_property.and_then(move |timestamp| {
1884 Ok(time_util::format_duration(×tamp, &now, &format)?)
1885 });
1886 Ok(out_property.into_dyn_wrapped())
1887 },
1888 );
1889 map.insert(
1890 "format",
1891 |language, diagnostics, build_ctx, self_property, function| {
1892 let [format_node] = function.expect_exact_arguments()?;
1893 let format_property =
1894 expect_stringify_expression(language, diagnostics, build_ctx, format_node)?;
1895 if let Ok(format) = format_property.extract() {
1896 let format = template_parser::catch_aliases(
1897 diagnostics,
1898 format_node,
1899 |_diagnostics, node| {
1900 time_util::FormattingItems::parse(&format).ok_or_else(|| {
1901 TemplateParseError::expression("Invalid time format", node.span)
1902 })
1903 },
1904 )?
1905 .into_owned();
1906 let out_property = self_property.and_then(move |timestamp| {
1907 Ok(time_util::format_absolute_timestamp_with(
1908 ×tamp, &format,
1909 )?)
1910 });
1911 Ok(out_property.into_dyn_wrapped())
1912 } else {
1913 let out_property =
1914 (self_property, format_property).and_then(move |(timestamp, format)| {
1915 let format =
1916 time_util::FormattingItems::parse(&format).ok_or_else(|| {
1917 let message = format!("Invalid time format: {format}");
1918 TemplatePropertyError(message.into())
1919 })?;
1920 Ok(time_util::format_absolute_timestamp_with(
1921 ×tamp, &format,
1922 )?)
1923 });
1924 Ok(out_property.into_dyn_wrapped())
1925 }
1926 },
1927 );
1928 map.insert(
1929 "utc",
1930 |_language, _diagnostics, _build_ctx, self_property, function| {
1931 function.expect_no_arguments()?;
1932 let out_property = self_property.map(|mut timestamp| {
1933 timestamp.tz_offset = 0;
1934 timestamp
1935 });
1936 Ok(out_property.into_dyn_wrapped())
1937 },
1938 );
1939 map.insert(
1940 "local",
1941 |_language, _diagnostics, _build_ctx, self_property, function| {
1942 function.expect_no_arguments()?;
1943 let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1944 .ok()
1945 .and_then(|tz_string| tz_string.parse::<i32>().ok())
1946 .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1947 let out_property = self_property.map(move |mut timestamp| {
1948 timestamp.tz_offset = tz_offset;
1949 timestamp
1950 });
1951 Ok(out_property.into_dyn_wrapped())
1952 },
1953 );
1954 map.insert(
1955 "after",
1956 |_language, diagnostics, _build_ctx, self_property, function| {
1957 let [date_pattern_node] = function.expect_exact_arguments()?;
1958 let now = chrono::Local::now();
1959 let date_pattern = template_parser::catch_aliases(
1960 diagnostics,
1961 date_pattern_node,
1962 |_diagnostics, node| {
1963 let date_pattern = template_parser::expect_string_literal(node)?;
1964 DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1965 TemplateParseError::expression("Invalid date pattern", node.span)
1966 .with_source(err)
1967 })
1968 },
1969 )?;
1970 let out_property = self_property.map(move |timestamp| date_pattern.matches(×tamp));
1971 Ok(out_property.into_dyn_wrapped())
1972 },
1973 );
1974 map.insert("before", map["after"]);
1975 map.insert(
1976 "since",
1977 |language, diagnostics, build_ctx, self_property, function| {
1978 let [date_node] = function.expect_exact_arguments()?;
1979 let date_property =
1980 expect_timestamp_expression(language, diagnostics, build_ctx, date_node)?;
1981 let out_property =
1982 (self_property, date_property).and_then(move |(self_timestamp, arg_timestamp)| {
1983 Ok(TimestampRange {
1984 start: arg_timestamp,
1985 end: self_timestamp,
1986 })
1987 });
1988 Ok(out_property.into_dyn_wrapped())
1989 },
1990 );
1991 map
1992}
1993
1994fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1995-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1996 let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1999 map.insert(
2000 "start",
2001 |_language, _diagnostics, _build_ctx, self_property, function| {
2002 function.expect_no_arguments()?;
2003 let out_property = self_property.map(|time_range| time_range.start);
2004 Ok(out_property.into_dyn_wrapped())
2005 },
2006 );
2007 map.insert(
2008 "end",
2009 |_language, _diagnostics, _build_ctx, self_property, function| {
2010 function.expect_no_arguments()?;
2011 let out_property = self_property.map(|time_range| time_range.end);
2012 Ok(out_property.into_dyn_wrapped())
2013 },
2014 );
2015 map.insert(
2016 "duration",
2017 |_language, _diagnostics, _build_ctx, self_property, function| {
2018 function.expect_no_arguments()?;
2019 let out_property = self_property.and_then(|time_range| {
2021 let mut f = timeago::Formatter::new();
2022 f.min_unit(timeago::TimeUnit::Microseconds).ago("");
2023 let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
2024 if duration == "now" {
2025 Ok("less than a microsecond".to_owned())
2026 } else {
2027 Ok(duration)
2028 }
2029 });
2030 Ok(out_property.into_dyn_wrapped())
2031 },
2032 );
2033 map
2034}
2035
2036fn builtin_any_list_methods<'a, L: TemplateLanguage<'a> + ?Sized>() -> BuildAnyMethodFnMap<'a, L> {
2037 let mut map = BuildAnyMethodFnMap::<L>::new();
2040 map.insert(
2041 "join",
2042 |language, diagnostics, build_ctx, self_template, function| {
2043 let [separator_node] = function.expect_exact_arguments()?;
2044 let separator =
2045 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2046 Ok(L::Property::wrap_template(
2047 self_template.try_join(separator).ok_or_else(|| {
2048 TemplateParseError::expected_type("Template", "AnyList", function.name_span)
2051 })?,
2052 ))
2053 },
2054 );
2055 map
2056}
2057
2058pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
2060where
2061 L: TemplateLanguage<'a> + ?Sized,
2062 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
2063 O: Template + Clone + 'a,
2064{
2065 let mut map = builtin_unformattable_list_methods::<L, O>();
2066 map.insert(
2067 "join",
2068 |language, diagnostics, build_ctx, self_property, function| {
2069 let [separator_node] = function.expect_exact_arguments()?;
2070 let separator =
2071 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2072 let template =
2073 ListPropertyTemplate::new(self_property, separator, |formatter, item| {
2074 item.format(formatter)
2075 });
2076 Ok(L::Property::wrap_template(Box::new(template)))
2077 },
2078 );
2079 map
2080}
2081
2082pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
2084where
2085 L: TemplateLanguage<'a> + ?Sized,
2086 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
2087 O: Clone + 'a,
2088{
2089 let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
2092 map.insert(
2093 "len",
2094 |_language, _diagnostics, _build_ctx, self_property, function| {
2095 function.expect_no_arguments()?;
2096 let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
2097 Ok(out_property.into_dyn_wrapped())
2098 },
2099 );
2100 map.insert(
2101 "filter",
2102 |language, diagnostics, build_ctx, self_property, function| {
2103 let out_property: BoxedTemplateProperty<'a, Vec<O>> =
2104 build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
2105 Ok(L::Property::wrap_property(out_property))
2106 },
2107 );
2108 map.insert(
2109 "map",
2110 |language, diagnostics, build_ctx, self_property, function| {
2111 let map_result =
2112 build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
2113 Ok(L::Property::wrap_any_list(map_result))
2114 },
2115 );
2116 map.insert(
2117 "any",
2118 |language, diagnostics, build_ctx, self_property, function| {
2119 let out_property =
2120 build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
2121 Ok(out_property.into_dyn_wrapped())
2122 },
2123 );
2124 map.insert(
2125 "all",
2126 |language, diagnostics, build_ctx, self_property, function| {
2127 let out_property =
2128 build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
2129 Ok(out_property.into_dyn_wrapped())
2130 },
2131 );
2132 map.insert(
2133 "first",
2134 |_language, _diagnostics, _build_ctx, self_property, function| {
2135 function.expect_no_arguments()?;
2136 let out_property = self_property.and_then(|items| {
2138 items
2139 .into_iter()
2140 .next()
2141 .ok_or_else(|| TemplatePropertyError("List is empty".into()))
2142 });
2143 Ok(L::Property::wrap_property(out_property.into_dyn()))
2144 },
2145 );
2146 map.insert(
2147 "last",
2148 |_language, _diagnostics, _build_ctx, self_property, function| {
2149 function.expect_no_arguments()?;
2150 let out_property = self_property.and_then(|mut items| {
2152 items
2153 .pop()
2154 .ok_or_else(|| TemplatePropertyError("List is empty".into()))
2155 });
2156 Ok(L::Property::wrap_property(out_property.into_dyn()))
2157 },
2158 );
2159 map.insert(
2160 "get",
2161 |language, diagnostics, build_ctx, self_property, function| {
2162 let [index_node] = function.expect_exact_arguments()?;
2163 let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
2164 let out_property = (self_property, index).and_then(|(mut items, index)| {
2166 if index < items.len() {
2167 Ok(items.remove(index))
2168 } else {
2169 Err(TemplatePropertyError(
2170 format!("Index {index} out of bounds").into(),
2171 ))
2172 }
2173 });
2174 Ok(L::Property::wrap_property(out_property.into_dyn()))
2175 },
2176 );
2177 map.insert(
2178 "reverse",
2179 |_language, _diagnostics, _build_ctx, self_property, function| {
2180 function.expect_no_arguments()?;
2181 let out_property = self_property.map(|mut items| {
2182 items.reverse();
2183 items
2184 });
2185 Ok(L::Property::wrap_property(out_property.into_dyn()))
2186 },
2187 );
2188 map.insert(
2189 "skip",
2190 |language, diagnostics, build_ctx, self_property, function| {
2191 let [count_node] = function.expect_exact_arguments()?;
2192 let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
2193 let out_property = (self_property, count)
2194 .map(|(items, count)| items.into_iter().skip(count).collect_vec());
2195 Ok(L::Property::wrap_property(out_property.into_dyn()))
2196 },
2197 );
2198 map.insert(
2199 "take",
2200 |language, diagnostics, build_ctx, self_property, function| {
2201 let [count_node] = function.expect_exact_arguments()?;
2202 let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
2203 let out_property = (self_property, count)
2204 .map(|(items, count)| items.into_iter().take(count).collect_vec());
2205 Ok(L::Property::wrap_property(out_property.into_dyn()))
2206 },
2207 );
2208 map
2209}
2210
2211fn build_filter_operation<'a, L, O, P, B>(
2213 language: &L,
2214 diagnostics: &mut TemplateDiagnostics,
2215 build_ctx: &BuildContext<L::Property>,
2216 self_property: P,
2217 function: &FunctionCallNode,
2218) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
2219where
2220 L: TemplateLanguage<'a> + ?Sized,
2221 L::Property: WrapTemplateProperty<'a, O>,
2222 P: TemplateProperty + 'a,
2223 P::Output: IntoIterator<Item = O>,
2224 O: Clone + 'a,
2225 B: FromIterator<O>,
2226{
2227 let [lambda_node] = function.expect_exact_arguments()?;
2228 let item_placeholder = PropertyPlaceholder::new();
2229 let item_predicate =
2230 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2231 let lambda = template_parser::expect_lambda(node)?;
2232 build_lambda_expression(
2233 build_ctx,
2234 lambda,
2235 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2236 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
2237 )
2238 })?;
2239 let out_property = self_property.and_then(move |items| {
2240 items
2241 .into_iter()
2242 .filter_map(|item| {
2243 item_placeholder.set(item);
2245 let result = item_predicate.extract();
2246 let item = item_placeholder.take().unwrap();
2247 result.map(|pred| pred.then_some(item)).transpose()
2248 })
2249 .collect()
2250 });
2251 Ok(out_property.into_dyn())
2252}
2253
2254fn build_map_operation<'a, L, O, P>(
2257 language: &L,
2258 diagnostics: &mut TemplateDiagnostics,
2259 build_ctx: &BuildContext<L::Property>,
2260 self_property: P,
2261 function: &FunctionCallNode,
2262) -> TemplateParseResult<BoxedAnyProperty<'a>>
2263where
2264 L: TemplateLanguage<'a> + ?Sized,
2265 L::Property: WrapTemplateProperty<'a, O>,
2266 P: TemplateProperty + 'a,
2267 P::Output: IntoIterator<Item = O>,
2268 O: Clone + 'a,
2269{
2270 let [lambda_node] = function.expect_exact_arguments()?;
2271 let item_placeholder = PropertyPlaceholder::new();
2272 let mapped_item =
2273 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2274 let lambda = template_parser::expect_lambda(node)?;
2275 build_lambda_expression(
2276 build_ctx,
2277 lambda,
2278 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2279 |build_ctx, body| expect_any_expression(language, diagnostics, build_ctx, body),
2280 )
2281 })?;
2282 let mapped_list = ListMapProperty::new(self_property, item_placeholder, mapped_item);
2283 Ok(Box::new(mapped_list))
2284}
2285
2286fn build_any_operation<'a, L, O, P>(
2289 language: &L,
2290 diagnostics: &mut TemplateDiagnostics,
2291 build_ctx: &BuildContext<L::Property>,
2292 self_property: P,
2293 function: &FunctionCallNode,
2294) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
2295where
2296 L: TemplateLanguage<'a> + ?Sized,
2297 L::Property: WrapTemplateProperty<'a, O>,
2298 P: TemplateProperty + 'a,
2299 P::Output: IntoIterator<Item = O>,
2300 O: Clone + 'a,
2301{
2302 let [lambda_node] = function.expect_exact_arguments()?;
2303 let item_placeholder = PropertyPlaceholder::new();
2304 let item_predicate =
2305 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2306 let lambda = template_parser::expect_lambda(node)?;
2307 build_lambda_expression(
2308 build_ctx,
2309 lambda,
2310 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2311 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
2312 )
2313 })?;
2314
2315 let out_property = self_property.and_then(move |items| {
2316 items
2317 .into_iter()
2318 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
2319 .process_results(|mut predicates| predicates.any(|p| p))
2320 });
2321 Ok(out_property.into_dyn())
2322}
2323
2324fn build_all_operation<'a, L, O, P>(
2327 language: &L,
2328 diagnostics: &mut TemplateDiagnostics,
2329 build_ctx: &BuildContext<L::Property>,
2330 self_property: P,
2331 function: &FunctionCallNode,
2332) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
2333where
2334 L: TemplateLanguage<'a> + ?Sized,
2335 L::Property: WrapTemplateProperty<'a, O>,
2336 P: TemplateProperty + 'a,
2337 P::Output: IntoIterator<Item = O>,
2338 O: Clone + 'a,
2339{
2340 let [lambda_node] = function.expect_exact_arguments()?;
2341 let item_placeholder = PropertyPlaceholder::new();
2342 let item_predicate =
2343 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2344 let lambda = template_parser::expect_lambda(node)?;
2345 build_lambda_expression(
2346 build_ctx,
2347 lambda,
2348 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2349 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
2350 )
2351 })?;
2352
2353 let out_property = self_property.and_then(move |items| {
2354 items
2355 .into_iter()
2356 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
2357 .process_results(|mut predicates| predicates.all(|p| p))
2358 });
2359 Ok(out_property.into_dyn())
2360}
2361
2362fn build_lambda_expression<'i, P, T>(
2365 build_ctx: &BuildContext<'i, P>,
2366 lambda: &LambdaNode<'i>,
2367 arg_fns: &[&'i dyn Fn() -> P],
2368 build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
2369) -> TemplateParseResult<T> {
2370 if lambda.params.len() != arg_fns.len() {
2371 return Err(TemplateParseError::expression(
2372 format!("Expected {} lambda parameters", arg_fns.len()),
2373 lambda.params_span,
2374 ));
2375 }
2376 let mut local_variables = build_ctx.local_variables.clone();
2377 local_variables.extend(iter::zip(&lambda.params, arg_fns));
2378 let inner_build_ctx = BuildContext {
2379 local_variables,
2380 self_variable: build_ctx.self_variable,
2381 };
2382 build_body(&inner_build_ctx, &lambda.body)
2383}
2384
2385fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
2386 let mut map = TemplateBuildFunctionFnMap::<L>::new();
2389 map.insert("fill", |language, diagnostics, build_ctx, function| {
2390 let [width_node, content_node] = function.expect_exact_arguments()?;
2391 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2392 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2393 let template =
2394 ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
2395 Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
2396 Err(err) => formatter.handle_error(err),
2397 });
2398 Ok(L::Property::wrap_template(Box::new(template)))
2399 });
2400 map.insert("indent", |language, diagnostics, build_ctx, function| {
2401 let [prefix_node, content_node] = function.expect_exact_arguments()?;
2402 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2403 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2404 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2405 let rewrap = formatter.rewrap_fn();
2406 text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
2407 prefix.format(&mut rewrap(formatter))
2408 })
2409 });
2410 Ok(L::Property::wrap_template(Box::new(template)))
2411 });
2412 map.insert("pad_start", |language, diagnostics, build_ctx, function| {
2413 let ([width_node, content_node], [fill_char_node]) =
2414 function.expect_named_arguments(&["", "", "fill_char"])?;
2415 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2416 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2417 let fill_char = fill_char_node
2418 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2419 .transpose()?;
2420 let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
2421 Ok(L::Property::wrap_template(template))
2422 });
2423 map.insert("pad_end", |language, diagnostics, build_ctx, function| {
2424 let ([width_node, content_node], [fill_char_node]) =
2425 function.expect_named_arguments(&["", "", "fill_char"])?;
2426 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2427 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2428 let fill_char = fill_char_node
2429 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2430 .transpose()?;
2431 let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
2432 Ok(L::Property::wrap_template(template))
2433 });
2434 map.insert(
2435 "pad_centered",
2436 |language, diagnostics, build_ctx, function| {
2437 let ([width_node, content_node], [fill_char_node]) =
2438 function.expect_named_arguments(&["", "", "fill_char"])?;
2439 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2440 let content =
2441 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2442 let fill_char = fill_char_node
2443 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2444 .transpose()?;
2445 let template =
2446 new_pad_template(content, fill_char, width, text_util::write_padded_centered);
2447 Ok(L::Property::wrap_template(template))
2448 },
2449 );
2450 map.insert(
2451 "truncate_start",
2452 |language, diagnostics, build_ctx, function| {
2453 let ([width_node, content_node], [ellipsis_node]) =
2454 function.expect_named_arguments(&["", "", "ellipsis"])?;
2455 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2456 let content =
2457 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2458 let ellipsis = ellipsis_node
2459 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2460 .transpose()?;
2461 let template =
2462 new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
2463 Ok(L::Property::wrap_template(template))
2464 },
2465 );
2466 map.insert(
2467 "truncate_end",
2468 |language, diagnostics, build_ctx, function| {
2469 let ([width_node, content_node], [ellipsis_node]) =
2470 function.expect_named_arguments(&["", "", "ellipsis"])?;
2471 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2472 let content =
2473 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2474 let ellipsis = ellipsis_node
2475 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2476 .transpose()?;
2477 let template =
2478 new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
2479 Ok(L::Property::wrap_template(template))
2480 },
2481 );
2482 map.insert("hash", |language, diagnostics, build_ctx, function| {
2483 let [content_node] = function.expect_exact_arguments()?;
2484 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2485 let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
2486 Ok(result.into_dyn_wrapped())
2487 });
2488 map.insert("label", |language, diagnostics, build_ctx, function| {
2489 let [label_node, content_node] = function.expect_exact_arguments()?;
2490 let label_property =
2491 expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
2492 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2493 let labels =
2494 label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
2495 Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
2496 content, labels,
2497 ))))
2498 });
2499 map.insert(
2500 "raw_escape_sequence",
2501 |language, diagnostics, build_ctx, function| {
2502 let [content_node] = function.expect_exact_arguments()?;
2503 let content =
2504 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2505 Ok(L::Property::wrap_template(Box::new(
2506 RawEscapeSequenceTemplate(content),
2507 )))
2508 },
2509 );
2510 map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
2511 let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
2512 let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
2513 let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
2514 let fallback = fallback_node
2515 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2516 .transpose()?;
2517 Ok(L::Property::wrap_template(Box::new(
2518 HyperlinkTemplate::new(url, text, fallback),
2519 )))
2520 });
2521 map.insert("replace", |language, diagnostics, build_ctx, function| {
2522 let ([pattern_node, content_node, replacement_lambda_node], []) =
2523 function.expect_arguments()?;
2524 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2525 let pattern = template_parser::expect_string_pattern(pattern_node)?;
2526
2527 let regex = pattern.to_regex();
2528
2529 let captures_placeholder = PropertyPlaceholder::new();
2530 let replacement_template = template_parser::catch_aliases(
2531 diagnostics,
2532 replacement_lambda_node,
2533 |diagnostics, node| {
2534 let lambda = template_parser::expect_lambda(node)?;
2535 build_lambda_expression(
2536 build_ctx,
2537 lambda,
2538 &[&|| captures_placeholder.clone().into_dyn_wrapped()],
2539 |build_ctx, body| {
2540 expect_template_expression(language, diagnostics, build_ctx, body)
2541 },
2542 )
2543 },
2544 )?;
2545
2546 let template = new_replace_template(
2547 content,
2548 regex.clone(),
2549 captures_placeholder.clone(),
2550 move |formatter| replacement_template.format(formatter),
2551 );
2552 Ok(L::Property::wrap_template(template))
2553 });
2554 map.insert("stringify", |language, diagnostics, build_ctx, function| {
2555 let [content_node] = function.expect_exact_arguments()?;
2556 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2557 Ok(L::Property::wrap_property(content))
2558 });
2559 map.insert("json", |language, diagnostics, build_ctx, function| {
2560 let [value_node] = function.expect_exact_arguments()?;
2564 let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
2565 let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
2566 Ok(out_property.into_dyn_wrapped())
2567 });
2568 map.insert("if", |language, diagnostics, build_ctx, function| {
2569 let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
2570 let condition =
2571 expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
2572 let true_any = expect_any_expression(language, diagnostics, build_ctx, true_node)?;
2573 let false_any = false_node
2574 .map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
2575 .transpose()?;
2576 let property = ConditionalProperty::new(condition, true_any, false_any);
2577 Ok(L::Property::wrap_any(Box::new(property)))
2578 });
2579 map.insert("try", |language, diagnostics, build_ctx, function| {
2580 let ([node], nodes) = function.expect_some_arguments()?;
2581 let properties = itertools::chain([node], nodes)
2582 .map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
2583 .try_collect()?;
2584 Ok(L::Property::wrap_any(Box::new(TryList::new(properties))))
2585 });
2586 map.insert("coalesce", |language, diagnostics, build_ctx, function| {
2587 let ([], content_nodes) = function.expect_some_arguments()?;
2588 let contents = content_nodes
2589 .iter()
2590 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2591 .try_collect()?;
2592 Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
2593 contents,
2594 ))))
2595 });
2596 map.insert("concat", |language, diagnostics, build_ctx, function| {
2597 let ([], content_nodes) = function.expect_some_arguments()?;
2598 let contents = content_nodes
2599 .iter()
2600 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2601 .try_collect()?;
2602 Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
2603 contents,
2604 ))))
2605 });
2606 map.insert("join", |language, diagnostics, build_ctx, function| {
2607 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2608 let separator =
2609 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2610 let contents = content_nodes
2611 .iter()
2612 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2613 .try_collect()?;
2614 Ok(L::Property::wrap_template(Box::new(JoinTemplate::new(
2615 separator, contents,
2616 ))))
2617 });
2618 map.insert("separate", |language, diagnostics, build_ctx, function| {
2619 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2620 let separator =
2621 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2622 let contents = content_nodes
2623 .iter()
2624 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2625 .try_collect()?;
2626 Ok(L::Property::wrap_template(Box::new(SeparateTemplate::new(
2627 separator, contents,
2628 ))))
2629 });
2630 map.insert("surround", |language, diagnostics, build_ctx, function| {
2631 let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
2632 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2633 let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
2634 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2635 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2636 if recorded.data().is_empty() {
2637 return Ok(());
2638 }
2639 prefix.format(formatter)?;
2640 recorded.replay(formatter.as_mut())?;
2641 suffix.format(formatter)?;
2642 Ok(())
2643 });
2644 Ok(L::Property::wrap_template(Box::new(template)))
2645 });
2646 map.insert("config", |language, diagnostics, build_ctx, function| {
2647 let [name_node] = function.expect_exact_arguments()?;
2648 let name_expression =
2649 expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
2650 if let Ok(name) = name_expression.extract() {
2651 let config_path: ConfigNamePathBuf =
2652 template_parser::catch_aliases(diagnostics, name_node, |_diagnostics, node| {
2653 name.parse().map_err(|err| {
2654 TemplateParseError::expression("Failed to parse config name", node.span)
2655 .with_source(err)
2656 })
2657 })?;
2658 let value = language
2659 .settings()
2660 .get_value(config_path)
2661 .optional()
2662 .map_err(|err| {
2663 TemplateParseError::expression("Failed to get config value", function.name_span)
2664 .with_source(err)
2665 })?;
2666 Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
2668 } else {
2669 let settings = language.settings().clone();
2670 let out_property = name_expression.and_then(move |name| {
2671 let config_path: ConfigNamePathBuf = name.parse()?;
2672 let value = settings.get_value(config_path).optional()?;
2673 Ok(value.map(|v| v.decorated("", "")))
2674 });
2675 Ok(out_property.into_dyn_wrapped())
2676 }
2677 });
2678 map
2679}
2680
2681fn new_pad_template<'a, W>(
2682 content: Box<dyn Template + 'a>,
2683 fill_char: Option<Box<dyn Template + 'a>>,
2684 width: BoxedTemplateProperty<'a, usize>,
2685 write_padded: W,
2686) -> Box<dyn Template + 'a>
2687where
2688 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
2689{
2690 let default_fill_char = FormatRecorder::with_data(" ");
2691 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2692 let width = match width.extract() {
2693 Ok(width) => width,
2694 Err(err) => return formatter.handle_error(err),
2695 };
2696 let mut fill_char_recorder;
2697 let recorded_fill_char = if let Some(fill_char) = &fill_char {
2698 let rewrap = formatter.rewrap_fn();
2699 fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
2700 fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
2701 &fill_char_recorder
2702 } else {
2703 &default_fill_char
2704 };
2705 write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
2706 });
2707 Box::new(template)
2708}
2709
2710fn new_truncate_template<'a, W>(
2711 content: Box<dyn Template + 'a>,
2712 ellipsis: Option<Box<dyn Template + 'a>>,
2713 width: BoxedTemplateProperty<'a, usize>,
2714 write_truncated: W,
2715) -> Box<dyn Template + 'a>
2716where
2717 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
2718{
2719 let default_ellipsis = FormatRecorder::with_data("");
2720 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2721 let width = match width.extract() {
2722 Ok(width) => width,
2723 Err(err) => return formatter.handle_error(err),
2724 };
2725 let mut ellipsis_recorder;
2726 let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
2727 let rewrap = formatter.rewrap_fn();
2728 ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
2729 ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
2730 &ellipsis_recorder
2731 } else {
2732 &default_ellipsis
2733 };
2734 write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
2735 Ok(())
2736 });
2737 Box::new(template)
2738}
2739
2740fn new_replace_template<'a, W>(
2741 content: Box<dyn Template + 'a>,
2742 regex: regex::bytes::Regex,
2743 captures_placeholder: PropertyPlaceholder<RegexCaptures>,
2744 write_replacement_content: W,
2745) -> Box<dyn Template + 'a>
2746where
2747 W: Fn(&mut TemplateFormatter) -> io::Result<()> + 'a,
2748{
2749 let names_map: HashMap<String, usize> = regex
2751 .capture_names()
2752 .enumerate()
2753 .filter_map(|(i, name)| name.map(|n| (n.to_string(), i)))
2754 .collect();
2755
2756 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2757 let data = recorded.data();
2758
2759 let mut recorded_replacements = vec![];
2760 let mut content_ranges = vec![];
2761
2762 for captures in regex.captures_iter(data) {
2763 let full_match = captures.get(0).expect("capture group 0 is always present");
2764 content_ranges.push(full_match.range());
2765
2766 let capture_ranges = captures
2767 .iter()
2768 .map(|m| m.map(|m| m.range()).unwrap_or_default())
2769 .collect_vec();
2770
2771 captures_placeholder.with_value(
2772 RegexCaptures::new(data.to_vec(), capture_ranges, names_map.clone()),
2773 || -> io::Result<()> {
2774 let mut recorder = FormatRecorder::new(formatter.maybe_color());
2775 let rewrap = formatter.rewrap_fn();
2776 write_replacement_content(&mut rewrap(&mut recorder))?;
2777 recorded_replacements.push(recorder);
2778 Ok(())
2779 },
2780 )?;
2781 }
2782
2783 write_replaced(
2784 formatter.as_mut(),
2785 recorded,
2786 &content_ranges,
2787 |formatter, index| recorded_replacements[index].replay(formatter),
2788 )
2789 });
2790
2791 Box::new(template)
2792}
2793
2794pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2796 language: &L,
2797 diagnostics: &mut TemplateDiagnostics,
2798 build_ctx: &BuildContext<L::Property>,
2799 node: &ExpressionNode,
2800) -> TemplateParseResult<Expression<L::Property>> {
2801 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
2802 ExpressionKind::Identifier(name) => {
2803 if let Some(make) = build_ctx.local_variables.get(name) {
2804 Ok(Expression::unlabeled(make()))
2806 } else if *name == "self" {
2807 let make = build_ctx.self_variable;
2809 Ok(Expression::unlabeled(make()))
2810 } else {
2811 let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
2812 .map_err(|err| {
2813 err.extend_keyword_candidates(itertools::chain(
2814 build_ctx.local_variables.keys().copied(),
2815 ["self"],
2816 ))
2817 })?;
2818 Ok(Expression::with_label(property, *name))
2819 }
2820 }
2821 ExpressionKind::Boolean(value) => {
2822 let property = Literal(*value).into_dyn_wrapped();
2823 Ok(Expression::unlabeled(property))
2824 }
2825 ExpressionKind::Integer(value) => {
2826 let property = Literal(*value).into_dyn_wrapped();
2827 Ok(Expression::unlabeled(property))
2828 }
2829 ExpressionKind::String(value) => {
2830 let property = Literal(value.clone()).into_dyn_wrapped();
2831 Ok(Expression::unlabeled(property))
2832 }
2833 ExpressionKind::Pattern(_) => Err(TemplateParseError::expression(
2834 "String patterns may not be used as expression values",
2835 node.span,
2836 )),
2837 ExpressionKind::Unary(op, arg_node) => {
2838 let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
2839 Ok(Expression::unlabeled(property))
2840 }
2841 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
2842 let property = build_binary_operation(
2843 language,
2844 diagnostics,
2845 build_ctx,
2846 *op,
2847 lhs_node,
2848 rhs_node,
2849 node.span,
2850 )?;
2851 Ok(Expression::unlabeled(property))
2852 }
2853 ExpressionKind::Concat(nodes) => {
2854 let templates = nodes
2855 .iter()
2856 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2857 .try_collect()?;
2858 let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
2859 Ok(Expression::unlabeled(property))
2860 }
2861 ExpressionKind::FunctionCall(function) => {
2862 let property = language.build_function(diagnostics, build_ctx, function)?;
2863 Ok(Expression::unlabeled(property))
2864 }
2865 ExpressionKind::MethodCall(method) => {
2866 let mut expression =
2867 build_expression(language, diagnostics, build_ctx, &method.object)?;
2868 expression.property = language.build_method(
2869 diagnostics,
2870 build_ctx,
2871 expression.property,
2872 &method.function,
2873 )?;
2874 expression.labels.push(method.function.name.to_owned());
2875 Ok(expression)
2876 }
2877 ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
2878 "Lambda cannot be defined here",
2879 node.span,
2880 )),
2881 ExpressionKind::AliasExpanded(..) => unreachable!(),
2882 })
2883}
2884
2885pub fn build<'a, C, L>(
2887 language: &L,
2888 diagnostics: &mut TemplateDiagnostics,
2889 node: &ExpressionNode,
2890) -> TemplateParseResult<TemplateRenderer<'a, C>>
2891where
2892 C: Clone + 'a,
2893 L: TemplateLanguage<'a> + ?Sized,
2894 L::Property: WrapTemplateProperty<'a, C>,
2895{
2896 let self_placeholder = PropertyPlaceholder::new();
2897 let build_ctx = BuildContext {
2898 local_variables: HashMap::new(),
2899 self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
2900 };
2901 let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
2902 Ok(TemplateRenderer::new(template, self_placeholder))
2903}
2904
2905pub fn parse<'a, C, L>(
2907 language: &L,
2908 diagnostics: &mut TemplateDiagnostics,
2909 template_text: &str,
2910 aliases_map: &TemplateAliasesMap,
2911) -> TemplateParseResult<TemplateRenderer<'a, C>>
2912where
2913 C: Clone + 'a,
2914 L: TemplateLanguage<'a> + ?Sized,
2915 L::Property: WrapTemplateProperty<'a, C>,
2916{
2917 let node = template_parser::parse(template_text, aliases_map)?;
2918 build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
2919}
2920
2921pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2922 language: &L,
2923 diagnostics: &mut TemplateDiagnostics,
2924 build_ctx: &BuildContext<L::Property>,
2925 node: &ExpressionNode,
2926) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
2927 expect_expression_of_type(
2928 language,
2929 diagnostics,
2930 build_ctx,
2931 node,
2932 "Boolean",
2933 |expression| expression.try_into_boolean(),
2934 )
2935}
2936
2937pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2938 language: &L,
2939 diagnostics: &mut TemplateDiagnostics,
2940 build_ctx: &BuildContext<L::Property>,
2941 node: &ExpressionNode,
2942) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
2943 expect_expression_of_type(
2944 language,
2945 diagnostics,
2946 build_ctx,
2947 node,
2948 "Integer",
2949 |expression| expression.try_into_integer(),
2950 )
2951}
2952
2953pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2955 language: &L,
2956 diagnostics: &mut TemplateDiagnostics,
2957 build_ctx: &BuildContext<L::Property>,
2958 node: &ExpressionNode,
2959) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
2960 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2961 let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
2962 Ok(isize_property.into_dyn())
2963}
2964
2965pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2967 language: &L,
2968 diagnostics: &mut TemplateDiagnostics,
2969 build_ctx: &BuildContext<L::Property>,
2970 node: &ExpressionNode,
2971) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
2972 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2973 let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
2974 Ok(usize_property.into_dyn())
2975}
2976
2977pub fn expect_byte_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2978 language: &L,
2979 diagnostics: &mut TemplateDiagnostics,
2980 build_ctx: &BuildContext<L::Property>,
2981 node: &ExpressionNode,
2982) -> TemplateParseResult<BoxedTemplateProperty<'a, BString>> {
2983 expect_expression_of_type(
2986 language,
2987 diagnostics,
2988 build_ctx,
2989 node,
2990 "ByteStringify",
2991 |expression| expression.try_into_byte_stringify(),
2992 )
2993}
2994
2995pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2996 language: &L,
2997 diagnostics: &mut TemplateDiagnostics,
2998 build_ctx: &BuildContext<L::Property>,
2999 node: &ExpressionNode,
3000) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
3001 expect_expression_of_type(
3004 language,
3005 diagnostics,
3006 build_ctx,
3007 node,
3008 "Stringify",
3009 |expression| expression.try_into_stringify(),
3010 )
3011}
3012
3013pub fn expect_timestamp_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
3014 language: &L,
3015 diagnostics: &mut TemplateDiagnostics,
3016 build_ctx: &BuildContext<L::Property>,
3017 node: &ExpressionNode,
3018) -> TemplateParseResult<BoxedTemplateProperty<'a, Timestamp>> {
3019 expect_expression_of_type(
3020 language,
3021 diagnostics,
3022 build_ctx,
3023 node,
3024 "Timestamp",
3025 |expression| expression.try_into_timestamp(),
3026 )
3027}
3028
3029pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
3030 language: &L,
3031 diagnostics: &mut TemplateDiagnostics,
3032 build_ctx: &BuildContext<L::Property>,
3033 node: &ExpressionNode,
3034) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
3035 expect_expression_of_type(
3036 language,
3037 diagnostics,
3038 build_ctx,
3039 node,
3040 "Serialize",
3041 |expression| expression.try_into_serialize(),
3042 )
3043}
3044
3045pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
3046 language: &L,
3047 diagnostics: &mut TemplateDiagnostics,
3048 build_ctx: &BuildContext<L::Property>,
3049 node: &ExpressionNode,
3050) -> TemplateParseResult<Box<dyn Template + 'a>> {
3051 expect_expression_of_type(
3052 language,
3053 diagnostics,
3054 build_ctx,
3055 node,
3056 "Template",
3057 |expression| expression.try_into_template(),
3058 )
3059}
3060
3061pub fn expect_any_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
3062 language: &L,
3063 diagnostics: &mut TemplateDiagnostics,
3064 build_ctx: &BuildContext<L::Property>,
3065 node: &ExpressionNode,
3066) -> TemplateParseResult<BoxedAnyProperty<'a>> {
3067 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
3068 Ok(
3069 Box::new(build_expression(language, diagnostics, build_ctx, node)?)
3070 as BoxedAnyProperty<'a>,
3071 )
3072 })
3073}
3074
3075fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
3076 language: &L,
3077 diagnostics: &mut TemplateDiagnostics,
3078 build_ctx: &BuildContext<L::Property>,
3079 node: &ExpressionNode,
3080 expected_type: &str,
3081 f: impl FnOnce(Expression<L::Property>) -> Option<T>,
3082) -> TemplateParseResult<T> {
3083 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
3084 let expression = build_expression(language, diagnostics, build_ctx, node)?;
3085 let actual_type = expression.type_name();
3086 f(expression)
3087 .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
3088 })
3089}
3090
3091#[cfg(test)]
3092mod tests {
3093 use std::path::Component;
3094
3095 use assert_matches::assert_matches;
3096 use jj_lib::backend::MillisSinceEpoch;
3097 use jj_lib::config::StackedConfig;
3098
3099 use super::*;
3100 use crate::formatter;
3101 use crate::formatter::ColorFormatter;
3102 use crate::generic_templater;
3103 use crate::generic_templater::GenericTemplateLanguage;
3104
3105 #[derive(Clone, Debug, serde::Serialize)]
3106 struct Context;
3107
3108 type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
3109 type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
3110
3111 generic_templater::impl_self_property_wrapper!(Context);
3112
3113 struct TestTemplateEnv {
3115 language: TestTemplateLanguage,
3116 aliases_map: TemplateAliasesMap,
3117 color_rules: Vec<(Vec<String>, formatter::Style)>,
3118 }
3119
3120 impl TestTemplateEnv {
3121 fn new() -> Self {
3122 Self::with_config(StackedConfig::with_defaults())
3123 }
3124
3125 fn with_config(config: StackedConfig) -> Self {
3126 Self::with_config_and_current_dir(config, default_current_dir())
3127 }
3128
3129 fn with_config_and_current_dir(config: StackedConfig, current_dir: PathBuf) -> Self {
3130 let settings = UserSettings::from_config(config).unwrap();
3131 Self {
3132 language: TestTemplateLanguage::new(&settings, ¤t_dir),
3133 aliases_map: TemplateAliasesMap::new(),
3134 color_rules: Vec::new(),
3135 }
3136 }
3137 }
3138
3139 fn default_current_dir() -> PathBuf {
3140 PathBuf::from(Component::RootDir.as_os_str()).join("cwd")
3141 }
3142
3143 impl TestTemplateEnv {
3144 fn add_keyword<F>(&mut self, name: &'static str, build: F)
3145 where
3146 F: Fn() -> TestTemplatePropertyKind + 'static,
3147 {
3148 self.language.add_keyword(name, move |_| Ok(build()));
3149 }
3150
3151 fn add_dynamic_keyword<O, F>(&mut self, name: &'static str, build: F)
3154 where
3155 O: 'static,
3156 F: Fn() -> O + Clone + 'static,
3157 TestTemplatePropertyKind: WrapTemplateProperty<'static, O>,
3158 {
3159 self.language.add_keyword(name, move |self_property| {
3160 let build = build.clone();
3161 Ok(self_property.map(move |_| build()).into_dyn_wrapped())
3162 });
3163 }
3164
3165 fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
3166 self.aliases_map.insert(decl, defn, None).unwrap();
3167 }
3168
3169 fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
3170 let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
3171 let style = formatter::Style {
3172 fg: Some(fg),
3173 ..Default::default()
3174 };
3175 self.color_rules.push((labels, style));
3176 }
3177
3178 fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
3179 parse(
3180 &self.language,
3181 &mut TemplateDiagnostics::new(),
3182 template,
3183 &self.aliases_map,
3184 )
3185 }
3186
3187 fn parse_err(&self, template: &str) -> String {
3188 let err = self
3189 .parse(template)
3190 .err()
3191 .expect("Got unexpected successful template rendering");
3192
3193 iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
3194 }
3195
3196 fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
3197 self.parse(template)
3198 .err()
3199 .expect("Got unexpected successful template rendering")
3200 .kind()
3201 .clone()
3202 }
3203
3204 fn render_ok(&self, template: &str) -> BString {
3205 let template = self.parse(template).unwrap();
3206 let mut output = Vec::new();
3207 let mut formatter =
3208 ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
3209 template.format(&Context, &mut formatter).unwrap();
3210 drop(formatter);
3211 output.into()
3212 }
3213
3214 fn render_plain(&self, template: &str) -> BString {
3215 let template = self.parse(template).unwrap();
3216 template.format_plain_text(&Context).into()
3217 }
3218 }
3219
3220 fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
3221 where
3222 O: Clone + 'a,
3223 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
3224 {
3225 Literal(value).into_dyn_wrapped()
3226 }
3227
3228 fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
3229 where
3230 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
3231 {
3232 Literal(())
3233 .and_then(|()| Err(TemplatePropertyError(message.into())))
3234 .into_dyn_wrapped()
3235 }
3236
3237 fn new_signature(name: &str, email: &str) -> Signature {
3238 Signature {
3239 name: name.to_owned(),
3240 email: email.to_owned(),
3241 timestamp: new_timestamp(0, 0),
3242 }
3243 }
3244
3245 fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
3246 Timestamp {
3247 timestamp: MillisSinceEpoch(msec),
3248 tz_offset,
3249 }
3250 }
3251
3252 #[test]
3253 fn test_parsed_tree() {
3254 let mut env = TestTemplateEnv::new();
3255 env.add_keyword("divergent", || literal(false));
3256 env.add_keyword("empty", || literal(true));
3257 env.add_keyword("hello", || literal("Hello".to_owned()));
3258
3259 insta::assert_snapshot!(env.render_ok(r#" "#), @"");
3261
3262 insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
3264
3265 insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
3267
3268 insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
3270
3271 insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
3273
3274 insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
3276
3277 insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
3279
3280 insta::assert_snapshot!(env.render_ok("hello\n .upper()"), @"HELLO");
3282 }
3283
3284 #[test]
3285 fn test_parse_error() {
3286 let mut env = TestTemplateEnv::new();
3287 env.add_keyword("description", || literal("".to_owned()));
3288 env.add_keyword("empty", || literal(true));
3289
3290 insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @"
3291 --> 1:5
3292 |
3293 1 | foo bar
3294 | ^---
3295 |
3296 = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
3297 ");
3298 insta::assert_snapshot!(env.parse_err("1 +"), @"
3299 --> 1:4
3300 |
3301 1 | 1 +
3302 | ^---
3303 |
3304 = expected `!`, `-`, or <primary>
3305 ");
3306 insta::assert_snapshot!(env.parse_err("self.timestamp"), @"
3307 --> 1:6
3308 |
3309 1 | self.timestamp
3310 | ^---
3311 |
3312 = expected <function>
3313 ");
3314
3315 insta::assert_snapshot!(env.parse_err(r#"foo"#), @"
3316 --> 1:1
3317 |
3318 1 | foo
3319 | ^-^
3320 |
3321 = Keyword `foo` doesn't exist
3322 ");
3323
3324 insta::assert_snapshot!(env.parse_err(r#"foo()"#), @"
3325 --> 1:1
3326 |
3327 1 | foo()
3328 | ^-^
3329 |
3330 = Function `foo` doesn't exist
3331 ");
3332 insta::assert_snapshot!(env.parse_err(r#"false()"#), @"
3333 --> 1:1
3334 |
3335 1 | false()
3336 | ^---^
3337 |
3338 = Expected identifier
3339 ");
3340
3341 insta::assert_snapshot!(env.parse_err(r#"!foo"#), @"
3342 --> 1:2
3343 |
3344 1 | !foo
3345 | ^-^
3346 |
3347 = Keyword `foo` doesn't exist
3348 ");
3349 insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @"
3350 --> 1:9
3351 |
3352 1 | true && 123
3353 | ^-^
3354 |
3355 = Expected expression of type `Boolean`, but actual type is `Integer`
3356 ");
3357 insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @"
3358 --> 1:1
3359 |
3360 1 | true == 1
3361 | ^-------^
3362 |
3363 = Cannot compare expressions of type `Boolean` and `Integer`
3364 ");
3365 insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @"
3366 --> 1:1
3367 |
3368 1 | true != 'a'
3369 | ^---------^
3370 |
3371 = Cannot compare expressions of type `Boolean` and `String`
3372 ");
3373 insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @"
3374 --> 1:1
3375 |
3376 1 | 1 == true
3377 | ^-------^
3378 |
3379 = Cannot compare expressions of type `Integer` and `Boolean`
3380 ");
3381 insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @"
3382 --> 1:1
3383 |
3384 1 | 1 != 'a'
3385 | ^------^
3386 |
3387 = Cannot compare expressions of type `Integer` and `String`
3388 ");
3389 insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @"
3390 --> 1:1
3391 |
3392 1 | 'a' == true
3393 | ^---------^
3394 |
3395 = Cannot compare expressions of type `String` and `Boolean`
3396 ");
3397 insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @"
3398 --> 1:1
3399 |
3400 1 | 'a' != 1
3401 | ^------^
3402 |
3403 = Cannot compare expressions of type `String` and `Integer`
3404 ");
3405 insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
3406 --> 1:1
3407 |
3408 1 | 'a' == label("", "")
3409 | ^------------------^
3410 |
3411 = Cannot compare expressions of type `String` and `Template`
3412 "#);
3413 insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @"
3414 --> 1:1
3415 |
3416 1 | 'a' > 1
3417 | ^-----^
3418 |
3419 = Cannot compare expressions of type `String` and `Integer`
3420 ");
3421
3422 insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @"
3423 --> 1:26
3424 |
3425 1 | description.first_line().foo()
3426 | ^-^
3427 |
3428 = Method `foo` doesn't exist for type `String`
3429 ");
3430
3431 insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @"
3432 --> 1:1
3433 |
3434 1 | 10000000000000000000
3435 | ^------------------^
3436 |
3437 = Invalid integer literal
3438 number too large to fit in target type
3439 ");
3440 insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @"
3441 --> 1:4
3442 |
3443 1 | 42.foo()
3444 | ^-^
3445 |
3446 = Method `foo` doesn't exist for type `Integer`
3447 ");
3448 insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @"
3449 --> 1:3
3450 |
3451 1 | (-empty)
3452 | ^---^
3453 |
3454 = Expected expression of type `Integer`, but actual type is `Boolean`
3455 ");
3456
3457 insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
3458 --> 1:18
3459 |
3460 1 | ("foo" ++ "bar").baz()
3461 | ^-^
3462 |
3463 = Method `baz` doesn't exist for type `Template`
3464 "#);
3465
3466 insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @"
3467 --> 1:22
3468 |
3469 1 | description.contains()
3470 | ^
3471 |
3472 = Function `contains`: Expected 1 arguments
3473 ");
3474
3475 insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
3476 --> 1:24
3477 |
3478 1 | description.first_line("foo")
3479 | ^---^
3480 |
3481 = Function `first_line`: Expected 0 arguments
3482 "#);
3483
3484 insta::assert_snapshot!(env.parse_err(r#"label()"#), @"
3485 --> 1:7
3486 |
3487 1 | label()
3488 | ^
3489 |
3490 = Function `label`: Expected 2 arguments
3491 ");
3492 insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
3493 --> 1:7
3494 |
3495 1 | label("foo", "bar", "baz")
3496 | ^-----------------^
3497 |
3498 = Function `label`: Expected 2 arguments
3499 "#);
3500
3501 insta::assert_snapshot!(env.parse_err(r#"if()"#), @"
3502 --> 1:4
3503 |
3504 1 | if()
3505 | ^
3506 |
3507 = Function `if`: Expected 2 to 3 arguments
3508 ");
3509 insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
3510 --> 1:4
3511 |
3512 1 | if("foo", "bar", "baz", "quux")
3513 | ^-------------------------^
3514 |
3515 = Function `if`: Expected 2 to 3 arguments
3516 "#);
3517
3518 insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
3519 --> 1:37
3520 |
3521 1 | pad_start("foo", fill_char = "bar", "baz")
3522 | ^---^
3523 |
3524 = Function `pad_start`: Positional argument follows keyword argument
3525 "#);
3526
3527 insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
3528 --> 1:4
3529 |
3530 1 | if(label("foo", "bar"), "baz")
3531 | ^-----------------^
3532 |
3533 = Expected expression of type `Boolean`, but actual type is `Template`
3534 "#);
3535
3536 insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @"
3537 --> 1:1
3538 |
3539 1 | |x| description
3540 | ^-------------^
3541 |
3542 = Lambda cannot be defined here
3543 ");
3544 }
3545
3546 #[test]
3547 fn test_self_keyword() {
3548 let mut env = TestTemplateEnv::new();
3549 env.add_keyword("say_hello", || literal("Hello".to_owned()));
3550
3551 insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
3552 insta::assert_snapshot!(env.parse_err(r#"self"#), @"
3553 --> 1:1
3554 |
3555 1 | self
3556 | ^--^
3557 |
3558 = Expected expression of type `Template`, but actual type is `Self`
3559 ");
3560 }
3561
3562 #[test]
3563 fn test_boolean_cast() {
3564 let mut env = TestTemplateEnv::new();
3565
3566 env.add_keyword("empty_bstr", || literal(BString::from("")));
3567 env.add_keyword("nonempty_bstr", || literal(BString::from("a")));
3568 insta::assert_snapshot!(env.render_ok("if(empty_bstr, true, false)"), @"false");
3569 insta::assert_snapshot!(env.render_ok("if(nonempty_bstr, true, false)"), @"true");
3570
3571 env.add_keyword("empty_bstr_list", || literal::<Vec<BString>>(vec![]));
3572 env.add_keyword("nonempty_bstr_list", || literal(vec![BString::from("")]));
3573 insta::assert_snapshot!(env.render_ok("if(empty_bstr_list, true, false)"), @"false");
3574 insta::assert_snapshot!(env.render_ok("if(nonempty_bstr_list, true, false)"), @"true");
3575
3576 insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
3577 insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
3578
3579 env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
3580 env.add_keyword("sl1", || literal(vec!["".to_owned()]));
3581 insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
3582 insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
3583
3584 env.add_keyword("fs_path", || literal(PathBuf::from("/repo/workspace")));
3585 insta::assert_snapshot!(env.parse_err(r#"if(fs_path, true, false)"#), @"
3586 --> 1:4
3587 |
3588 1 | if(fs_path, true, false)
3589 | ^-----^
3590 |
3591 = Expected expression of type `Boolean`, but actual type is `FsPath`
3592 ");
3593
3594 env.add_keyword("none_fs_path", || literal(None::<PathBuf>));
3595 env.add_keyword("some_fs_path", || {
3596 literal(Some(PathBuf::from("/repo/workspace")))
3597 });
3598 insta::assert_snapshot!(env.render_ok(r#"if(none_fs_path, true, false)"#), @"false");
3599 insta::assert_snapshot!(env.render_ok(r#"if(some_fs_path, true, false)"#), @"true");
3600
3601 insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @"
3603 --> 1:4
3604 |
3605 1 | if(0, true, false)
3606 | ^
3607 |
3608 = Expected expression of type `Boolean`, but actual type is `Integer`
3609 ");
3610
3611 env.add_keyword("none_i64", || literal(None::<i64>));
3613 env.add_keyword("some_i64", || literal(Some(0)));
3614 insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
3615 insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
3616
3617 insta::assert_snapshot!(
3619 env.render_ok("if(-none_i64 == 1, true, false)"),
3620 @"<Error: No Integer available>"
3621 );
3622
3623 insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
3624 --> 1:4
3625 |
3626 1 | if(label("", ""), true, false)
3627 | ^-----------^
3628 |
3629 = Expected expression of type `Boolean`, but actual type is `Template`
3630 "#);
3631 insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @"
3632 --> 1:4
3633 |
3634 1 | if(sl0.map(|x| x), true, false)
3635 | ^------------^
3636 |
3637 = Expected expression of type `Boolean`, but actual type is `AnyList`
3638 ");
3639
3640 env.add_keyword("empty_email", || literal(Email("".to_owned())));
3641 env.add_keyword("nonempty_email", || {
3642 literal(Email("local@domain".to_owned()))
3643 });
3644 insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
3645 insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
3646
3647 env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
3649 insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
3650 --> 1:4
3651 |
3652 1 | if(config_bool, true, false)
3653 | ^---------^
3654 |
3655 = Expected expression of type `Boolean`, but actual type is `ConfigValue`
3656 ");
3657
3658 env.add_keyword("signature", || {
3660 literal(new_signature("Test User", "test.user@example.com"))
3661 });
3662 env.add_keyword("size_hint", || literal((5, None)));
3663 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3664 env.add_keyword("timestamp_range", || {
3665 literal(TimestampRange {
3666 start: new_timestamp(0, 0),
3667 end: new_timestamp(0, 0),
3668 })
3669 });
3670 assert_matches!(
3671 env.parse_err_kind("if(signature, true, false)"),
3672 TemplateParseErrorKind::Expression(_)
3673 );
3674 assert_matches!(
3675 env.parse_err_kind("if(size_hint, true, false)"),
3676 TemplateParseErrorKind::Expression(_)
3677 );
3678 assert_matches!(
3679 env.parse_err_kind("if(timestamp, true, false)"),
3680 TemplateParseErrorKind::Expression(_)
3681 );
3682 assert_matches!(
3683 env.parse_err_kind("if(timestamp_range, true, false)"),
3684 TemplateParseErrorKind::Expression(_)
3685 );
3686 assert_matches!(
3687 env.parse_err_kind("if(if(true, true), true, false)"),
3688 TemplateParseErrorKind::Expression(_)
3689 );
3690 assert_matches!(
3691 env.parse_err_kind("if(sl0.map(|s| s), true, false)"),
3692 TemplateParseErrorKind::Expression(_)
3693 );
3694 }
3695
3696 #[test]
3697 fn test_arithmetic_operation() {
3698 let mut env = TestTemplateEnv::new();
3699 env.add_keyword("none_i64", || literal(None::<i64>));
3700 env.add_keyword("some_i64", || literal(Some(1)));
3701 env.add_keyword("i64_min", || literal(i64::MIN));
3702 env.add_keyword("i64_max", || literal(i64::MAX));
3703
3704 insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
3705 insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
3706 insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
3707 insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
3708 insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
3709 insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
3710 insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
3711 insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
3712 insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
3713
3714 insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
3717 insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
3718 insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
3719 insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
3720 insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
3721 insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
3722
3723 insta::assert_snapshot!(
3725 env.render_ok(r#"-i64_min"#),
3726 @"<Error: Attempt to negate with overflow>");
3727 insta::assert_snapshot!(
3728 env.render_ok(r#"i64_max + 1"#),
3729 @"<Error: Attempt to add with overflow>");
3730 insta::assert_snapshot!(
3731 env.render_ok(r#"i64_min - 1"#),
3732 @"<Error: Attempt to subtract with overflow>");
3733 insta::assert_snapshot!(
3734 env.render_ok(r#"i64_max * 2"#),
3735 @"<Error: Attempt to multiply with overflow>");
3736 insta::assert_snapshot!(
3737 env.render_ok(r#"i64_min / -1"#),
3738 @"<Error: Attempt to divide with overflow>");
3739 insta::assert_snapshot!(
3740 env.render_ok(r#"1 / 0"#),
3741 @"<Error: Attempt to divide by zero>");
3742 insta::assert_snapshot!(
3743 env.render_ok("i64_min % -1"),
3744 @"<Error: Attempt to divide with overflow>");
3745 insta::assert_snapshot!(
3746 env.render_ok(r#"1 % 0"#),
3747 @"<Error: Attempt to divide by zero>");
3748 }
3749
3750 #[test]
3751 fn test_fs_path_methods() {
3752 let cwd = default_current_dir();
3753 let mut env = TestTemplateEnv::with_config_and_current_dir(
3754 StackedConfig::with_defaults(),
3755 cwd.clone(),
3756 );
3757 let path = cwd.join("workspace");
3758 env.add_keyword("fs_path", {
3759 let path = path.clone();
3760 move || literal(path.clone())
3761 });
3762 env.add_keyword("none_fs_path", || literal(None::<PathBuf>));
3763
3764 assert_eq!(
3765 env.render_ok("fs_path"),
3766 BString::from(path.to_str().unwrap())
3767 );
3768 assert_eq!(
3769 env.render_ok("fs_path.absolute()"),
3770 BString::from(path.to_str().unwrap())
3771 );
3772 assert_eq!(
3773 env.render_ok("fs_path.relative()"),
3774 BString::from("workspace")
3775 );
3776 assert_eq!(
3777 env.render_ok("fs_path.relative().absolute()"),
3778 BString::from(path.to_str().unwrap())
3779 );
3780 insta::assert_snapshot!(env.render_ok("json(fs_path.relative())"), @r#""workspace""#);
3781 insta::assert_snapshot!(
3782 env.render_ok("none_fs_path.relative()"),
3783 @"<Error: No FsPath available>"
3784 );
3785 }
3786
3787 #[cfg(unix)]
3788 #[test]
3789 fn test_fs_path_methods_non_utf8() {
3790 use std::ffi::OsString;
3791 use std::os::unix::ffi::OsStrExt as _;
3792 use std::os::unix::ffi::OsStringExt as _;
3793
3794 let cwd = default_current_dir();
3795 let mut env = TestTemplateEnv::with_config_and_current_dir(
3796 StackedConfig::with_defaults(),
3797 cwd.clone(),
3798 );
3799 let mut path_bytes = cwd.as_os_str().as_bytes().to_vec();
3800 if !path_bytes.ends_with(b"/") {
3801 path_bytes.push(b'/');
3802 }
3803 path_bytes.extend(b"\x80workspace");
3804 let path = PathBuf::from(OsString::from_vec(path_bytes.clone()));
3805 env.add_keyword("fs_path", {
3806 let path = path.clone();
3807 move || literal(path.clone())
3808 });
3809 env.add_keyword("some_fs_path", {
3810 let path = path.clone();
3811 move || literal(Some(path.clone()))
3812 });
3813
3814 assert_eq!(
3817 env.render_ok("fs_path"),
3818 BString::from(path_bytes.as_slice()),
3819 "direct FsPath rendering should not require UTF-8"
3820 );
3821 assert_eq!(
3822 env.render_ok("some_fs_path"),
3823 BString::from(path_bytes.as_slice()),
3824 "direct Option<FsPath> rendering should not require UTF-8"
3825 );
3826 assert_eq!(
3827 env.render_ok("fs_path.absolute()"),
3828 BString::from(path_bytes.as_slice())
3829 );
3830 assert_eq!(
3831 env.render_ok("fs_path.relative()"),
3832 BString::from(b"\x80workspace".as_slice())
3833 );
3834 assert_eq!(
3835 env.render_ok("fs_path.relative().absolute()"),
3836 BString::from(path_bytes.as_slice())
3837 );
3838 insta::assert_snapshot!(
3839 env.render_ok("json(fs_path)"),
3840 @r#"<Error: path contains invalid UTF-8 characters>"#);
3841 }
3842
3843 #[test]
3844 fn test_relational_operation() {
3845 let mut env = TestTemplateEnv::new();
3846 env.add_keyword("none_i64", || literal(None::<i64>));
3847 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3848 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3849
3850 insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
3851 insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
3852 insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
3853 insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
3854 insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
3855 insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
3856 insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
3857 insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
3858
3859 insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
3861 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
3862 insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
3863 insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
3864
3865 assert_matches!(
3867 env.parse_err_kind("42 >= true"),
3868 TemplateParseErrorKind::Expression(_)
3869 );
3870 assert_matches!(
3871 env.parse_err_kind("none_i64 >= true"),
3872 TemplateParseErrorKind::Expression(_)
3873 );
3874
3875 env.add_keyword("str_list", || {
3877 literal(vec!["foo".to_owned(), "bar".to_owned()])
3878 });
3879 env.add_keyword("cfg_val", || {
3880 literal(ConfigValue::from_iter([("foo", "bar")]))
3881 });
3882 env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(1))));
3883 env.add_keyword("signature", || {
3884 literal(new_signature("User", "user@example.com"))
3885 });
3886 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
3887 env.add_keyword("size_hint", || literal((10, None)));
3888 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3889 env.add_keyword("timestamp_range", || {
3890 literal(TimestampRange {
3891 start: new_timestamp(0, 0),
3892 end: new_timestamp(0, 0),
3893 })
3894 });
3895 assert_matches!(
3896 env.parse_err_kind("'a' >= 'a'"),
3897 TemplateParseErrorKind::Expression(_)
3898 );
3899 assert_matches!(
3900 env.parse_err_kind("str_list >= str_list"),
3901 TemplateParseErrorKind::Expression(_)
3902 );
3903 assert_matches!(
3904 env.parse_err_kind("true >= true"),
3905 TemplateParseErrorKind::Expression(_)
3906 );
3907 assert_matches!(
3908 env.parse_err_kind("cfg_val >= cfg_val"),
3909 TemplateParseErrorKind::Expression(_)
3910 );
3911 assert_matches!(
3912 env.parse_err_kind("some_cfg >= some_cfg"),
3913 TemplateParseErrorKind::Expression(_)
3914 );
3915 assert_matches!(
3916 env.parse_err_kind("signature >= signature"),
3917 TemplateParseErrorKind::Expression(_)
3918 );
3919 assert_matches!(
3920 env.parse_err_kind("email >= email"),
3921 TemplateParseErrorKind::Expression(_)
3922 );
3923 assert_matches!(
3924 env.parse_err_kind("size_hint >= size_hint"),
3925 TemplateParseErrorKind::Expression(_)
3926 );
3927 assert_matches!(
3928 env.parse_err_kind("timestamp >= timestamp"),
3929 TemplateParseErrorKind::Expression(_)
3930 );
3931 assert_matches!(
3932 env.parse_err_kind("timestamp_range >= timestamp_range"),
3933 TemplateParseErrorKind::Expression(_)
3934 );
3935 assert_matches!(
3936 env.parse_err_kind("label('', '') >= label('', '')"),
3937 TemplateParseErrorKind::Expression(_)
3938 );
3939 assert_matches!(
3940 env.parse_err_kind("if(true, true) >= if(true, true)"),
3941 TemplateParseErrorKind::Expression(_)
3942 );
3943 assert_matches!(
3944 env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
3945 TemplateParseErrorKind::Expression(_)
3946 );
3947 }
3948
3949 #[test]
3950 fn test_logical_operation() {
3951 let mut env = TestTemplateEnv::new();
3952 env.add_keyword("none_i64", || literal::<Option<i64>>(None));
3953 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3954 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3955 env.add_keyword("bstr1", || literal(BString::from("1")));
3956 env.add_keyword("bstr2", || literal(BString::from("2")));
3957 env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
3958 env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
3959
3960 insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
3961 insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
3962 insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
3963 insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
3964 insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
3965 insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
3966 insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
3967
3968 insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
3969 insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
3970 insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
3971 insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
3972 insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
3973 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
3974 insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
3975 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
3976 insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
3977
3978 insta::assert_snapshot!(env.render_ok("bstr1 == bstr1"), @"true");
3979 insta::assert_snapshot!(env.render_ok("bstr1 == bstr2"), @"false");
3980 insta::assert_snapshot!(env.render_ok("bstr1 == '1'"), @"true");
3981 insta::assert_snapshot!(env.render_ok("'2' != bstr2"), @"false");
3982
3983 insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
3984 insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
3985 insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
3986 insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
3987 insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
3988 insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
3989 insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
3990 insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
3991 insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
3992 insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
3993
3994 insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
3995 insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
3996
3997 env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
3999 insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
4000 insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
4001 insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
4002 insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
4003
4004 assert_matches!(
4006 env.parse_err_kind("some_i64_0 == '0'"),
4007 TemplateParseErrorKind::Expression(_)
4008 );
4009 assert_matches!(
4010 env.parse_err_kind("email1 == 42"),
4011 TemplateParseErrorKind::Expression(_)
4012 );
4013
4014 env.add_keyword("str_list", || {
4016 literal(vec!["foo".to_owned(), "bar".to_owned()])
4017 });
4018 env.add_keyword("cfg_val", || {
4019 literal(ConfigValue::from_iter([("foo", "bar")]))
4020 });
4021 env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(true))));
4022 env.add_keyword("signature", || {
4023 literal(new_signature("User", "user@example.com"))
4024 });
4025 env.add_keyword("size_hint", || literal((10, None)));
4026 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
4027 env.add_keyword("timestamp_range", || {
4028 literal(TimestampRange {
4029 start: new_timestamp(0, 0),
4030 end: new_timestamp(0, 0),
4031 })
4032 });
4033 assert_matches!(
4034 env.parse_err_kind("str_list == str_list"),
4035 TemplateParseErrorKind::Expression(_)
4036 );
4037 assert_matches!(
4038 env.parse_err_kind("cfg_val == cfg_val"),
4039 TemplateParseErrorKind::Expression(_)
4040 );
4041 assert_matches!(
4042 env.parse_err_kind("some_cfg == some_cfg"),
4043 TemplateParseErrorKind::Expression(_)
4044 );
4045 assert_matches!(
4046 env.parse_err_kind("signature == signature"),
4047 TemplateParseErrorKind::Expression(_)
4048 );
4049 assert_matches!(
4050 env.parse_err_kind("size_hint == size_hint"),
4051 TemplateParseErrorKind::Expression(_)
4052 );
4053 assert_matches!(
4054 env.parse_err_kind("timestamp == timestamp"),
4055 TemplateParseErrorKind::Expression(_)
4056 );
4057 assert_matches!(
4058 env.parse_err_kind("timestamp_range == timestamp_range"),
4059 TemplateParseErrorKind::Expression(_)
4060 );
4061 assert_matches!(
4062 env.parse_err_kind("label('', '') == label('', '')"),
4063 TemplateParseErrorKind::Expression(_)
4064 );
4065 assert_matches!(
4066 env.parse_err_kind("if(true, true) == if(true, true)"),
4067 TemplateParseErrorKind::Expression(_)
4068 );
4069 assert_matches!(
4070 env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
4071 TemplateParseErrorKind::Expression(_)
4072 );
4073 }
4074
4075 #[test]
4076 fn test_list_method() {
4077 let mut env = TestTemplateEnv::new();
4078 env.add_keyword("empty", || literal(true));
4079 env.add_keyword("sep", || literal("sep".to_owned()));
4080
4081 insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
4082 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
4083
4084 insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
4085 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
4086 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
4088 insta::assert_snapshot!(
4090 env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
4091 @"aSEPbSEPc");
4092
4093 insta::assert_snapshot!(
4094 env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
4095 @"a c");
4096
4097 insta::assert_snapshot!(
4098 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
4099 @"aa bb cc");
4100
4101 insta::assert_snapshot!(
4103 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
4104 @"true");
4105 insta::assert_snapshot!(
4106 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
4107 @"false");
4108 insta::assert_snapshot!(
4109 env.render_ok(r#""".lines().any(|s| s == "a")"#),
4110 @"false");
4111 insta::assert_snapshot!(
4113 env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
4114 @"true");
4115 insta::assert_snapshot!(
4116 env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
4117 @"true");
4118
4119 insta::assert_snapshot!(
4121 env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
4122 @"true");
4123 insta::assert_snapshot!(
4124 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
4125 @"false");
4126 insta::assert_snapshot!(
4128 env.render_ok(r#""".lines().all(|s| s == "a")"#),
4129 @"true");
4130 insta::assert_snapshot!(
4132 env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
4133 @"true");
4134 insta::assert_snapshot!(
4135 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
4136 @"true");
4137
4138 insta::assert_snapshot!(
4140 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
4141 @"true");
4142 insta::assert_snapshot!(
4143 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
4144 @"true");
4145
4146 insta::assert_snapshot!(
4148 env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
4149 @"found");
4150 insta::assert_snapshot!(
4151 env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
4152 @"all single");
4153
4154 insta::assert_snapshot!(
4156 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
4157 @"atrue btrue ctrue");
4158 insta::assert_snapshot!(
4160 env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
4161 @"atrue btrue ctrue");
4162 insta::assert_snapshot!(
4164 env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
4165 @"a b c");
4166 insta::assert_snapshot!(
4168 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
4169 @"ax ay bx by cx cy");
4170 insta::assert_snapshot!(
4172 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
4173 @"ax,ay;bx,by;cx,cy");
4174 insta::assert_snapshot!(
4176 env.render_ok(r#""! a\n!b\nc\n end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
4177 @"a b c");
4178
4179 env.add_alias("identity", "|x| x");
4181 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
4182
4183 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
4185 --> 1:17
4186 |
4187 1 | "a".lines().map(empty)
4188 | ^---^
4189 |
4190 = Expected lambda expression
4191 "#);
4192 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
4194 --> 1:18
4195 |
4196 1 | "a".lines().map(|| "")
4197 | ^
4198 |
4199 = Expected 1 lambda parameters
4200 "#);
4201 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
4202 --> 1:18
4203 |
4204 1 | "a".lines().map(|a, b| "")
4205 | ^--^
4206 |
4207 = Expected 1 lambda parameters
4208 "#);
4209 insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
4211 --> 1:24
4212 |
4213 1 | "a".lines().filter(|s| s ++ "\n")
4214 | ^-------^
4215 |
4216 = Expected expression of type `Boolean`, but actual type is `Template`
4217 "#);
4218
4219 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
4221 --> 1:21
4222 |
4223 1 | "a".lines().any(|s| s.len())
4224 | ^-----^
4225 |
4226 = Expected expression of type `Boolean`, but actual type is `Integer`
4227 "#);
4228 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
4230 --> 1:21
4231 |
4232 1 | "a".lines().all(|s| s ++ "x")
4233 | ^------^
4234 |
4235 = Expected expression of type `Boolean`, but actual type is `Template`
4236 "#);
4237 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
4239 --> 1:18
4240 |
4241 1 | "a".lines().any(|| true)
4242 | ^
4243 |
4244 = Expected 1 lambda parameters
4245 "#);
4246 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
4248 --> 1:18
4249 |
4250 1 | "a".lines().all(|a, b| true)
4251 | ^--^
4252 |
4253 = Expected 1 lambda parameters
4254 "#);
4255 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
4257 --> 1:23
4258 |
4259 1 | "a".lines().map(|s| s.unknown())
4260 | ^-----^
4261 |
4262 = Method `unknown` doesn't exist for type `String`
4263 "#);
4264 env.add_alias("too_many_params", "|x, y| x");
4266 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
4267 --> 1:17
4268 |
4269 1 | "a".lines().map(too_many_params)
4270 | ^-------------^
4271 |
4272 = In alias `too_many_params`
4273 --> 1:2
4274 |
4275 1 | |x, y| x
4276 | ^--^
4277 |
4278 = Expected 1 lambda parameters
4279 "#);
4280
4281 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().first()"#), @"a");
4283 insta::assert_snapshot!(env.render_ok(r#""".lines().first()"#), @"<Error: List is empty>");
4284
4285 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().last()"#), @"c");
4287 insta::assert_snapshot!(env.render_ok(r#""".lines().last()"#), @"<Error: List is empty>");
4288
4289 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(0)"#), @"a");
4291 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(1)"#), @"b");
4292 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(2)"#), @"c");
4293 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(3)"#), @"<Error: Index 3 out of bounds>");
4294 insta::assert_snapshot!(env.render_ok(r#""".lines().get(0)"#), @"<Error: Index 0 out of bounds>");
4295
4296 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().reverse().join("|")"#), @"c|b|a");
4298 insta::assert_snapshot!(env.render_ok(r#""".lines().reverse().join("|")"#), @"");
4299
4300 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(0).join("|")"#), @"a|b|c");
4302 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(1).join("|")"#), @"b|c");
4303 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(2).join("|")"#), @"c");
4304 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(3).join("|")"#), @"");
4305 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(10).join("|")"#), @"");
4306
4307 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(0).join("|")"#), @"");
4309 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(1).join("|")"#), @"a");
4310 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(2).join("|")"#), @"a|b");
4311 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(3).join("|")"#), @"a|b|c");
4312 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(10).join("|")"#), @"a|b|c");
4313
4314 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\nd".lines().skip(1).take(2).join("|")"#), @"b|c");
4316 }
4317
4318 #[test]
4319 fn test_byte_string_method() {
4320 let mut env = TestTemplateEnv::new();
4321 env.add_keyword("empty", || literal(BString::from("")));
4322 env.add_keyword("foo", || literal(BString::from("foo")));
4323 env.add_keyword("bar", || literal(BString::from("bar")));
4324 env.add_keyword("foobar", || literal(BString::from("foobar")));
4325 env.add_keyword("foo_ws", || literal(BString::from(" \n \r foo \t \r ")));
4326 env.add_keyword("foo_bar_nl", || literal(BString::from("foo\nbar\n")));
4327 env.add_keyword("foo_bar_case", || literal(BString::from("foo BAR")));
4328 env.add_keyword("odd", || literal(BString::from(b"\x80")));
4329 env.add_keyword("odd_ws", || literal(BString::from(b" \x80 ")));
4330 env.add_keyword("odd_case", || literal(BString::from(b"A\x80z")));
4331
4332 insta::assert_snapshot!(env.render_ok("empty.len()"), @"0");
4333 insta::assert_snapshot!(env.render_ok("foo.len()"), @"3");
4334 insta::assert_snapshot!(env.render_ok("odd.len()"), @"1");
4335
4336 insta::assert_snapshot!(env.render_ok("foobar.contains(foo)"), @"true");
4337 insta::assert_snapshot!(env.render_ok("foo.contains(foobar)"), @"false");
4338 insta::assert_snapshot!(env.render_ok("foo.contains('foo')"), @"true");
4339 insta::assert_snapshot!(env.render_ok("odd_case.contains(odd)"), @"true");
4340
4341 insta::assert_snapshot!(env.render_ok("foobar.match(regex:'[a-f]o+')"), @"foo");
4342 insta::assert_snapshot!(env.render_ok("foobar.match(regex:'^$')"), @"");
4343 insta::assert_snapshot!(env.render_ok("json(odd.match(regex:'(?-u:.)'))"), @"[128]");
4344
4345 insta::assert_snapshot!(env.render_ok("foobar.starts_with(foo)"), @"true");
4346 insta::assert_snapshot!(env.render_ok("foobar.starts_with(bar)"), @"false");
4347 insta::assert_snapshot!(env.render_ok("foobar.starts_with('foo')"), @"true");
4348 insta::assert_snapshot!(env.render_ok("foobar.ends_with(foo)"), @"false");
4349 insta::assert_snapshot!(env.render_ok("foobar.ends_with(bar)"), @"true");
4350 insta::assert_snapshot!(env.render_ok("foobar.ends_with('foo')"), @"false");
4351 insta::assert_snapshot!(env.render_ok("odd_case.starts_with('A' ++ odd)"), @"true");
4352 insta::assert_snapshot!(env.render_ok("odd_case.ends_with(odd ++ 'z')"), @"true");
4353
4354 insta::assert_snapshot!(env.render_ok("foobar.remove_prefix(foo)"), @"bar");
4355 insta::assert_snapshot!(env.render_ok("foobar.remove_prefix(bar)"), @"foobar");
4356 insta::assert_snapshot!(env.render_ok("foobar.remove_prefix('foo')"), @"bar");
4357 insta::assert_snapshot!(env.render_ok("foobar.remove_suffix(foo)"), @"foobar");
4358 insta::assert_snapshot!(env.render_ok("foobar.remove_suffix(bar)"), @"foo");
4359 insta::assert_snapshot!(env.render_ok("foobar.remove_suffix('foo')"), @"foobar");
4360 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_prefix('A'))"), @"[128,122]");
4361 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_prefix('A' ++ odd))"), @"[122]");
4362 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_suffix('z'))"), @"[65,128]");
4363 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_suffix(odd ++ 'z'))"), @"[65]");
4364
4365 insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim() ++ '|'"), @"|foo|");
4366 insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim_start() ++ '|'"), @"|foo \t \r |");
4367 insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim_end() ++ '|'"), @"| \n \r foo|");
4368 insta::assert_snapshot!(env.render_ok("json(odd_ws.trim())"), @"[128]");
4369 insta::assert_snapshot!(env.render_ok("json(odd_ws.trim_start())"), @"[128,32]");
4370 insta::assert_snapshot!(env.render_ok("json(odd_ws.trim_end())"), @"[32,128]");
4371
4372 insta::assert_snapshot!(env.render_ok("bar.substr(0)"), @"bar");
4373 insta::assert_snapshot!(env.render_ok("bar.substr(1)"), @"ar");
4374 insta::assert_snapshot!(env.render_ok("bar.substr(2)"), @"r");
4375 insta::assert_snapshot!(env.render_ok("bar.substr(3)"), @"");
4376 insta::assert_snapshot!(env.render_ok("bar.substr(4)"), @"");
4377 insta::assert_snapshot!(env.render_ok("bar.substr(-1)"), @"r");
4378 insta::assert_snapshot!(env.render_ok("bar.substr(-2)"), @"ar");
4379 insta::assert_snapshot!(env.render_ok("bar.substr(-3)"), @"bar");
4380 insta::assert_snapshot!(env.render_ok("bar.substr(-4)"), @"bar");
4381 insta::assert_snapshot!(env.render_ok("bar.substr(1, 0)"), @"");
4382 insta::assert_snapshot!(env.render_ok("bar.substr(1, 2)"), @"a");
4383 insta::assert_snapshot!(env.render_ok("bar.substr(1, -1)"), @"a");
4384 insta::assert_snapshot!(env.render_ok("bar.substr(2, -1)"), @"");
4385 insta::assert_snapshot!(env.render_ok("bar.substr(4, -4)"), @"");
4386 insta::assert_snapshot!(env.render_ok("json(odd_case.substr(1, 2))"), @"[128]");
4387
4388 insta::assert_snapshot!(env.render_ok("'|' ++ empty.first_line() ++ '|'"), @"||");
4389 insta::assert_snapshot!(env.render_ok("'|' ++ foo_bar_nl.first_line() ++ '|'"), @"|foo|");
4390 insta::assert_snapshot!(env.render_ok("empty.lines().len()"), @"0");
4391 insta::assert_snapshot!(env.render_ok("foo_bar_nl.lines()"), @"foo bar");
4392 insta::assert_snapshot!(env.render_ok("json(odd.first_line())"), @"[128]");
4393 insta::assert_snapshot!(env.render_ok("json(odd.lines())"), @"[[128]]");
4394
4395 insta::assert_snapshot!(env.render_ok("empty.split(' ').join(',')"), @"");
4396 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ').join(',')"), @"foo,BAR");
4397 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 0).join(',')"), @"");
4398 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 1).join(',')"), @"foo BAR");
4399 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 2).join(',')"), @"foo,BAR");
4400 insta::assert_snapshot!(
4401 env.render_ok("json(odd_case.split(regex:'(?-u)[^Az]'))"), @"[[65],[122]]");
4402 insta::assert_snapshot!(env.render_ok("json(odd_case.split(regex:'A'))"), @"[[],[128,122]]");
4403
4404 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*')"), @"f**");
4405 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 0)"), @"foo");
4406 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 1)"), @"f*o");
4407 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 2)"), @"f**");
4408 insta::assert_snapshot!(env.render_ok("bar.replace(regex:'b(a)', '$0$1')"), @"baar");
4409 insta::assert_snapshot!(env.render_ok("json(foo.replace('o', odd))"), @"[102,128,128]");
4410 insta::assert_snapshot!(
4411 env.render_ok("json(odd_case.replace(regex:'(?-u)[^Az]', ' '))"), @"[65,32,122]");
4412 insta::assert_snapshot!(
4413 env.render_ok("json(odd_case.replace(regex:'A', ' '))"), @"[32,128,122]");
4414
4415 insta::assert_snapshot!(env.render_ok("foo_bar_case.upper()"), @"FOO BAR");
4416 insta::assert_snapshot!(env.render_ok("foo_bar_case.lower()"), @"foo bar");
4417 insta::assert_snapshot!(env.render_ok("json(odd_case.upper())"), @"[65,128,90]");
4418 insta::assert_snapshot!(env.render_ok("json(odd_case.lower())"), @"[97,128,122]");
4419 }
4420
4421 #[test]
4422 fn test_string_method() {
4423 let mut env = TestTemplateEnv::new();
4424 env.add_keyword("description", || literal("description 1".to_owned()));
4425 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4426
4427 insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
4428 insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
4429 insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
4430
4431 insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
4432 insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
4433 insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
4434 insta::assert_snapshot!(
4435 env.render_ok(r#""description 123".contains(description.first_line())"#),
4436 @"true");
4437
4438 insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
4440 --> 1:18
4441 |
4442 1 | "fa".starts_with(regex:'[a-f]o+')
4443 | ^-------------^
4444 |
4445 = String patterns may not be used as expression values
4446 "#);
4447
4448 insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
4450 insta::assert_snapshot!(
4451 env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
4452 insta::assert_snapshot!(
4453 env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
4454
4455 insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
4456 insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
4457 insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
4458 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
4459 insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
4460 insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
4461 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
4462 insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
4463 insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
4464 insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
4465
4466 insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
4470
4471 insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
4472 --> 1:15
4473 |
4474 1 | "hello".match(false)
4475 | ^---^
4476 |
4477 = Expected string pattern
4478 "#);
4479 insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
4480 --> 1:11
4481 |
4482 1 | "🥺".match(not-a-pattern:"abc")
4483 | ^-----------------^
4484 |
4485 = Bad string pattern
4486 Invalid string pattern kind `not-a-pattern:`
4487 "#);
4488
4489 insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
4490 insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
4491
4492 insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
4493 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
4494
4495 insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
4496 insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
4497 insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
4498 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
4499 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
4500 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
4501 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
4502 insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
4503 insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
4504 insta::assert_snapshot!(env.render_ok(r#""foo bar baz".split(regex:'\s+')"#), @"foo bar baz");
4505 insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
4506 insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
4507
4508 insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
4509 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
4510 insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
4511 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
4512
4513 insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
4514 insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
4515 insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
4516 insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
4517 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
4518 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
4519
4520 insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
4521 insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
4522 insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
4523 insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
4524 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
4525 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
4526
4527 insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
4528 insta::assert_snapshot!(
4529 env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
4530 @"testing");
4531
4532 insta::assert_snapshot!(
4533 env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
4534 @"bar@my.example.com");
4535 insta::assert_snapshot!(
4536 env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
4537 @"bar");
4538
4539 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim()"#), @"");
4540 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim()"#), @"foo bar");
4541
4542 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_start()"#), @"");
4543 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_start()"#), @"foo bar");
4544
4545 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_end()"#), @"");
4546 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_end()"#), @"\n\r foo bar");
4547
4548 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
4549 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
4550 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
4551 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
4552 insta::assert_snapshot!(env.render_ok(r#""foo".substr(1, 3)"#), @"oo");
4553 insta::assert_snapshot!(env.render_ok(r#""foo".substr(1)"#), @"oo");
4554 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0)"#), @"foo");
4555 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
4556 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
4557 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3)"#), @"def");
4558 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
4559 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
4560 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-100)"#), @"abcdef");
4561
4562 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
4564 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
4565 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
4566 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
4567 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
4568 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
4569 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
4570 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
4571 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
4572 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
4573 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
4574 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(0)"#), @"abc💩");
4575 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(1)"#), @"bc💩");
4576 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3)"#), @"💩");
4577 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(4)"#), @"💩");
4578 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-5)"#), @"c💩");
4579 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4)"#), @"💩");
4580 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3)"#), @"");
4581 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-2)"#), @"");
4582 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1)"#), @"");
4583
4584 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
4586 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
4587
4588 insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
4589 insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
4590
4591 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
4593 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
4594 insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
4595
4596 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
4598 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
4599 insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
4600
4601 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
4603 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
4604
4605 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
4607 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
4608
4609 insta::assert_snapshot!(env.render_ok(r#""HELLO WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
4611 insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
4612 insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
4613
4614 insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
4616 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
4617 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
4618
4619 insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
4621 insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
4622 insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
4623
4624 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
4626
4627 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
4629 }
4630
4631 #[test]
4632 fn test_config_value_method() {
4633 let mut env = TestTemplateEnv::new();
4634 env.add_keyword("boolean", || literal(ConfigValue::from(true)));
4635 env.add_keyword("integer", || literal(ConfigValue::from(42)));
4636 env.add_keyword("string", || literal(ConfigValue::from("foo")));
4637 env.add_keyword("string_list", || {
4638 literal(ConfigValue::from_iter(["foo", "bar"]))
4639 });
4640
4641 insta::assert_snapshot!(env.render_ok("boolean"), @"true");
4642 insta::assert_snapshot!(env.render_ok("integer"), @"42");
4643 insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
4644 insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
4645
4646 insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
4647 insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
4648 insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
4649 insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
4650
4651 insta::assert_snapshot!(
4652 env.render_ok("boolean.as_integer()"),
4653 @"<Error: invalid type: boolean `true`, expected i64>");
4654 insta::assert_snapshot!(
4655 env.render_ok("integer.as_string()"),
4656 @"<Error: invalid type: integer `42`, expected a string>");
4657 insta::assert_snapshot!(
4658 env.render_ok("string.as_string_list()"),
4659 @r#"<Error: invalid type: string "foo", expected a sequence>"#);
4660 insta::assert_snapshot!(
4661 env.render_ok("string_list.as_boolean()"),
4662 @"<Error: invalid type: sequence, expected a boolean>");
4663 }
4664
4665 #[test]
4666 fn test_signature_and_email_methods() {
4667 let mut env = TestTemplateEnv::new();
4668
4669 env.add_keyword("author", || {
4670 literal(new_signature("Test User", "test.user@example.com"))
4671 });
4672 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
4673 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
4674 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
4675 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
4676 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
4677 insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
4678
4679 env.add_keyword("author", || {
4680 literal(new_signature("Another Test User", "test.user@example.com"))
4681 });
4682 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
4683 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
4684 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
4685
4686 env.add_keyword("author", || {
4687 literal(new_signature("Test User", "test.user@invalid@example.com"))
4688 });
4689 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
4690 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
4691 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
4692 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
4693 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
4694
4695 env.add_keyword("author", || {
4696 literal(new_signature("Test User", "test.user"))
4697 });
4698 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
4699 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
4700 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
4701 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
4702
4703 env.add_keyword("author", || {
4704 literal(new_signature("Test User", "test.user+tag@example.com"))
4705 });
4706 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
4707 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
4708 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
4709 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
4710
4711 env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
4712 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
4713 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
4714 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
4715 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
4716
4717 env.add_keyword("author", || {
4718 literal(new_signature("", "test.user@example.com"))
4719 });
4720 insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
4721 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
4722 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
4723
4724 env.add_keyword("author", || literal(new_signature("Test User", "")));
4725 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
4726 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
4727 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
4728 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
4729 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
4730
4731 env.add_keyword("author", || literal(new_signature("", "")));
4732 insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
4733 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
4734 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
4735 }
4736
4737 #[test]
4738 fn test_size_hint_method() {
4739 let mut env = TestTemplateEnv::new();
4740
4741 env.add_keyword("unbounded", || literal((5, None)));
4742 insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
4743 insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
4744 insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
4745 insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
4746
4747 env.add_keyword("bounded", || literal((0, Some(10))));
4748 insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
4749 insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
4750 insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
4751 insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
4752
4753 env.add_keyword("zero", || literal((0, Some(0))));
4754 insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
4755 insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
4756 insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
4757 insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
4758 }
4759
4760 #[test]
4761 fn test_timestamp_method() {
4762 let mut env = TestTemplateEnv::new();
4763 env.add_keyword("now", || literal(Timestamp::now()));
4764 env.add_keyword("t0", || literal(new_timestamp(0, 0)));
4765 env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
4766 env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
4767
4768 insta::assert_snapshot!(env.render_ok("tmax"),
4770 @"<Error: Out-of-range date>");
4771
4772 insta::assert_snapshot!(
4773 env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
4774 @"19700101 00:00:00");
4775
4776 insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
4778 --> 1:11
4779 |
4780 1 | t0.format("%_")
4781 | ^--^
4782 |
4783 = Invalid time format
4784 "#);
4785
4786 env.add_dynamic_keyword("good_dyn_format", || "%Y%m".to_owned());
4788 env.add_dynamic_keyword("bad_dyn_format", || "%_".to_owned());
4789 insta::assert_snapshot!(env.render_ok("t0.format(good_dyn_format)"), @"197001");
4790 insta::assert_snapshot!(
4791 env.render_ok("t0.format(bad_dyn_format)"),
4792 @"<Error: Invalid time format: %_>");
4793
4794 env.add_alias("time_format", r#""%Y-%m-%d""#);
4796 env.add_alias("bad_time_format", r#""%_""#);
4797 insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
4798 insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
4799 --> 1:11
4800 |
4801 1 | t0.format(bad_time_format)
4802 | ^-------------^
4803 |
4804 = In alias `bad_time_format`
4805 --> 1:1
4806 |
4807 1 | "%_"
4808 | ^--^
4809 |
4810 = Invalid time format
4811 "#);
4812
4813 insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
4814
4815 assert!(!env.render_ok("now.ago()").is_empty());
4818 assert!(!env.render_ok("now.local()").is_empty());
4819
4820 insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
4821 insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
4822 insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
4823 insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
4824 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
4825 --> 1:11
4826 |
4827 1 | t0.before('invalid')
4828 | ^-------^
4829 |
4830 = Invalid date pattern
4831 expected unsupported identifier as position 0..7
4832 ");
4833 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
4834 --> 1:11
4835 |
4836 1 | t0.before('invalid')
4837 | ^-------^
4838 |
4839 = Invalid date pattern
4840 expected unsupported identifier as position 0..7
4841 ");
4842
4843 insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
4845 --> 1:10
4846 |
4847 1 | t0.after(t0)
4848 | ^^
4849 |
4850 = Expected string literal
4851 ");
4852 insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
4853 --> 1:11
4854 |
4855 1 | t0.before(t0)
4856 | ^^
4857 |
4858 = Expected string literal
4859 ");
4860
4861 insta::assert_snapshot!(env.render_ok("t0.since(t0_plus1)"), @"1970-01-01 01:00:00.000 +01:00 - 1970-01-01 00:00:00.000 +00:00");
4862 insta::assert_snapshot!(env.render_ok("t0_plus1.since(t0)"), @"1970-01-01 00:00:00.000 +00:00 - 1970-01-01 01:00:00.000 +01:00");
4863 insta::assert_snapshot!(env.parse_err("t0.since(false)"), @"
4864 --> 1:10
4865 |
4866 1 | t0.since(false)
4867 | ^---^
4868 |
4869 = Expected expression of type `Timestamp`, but actual type is `Boolean`
4870 ");
4871 }
4872
4873 #[test]
4874 fn test_timestamp_range_method() {
4875 let mut env = TestTemplateEnv::new();
4876 env.add_keyword("instant", || {
4877 literal(TimestampRange {
4878 start: new_timestamp(0, 0),
4879 end: new_timestamp(0, 0),
4880 })
4881 });
4882 env.add_keyword("one_msec", || {
4883 literal(TimestampRange {
4884 start: new_timestamp(0, 0),
4885 end: new_timestamp(1, -60),
4886 })
4887 });
4888
4889 insta::assert_snapshot!(
4890 env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
4891 @"19700101 00:00:00 +00:00");
4892 insta::assert_snapshot!(
4893 env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
4894 @"19691231 23:00:00 -01:00");
4895
4896 insta::assert_snapshot!(
4897 env.render_ok("instant.duration()"), @"less than a microsecond");
4898 insta::assert_snapshot!(
4899 env.render_ok("one_msec.duration()"), @"1 millisecond");
4900 }
4901
4902 #[test]
4903 fn test_fill_function() {
4904 let mut env = TestTemplateEnv::new();
4905 env.add_color("error", crossterm::style::Color::DarkRed);
4906
4907 insta::assert_snapshot!(
4908 env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
4909 label("error", "lazy") ++ " dog\n")"#),
4910 @"
4911 The quick fox jumps
4912 over the [38;5;1mlazy[39m dog
4913 ");
4914
4915 insta::assert_snapshot!(
4917 env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
4918 label("error", "longlonglongword and short words") ++
4919 " back out\n")"#),
4920 @"
4921 Longlonglongword
4922 an some
4923 short
4924 words
4925 [38;5;1mlonglonglongword[39m
4926 [38;5;1mand short[39m
4927 [38;5;1mwords[39m
4928 back out
4929 ");
4930
4931 insta::assert_snapshot!(
4933 env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
4934 label("error", "lazy") ++ " dog\n")"#),
4935 @"
4936 The
4937 quick
4938 fox
4939 jumps
4940 over
4941 the
4942 [38;5;1mlazy[39m
4943 dog
4944 ");
4945
4946 insta::assert_snapshot!(
4948 env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
4949 label("error", "lazy") ++ " dog\n")"#),
4950 @"
4951 The
4952 quick
4953 fox
4954 jumps
4955 over
4956 the
4957 [38;5;1mlazy[39m
4958 dog
4959 ");
4960
4961 insta::assert_snapshot!(
4963 env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
4964 label("error", "lazy") ++ " dog\n")"#),
4965 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
4966
4967 insta::assert_snapshot!(
4969 env.render_ok(r#""START marker to help insta\n" ++
4970 indent(" ", fill(20, "The quick fox jumps over the " ++
4971 label("error", "lazy") ++ " dog\n"))"#),
4972 @"
4973 START marker to help insta
4974 The quick fox jumps
4975 over the [38;5;1mlazy[39m dog
4976 ");
4977
4978 insta::assert_snapshot!(
4980 env.render_ok(r#""START marker to help insta\n" ++
4981 fill(20, indent(" ", "The quick fox jumps over the " ++
4982 label("error", "lazy") ++ " dog\n"))"#),
4983 @"
4984 START marker to help insta
4985 The quick fox
4986 jumps over the [38;5;1mlazy[39m
4987 dog
4988 ");
4989 }
4990
4991 #[test]
4992 fn test_replace_function() {
4993 let mut env = TestTemplateEnv::new();
4994 env.add_color("error", crossterm::style::Color::DarkRed);
4995 env.add_color("warning", crossterm::style::Color::DarkYellow);
4996
4997 insta::assert_snapshot!(
4999 env.render_ok(r#"replace("Hi", label("error", "Hi world"), |_| "Hello")"#),
5000 @"[38;5;1mHello world[39m");
5001
5002 insta::assert_snapshot!(
5004 env.render_ok(r#"replace("Hi", label("error", "Hi Hi world"), |_| "Hello")"#),
5005 @"[38;5;1mHello Hello world[39m");
5006
5007 insta::assert_snapshot!(
5010 env.render_ok(r#"replace("ello w",
5011 label("error", "Hello") ++ " " ++ label("warning", "world"),
5012 |c| c.get(0).upper())"#),
5013 @"[38;5;1mHELLO W[38;5;3morld[39m");
5014
5015 insta::assert_snapshot!(
5017 env.render_ok(r#"replace(regex-i:"(ello) (w)",
5018 label("error", "HELLO") ++ " " ++ label("warning", "world"),
5019 |c| c.get(1).lower() ++ " " ++ c.get(2).upper())"#),
5020 @"[38;5;1mHello W[38;5;3morld[39m");
5021
5022 insta::assert_snapshot!(
5024 env.render_ok(r#"replace(regex-i:'(?P<h>ello) (?P<w>w)',
5025 label("error", "HELLO") ++ " " ++ label("warning", "world"),
5026 |c| c.name("h").lower() ++ " " ++ c.name("w").upper())"#),
5027 @"[38;5;1mHello W[38;5;3morld[39m");
5028
5029 insta::assert_snapshot!(
5031 env.render_ok(r#"replace(regex-i:"(hello) (b)?",
5032 label("error", "HELLO") ++ " " ++ label("warning", "world"),
5033 |c| c.get(1).lower() ++ " " ++ c.get(2).upper())"#),
5034 @"[38;5;1mhello [38;5;3mworld[39m");
5035
5036 insta::assert_snapshot!(
5038 env.render_ok(r#"replace(regex-i:'(?P<h>ello) (?P<b>b)',
5039 label("error", "HELLO") ++ " " ++ label("warning", "world"),
5040 |c| c.name("h").lower() ++ " " ++ c.name("b").upper())"#),
5041 @"[38;5;1mHELLO[39m [38;5;3mworld[39m");
5042
5043 insta::assert_snapshot!(
5045 env.render_ok(r#"replace(regex:"(a)(b)", "ab", |c| c.len())"#),
5046 @"3");
5047
5048 insta::assert_snapshot!(
5050 env.render_ok(r#"replace("Hi", "Hi world", |c| c.get(99))"#),
5051 @"[38;5;1m<Error: Could not get capture group with index 99>[39m world");
5052
5053 insta::assert_snapshot!(
5055 env.render_ok(r#"replace("Hi", "Hi world", |c| c.name("no_such_group"))"#),
5056 @"[38;5;1m<Error: Could not get capture group with name no_such_group>[39m world");
5057
5058 insta::assert_snapshot!(
5060 env.render_ok(r#"replace(regex:'(?-u)^(.{3})(.)$', "🥺", |c| json(c.get(1)))"#),
5061 @"[240,159,165]");
5062 }
5063
5064 #[test]
5065 fn test_indent_function() {
5066 let mut env = TestTemplateEnv::new();
5067 env.add_color("error", crossterm::style::Color::DarkRed);
5068 env.add_color("warning", crossterm::style::Color::DarkYellow);
5069 env.add_color("hint", crossterm::style::Color::DarkCyan);
5070
5071 assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
5074 assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
5075 assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
5076
5077 insta::assert_snapshot!(
5079 env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
5080 @"
5081 [38;5;1m__a[39m
5082 [38;5;3m__b[39m
5083 ");
5084
5085 insta::assert_snapshot!(
5087 env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
5088 @"
5089 [38;5;1m__a[38;5;3mb[39m
5090 [38;5;3m__c[39m
5091 ");
5092
5093 insta::assert_snapshot!(
5095 env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
5096 @"
5097 [38;5;1mXX[39ma
5098 [38;5;1mXX[39mb
5099 ");
5100
5101 insta::assert_snapshot!(
5103 env.render_ok(r#"indent(label("hint", "A"),
5104 label("warning", indent(label("hint", "B"),
5105 label("error", "x\n") ++ "y")))"#),
5106 @"
5107 [38;5;6mAB[38;5;1mx[39m
5108 [38;5;6mAB[38;5;3my[39m
5109 ");
5110 }
5111
5112 #[test]
5113 fn test_pad_function() {
5114 let mut env = TestTemplateEnv::new();
5115 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5116 env.add_color("red", crossterm::style::Color::Red);
5117 env.add_color("cyan", crossterm::style::Color::DarkCyan);
5118
5119 insta::assert_snapshot!(
5121 env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
5122 @"{ [38;5;9mfoo[39m}");
5123 insta::assert_snapshot!(
5124 env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
5125 @"{[38;5;9mfoo[39m }");
5126 insta::assert_snapshot!(
5127 env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
5128 @"{ [38;5;9mfoo[39m }");
5129
5130 insta::assert_snapshot!(
5132 env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
5133 @"[38;5;6m==[38;5;9mfoo[39m");
5134 insta::assert_snapshot!(
5135 env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
5136 @"[38;5;9mfoo[38;5;6m==[39m");
5137 insta::assert_snapshot!(
5138 env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
5139 @"[38;5;6m=[38;5;9mfoo[38;5;6m=[39m");
5140
5141 insta::assert_snapshot!(
5144 env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
5145 @"foo");
5146 insta::assert_snapshot!(
5147 env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
5148 @"foo<<Error: Error: Bad>Bad>");
5149 insta::assert_snapshot!(
5150 env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
5151 @"<Error: Bad>foo<Error: Bad>");
5152
5153 insta::assert_snapshot!(
5155 env.render_ok("pad_start(-1, 'foo')"),
5156 @"<Error: out of range integral type conversion attempted>");
5157 }
5158
5159 #[test]
5160 fn test_hash_function() {
5161 let mut env = TestTemplateEnv::new();
5162 env.add_color("red", crossterm::style::Color::Red);
5163
5164 assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
5167 assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
5168 assert_eq!(
5169 env.render_ok("hash(0)"),
5170 env.render_ok("hash(label('red', '0'))")
5171 );
5172 }
5173
5174 #[test]
5175 fn test_truncate_function() {
5176 let mut env = TestTemplateEnv::new();
5177 env.add_color("red", crossterm::style::Color::Red);
5178
5179 insta::assert_snapshot!(
5180 env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
5181 @"[38;5;9mar[39mbaz");
5182 insta::assert_snapshot!(
5183 env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
5184 insta::assert_snapshot!(
5185 env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
5186
5187 insta::assert_snapshot!(
5188 env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
5189 @"[38;5;9mfo[39mbaz");
5190 insta::assert_snapshot!(
5191 env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
5192 insta::assert_snapshot!(
5193 env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
5194
5195 insta::assert_snapshot!(
5197 env.render_ok("truncate_end(-1, 'foo')"),
5198 @"<Error: out of range integral type conversion attempted>");
5199 }
5200
5201 #[test]
5202 fn test_label_function() {
5203 let mut env = TestTemplateEnv::new();
5204 env.add_keyword("empty", || literal(true));
5205 env.add_color("error", crossterm::style::Color::DarkRed);
5206 env.add_color("warning", crossterm::style::Color::DarkYellow);
5207
5208 insta::assert_snapshot!(
5210 env.render_ok(r#"label("error", "text")"#),
5211 @"[38;5;1mtext[39m");
5212
5213 insta::assert_snapshot!(
5215 env.render_ok(r#"label("error".first_line(), "text")"#),
5216 @"[38;5;1mtext[39m");
5217
5218 insta::assert_snapshot!(
5220 env.render_ok("label(fill(-1, 'foo'), 'text')"),
5221 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
5222
5223 insta::assert_snapshot!(
5225 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
5226 @"[38;5;1mtext[39m");
5227 }
5228
5229 #[test]
5230 fn test_raw_escape_sequence_function_strip_labels() {
5231 let mut env = TestTemplateEnv::new();
5232 env.add_color("error", crossterm::style::Color::DarkRed);
5233 env.add_color("warning", crossterm::style::Color::DarkYellow);
5234
5235 insta::assert_snapshot!(
5236 env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
5237 @"text",
5238 );
5239 }
5240
5241 #[test]
5242 fn test_raw_escape_sequence_function_ansi_escape() {
5243 let env = TestTemplateEnv::new();
5244
5245 insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
5247 insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
5248 insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
5249 insta::assert_snapshot!(
5250 env.render_ok(r#""]8;;"
5251 ++ "http://example.com"
5252 ++ "\e\\"
5253 ++ "Example"
5254 ++ "\x1b]8;;\x1B\\""#),
5255 @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
5256
5257 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
5259 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
5260 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
5261 insta::assert_snapshot!(
5262 env.render_ok(r#"raw_escape_sequence("]8;;"
5263 ++ "http://example.com"
5264 ++ "\e\\"
5265 ++ "Example"
5266 ++ "\x1b]8;;\x1B\\")"#),
5267 @r"]8;;http://example.com\Example]8;;\");
5268 }
5269
5270 #[test]
5271 fn test_hyperlink_function_with_color() {
5272 let mut env = TestTemplateEnv::new();
5273 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5274 insta::assert_snapshot!(
5276 env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
5277 @r"]8;;http://example.com\Example]8;;\");
5278 insta::assert_snapshot!(
5279 env.render_ok(r#"hyperlink(bad_string, "Example")"#),
5280 @"<Error: Bad>");
5281 }
5282
5283 #[test]
5284 fn test_hyperlink_function_without_color() {
5285 let env = TestTemplateEnv::new();
5286 insta::assert_snapshot!(
5288 env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
5289 @"Example");
5290 }
5291
5292 #[test]
5293 fn test_hyperlink_function_custom_fallback() {
5294 let env = TestTemplateEnv::new();
5295 insta::assert_snapshot!(
5297 env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
5298 @"URL: http://example.com");
5299 }
5300
5301 #[test]
5302 fn test_hyperlink_function_stringify() {
5303 let env = TestTemplateEnv::new();
5304 insta::assert_snapshot!(
5306 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
5307 @"Example");
5308 insta::assert_snapshot!(
5310 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
5311 @"EXAMPLE");
5312 }
5313
5314 #[test]
5315 fn test_hyperlink_function_with_separate() {
5316 let env = TestTemplateEnv::new();
5317 insta::assert_snapshot!(
5319 env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
5320 @r"]8;;http://a.com\A]8;;\ | ]8;;http://b.com\B]8;;\");
5321 }
5322
5323 #[test]
5324 fn test_hyperlink_function_with_coalesce() {
5325 let env = TestTemplateEnv::new();
5326 insta::assert_snapshot!(
5328 env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
5329 @r"]8;;http://example.com\Link]8;;\");
5330 insta::assert_snapshot!(
5332 env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
5333 @"fallback");
5334 }
5335
5336 #[test]
5337 fn test_hyperlink_function_with_if() {
5338 let env = TestTemplateEnv::new();
5339 insta::assert_snapshot!(
5341 env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
5342 @r"]8;;http://example.com\Yes]8;;\");
5343 insta::assert_snapshot!(
5344 env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
5345 @r"]8;;http://example.com\No]8;;\");
5346 }
5347
5348 #[test]
5349 fn test_hyperlink_function_plain_with_separate() {
5350 let env = TestTemplateEnv::new();
5351 insta::assert_snapshot!(
5353 env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
5354 @"A | B");
5355 }
5356
5357 #[test]
5358 fn test_stringify_function() {
5359 let mut env = TestTemplateEnv::new();
5360 env.add_keyword("none_i64", || literal(None::<i64>));
5361 env.add_keyword("ascii_bstr", || literal(BString::from("foo")));
5362 env.add_keyword("odd_bstr", || literal(BString::from(b"\x80")));
5363 env.add_color("error", crossterm::style::Color::DarkRed);
5364
5365 insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
5366 insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
5367 insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
5368 insta::assert_snapshot!(env.render_ok("stringify(ascii_bstr)"), @"foo");
5369 insta::assert_snapshot!(
5370 env.render_ok("stringify(odd_bstr)"),
5371 @"[38;5;1m<Error: invalid utf-8 sequence of 1 bytes from index 0>[39m");
5372 insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
5373 }
5374
5375 #[test]
5376 fn test_json_function() {
5377 let mut env = TestTemplateEnv::new();
5378 env.add_keyword("none_i64", || literal(None::<i64>));
5379 env.add_keyword("ascii_bstr", || literal(BString::from("foo")));
5380 env.add_keyword("fs_path", || literal(PathBuf::from("file")));
5381 env.add_keyword("none_fs_path", || literal(None::<PathBuf>));
5382 env.add_keyword("string_list", || {
5383 literal(vec!["foo".to_owned(), "bar".to_owned()])
5384 });
5385 env.add_keyword("config_value_table", || {
5386 literal(ConfigValue::from_iter([("foo", "bar")]))
5387 });
5388 env.add_keyword("some_cfgval", || literal(Some(ConfigValue::from(1))));
5389 env.add_keyword("none_cfgval", || literal(None::<ConfigValue>));
5390 env.add_keyword("signature", || {
5391 literal(Signature {
5392 name: "Test User".to_owned(),
5393 email: "test.user@example.com".to_owned(),
5394 timestamp: Timestamp {
5395 timestamp: MillisSinceEpoch(0),
5396 tz_offset: 0,
5397 },
5398 })
5399 });
5400 env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
5401 env.add_keyword("size_hint", || literal((5, None)));
5402 env.add_keyword("timestamp", || {
5403 literal(Timestamp {
5404 timestamp: MillisSinceEpoch(0),
5405 tz_offset: 0,
5406 })
5407 });
5408 env.add_keyword("timestamp_range", || {
5409 literal(TimestampRange {
5410 start: Timestamp {
5411 timestamp: MillisSinceEpoch(0),
5412 tz_offset: 0,
5413 },
5414 end: Timestamp {
5415 timestamp: MillisSinceEpoch(86_400_000),
5416 tz_offset: -60,
5417 },
5418 })
5419 });
5420
5421 insta::assert_snapshot!(env.render_ok(r#"json(ascii_bstr)"#), @"[102,111,111]");
5422 insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
5423 insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
5424 insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
5425 insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
5426 insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
5427 insta::assert_snapshot!(env.render_ok("json(fs_path)"), @r#""file""#);
5428 insta::assert_snapshot!(env.render_ok("json(none_fs_path)"), @"null");
5429 insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
5430 insta::assert_snapshot!(env.render_ok(r"json(some_cfgval)"), @"1");
5431 insta::assert_snapshot!(env.render_ok(r"json(none_cfgval)"), @"null");
5432 insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
5433 insta::assert_snapshot!(
5434 env.render_ok("json(signature)"),
5435 @r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
5436 insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
5437 insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
5438 insta::assert_snapshot!(
5439 env.render_ok("json(timestamp_range)"),
5440 @r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
5441
5442 insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| s))"#), @r#"["foo","bar"]"#);
5444 insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| size_hint))"#), @"[[5,null],[5,null]]");
5445
5446 insta::assert_snapshot!(env.render_ok(r#"json(if(true, email, timestamp))"#), @r#""foo@bar""#);
5448 insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, config_value_table))"#), @"[5,null]");
5449
5450 insta::assert_snapshot!(env.parse_err(r#"json(if(true, email))"#), @r###"
5453 --> 1:6
5454 |
5455 1 | json(if(true, email))
5456 | ^-------------^
5457 |
5458 = Expected expression of type `Serialize`, but actual type is `Any`
5459 "###);
5460 insta::assert_snapshot!(env.parse_err(r#"json(if(false, email))"#), @r###"
5461 --> 1:6
5462 |
5463 1 | json(if(false, email))
5464 | ^--------------^
5465 |
5466 = Expected expression of type `Serialize`, but actual type is `Any`
5467 "###);
5468 }
5469
5470 #[test]
5471 fn test_try_function() {
5472 let mut env = TestTemplateEnv::new();
5473 env.add_keyword("bad_string", || new_error_property::<String>("Bad string"));
5474 env.add_keyword("bad_i64", || new_error_property::<i64>("Bad i64"));
5475 env.add_keyword("bad_size_hint", || {
5476 new_error_property::<SizeHint>("Bad size hint")
5477 });
5478 env.add_color("red", crossterm::style::Color::Red);
5479
5480 insta::assert_snapshot!(env.render_ok("try(bad_string)"), @"<Error: Bad string>");
5481 insta::assert_snapshot!(env.render_ok("try(bad_string, bad_i64)"), @"<Error: Bad i64>");
5482 insta::assert_snapshot!(env.render_ok("try('foo', 'bar', bad_string)"), @"foo");
5483 insta::assert_snapshot!(env.render_ok("try(bad_string, 'foo')"), @"foo");
5484 insta::assert_snapshot!(env.render_ok("try(bad_string, 'foo', 'bar')"), @"foo");
5485
5486 insta::assert_snapshot!(env.render_ok("try('foo' ++ bad_string, 'bar')"), @"bar");
5488 insta::assert_snapshot!(
5489 env.render_ok("try('foo' ++ bad_string, 'bar' ++ bad_i64)"), @"bar<Error: Bad i64>");
5490 insta::assert_snapshot!(env.render_ok("try(try(bad_string, 'foo'), 'bar')"), @"foo");
5491 insta::assert_snapshot!(env.render_ok("try(try(bad_string, bad_i64), 'foo')"), @"foo");
5492
5493 insta::assert_snapshot!(env.render_ok("try(bad_string, label('red', 'foo'))"), @"[38;5;9mfoo[39m");
5495 insta::assert_snapshot!(env.render_ok("try(label('red', 'foo'), bad_string)"), @"[38;5;9mfoo[39m");
5496
5497 insta::assert_snapshot!(env.render_ok("json(try(bad_string, 'foo'))"), @r#""foo""#);
5499 insta::assert_snapshot!(env.render_ok("json(try('foo', bad_string))"), @r#""foo""#);
5500 insta::assert_snapshot!(env.render_ok("json(try(bad_string, bad_i64, 0, ''))"), @"0");
5501
5502 insta::assert_snapshot!(env.parse_err("try()"), @"
5504 --> 1:5
5505 |
5506 1 | try()
5507 | ^
5508 |
5509 = Function `try`: Expected at least 1 arguments
5510 ");
5511
5512 insta::assert_snapshot!(env.parse_err("try('foo' == 0, '')"), @"
5514 --> 1:5
5515 |
5516 1 | try('foo' == 0, '')
5517 | ^--------^
5518 |
5519 = Cannot compare expressions of type `String` and `Integer`
5520 ");
5521
5522 insta::assert_snapshot!(env.parse_err("json(try('foo' ++ bad_string, ''))"), @"
5524 --> 1:6
5525 |
5526 1 | json(try('foo' ++ bad_string, ''))
5527 | ^--------------------------^
5528 |
5529 = Expected expression of type `Serialize`, but actual type is `Any`
5530 ");
5531 insta::assert_snapshot!(env.parse_err("try(bad_size_hint, '')"), @"
5532 --> 1:1
5533 |
5534 1 | try(bad_size_hint, '')
5535 | ^--------------------^
5536 |
5537 = Expected expression of type `Template`, but actual type is `Any`
5538 ");
5539 }
5540
5541 #[test]
5542 fn test_coalesce_function() {
5543 let mut env = TestTemplateEnv::new();
5544 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5545 env.add_keyword("empty_string", || literal("".to_owned()));
5546 env.add_keyword("non_empty_string", || literal("a".to_owned()));
5547
5548 insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
5549 insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
5550 insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
5551 insta::assert_snapshot!(
5552 env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
5553
5554 insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
5556
5557 insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
5559 insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
5561
5562 insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
5564 --> 1:15
5565 |
5566 1 | coalesce("a", value2="b")
5567 | ^--------^
5568 |
5569 = Function `coalesce`: Unexpected keyword arguments
5570 "#);
5571 }
5572
5573 #[test]
5574 fn test_concat_function() {
5575 let mut env = TestTemplateEnv::new();
5576 env.add_keyword("empty", || literal(true));
5577 env.add_keyword("hidden", || literal(false));
5578 env.add_color("empty", crossterm::style::Color::DarkGreen);
5579 env.add_color("error", crossterm::style::Color::DarkRed);
5580 env.add_color("warning", crossterm::style::Color::DarkYellow);
5581
5582 insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
5583 insta::assert_snapshot!(
5584 env.render_ok(r#"concat(hidden, empty)"#),
5585 @"false[38;5;2mtrue[39m");
5586 insta::assert_snapshot!(
5587 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
5588 @"[38;5;3ma[39mb");
5589
5590 insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
5592 --> 1:13
5593 |
5594 1 | concat("a", value2="b")
5595 | ^--------^
5596 |
5597 = Function `concat`: Unexpected keyword arguments
5598 "#);
5599 }
5600
5601 #[test]
5602 fn test_join_function() {
5603 let mut env = TestTemplateEnv::new();
5604 env.add_keyword("description", || literal("".to_owned()));
5605 env.add_keyword("empty", || literal(true));
5606 env.add_keyword("hidden", || literal(false));
5607 env.add_color("empty", crossterm::style::Color::DarkGreen);
5608 env.add_color("error", crossterm::style::Color::DarkRed);
5609 env.add_color("warning", crossterm::style::Color::DarkYellow);
5610
5611 insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
5613 insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
5614 insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
5615 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
5616 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
5617 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
5618 insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
5619 insta::assert_snapshot!(
5620 env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
5621 @"1----true--test--");
5622
5623 insta::assert_snapshot!(env.parse_err(r#"join()"#), @"
5625 --> 1:6
5626 |
5627 1 | join()
5628 | ^
5629 |
5630 = Function `join`: Expected at least 1 arguments
5631 ");
5632
5633 insta::assert_snapshot!(
5635 env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
5636 @",[38;5;3ma[39m,b");
5637 insta::assert_snapshot!(
5638 env.render_ok(
5639 r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
5640 @"[38;5;1ma[38;5;2m<><>[39mb");
5641
5642 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
5644 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
5645
5646 insta::assert_snapshot!(
5648 env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
5649 insta::assert_snapshot!(
5650 env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
5651 insta::assert_snapshot!(
5652 env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
5653
5654 insta::assert_snapshot!(
5656 env.render_ok(r#"join(",", hidden, description, empty)"#),
5657 @"false,,[38;5;2mtrue[39m");
5658 insta::assert_snapshot!(
5659 env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
5660 @"XfalseYfalseZ");
5661 insta::assert_snapshot!(
5662 env.render_ok(r#"join(hidden, empty)"#),
5663 @"[38;5;2mtrue[39m");
5664
5665 insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
5667 --> 1:16
5668 |
5669 1 | join(",", "a", arg="b")
5670 | ^-----^
5671 |
5672 = Function `join`: Unexpected keyword arguments
5673 "#);
5674
5675 env.add_keyword("str_list", || {
5677 literal(vec!["foo".to_owned(), "bar".to_owned()])
5678 });
5679 env.add_keyword("none_int", || literal(None::<i64>));
5680 env.add_keyword("some_int", || literal(Some(67)));
5681 env.add_keyword("cfg_val", || {
5682 literal(ConfigValue::from_iter([("foo", "bar")]))
5683 });
5684 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
5685 env.add_keyword("signature", || {
5686 literal(new_signature("User", "user@example.com"))
5687 });
5688 env.add_keyword("size_hint", || literal((10, None)));
5689 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
5690 env.add_keyword("timestamp_range", || {
5691 literal(TimestampRange {
5692 start: new_timestamp(0, 0),
5693 end: new_timestamp(0, 0),
5694 })
5695 });
5696 insta::assert_snapshot!(
5697 env.render_ok("join('|', str_list, 42, none_int, some_int)"),
5698 @"foo bar|42||67");
5699 insta::assert_snapshot!(
5700 env.render_ok("join('|', cfg_val, email, signature, if(true, 42), if(false, 42))"),
5701 @r#"{ foo = "bar" }|me@example.com|User <user@example.com>|42|"#);
5702 insta::assert_snapshot!(
5703 env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
5704 @"1970-01-01 00:00:00.000 +00:00|1970-01-01 00:00:00.000 +00:00 - 1970-01-01 00:00:00.000 +00:00|foo bar");
5705 assert_matches!(
5706 env.parse_err_kind("join('|', size_hint)"),
5707 TemplateParseErrorKind::Expression(_)
5708 );
5709 }
5710
5711 #[test]
5712 fn test_separate_function() {
5713 let mut env = TestTemplateEnv::new();
5714 env.add_keyword("description", || literal("".to_owned()));
5715 env.add_keyword("empty", || literal(true));
5716 env.add_keyword("hidden", || literal(false));
5717 env.add_color("empty", crossterm::style::Color::DarkGreen);
5718 env.add_color("error", crossterm::style::Color::DarkRed);
5719 env.add_color("warning", crossterm::style::Color::DarkYellow);
5720
5721 insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
5722 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
5723 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
5724 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
5725 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
5726 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
5727 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
5728
5729 insta::assert_snapshot!(
5731 env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
5732 @"[38;5;3ma[39m b");
5733
5734 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
5736 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
5737
5738 insta::assert_snapshot!(
5740 env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
5741 insta::assert_snapshot!(
5742 env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
5743 insta::assert_snapshot!(
5744 env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
5745
5746 insta::assert_snapshot!(
5748 env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
5749 insta::assert_snapshot!(
5750 env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
5751 insta::assert_snapshot!(
5752 env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
5753 insta::assert_snapshot!(
5754 env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
5755 insta::assert_snapshot!(
5756 env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
5757
5758 insta::assert_snapshot!(
5760 env.render_ok(r#"separate(" ", hidden, description, empty)"#),
5761 @"false [38;5;2mtrue[39m");
5762
5763 insta::assert_snapshot!(
5765 env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
5766 @"XfalseYfalseZ");
5767
5768 insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
5770 --> 1:20
5771 |
5772 1 | separate(" ", "a", value2="b")
5773 | ^--------^
5774 |
5775 = Function `separate`: Unexpected keyword arguments
5776 "#);
5777 }
5778
5779 #[test]
5780 fn test_surround_function() {
5781 let mut env = TestTemplateEnv::new();
5782 env.add_keyword("lt", || literal("<".to_owned()));
5783 env.add_keyword("gt", || literal(">".to_owned()));
5784 env.add_keyword("content", || literal("content".to_owned()));
5785 env.add_keyword("empty_content", || literal("".to_owned()));
5786 env.add_color("error", crossterm::style::Color::DarkRed);
5787 env.add_color("paren", crossterm::style::Color::Cyan);
5788
5789 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
5790 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
5791
5792 insta::assert_snapshot!(
5794 env.render_ok(
5795 r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
5796 @"[38;5;14m([38;5;1ma[38;5;14m)[39m");
5797
5798 insta::assert_snapshot!(
5800 env.render_ok(r#"surround(lt, gt, content)"#),
5801 @"<content>");
5802 insta::assert_snapshot!(
5803 env.render_ok(r#"surround(lt, gt, empty_content)"#),
5804 @"");
5805
5806 insta::assert_snapshot!(
5808 env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
5809 @"<empty>");
5810 insta::assert_snapshot!(
5811 env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
5812 @"");
5813 }
5814
5815 #[test]
5816 fn test_config_function() {
5817 use jj_lib::config::ConfigLayer;
5818 use jj_lib::config::ConfigSource;
5819
5820 let mut config = StackedConfig::with_defaults();
5821 config
5822 .add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
5823 config.add_layer(
5824 ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
5825 );
5826
5827 let mut env = TestTemplateEnv::with_config(config);
5828
5829 insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
5831 insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
5832 insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
5833
5834 insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
5836
5837 insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
5839 insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
5840
5841 env.add_alias("bad_config_name", "'user|name'");
5843 insta::assert_snapshot!(env.parse_err("config(bad_config_name)"), @"
5844 --> 1:8
5845 |
5846 1 | config(bad_config_name)
5847 | ^-------------^
5848 |
5849 = In alias `bad_config_name`
5850 --> 1:1
5851 |
5852 1 | 'user|name'
5853 | ^---------^
5854 |
5855 = Failed to parse config name
5856 TOML parse error at line 1, column 5
5857 |
5858 1 | user|name
5859 | ^
5860 invalid unquoted key, expected letters, numbers, `-`, `_`
5861 ");
5862
5863 env.add_alias("config_key", r#""name""#);
5865 insta::assert_snapshot!(env.render_ok(r#"config("user." ++ "name")"#), @"'Test User'");
5866 insta::assert_snapshot!(env.render_ok(r#"config("us" ++ "er")"#), @"{ email = 'test@example.com', name = 'Test User' }");
5867 insta::assert_snapshot!(env.render_ok(r#"config("user." ++ config_key)"#), @"'Test User'");
5868
5869 insta::assert_snapshot!(env.parse_err(r#"config("user." ++)"#), @r#"
5871 --> 1:18
5872 |
5873 1 | config("user." ++)
5874 | ^---
5875 |
5876 = expected <expression>
5877 "#);
5878 insta::assert_snapshot!(env.parse_err(r#"config("user|" ++ "name")"#), @r#"
5879 --> 1:8
5880 |
5881 1 | config("user|" ++ "name")
5882 | ^---------------^
5883 |
5884 = Failed to parse config name
5885 TOML parse error at line 1, column 5
5886 |
5887 1 | user|name
5888 | ^
5889 invalid unquoted key, expected letters, numbers, `-`, `_`
5890 "#);
5891 insta::assert_snapshot!(env.parse_err(r#"config(invalid)"#), @"
5892 --> 1:8
5893 |
5894 1 | config(invalid)
5895 | ^-----^
5896 |
5897 = Keyword `invalid` doesn't exist
5898 ");
5899
5900 env.add_dynamic_keyword("dyn_config_name", || "user.name".to_owned());
5902 insta::assert_snapshot!(
5903 env.render_ok(r#"config(dyn_config_name)"#), @"'Test User'"
5904 );
5905
5906 env.add_dynamic_keyword("dyn_missing", || "non.existent".to_owned());
5908 insta::assert_snapshot!(env.render_ok(r#"config(dyn_missing)"#), @"");
5909
5910 env.add_dynamic_keyword("dyn_bad_path", || "user|name".to_owned());
5912 insta::assert_snapshot!(env.render_ok(r#"config(dyn_bad_path)"#), @r"
5913 <Error: TOML parse error at line 1, column 5
5914 |
5915 1 | user|name
5916 | ^
5917 invalid unquoted key, expected letters, numbers, `-`, `_`
5918 >
5919 ");
5920
5921 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5923 insta::assert_snapshot!(env.render_ok(r#"config(bad_string)"#), @"<Error: Bad>");
5924 }
5925
5926 #[test]
5927 fn test_any_type() {
5928 let mut env = TestTemplateEnv::new();
5929 env.add_keyword("size_hint", || literal((5, None)));
5930 env.add_keyword("size_hint_2", || literal((10, None)));
5931 env.add_keyword("words", || {
5932 literal(vec!["foo".to_owned(), "bar".to_owned()])
5933 });
5934 env.add_color("red", crossterm::style::Color::Red);
5935
5936 insta::assert_snapshot!(env.render_ok(r#"if(true, label("red", "a"), "b")"#), @"[38;5;9ma[39m");
5938 insta::assert_snapshot!(env.render_ok(r#"if(false, label("red", "a"), "b")"#), @"b");
5939 insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, size_hint_2))"#), @"[5,null]");
5940 insta::assert_snapshot!(env.render_ok(r#"json(if(false, size_hint, size_hint_2))"#), @"[10,null]");
5941
5942 insta::assert_snapshot!(env.parse_err(r#"if(true, label("red", "a"), size_hint)"#), @r#"
5945 --> 1:1
5946 |
5947 1 | if(true, label("red", "a"), size_hint)
5948 | ^------------------------------------^
5949 |
5950 = Expected expression of type `Template`, but actual type is `Any`
5951 "#);
5952 insta::assert_snapshot!(env.parse_err(r#"json(if(true, size_hint, label("red", "a")))"#), @r#"
5953 --> 1:6
5954 |
5955 1 | json(if(true, size_hint, label("red", "a")))
5956 | ^------------------------------------^
5957 |
5958 = Expected expression of type `Serialize`, but actual type is `Any`
5959 "#);
5960
5961 insta::assert_snapshot!(env.parse_err(r#"if(true,words,words).join(", ")"#), @r#"
5963 --> 1:22
5964 |
5965 1 | if(true,words,words).join(", ")
5966 | ^--^
5967 |
5968 = Method `join` doesn't exist for type `Any`
5969 "#);
5970 }
5971
5972 #[test]
5973 fn test_any_list_type() {
5974 let mut env = TestTemplateEnv::new();
5975 env.add_keyword("words", || {
5976 literal(vec!["foo".to_owned(), "bar".to_owned()])
5977 });
5978 env.add_keyword("size_hint", || literal((10, None)));
5979 env.add_color("red", crossterm::style::Color::Red);
5980
5981 insta::assert_snapshot!(env.render_ok(
5983 r#"words.map(|x| label("red", x))"#),
5984 @"[38;5;9mfoo[39m [38;5;9mbar[39m");
5985 insta::assert_snapshot!(env.render_ok(
5986 r#"words.map(|x| label("red", x)).join(",")"#),
5987 @"[38;5;9mfoo[39m,[38;5;9mbar[39m");
5988 insta::assert_snapshot!(env.render_ok(
5989 r#"json(words.map(|x| size_hint))"#),
5990 @"[[10,null],[10,null]]");
5991
5992 insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint)"#), @r#"
5994 --> 1:1
5995 |
5996 1 | words.map(|x| size_hint)
5997 | ^----------------------^
5998 |
5999 = Expected expression of type `Template`, but actual type is `AnyList`
6000 "#);
6001 insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint).join(",")"#), @r#"
6002 --> 1:26
6003 |
6004 1 | words.map(|x| size_hint).join(",")
6005 | ^--^
6006 |
6007 = Expected expression of type `Template`, but actual type is `AnyList`
6008 "#);
6009 insta::assert_snapshot!(env.parse_err(r#"json(words.map(|x| label("red", x)))"#), @r#"
6010 --> 1:6
6011 |
6012 1 | json(words.map(|x| label("red", x)))
6013 | ^----------------------------^
6014 |
6015 = Expected expression of type `Serialize`, but actual type is `AnyList`
6016 "#);
6017 }
6018}