use std::collections::HashMap;
use itertools::Itertools as _;
use jj_lib::backend::{Signature, Timestamp};
use jj_lib::dsl_util::AliasExpandError as _;
use crate::template_parser::{
self, BinaryOp, ExpressionKind, ExpressionNode, FunctionCallNode, TemplateAliasesMap,
TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp,
};
use crate::templater::{
CoalesceTemplate, ConcatTemplate, ConditionalTemplate, LabelTemplate, ListPropertyTemplate,
ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate,
SeparateTemplate, SizeHint, Template, TemplateProperty, TemplatePropertyError,
TemplatePropertyExt as _, TemplateRenderer, TimestampRange,
};
use crate::{text_util, time_util};
pub trait TemplateLanguage<'a> {
type Property: IntoTemplateProperty<'a>;
fn wrap_string(property: impl TemplateProperty<Output = String> + 'a) -> Self::Property;
fn wrap_string_list(
property: impl TemplateProperty<Output = Vec<String>> + 'a,
) -> Self::Property;
fn wrap_boolean(property: impl TemplateProperty<Output = bool> + 'a) -> Self::Property;
fn wrap_integer(property: impl TemplateProperty<Output = i64> + 'a) -> Self::Property;
fn wrap_integer_opt(
property: impl TemplateProperty<Output = Option<i64>> + 'a,
) -> Self::Property;
fn wrap_signature(property: impl TemplateProperty<Output = Signature> + 'a) -> Self::Property;
fn wrap_size_hint(property: impl TemplateProperty<Output = SizeHint> + 'a) -> Self::Property;
fn wrap_timestamp(property: impl TemplateProperty<Output = Timestamp> + 'a) -> Self::Property;
fn wrap_timestamp_range(
property: impl TemplateProperty<Output = TimestampRange> + 'a,
) -> Self::Property;
fn wrap_template(template: Box<dyn Template + 'a>) -> Self::Property;
fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> Self::Property;
fn build_function(
&self,
build_ctx: &BuildContext<Self::Property>,
function: &FunctionCallNode,
) -> TemplateParseResult<Self::Property>;
fn build_method(
&self,
build_ctx: &BuildContext<Self::Property>,
property: Self::Property,
function: &FunctionCallNode,
) -> TemplateParseResult<Self::Property>;
}
macro_rules! impl_core_wrap_property_fns {
($a:lifetime) => {
$crate::template_builder::impl_core_wrap_property_fns!($a, std::convert::identity);
};
($a:lifetime, $outer:path) => {
$crate::template_builder::impl_wrap_property_fns!(
$a, $crate::template_builder::CoreTemplatePropertyKind, $outer, {
wrap_string(String) => String,
wrap_string_list(Vec<String>) => StringList,
wrap_boolean(bool) => Boolean,
wrap_integer(i64) => Integer,
wrap_integer_opt(Option<i64>) => IntegerOpt,
wrap_signature(jj_lib::backend::Signature) => Signature,
wrap_size_hint($crate::templater::SizeHint) => SizeHint,
wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp,
wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange,
}
);
fn wrap_template(
template: Box<dyn $crate::templater::Template + $a>,
) -> Self::Property {
use $crate::template_builder::CoreTemplatePropertyKind as Kind;
$outer(Kind::Template(template))
}
fn wrap_list_template(
template: Box<dyn $crate::templater::ListTemplate + $a>,
) -> Self::Property {
use $crate::template_builder::CoreTemplatePropertyKind as Kind;
$outer(Kind::ListTemplate(template))
}
};
}
macro_rules! impl_wrap_property_fns {
($a:lifetime, $kind:path, $outer:path, { $( $func:ident($ty:ty) => $var:ident, )+ }) => {
$(
fn $func(
property: impl $crate::templater::TemplateProperty<Output = $ty> + $a,
) -> Self::Property {
use $kind as Kind; $outer(Kind::$var(Box::new(property)))
}
)+
};
}
pub(crate) use {impl_core_wrap_property_fns, impl_wrap_property_fns};
pub trait IntoTemplateProperty<'a> {
fn type_name(&self) -> &'static str;
fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>>;
fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>>;
fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>>;
fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
}
pub enum CoreTemplatePropertyKind<'a> {
String(Box<dyn TemplateProperty<Output = String> + 'a>),
StringList(Box<dyn TemplateProperty<Output = Vec<String>> + 'a>),
Boolean(Box<dyn TemplateProperty<Output = bool> + 'a>),
Integer(Box<dyn TemplateProperty<Output = i64> + 'a>),
IntegerOpt(Box<dyn TemplateProperty<Output = Option<i64>> + 'a>),
Signature(Box<dyn TemplateProperty<Output = Signature> + 'a>),
SizeHint(Box<dyn TemplateProperty<Output = SizeHint> + 'a>),
Timestamp(Box<dyn TemplateProperty<Output = Timestamp> + 'a>),
TimestampRange(Box<dyn TemplateProperty<Output = TimestampRange> + 'a>),
Template(Box<dyn Template + 'a>),
ListTemplate(Box<dyn ListTemplate + 'a>),
}
impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
fn type_name(&self) -> &'static str {
match self {
CoreTemplatePropertyKind::String(_) => "String",
CoreTemplatePropertyKind::StringList(_) => "List<String>",
CoreTemplatePropertyKind::Boolean(_) => "Boolean",
CoreTemplatePropertyKind::Integer(_) => "Integer",
CoreTemplatePropertyKind::IntegerOpt(_) => "Option<Integer>",
CoreTemplatePropertyKind::Signature(_) => "Signature",
CoreTemplatePropertyKind::SizeHint(_) => "SizeHint",
CoreTemplatePropertyKind::Timestamp(_) => "Timestamp",
CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange",
CoreTemplatePropertyKind::Template(_) => "Template",
CoreTemplatePropertyKind::ListTemplate(_) => "ListTemplate",
}
}
fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
match self {
CoreTemplatePropertyKind::String(property) => {
Some(Box::new(property.map(|s| !s.is_empty())))
}
CoreTemplatePropertyKind::StringList(property) => {
Some(Box::new(property.map(|l| !l.is_empty())))
}
CoreTemplatePropertyKind::Boolean(property) => Some(property),
CoreTemplatePropertyKind::Integer(_) => None,
CoreTemplatePropertyKind::IntegerOpt(property) => {
Some(Box::new(property.map(|opt| opt.is_some())))
}
CoreTemplatePropertyKind::Signature(_) => None,
CoreTemplatePropertyKind::SizeHint(_) => None,
CoreTemplatePropertyKind::Timestamp(_) => None,
CoreTemplatePropertyKind::TimestampRange(_) => None,
CoreTemplatePropertyKind::Template(_) => None,
CoreTemplatePropertyKind::ListTemplate(_) => None,
}
}
fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
match self {
CoreTemplatePropertyKind::Integer(property) => Some(property),
CoreTemplatePropertyKind::IntegerOpt(property) => {
Some(Box::new(property.try_unwrap("Integer")))
}
_ => None,
}
}
fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>> {
match self {
CoreTemplatePropertyKind::String(property) => Some(property),
_ => {
let template = self.try_into_template()?;
Some(Box::new(PlainTextFormattedProperty::new(template)))
}
}
}
fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
match self {
CoreTemplatePropertyKind::String(property) => Some(property.into_template()),
CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()),
CoreTemplatePropertyKind::IntegerOpt(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()),
CoreTemplatePropertyKind::SizeHint(_) => None,
CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()),
CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Template(template) => Some(template),
CoreTemplatePropertyKind::ListTemplate(template) => Some(template.into_template()),
}
}
}
pub type TemplateBuildFunctionFn<'a, L> =
fn(
&L,
&BuildContext<<L as TemplateLanguage<'a>>::Property>,
&FunctionCallNode,
) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
pub type TemplateBuildMethodFn<'a, L, T> =
fn(
&L,
&BuildContext<<L as TemplateLanguage<'a>>::Property>,
Box<dyn TemplateProperty<Output = T> + 'a>,
&FunctionCallNode,
) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
pub type TemplateBuildFunctionFnMap<'a, L> = HashMap<&'static str, TemplateBuildFunctionFn<'a, L>>;
pub type TemplateBuildMethodFnMap<'a, L, T> =
HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>;
pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> {
pub functions: TemplateBuildFunctionFnMap<'a, L>,
pub string_methods: TemplateBuildMethodFnMap<'a, L, String>,
pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>,
pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>,
pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>,
pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint>,
pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>,
pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>,
}
pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
for (name, function) in extension {
if base.insert(name, function).is_some() {
panic!("Conflicting template definitions for '{name}' function");
}
}
}
impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
pub fn builtin() -> Self {
CoreTemplateBuildFnTable {
functions: builtin_functions(),
string_methods: builtin_string_methods(),
boolean_methods: HashMap::new(),
integer_methods: HashMap::new(),
signature_methods: builtin_signature_methods(),
size_hint_methods: builtin_size_hint_methods(),
timestamp_methods: builtin_timestamp_methods(),
timestamp_range_methods: builtin_timestamp_range_methods(),
}
}
pub fn empty() -> Self {
CoreTemplateBuildFnTable {
functions: HashMap::new(),
string_methods: HashMap::new(),
boolean_methods: HashMap::new(),
integer_methods: HashMap::new(),
signature_methods: HashMap::new(),
size_hint_methods: HashMap::new(),
timestamp_methods: HashMap::new(),
timestamp_range_methods: HashMap::new(),
}
}
pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) {
let CoreTemplateBuildFnTable {
functions,
string_methods,
boolean_methods,
integer_methods,
signature_methods,
size_hint_methods,
timestamp_methods,
timestamp_range_methods,
} = extension;
merge_fn_map(&mut self.functions, functions);
merge_fn_map(&mut self.string_methods, string_methods);
merge_fn_map(&mut self.boolean_methods, boolean_methods);
merge_fn_map(&mut self.integer_methods, integer_methods);
merge_fn_map(&mut self.signature_methods, signature_methods);
merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
}
pub fn build_function(
&self,
language: &L,
build_ctx: &BuildContext<L::Property>,
function: &FunctionCallNode,
) -> TemplateParseResult<L::Property> {
let table = &self.functions;
let build = template_parser::lookup_function(table, function)?;
build(language, build_ctx, function)
}
pub fn build_method(
&self,
language: &L,
build_ctx: &BuildContext<L::Property>,
property: CoreTemplatePropertyKind<'a>,
function: &FunctionCallNode,
) -> TemplateParseResult<L::Property> {
let type_name = property.type_name();
match property {
CoreTemplatePropertyKind::String(property) => {
let table = &self.string_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::StringList(property) => {
build_formattable_list_method(language, build_ctx, property, function, |item| {
L::wrap_string(item)
})
}
CoreTemplatePropertyKind::Boolean(property) => {
let table = &self.boolean_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::Integer(property) => {
let table = &self.integer_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::IntegerOpt(property) => {
let type_name = "Integer";
let table = &self.integer_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
let inner_property = property.try_unwrap(type_name);
build(language, build_ctx, Box::new(inner_property), function)
}
CoreTemplatePropertyKind::Signature(property) => {
let table = &self.signature_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::SizeHint(property) => {
let table = &self.size_hint_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::Timestamp(property) => {
let table = &self.timestamp_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::TimestampRange(property) => {
let table = &self.timestamp_range_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::Template(_) => {
Err(TemplateParseError::no_such_method(type_name, function))
}
CoreTemplatePropertyKind::ListTemplate(template) => {
build_list_template_method(language, build_ctx, template, function)
}
}
}
}
pub struct Expression<P> {
property: P,
labels: Vec<String>,
}
impl<P> Expression<P> {
fn unlabeled(property: P) -> Self {
let labels = vec![];
Expression { property, labels }
}
fn with_label(property: P, label: impl Into<String>) -> Self {
let labels = vec![label.into()];
Expression { property, labels }
}
}
impl<'a, P: IntoTemplateProperty<'a>> Expression<P> {
pub fn type_name(&self) -> &'static str {
self.property.type_name()
}
pub fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'a>> {
self.property.try_into_boolean()
}
pub fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
self.property.try_into_integer()
}
pub fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'a>> {
self.property.try_into_plain_text()
}
pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
let template = self.property.try_into_template()?;
if self.labels.is_empty() {
Some(template)
} else {
Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
}
}
}
pub struct BuildContext<'i, P> {
local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>,
self_variable: &'i (dyn Fn() -> P),
}
fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
name: &str,
name_span: pest::Span<'_>,
) -> TemplateParseResult<L::Property> {
let self_property = (build_ctx.self_variable)();
let function = FunctionCallNode {
name,
name_span,
args: vec![],
keyword_args: vec![],
args_span: name_span.end_pos().span(&name_span.end_pos()),
};
language
.build_method(build_ctx, self_property, &function)
.map_err(|err| match err.kind() {
TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
let kind = TemplateParseErrorKind::NoSuchKeyword {
name: name.to_owned(),
candidates: candidates.clone(),
};
TemplateParseError::with_span(kind, name_span)
}
TemplateParseErrorKind::InvalidArguments { .. } => {
let kind = TemplateParseErrorKind::NoSuchKeyword {
name: name.to_owned(),
candidates: vec![format!("self.{name}(..)")],
};
TemplateParseError::with_span(kind, name_span)
}
_ => err,
})
}
fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
op: UnaryOp,
arg_node: &ExpressionNode,
) -> TemplateParseResult<L::Property> {
match op {
UnaryOp::LogicalNot => {
let arg = expect_boolean_expression(language, build_ctx, arg_node)?;
Ok(L::wrap_boolean(arg.map(|v| !v)))
}
UnaryOp::Negate => {
let arg = expect_integer_expression(language, build_ctx, arg_node)?;
Ok(L::wrap_integer(arg.and_then(|v| {
v.checked_neg()
.ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
})))
}
}
}
fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
op: BinaryOp,
lhs_node: &ExpressionNode,
rhs_node: &ExpressionNode,
) -> TemplateParseResult<L::Property> {
match op {
BinaryOp::LogicalOr => {
let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?;
let rhs = expect_boolean_expression(language, build_ctx, rhs_node)?;
let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
Ok(L::wrap_boolean(out))
}
BinaryOp::LogicalAnd => {
let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?;
let rhs = expect_boolean_expression(language, build_ctx, rhs_node)?;
let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
Ok(L::wrap_boolean(out))
}
}
}
fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, String> {
let mut map = TemplateBuildMethodFnMap::<L, String>::new();
map.insert("len", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|s| Ok(s.len().try_into()?));
Ok(L::wrap_integer(out_property))
});
map.insert(
"contains",
|language, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.contains(&needle));
Ok(L::wrap_boolean(out_property))
},
);
map.insert(
"starts_with",
|language, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.starts_with(&needle));
Ok(L::wrap_boolean(out_property))
},
);
map.insert(
"ends_with",
|language, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.ends_with(&needle));
Ok(L::wrap_boolean(out_property))
},
);
map.insert(
"remove_prefix",
|language, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
let out_property = (self_property, needle_property).map(|(haystack, needle)| {
haystack
.strip_prefix(&needle)
.map(ToOwned::to_owned)
.unwrap_or(haystack)
});
Ok(L::wrap_string(out_property))
},
);
map.insert(
"remove_suffix",
|language, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?;
let out_property = (self_property, needle_property).map(|(haystack, needle)| {
haystack
.strip_suffix(&needle)
.map(ToOwned::to_owned)
.unwrap_or(haystack)
});
Ok(L::wrap_string(out_property))
},
);
map.insert("substr", |language, build_ctx, self_property, function| {
let [start_idx, end_idx] = function.expect_exact_arguments()?;
let start_idx_property = expect_isize_expression(language, build_ctx, start_idx)?;
let end_idx_property = expect_isize_expression(language, build_ctx, end_idx)?;
let out_property =
(self_property, start_idx_property, end_idx_property).map(|(s, start_idx, end_idx)| {
let start_idx = string_index_to_char_boundary(&s, start_idx);
let end_idx = string_index_to_char_boundary(&s, end_idx);
s.get(start_idx..end_idx).unwrap_or_default().to_owned()
});
Ok(L::wrap_string(out_property))
});
map.insert(
"first_line",
|_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property =
self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
Ok(L::wrap_string(out_property))
},
);
map.insert("lines", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect());
Ok(L::wrap_string_list(out_property))
});
map.insert("upper", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.to_uppercase());
Ok(L::wrap_string(out_property))
});
map.insert("lower", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.to_lowercase());
Ok(L::wrap_string(out_property))
});
map
}
fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
let magnitude = i.unsigned_abs();
if i < 0 {
let p = s.len().saturating_sub(magnitude);
(p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
} else {
let p = magnitude.min(s.len());
(0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
}
}
fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, Signature> {
let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
map.insert("name", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| signature.name);
Ok(L::wrap_string(out_property))
});
map.insert("email", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| signature.email);
Ok(L::wrap_string(out_property))
});
map.insert(
"username",
|_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| {
let (username, _) = text_util::split_email(&signature.email);
username.to_owned()
});
Ok(L::wrap_string(out_property))
},
);
map.insert(
"timestamp",
|_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| signature.timestamp);
Ok(L::wrap_timestamp(out_property))
},
);
map
}
fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, SizeHint> {
let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
map.insert("lower", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
Ok(L::wrap_integer(out_property))
});
map.insert("upper", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property =
self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
Ok(L::wrap_integer_opt(out_property))
});
map.insert("exact", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|(lower, upper)| {
let exact = (Some(lower) == upper).then_some(lower);
Ok(exact.map(i64::try_from).transpose()?)
});
Ok(L::wrap_integer_opt(out_property))
});
map.insert("zero", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|(_, upper)| upper == Some(0));
Ok(L::wrap_boolean(out_property))
});
map
}
fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, Timestamp> {
let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
map.insert("ago", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let now = Timestamp::now();
let format = timeago::Formatter::new();
let out_property = self_property
.and_then(move |timestamp| Ok(time_util::format_duration(×tamp, &now, &format)?));
Ok(L::wrap_string(out_property))
});
map.insert(
"format",
|_language, _build_ctx, self_property, function| {
let [format_node] = function.expect_exact_arguments()?;
let format =
template_parser::expect_string_literal_with(format_node, |format, span| {
time_util::FormattingItems::parse(format)
.ok_or_else(|| TemplateParseError::expression("Invalid time format", span))
})?
.into_owned();
let out_property = self_property.and_then(move |timestamp| {
Ok(time_util::format_absolute_timestamp_with(
×tamp, &format,
)?)
});
Ok(L::wrap_string(out_property))
},
);
map.insert("utc", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|mut timestamp| {
timestamp.tz_offset = 0;
timestamp
});
Ok(L::wrap_timestamp(out_property))
});
map.insert("local", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
.ok()
.and_then(|tz_string| tz_string.parse::<i32>().ok())
.unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
let out_property = self_property.map(move |mut timestamp| {
timestamp.tz_offset = tz_offset;
timestamp
});
Ok(L::wrap_timestamp(out_property))
});
map
}
fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
map.insert("start", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|time_range| time_range.start);
Ok(L::wrap_timestamp(out_property))
});
map.insert("end", |_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|time_range| time_range.end);
Ok(L::wrap_timestamp(out_property))
});
map.insert(
"duration",
|_language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|time_range| Ok(time_range.duration()?));
Ok(L::wrap_string(out_property))
},
);
map
}
fn build_list_template_method<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
self_template: Box<dyn ListTemplate + 'a>,
function: &FunctionCallNode,
) -> TemplateParseResult<L::Property> {
let property = match function.name {
"join" => {
let [separator_node] = function.expect_exact_arguments()?;
let separator = expect_template_expression(language, build_ctx, separator_node)?;
L::wrap_template(self_template.join(separator))
}
_ => return Err(TemplateParseError::no_such_method("ListTemplate", function)),
};
Ok(property)
}
pub fn build_formattable_list_method<'a, L, O>(
language: &L,
build_ctx: &BuildContext<L::Property>,
self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
function: &FunctionCallNode,
wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
) -> TemplateParseResult<L::Property>
where
L: TemplateLanguage<'a> + ?Sized,
O: Template + Clone + 'a,
{
let property = match function.name {
"len" => {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
L::wrap_integer(out_property)
}
"join" => {
let [separator_node] = function.expect_exact_arguments()?;
let separator = expect_template_expression(language, build_ctx, separator_node)?;
let template =
ListPropertyTemplate::new(self_property, separator, |formatter, item| {
item.format(formatter)
});
L::wrap_template(Box::new(template))
}
"map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?,
_ => return Err(TemplateParseError::no_such_method("List", function)),
};
Ok(property)
}
pub fn build_unformattable_list_method<'a, L, O>(
language: &L,
build_ctx: &BuildContext<L::Property>,
self_property: impl TemplateProperty<Output = Vec<O>> + 'a,
function: &FunctionCallNode,
wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
) -> TemplateParseResult<L::Property>
where
L: TemplateLanguage<'a> + ?Sized,
O: Clone + 'a,
{
let property = match function.name {
"len" => {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|items| Ok(items.len().try_into()?));
L::wrap_integer(out_property)
}
"map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?,
_ => return Err(TemplateParseError::no_such_method("List", function)),
};
Ok(property)
}
fn build_map_operation<'a, L, O, P>(
language: &L,
build_ctx: &BuildContext<L::Property>,
self_property: P,
function: &FunctionCallNode,
wrap_item: impl Fn(PropertyPlaceholder<O>) -> L::Property,
) -> TemplateParseResult<L::Property>
where
L: TemplateLanguage<'a> + ?Sized,
P: TemplateProperty + 'a,
P::Output: IntoIterator<Item = O>,
O: Clone + 'a,
{
let [lambda_node] = function.expect_exact_arguments()?;
let item_placeholder = PropertyPlaceholder::new();
let item_template = template_parser::expect_lambda_with(lambda_node, |lambda, _span| {
let item_fn = || wrap_item(item_placeholder.clone());
let mut local_variables = build_ctx.local_variables.clone();
if let [name] = lambda.params.as_slice() {
local_variables.insert(name, &item_fn);
} else {
return Err(TemplateParseError::expression(
"Expected 1 lambda parameters",
lambda.params_span,
));
}
let inner_build_ctx = BuildContext {
local_variables,
self_variable: build_ctx.self_variable,
};
expect_template_expression(language, &inner_build_ctx, &lambda.body)
})?;
let list_template = ListPropertyTemplate::new(
self_property,
Literal(" "), move |formatter, item| {
item_placeholder.with_value(item, || item_template.format(formatter))
},
);
Ok(L::wrap_list_template(Box::new(list_template)))
}
fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
let mut map = TemplateBuildFunctionFnMap::<L>::new();
map.insert("fill", |language, build_ctx, function| {
let [width_node, content_node] = function.expect_exact_arguments()?;
let width = expect_usize_expression(language, build_ctx, width_node)?;
let content = expect_template_expression(language, build_ctx, content_node)?;
let template =
ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
Err(err) => formatter.handle_error(err),
});
Ok(L::wrap_template(Box::new(template)))
});
map.insert("indent", |language, build_ctx, function| {
let [prefix_node, content_node] = function.expect_exact_arguments()?;
let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
let content = expect_template_expression(language, build_ctx, content_node)?;
let template = ReformatTemplate::new(content, move |formatter, recorded| {
let rewrap = formatter.rewrap_fn();
text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
prefix.format(&mut rewrap(formatter))
})
});
Ok(L::wrap_template(Box::new(template)))
});
map.insert("label", |language, build_ctx, function| {
let [label_node, content_node] = function.expect_exact_arguments()?;
let label_property = expect_plain_text_expression(language, build_ctx, label_node)?;
let content = expect_template_expression(language, build_ctx, content_node)?;
let labels =
label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
Ok(L::wrap_template(Box::new(LabelTemplate::new(
content, labels,
))))
});
map.insert("if", |language, build_ctx, function| {
let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
let condition = expect_boolean_expression(language, build_ctx, condition_node)?;
let true_template = expect_template_expression(language, build_ctx, true_node)?;
let false_template = false_node
.map(|node| expect_template_expression(language, build_ctx, node))
.transpose()?;
let template = ConditionalTemplate::new(condition, true_template, false_template);
Ok(L::wrap_template(Box::new(template)))
});
map.insert("coalesce", |language, build_ctx, function| {
let contents = function
.args
.iter()
.map(|node| expect_template_expression(language, build_ctx, node))
.try_collect()?;
Ok(L::wrap_template(Box::new(CoalesceTemplate(contents))))
});
map.insert("concat", |language, build_ctx, function| {
let contents = function
.args
.iter()
.map(|node| expect_template_expression(language, build_ctx, node))
.try_collect()?;
Ok(L::wrap_template(Box::new(ConcatTemplate(contents))))
});
map.insert("separate", |language, build_ctx, function| {
let ([separator_node], content_nodes) = function.expect_some_arguments()?;
let separator = expect_template_expression(language, build_ctx, separator_node)?;
let contents = content_nodes
.iter()
.map(|node| expect_template_expression(language, build_ctx, node))
.try_collect()?;
Ok(L::wrap_template(Box::new(SeparateTemplate::new(
separator, contents,
))))
});
map.insert("surround", |language, build_ctx, function| {
let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
let suffix = expect_template_expression(language, build_ctx, suffix_node)?;
let content = expect_template_expression(language, build_ctx, content_node)?;
let template = ReformatTemplate::new(content, move |formatter, recorded| {
if recorded.data().is_empty() {
return Ok(());
}
prefix.format(formatter)?;
recorded.replay(formatter.as_mut())?;
suffix.format(formatter)?;
Ok(())
});
Ok(L::wrap_template(Box::new(template)))
});
map
}
pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Expression<L::Property>> {
match &node.kind {
ExpressionKind::Identifier(name) => {
if let Some(make) = build_ctx.local_variables.get(name) {
Ok(Expression::unlabeled(make()))
} else if *name == "self" {
let make = build_ctx.self_variable;
Ok(Expression::unlabeled(make()))
} else {
let property =
build_keyword(language, build_ctx, name, node.span).map_err(|err| {
err.extend_keyword_candidates(itertools::chain(
build_ctx.local_variables.keys().copied(),
["self"],
))
})?;
Ok(Expression::with_label(property, *name))
}
}
ExpressionKind::Boolean(value) => {
let property = L::wrap_boolean(Literal(*value));
Ok(Expression::unlabeled(property))
}
ExpressionKind::Integer(value) => {
let property = L::wrap_integer(Literal(*value));
Ok(Expression::unlabeled(property))
}
ExpressionKind::String(value) => {
let property = L::wrap_string(Literal(value.clone()));
Ok(Expression::unlabeled(property))
}
ExpressionKind::Unary(op, arg_node) => {
let property = build_unary_operation(language, build_ctx, *op, arg_node)?;
Ok(Expression::unlabeled(property))
}
ExpressionKind::Binary(op, lhs_node, rhs_node) => {
let property = build_binary_operation(language, build_ctx, *op, lhs_node, rhs_node)?;
Ok(Expression::unlabeled(property))
}
ExpressionKind::Concat(nodes) => {
let templates = nodes
.iter()
.map(|node| expect_template_expression(language, build_ctx, node))
.try_collect()?;
let property = L::wrap_template(Box::new(ConcatTemplate(templates)));
Ok(Expression::unlabeled(property))
}
ExpressionKind::FunctionCall(function) => {
let property = language.build_function(build_ctx, function)?;
Ok(Expression::unlabeled(property))
}
ExpressionKind::MethodCall(method) => {
let mut expression = build_expression(language, build_ctx, &method.object)?;
expression.property =
language.build_method(build_ctx, expression.property, &method.function)?;
expression.labels.push(method.function.name.to_owned());
Ok(expression)
}
ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
"Lambda cannot be defined here",
node.span,
)),
ExpressionKind::AliasExpanded(id, subst) => build_expression(language, build_ctx, subst)
.map_err(|e| e.within_alias_expansion(*id, node.span)),
}
}
pub fn build<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
node: &ExpressionNode,
wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
) -> TemplateParseResult<TemplateRenderer<'a, C>> {
let self_placeholder = PropertyPlaceholder::new();
let build_ctx = BuildContext {
local_variables: HashMap::new(),
self_variable: &|| wrap_self(self_placeholder.clone()),
};
let template = expect_template_expression(language, &build_ctx, node)?;
Ok(TemplateRenderer::new(template, self_placeholder))
}
pub fn parse<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
template_text: &str,
aliases_map: &TemplateAliasesMap,
wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
) -> TemplateParseResult<TemplateRenderer<'a, C>> {
let node = template_parser::parse(template_text, aliases_map)?;
build(language, &node, wrap_self).map_err(|err| err.extend_alias_candidates(aliases_map))
}
pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn TemplateProperty<Output = bool> + 'a>> {
expect_expression_of_type(language, build_ctx, node, "Boolean", |expression| {
expression.try_into_boolean()
})
}
pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn TemplateProperty<Output = i64> + 'a>> {
expect_expression_of_type(language, build_ctx, node, "Integer", |expression| {
expression.try_into_integer()
})
}
pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn TemplateProperty<Output = isize> + 'a>> {
let i64_property = expect_integer_expression(language, build_ctx, node)?;
let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
Ok(Box::new(isize_property))
}
pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn TemplateProperty<Output = usize> + 'a>> {
let i64_property = expect_integer_expression(language, build_ctx, node)?;
let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
Ok(Box::new(usize_property))
}
pub fn expect_plain_text_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn TemplateProperty<Output = String> + 'a>> {
expect_expression_of_type(language, build_ctx, node, "Template", |expression| {
expression.try_into_plain_text()
})
}
pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn Template + 'a>> {
expect_expression_of_type(language, build_ctx, node, "Template", |expression| {
expression.try_into_template()
})
}
fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
language: &L,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
expected_type: &str,
f: impl FnOnce(Expression<L::Property>) -> Option<T>,
) -> TemplateParseResult<T> {
if let ExpressionKind::AliasExpanded(id, subst) = &node.kind {
expect_expression_of_type(language, build_ctx, subst, expected_type, f)
.map_err(|e| e.within_alias_expansion(*id, node.span))
} else {
let expression = build_expression(language, build_ctx, node)?;
let actual_type = expression.type_name();
f(expression)
.ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
}
}
#[cfg(test)]
mod tests {
use std::iter;
use jj_lib::backend::MillisSinceEpoch;
use super::*;
use crate::formatter::{self, ColorFormatter};
use crate::generic_templater::GenericTemplateLanguage;
type L = GenericTemplateLanguage<'static, ()>;
type TestTemplatePropertyKind = <L as TemplateLanguage<'static>>::Property;
struct TestTemplateEnv {
language: L,
aliases_map: TemplateAliasesMap,
color_rules: Vec<(Vec<String>, formatter::Style)>,
}
impl TestTemplateEnv {
fn new() -> Self {
TestTemplateEnv {
language: L::new(),
aliases_map: TemplateAliasesMap::new(),
color_rules: Vec::new(),
}
}
}
impl TestTemplateEnv {
fn add_keyword<F>(&mut self, name: &'static str, build: F)
where
F: Fn() -> TestTemplatePropertyKind + 'static,
{
self.language.add_keyword(name, move |_| Ok(build()));
}
fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
self.aliases_map.insert(decl, defn).unwrap();
}
fn add_color(&mut self, label: &str, fg_color: crossterm::style::Color) {
let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
let style = formatter::Style {
fg_color: Some(fg_color),
..Default::default()
};
self.color_rules.push((labels, style));
}
fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, ()>> {
parse(&self.language, template, &self.aliases_map, L::wrap_self)
}
fn parse_err(&self, template: &str) -> String {
let err = self.parse(template).err().unwrap();
iter::successors(Some(&err), |e| e.origin()).join("\n")
}
fn render_ok(&self, template: &str) -> String {
let template = self.parse(template).unwrap();
let mut output = Vec::new();
let mut formatter =
ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
template.format(&(), &mut formatter).unwrap();
drop(formatter);
String::from_utf8(output).unwrap()
}
}
fn new_error_property<O>(message: &str) -> impl TemplateProperty<Output = O> + '_ {
Literal(()).and_then(|()| Err(TemplatePropertyError(message.into())))
}
fn new_signature(name: &str, email: &str) -> Signature {
Signature {
name: name.to_owned(),
email: email.to_owned(),
timestamp: new_timestamp(0, 0),
}
}
fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
Timestamp {
timestamp: MillisSinceEpoch(msec),
tz_offset,
}
}
#[test]
fn test_parsed_tree() {
let mut env = TestTemplateEnv::new();
env.add_keyword("divergent", || L::wrap_boolean(Literal(false)));
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
env.add_keyword("hello", || L::wrap_string(Literal("Hello".to_owned())));
insta::assert_snapshot!(env.render_ok(r#" "#), @"");
insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
}
#[test]
fn test_parse_error() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r###"
--> 1:13
|
1 | description ()
| ^---
|
= expected <EOI>, `++`, `||`, or `&&`
"###);
insta::assert_snapshot!(env.parse_err(r#"foo"#), @r###"
--> 1:1
|
1 | foo
| ^-^
|
= Keyword "foo" doesn't exist
"###);
insta::assert_snapshot!(env.parse_err(r#"foo()"#), @r###"
--> 1:1
|
1 | foo()
| ^-^
|
= Function "foo" doesn't exist
"###);
insta::assert_snapshot!(env.parse_err(r#"false()"#), @r###"
--> 1:1
|
1 | false()
| ^---^
|
= Expected identifier
"###);
insta::assert_snapshot!(env.parse_err(r#"!foo"#), @r###"
--> 1:2
|
1 | !foo
| ^-^
|
= Keyword "foo" doesn't exist
"###);
insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @r###"
--> 1:9
|
1 | true && 123
| ^-^
|
= Expected expression of type "Boolean", but actual type is "Integer"
"###);
insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r###"
--> 1:26
|
1 | description.first_line().foo()
| ^-^
|
= Method "foo" doesn't exist for type "String"
"###);
insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @r###"
--> 1:1
|
1 | 10000000000000000000
| ^------------------^
|
= Invalid integer literal
"###);
insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @r###"
--> 1:4
|
1 | 42.foo()
| ^-^
|
= Method "foo" doesn't exist for type "Integer"
"###);
insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @r###"
--> 1:3
|
1 | (-empty)
| ^---^
|
= Expected expression of type "Integer", but actual type is "Boolean"
"###);
insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r###"
--> 1:18
|
1 | ("foo" ++ "bar").baz()
| ^-^
|
= Method "baz" doesn't exist for type "Template"
"###);
insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @r###"
--> 1:22
|
1 | description.contains()
| ^
|
= Function "contains": Expected 1 arguments
"###);
insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r###"
--> 1:24
|
1 | description.first_line("foo")
| ^---^
|
= Function "first_line": Expected 0 arguments
"###);
insta::assert_snapshot!(env.parse_err(r#"label()"#), @r###"
--> 1:7
|
1 | label()
| ^
|
= Function "label": Expected 2 arguments
"###);
insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r###"
--> 1:7
|
1 | label("foo", "bar", "baz")
| ^-----------------^
|
= Function "label": Expected 2 arguments
"###);
insta::assert_snapshot!(env.parse_err(r#"if()"#), @r###"
--> 1:4
|
1 | if()
| ^
|
= Function "if": Expected 2 to 3 arguments
"###);
insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r###"
--> 1:4
|
1 | if("foo", "bar", "baz", "quux")
| ^-------------------------^
|
= Function "if": Expected 2 to 3 arguments
"###);
insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r###"
--> 1:4
|
1 | if(label("foo", "bar"), "baz")
| ^-----------------^
|
= Expected expression of type "Boolean", but actual type is "Template"
"###);
insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @r###"
--> 1:1
|
1 | |x| description
| ^-------------^
|
= Lambda cannot be defined here
"###);
}
#[test]
fn test_self_keyword() {
let mut env = TestTemplateEnv::new();
env.add_keyword("say_hello", || L::wrap_string(Literal("Hello".to_owned())));
insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
insta::assert_snapshot!(env.parse_err(r#"self"#), @r###"
--> 1:1
|
1 | self
| ^--^
|
= Expected expression of type "Template", but actual type is "Self"
"###);
}
#[test]
fn test_boolean_cast() {
let mut env = TestTemplateEnv::new();
insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
env.add_keyword("sl0", || {
L::wrap_string_list(Literal::<Vec<String>>(vec![]))
});
env.add_keyword("sl1", || L::wrap_string_list(Literal(vec!["".to_owned()])));
insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @r###"
--> 1:4
|
1 | if(0, true, false)
| ^
|
= Expected expression of type "Boolean", but actual type is "Integer"
"###);
env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None)));
env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(0))));
insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r###"
--> 1:4
|
1 | if(label("", ""), true, false)
| ^-----------^
|
= Expected expression of type "Boolean", but actual type is "Template"
"###);
insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @r###"
--> 1:4
|
1 | if(sl0.map(|x| x), true, false)
| ^------------^
|
= Expected expression of type "Boolean", but actual type is "ListTemplate"
"###);
}
#[test]
fn test_arithmetic_operation() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None)));
env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(1))));
env.add_keyword("i64_min", || L::wrap_integer(Literal(i64::MIN)));
insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
insta::assert_snapshot!(
env.render_ok(r#"-i64_min"#),
@"<Error: Attempt to negate with overflow>");
}
#[test]
fn test_logical_operation() {
let mut env = TestTemplateEnv::new();
insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
env.add_keyword("bad_bool", || L::wrap_boolean(new_error_property("Bad")));
insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
}
#[test]
fn test_list_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
env.add_keyword("sep", || L::wrap_string(Literal("sep".to_owned())));
insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
@"aSEPbSEPc");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
@"aa bb cc");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
@"atrue btrue ctrue");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
@"atrue btrue ctrue");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
@"a b c");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
@"ax ay bx by cx cy");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
@"ax,ay;bx,by;cx,cy");
insta::assert_snapshot!(
env.render_ok(r#""!a\n!b\nc\nend".remove_suffix("end").lines().map(|s| s.remove_prefix("!"))"#),
@"a b c");
env.add_alias("identity", "|x| x");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r###"
--> 1:17
|
1 | "a".lines().map(empty)
| ^---^
|
= Expected lambda expression
"###);
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r###"
--> 1:18
|
1 | "a".lines().map(|| "")
| ^
|
= Expected 1 lambda parameters
"###);
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r###"
--> 1:18
|
1 | "a".lines().map(|a, b| "")
| ^--^
|
= Expected 1 lambda parameters
"###);
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r###"
--> 1:23
|
1 | "a".lines().map(|s| s.unknown())
| ^-----^
|
= Method "unknown" doesn't exist for type "String"
"###);
env.add_alias("too_many_params", "|x, y| x");
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r###"
--> 1:17
|
1 | "a".lines().map(too_many_params)
| ^-------------^
|
= Alias "too_many_params" cannot be expanded
--> 1:2
|
1 | |x, y| x
| ^--^
|
= Expected 1 lambda parameters
"###);
}
#[test]
fn test_string_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || {
L::wrap_string(Literal("description 1".to_owned()))
});
env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
insta::assert_snapshot!(
env.render_ok(r#""description 123".contains(description.first_line())"#),
@"true");
insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
insta::assert_snapshot!(
env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
insta::assert_snapshot!(
env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
insta::assert_snapshot!(
env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
@"testing");
insta::assert_snapshot!(
env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
@"bar@my.example.com");
insta::assert_snapshot!(
env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
@"bar");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
}
#[test]
fn test_signature() {
let mut env = TestTemplateEnv::new();
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature("Test User", "test.user@example.com")))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature(
"Another Test User",
"test.user@example.com",
)))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature(
"Test User",
"test.user@invalid@example.com",
)))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature("Test User", "test.user")))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature(
"Test User",
"test.user+tag@example.com",
)))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user+tag");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature("Test User", "x@y")))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"x");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature("", "test.user@example.com")))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature("Test User", "")))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
env.add_keyword("author", || {
L::wrap_signature(Literal(new_signature("", "")))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
}
#[test]
fn test_size_hint_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("unbounded", || L::wrap_size_hint(Literal((5, None))));
insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
env.add_keyword("bounded", || L::wrap_size_hint(Literal((0, Some(10)))));
insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
env.add_keyword("zero", || L::wrap_size_hint(Literal((0, Some(0)))));
insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
}
#[test]
fn test_timestamp_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("t0", || L::wrap_timestamp(Literal(new_timestamp(0, 0))));
insta::assert_snapshot!(
env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
@"19700101 00:00:00");
insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r###"
--> 1:11
|
1 | t0.format("%_")
| ^--^
|
= Invalid time format
"###);
insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @r###"
--> 1:11
|
1 | t0.format(0)
| ^
|
= Expected string literal
"###);
insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r###"
--> 1:11
|
1 | t0.format("%Y" ++ "%m")
| ^----------^
|
= Expected string literal
"###);
env.add_alias("time_format", r#""%Y-%m-%d""#);
env.add_alias("bad_time_format", r#""%_""#);
insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r###"
--> 1:11
|
1 | t0.format(bad_time_format)
| ^-------------^
|
= Alias "bad_time_format" cannot be expanded
--> 1:1
|
1 | "%_"
| ^--^
|
= Invalid time format
"###);
}
#[test]
fn test_fill_function() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
insta::assert_snapshot!(
env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@r###"
The quick fox jumps
over the [38;5;1mlazy[39m dog
"###);
insta::assert_snapshot!(
env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
label("error", "longlonglongword and short words") ++
" back out\n")"#),
@r###"
Longlonglongword
an some
short
words
[38;5;1mlonglonglongword[39m
[38;5;1mand short[39m
[38;5;1mwords[39m
back out
"###);
insta::assert_snapshot!(
env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@r###"
The
quick
fox
jumps
over
the
[38;5;1mlazy[39m
dog
"###);
insta::assert_snapshot!(
env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@r###"
The
quick
fox
jumps
over
the
[38;5;1mlazy[39m
dog
"###);
insta::assert_snapshot!(
env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
insta::assert_snapshot!(
env.render_ok(r#""START marker to help insta\n" ++
indent(" ", fill(20, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n"))"#),
@r###"
START marker to help insta
The quick fox jumps
over the [38;5;1mlazy[39m dog
"###);
insta::assert_snapshot!(
env.render_ok(r#""START marker to help insta\n" ++
fill(20, indent(" ", "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n"))"#),
@r###"
START marker to help insta
The quick fox
jumps over the [38;5;1mlazy[39m
dog
"###);
}
#[test]
fn test_indent_function() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
env.add_color("hint", crossterm::style::Color::DarkCyan);
assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
insta::assert_snapshot!(
env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
@r###"
[38;5;1m__a[39m
[38;5;3m__b[39m
"###);
insta::assert_snapshot!(
env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
@r###"
[38;5;1m__a[39m[38;5;3mb[39m
[38;5;3m__c[39m
"###);
insta::assert_snapshot!(
env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
@r###"
[38;5;1mXX[39ma
[38;5;1mXX[39mb
"###);
insta::assert_snapshot!(
env.render_ok(r#"indent(label("hint", "A"),
label("warning", indent(label("hint", "B"),
label("error", "x\n") ++ "y")))"#),
@r###"
[38;5;6mAB[38;5;1mx[39m
[38;5;6mAB[38;5;3my[39m
"###);
}
#[test]
fn test_label_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(
env.render_ok(r#"label("error", "text")"#),
@"[38;5;1mtext[39m");
insta::assert_snapshot!(
env.render_ok(r#"label("error".first_line(), "text")"#),
@"[38;5;1mtext[39m");
insta::assert_snapshot!(
env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
@"[38;5;1mtext[39m");
}
#[test]
fn test_coalesce_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
env.add_keyword("empty_string", || L::wrap_string(Literal("".to_owned())));
env.add_keyword("non_empty_string", || {
L::wrap_string(Literal("a".to_owned()))
});
insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
}
#[test]
fn test_concat_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
env.add_keyword("hidden", || L::wrap_boolean(Literal(false)));
env.add_color("empty", crossterm::style::Color::DarkGreen);
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
insta::assert_snapshot!(
env.render_ok(r#"concat(hidden, empty)"#),
@"false[38;5;2mtrue[39m");
insta::assert_snapshot!(
env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
@"[38;5;3ma[39mb");
}
#[test]
fn test_separate_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || L::wrap_string(Literal("".to_owned())));
env.add_keyword("empty", || L::wrap_boolean(Literal(true)));
env.add_keyword("hidden", || L::wrap_boolean(Literal(false)));
env.add_color("empty", crossterm::style::Color::DarkGreen);
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
@"[38;5;3ma[39m b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", hidden, description, empty)"#),
@"false [38;5;2mtrue[39m");
insta::assert_snapshot!(
env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
@"XfalseYfalseZ");
}
#[test]
fn test_surround_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("lt", || L::wrap_string(Literal("<".to_owned())));
env.add_keyword("gt", || L::wrap_string(Literal(">".to_owned())));
env.add_keyword("content", || L::wrap_string(Literal("content".to_owned())));
env.add_keyword("empty_content", || L::wrap_string(Literal("".to_owned())));
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("paren", crossterm::style::Color::Cyan);
insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
insta::assert_snapshot!(
env.render_ok(
r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
@"[38;5;14m([39m[38;5;1ma[39m[38;5;14m)[39m");
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, content)"#),
@"<content>");
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, empty_content)"#),
@"");
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
@"<empty>");
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
@"");
}
}