use crate::error::Error;
use crate::utils::write_escaped;
use crate::value::Value;
use crate::vm::State;
use crate::{AutoEscape, Output};
#[deprecated = "Use the minijinja::functions::Function instead"]
#[doc(hidden)]
pub use crate::functions::Function as Filter;
pub fn safe(v: String) -> Value {
Value::from_safe_string(v)
}
pub fn escape(state: &State, v: &Value) -> Result<Value, Error> {
if v.is_safe() {
return Ok(v.clone());
}
let auto_escape = match state.auto_escape() {
AutoEscape::None => match state.env().initial_auto_escape(state.name()) {
AutoEscape::None => AutoEscape::Html,
other => other,
},
other => other,
};
let mut rv = match v.as_str() {
Some(s) => String::with_capacity(s.len()),
None => String::new(),
};
let mut out = Output::new(&mut rv);
if matches!(auto_escape, AutoEscape::Custom(_)) {
ok!(state.with_auto_escape(auto_escape, |state| {
state.env().format(v, state, &mut out)
}));
} else {
ok!(write_escaped(&mut out, auto_escape, v));
}
Ok(Value::from_safe_string(rv))
}
#[cfg(feature = "builtins")]
mod builtins {
use super::*;
use crate::error::ErrorKind;
use crate::format_utils::{format_filter, FormatStyle};
use crate::utils::{safe_sort, splitn_whitespace};
use crate::value::merge_object::{MergeDict, MergeSeq};
use crate::value::ops::{self, as_f64, LenIterWrap};
use crate::value::{Enumerator, Kwargs, Object, ObjectRepr, Rest, ValueKind, ValueRepr};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Write;
use std::mem;
use std::sync::Arc;
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn upper(v: Cow<'_, str>) -> String {
if v.is_ascii() && !v.bytes().any(|x| x.is_ascii_lowercase()) {
return v.into_owned();
}
v.to_uppercase()
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn lower(v: Cow<'_, str>) -> String {
v.to_lowercase()
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn title(v: Cow<'_, str>) -> String {
let mut rv = String::new();
let mut capitalize = true;
for c in v.chars() {
if c.is_ascii_punctuation() || c.is_whitespace() {
rv.push(c);
capitalize = true;
} else if capitalize {
write!(rv, "{}", c.to_uppercase()).unwrap();
capitalize = false;
} else {
write!(rv, "{}", c.to_lowercase()).unwrap();
}
}
rv
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn capitalize(text: Cow<'_, str>) -> String {
let mut chars = text.chars();
match chars.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn replace(
_state: &State,
v: Cow<'_, str>,
from: Cow<'_, str>,
to: Cow<'_, str>,
) -> String {
let from = from.as_ref();
let to = to.as_ref();
if from == to {
return v.into_owned();
}
if from.len() > 1 && !v.contains(from) {
return v.into_owned();
}
v.replace(from, to)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn length(v: &Value) -> Result<usize, Error> {
v.len().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
format!("cannot calculate length of value of type {}", v.kind()),
)
})
}
fn cmp_helper(a: &Value, b: &Value, case_sensitive: bool, reverse: bool) -> Ordering {
let ordering = if !case_sensitive {
if let (Some(a), Some(b)) = (a.as_str(), b.as_str()) {
#[cfg(feature = "unicode")]
{
unicase::UniCase::new(a).cmp(&unicase::UniCase::new(b))
}
#[cfg(not(feature = "unicode"))]
{
a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase())
}
} else {
a.cmp(b)
}
} else {
a.cmp(b)
};
if reverse {
ordering.reverse()
} else {
ordering
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn dictsort(v: &Value, kwargs: Kwargs) -> Result<Value, Error> {
if v.kind() != ValueKind::Map {
return Err(Error::new(
ErrorKind::InvalidOperation,
"cannot convert value into pair list",
));
}
let by_value = matches!(ok!(kwargs.get("by")), Some("value"));
let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
let reverse = ok!(kwargs.get::<Option<bool>>("reverse")).unwrap_or(false);
let mut rv: Vec<_> = ok!(v.try_iter())
.map(|key| (key.clone(), v.get_item(&key).unwrap_or(Value::UNDEFINED)))
.collect();
safe_sort(&mut rv, |a, b| {
let (a, b) = if by_value { (&a.1, &b.1) } else { (&a.0, &b.0) };
cmp_helper(a, b, case_sensitive, reverse)
})?;
kwargs.assert_all_used()?;
Ok(rv
.into_iter()
.map(|(k, v)| Value::from(vec![k, v]))
.collect())
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn items(v: &Value) -> Result<Value, Error> {
if v.kind() == ValueKind::Map {
Ok(Value::make_object_iterable(v.clone(), |v| {
match v.as_object().and_then(|v| v.try_iter_pairs()) {
Some(iter) => Box::new(iter.map(|(key, value)| Value::from(vec![key, value]))),
None => Box::new(
Some(Value::from(Error::new(
ErrorKind::InvalidOperation,
format!("{} is not iterable", v.kind()),
)))
.into_iter(),
),
}
}))
} else {
Err(Error::new(
ErrorKind::InvalidOperation,
"cannot convert value into pairs",
))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn reverse(v: &Value) -> Result<Value, Error> {
v.reverse()
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn trim(s: Cow<'_, str>, chars: Option<Cow<'_, str>>) -> String {
match chars {
Some(chars) => {
let chars = chars.chars().collect::<Vec<_>>();
s.trim_matches(&chars[..]).to_string()
}
None => s.trim().to_string(),
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn join(val: &Value, joiner: Option<Cow<'_, str>>) -> Result<String, Error> {
if val.is_undefined() || val.is_none() {
return Ok(String::new());
}
let joiner = joiner.as_ref().unwrap_or(&Cow::Borrowed(""));
let iter = ok!(val.try_iter().map_err(|err| {
Error::new(
ErrorKind::InvalidOperation,
format!("cannot join value of type {}", val.kind()),
)
.with_source(err)
}));
let mut rv = String::new();
for (idx, item) in iter.enumerate() {
if idx > 0 {
rv.push_str(joiner);
}
if let Some(s) = item.as_str() {
rv.push_str(s);
} else {
write!(rv, "{item}").ok();
}
}
Ok(rv)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn split(s: Arc<str>, split: Option<Arc<str>>, maxsplits: Option<i64>) -> Value {
let maxsplits = maxsplits.and_then(|x| if x >= 0 { Some(x as usize + 1) } else { None });
Value::make_object_iterable((s, split), move |(s, split)| match (split, maxsplits) {
(None, None) => Box::new(s.split_whitespace().map(Value::from)),
(Some(split), None) => Box::new(s.split(split as &str).map(Value::from)),
(None, Some(n)) => Box::new(splitn_whitespace(s, n).map(Value::from)),
(Some(split), Some(n)) => Box::new(s.splitn(n, split as &str).map(Value::from)),
})
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn lines(s: Arc<str>) -> Value {
Value::from_iter(s.lines().map(|x| x.to_string()))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn default(state: &State, value: &Value, args: Rest<Value>) -> Result<Value, Error> {
if args.len() > 2 {
return Err(Error::from(ErrorKind::TooManyArguments));
}
let other = args.first().cloned().unwrap_or_else(|| Value::from(""));
let lax = if let Some(lax) = args.get(1) {
ok!(state.undefined_behavior().is_true(lax))
} else {
false
};
if value.is_undefined() || (lax && !value.is_true()) {
Ok(other)
} else {
Ok(value.clone())
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn abs(value: Value) -> Result<Value, Error> {
match value.0 {
ValueRepr::U64(_) | ValueRepr::U128(_) => Ok(value),
ValueRepr::I64(x) => match x.checked_abs() {
Some(rv) => Ok(Value::from(rv)),
None => Ok(Value::from((x as i128).abs())), },
ValueRepr::I128(x) => {
x.0.checked_abs()
.map(Value::from)
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "overflow on abs"))
}
ValueRepr::F64(x) => Ok(Value::from(x.abs())),
_ => Err(Error::new(
ErrorKind::InvalidOperation,
"cannot get absolute value",
)),
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn int(state: &State, value: &Value) -> Result<Value, Error> {
match &value.0 {
ValueRepr::Undefined(_) | ValueRepr::None => {
ok!(state.undefined_behavior().assert_value_not_undefined(value));
Ok(Value::from(0))
}
ValueRepr::Bool(x) => Ok(Value::from(*x as u64)),
ValueRepr::U64(_) | ValueRepr::I64(_) | ValueRepr::U128(_) | ValueRepr::I128(_) => {
Ok(value.clone())
}
ValueRepr::F64(v) => Ok(Value::from(*v as i128)),
ValueRepr::String(..) | ValueRepr::SmallStr(_) => {
let s = value.as_str().unwrap();
if let Ok(i) = s.parse::<i128>() {
Ok(Value::from(i))
} else {
match s.parse::<f64>() {
Ok(f) => Ok(Value::from(f as i128)),
Err(err) => Err(Error::new(ErrorKind::InvalidOperation, err.to_string())),
}
}
}
ValueRepr::Bytes(_) | ValueRepr::Object(_) => Err(Error::new(
ErrorKind::InvalidOperation,
format!("cannot convert {} to integer", value.kind()),
)),
ValueRepr::Invalid(_) => value.clone().validate(),
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn float(state: &State, value: &Value) -> Result<Value, Error> {
match &value.0 {
ValueRepr::Undefined(_) | ValueRepr::None => {
ok!(state.undefined_behavior().assert_value_not_undefined(value));
Ok(Value::from(0.0))
}
ValueRepr::Bool(x) => Ok(Value::from(*x as u64 as f64)),
ValueRepr::String(..) | ValueRepr::SmallStr(_) => value
.as_str()
.unwrap()
.parse::<f64>()
.map(Value::from)
.map_err(|err| Error::new(ErrorKind::InvalidOperation, err.to_string())),
ValueRepr::Invalid(_) => value.clone().validate(),
_ => as_f64(value, true).map(Value::from).ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
format!("cannot convert {} to float", value.kind()),
)
}),
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn sum(state: &State, values: Value) -> Result<Value, Error> {
let mut rv = Value::from(0);
let iter = ok!(state.undefined_behavior().try_iter(values));
for value in iter {
if value.is_undefined() {
ok!(state.undefined_behavior().handle_undefined(false));
continue;
} else if !value.is_number() {
return Err(Error::new(
ErrorKind::InvalidOperation,
format!("can only sum numbers, got {}", value.kind()),
));
}
rv = ok!(ops::add(&rv, &value));
}
Ok(rv)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn attr(value: &Value, key: &Value) -> Result<Value, Error> {
value.get_item(key)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn round(value: Value, precision: Option<i32>) -> Result<Value, Error> {
match value.0 {
ValueRepr::I64(_) | ValueRepr::I128(_) | ValueRepr::U64(_) | ValueRepr::U128(_) => {
Ok(value)
}
ValueRepr::F64(val) => {
let x = 10f64.powi(precision.unwrap_or(0));
Ok(Value::from((x * val).round() / x))
}
_ => Err(Error::new(
ErrorKind::InvalidOperation,
format!("cannot round value ({})", value.kind()),
)),
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn first(value: &Value) -> Result<Value, Error> {
if let Some(s) = value.as_str() {
Ok(s.chars().next().map_or(Value::UNDEFINED, Value::from))
} else if let Some(mut iter) = value.as_object().and_then(|x| x.try_iter()) {
Ok(iter.next().unwrap_or(Value::UNDEFINED))
} else {
Err(Error::new(
ErrorKind::InvalidOperation,
"cannot get first item from value",
))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn last(value: Value) -> Result<Value, Error> {
if let Some(s) = value.as_str() {
Ok(s.chars().next_back().map_or(Value::UNDEFINED, Value::from))
} else if matches!(value.kind(), ValueKind::Seq | ValueKind::Iterable) {
let rev = ok!(value.reverse());
let mut iter = ok!(rev.try_iter());
Ok(iter.next().unwrap_or_default())
} else {
Err(Error::new(
ErrorKind::InvalidOperation,
"cannot get last item from value",
))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn min(state: &State, value: Value) -> Result<Value, Error> {
let iter = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
}));
Ok(iter.min().unwrap_or(Value::UNDEFINED))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn max(state: &State, value: Value) -> Result<Value, Error> {
let iter = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
}));
Ok(iter.max().unwrap_or(Value::UNDEFINED))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn sort(state: &State, value: Value, kwargs: Kwargs) -> Result<Value, Error> {
let mut items = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
}))
.collect::<Vec<_>>();
let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
let reverse = ok!(kwargs.get::<Option<bool>>("reverse")).unwrap_or(false);
if let Some(attr) = ok!(kwargs.get::<Option<&str>>("attribute")) {
let keys: Vec<_> = attr
.split(',')
.filter_map(|key| {
let trimmed = key.trim();
if !key.is_empty() {
Some(trimmed)
} else {
None
}
})
.collect();
if keys.len() > 1 {
safe_sort(&mut items, |a, b| {
let key_a = Value::from_iter(
keys.iter()
.map(|k| a.get_path_or_default(k, &Value::UNDEFINED)),
);
let key_b = Value::from_iter(
keys.iter()
.map(|k| b.get_path_or_default(k, &Value::UNDEFINED)),
);
cmp_helper(&key_a, &key_b, case_sensitive, reverse)
})?;
} else {
let key = if !keys.is_empty() { keys[0] } else { attr };
safe_sort(&mut items, |a, b| {
match (a.get_path(key), b.get_path(key)) {
(Ok(a), Ok(b)) => cmp_helper(&a, &b, case_sensitive, reverse),
_ => Ordering::Equal,
}
})?;
}
} else {
safe_sort(&mut items, |a, b| cmp_helper(a, b, case_sensitive, reverse))?;
}
ok!(kwargs.assert_all_used());
Ok(Value::from(items))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn list(state: &State, value: Value) -> Result<Value, Error> {
let iter = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
}));
Ok(Value::from(iter.collect::<Vec<_>>()))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn string(state: &State, value: &Value) -> Result<Value, Error> {
ok!(state.undefined_behavior().assert_value_not_undefined(value));
Ok(if value.kind() == ValueKind::String {
value.clone()
} else {
value.to_string().into()
})
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn bool(state: &State, value: &Value) -> Result<bool, Error> {
state.undefined_behavior().is_true(value)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn slice(
state: &State,
value: Value,
count: usize,
fill_with: Option<Value>,
) -> Result<Value, Error> {
if count == 0 {
return Err(Error::new(ErrorKind::InvalidOperation, "count cannot be 0"));
}
let items = ok!(state.undefined_behavior().try_iter(value)).collect::<Vec<_>>();
let len = items.len();
let items_per_slice = len / count;
let slices_with_extra = len % count;
let mut offset = 0;
let mut rv = Vec::with_capacity(count);
for slice in 0..count {
let start = offset + slice * items_per_slice;
if slice < slices_with_extra {
offset += 1;
}
let end = offset + (slice + 1) * items_per_slice;
let tmp = &items[start..end];
if let Some(ref filler) = fill_with {
if slice >= slices_with_extra {
let mut tmp = tmp.to_vec();
tmp.push(filler.clone());
rv.push(Value::from(tmp));
continue;
}
}
rv.push(Value::from(tmp.to_vec()));
}
Ok(Value::from(rv))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn batch(
state: &State,
value: Value,
count: usize,
fill_with: Option<Value>,
) -> Result<Value, Error> {
if count == 0 {
return Err(Error::new(ErrorKind::InvalidOperation, "count cannot be 0"));
}
let mut rv = Vec::with_capacity(value.len().unwrap_or(0) / count);
let mut tmp = Vec::with_capacity(count);
for item in ok!(state.undefined_behavior().try_iter(value)) {
if tmp.len() == count {
rv.push(Value::from(mem::replace(
&mut tmp,
Vec::with_capacity(count),
)));
}
tmp.push(item);
}
if !tmp.is_empty() {
if let Some(filler) = fill_with {
for _ in 0..count - tmp.len() {
tmp.push(filler.clone());
}
}
rv.push(Value::from(tmp));
}
Ok(Value::from(rv))
}
#[cfg_attr(docsrs, doc(cfg(all(feature = "builtins", feature = "json"))))]
#[cfg(feature = "json")]
pub fn tojson(value: &Value, indent: Option<Value>, args: Kwargs) -> Result<Value, Error> {
let indent = match indent {
Some(indent) => Some(indent),
None => ok!(args.get("indent")),
};
let indent = match indent {
None => None,
Some(ref val) => match bool::try_from(val.clone()).ok() {
Some(true) => Some(2),
Some(false) => None,
None => Some(ok!(usize::try_from(val.clone()))),
},
};
ok!(args.assert_all_used());
if let Some(indent) = indent {
let mut out = Vec::<u8>::new();
let indentation = " ".repeat(indent);
let formatter = serde_json::ser::PrettyFormatter::with_indent(indentation.as_bytes());
let mut s = serde_json::Serializer::with_formatter(&mut out, formatter);
serde::Serialize::serialize(&value, &mut s)
.map(|_| unsafe { String::from_utf8_unchecked(out) })
} else {
serde_json::to_string(&value)
}
.map_err(|err| {
Error::new(ErrorKind::InvalidOperation, "cannot serialize to JSON").with_source(err)
})
.map(|s| {
let mut rv = String::with_capacity(s.len());
for c in s.chars() {
match c {
'<' => rv.push_str("\\u003c"),
'>' => rv.push_str("\\u003e"),
'&' => rv.push_str("\\u0026"),
'\'' => rv.push_str("\\u0027"),
_ => rv.push(c),
}
}
Value::from_safe_string(rv)
})
}
#[cfg_attr(docsrs, doc(cfg(all(feature = "builtins"))))]
pub fn indent(
mut value: String,
width: Option<usize>,
indent_first_line: Option<bool>,
indent_blank_lines: Option<bool>,
kwargs: Kwargs,
) -> Result<String, Error> {
fn strip_trailing_newline(input: &mut String) {
if input.ends_with('\n') {
input.truncate(input.len() - 1);
}
if input.ends_with('\r') {
input.truncate(input.len() - 1);
}
}
let width: usize = match width {
Some(width) => width,
None => ok!(kwargs.get::<Option<usize>>("width")).unwrap_or(4),
};
let indent_first_line: bool = match indent_first_line {
Some(v) => v,
None => ok!(kwargs.get::<Option<bool>>("first")).unwrap_or(false),
};
let indent_blank_lines: bool = match indent_blank_lines {
Some(v) => v,
None => ok!(kwargs.get::<Option<bool>>("blank")).unwrap_or(false),
};
ok!(kwargs.assert_all_used());
strip_trailing_newline(&mut value);
let indent_with = " ".repeat(width);
let mut output = String::new();
let mut iterator = value.split('\n');
if !indent_first_line {
output.push_str(iterator.next().unwrap());
output.push('\n');
}
for line in iterator {
if line.is_empty() {
if indent_blank_lines {
output.push_str(&indent_with);
}
} else {
write!(output, "{indent_with}{line}").ok();
}
output.push('\n');
}
strip_trailing_newline(&mut output);
Ok(output)
}
#[cfg_attr(docsrs, doc(cfg(all(feature = "builtins", feature = "urlencode"))))]
#[cfg(feature = "urlencode")]
pub fn urlencode(value: &Value) -> Result<String, Error> {
const SET: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'/')
.remove(b'.')
.remove(b'-')
.remove(b'_')
.add(b' ');
if value.kind() == ValueKind::Map {
let mut rv = String::new();
for k in ok!(value.try_iter()) {
let v = ok!(value.get_item(&k));
if v.is_none() || v.is_undefined() {
continue;
}
if !rv.is_empty() {
rv.push('&');
}
write!(
rv,
"{}={}",
percent_encoding::utf8_percent_encode(&k.to_string(), SET),
percent_encoding::utf8_percent_encode(&v.to_string(), SET)
)
.unwrap();
}
Ok(rv)
} else {
match &value.0 {
ValueRepr::None | ValueRepr::Undefined(_) => Ok("".into()),
ValueRepr::Bytes(b) => Ok(percent_encoding::percent_encode(b, SET).to_string()),
ValueRepr::String(..) | ValueRepr::SmallStr(_) => Ok(
percent_encoding::utf8_percent_encode(value.as_str().unwrap(), SET).to_string(),
),
_ => Ok(percent_encoding::utf8_percent_encode(&value.to_string(), SET).to_string()),
}
}
}
fn select_or_reject(
state: &State,
invert: bool,
value: Value,
attr: Option<Cow<'_, str>>,
test_name: Option<Cow<'_, str>>,
args: crate::value::Rest<Value>,
) -> Result<Vec<Value>, Error> {
let mut rv = vec![];
let test = if let Some(test_name) = test_name {
Some(ok!(state
.env()
.get_test(&test_name)
.ok_or_else(|| Error::from(ErrorKind::UnknownTest))))
} else {
None
};
for value in ok!(state.undefined_behavior().try_iter(value)) {
let test_value = if let Some(ref attr) = attr {
ok!(value.get_path(attr))
} else {
value.clone()
};
let passed = if let Some(test) = test {
let new_args = Some(test_value)
.into_iter()
.chain(args.0.iter().cloned())
.collect::<Vec<_>>();
ok!(test.call(state, &new_args)).is_true()
} else {
test_value.is_true()
};
if passed != invert {
rv.push(value);
}
}
Ok(rv)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn select(
state: &State,
value: Value,
test_name: Option<Cow<'_, str>>,
args: crate::value::Rest<Value>,
) -> Result<Vec<Value>, Error> {
select_or_reject(state, false, value, None, test_name, args)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn selectattr(
state: &State,
value: Value,
attr: Cow<'_, str>,
test_name: Option<Cow<'_, str>>,
args: crate::value::Rest<Value>,
) -> Result<Vec<Value>, Error> {
select_or_reject(state, false, value, Some(attr), test_name, args)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn reject(
state: &State,
value: Value,
test_name: Option<Cow<'_, str>>,
args: crate::value::Rest<Value>,
) -> Result<Vec<Value>, Error> {
select_or_reject(state, true, value, None, test_name, args)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn rejectattr(
state: &State,
value: Value,
attr: Cow<'_, str>,
test_name: Option<Cow<'_, str>>,
args: crate::value::Rest<Value>,
) -> Result<Vec<Value>, Error> {
select_or_reject(state, true, value, Some(attr), test_name, args)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn map(
state: &State,
value: Value,
args: crate::value::Rest<Value>,
) -> Result<Vec<Value>, Error> {
let mut rv = Vec::with_capacity(value.len().unwrap_or(0));
let (args, kwargs): (&[Value], Kwargs) = crate::value::from_args(&args)?;
if let Some(attr) = ok!(kwargs.get::<Option<Value>>("attribute")) {
if !args.is_empty() {
return Err(Error::from(ErrorKind::TooManyArguments));
}
let default = if kwargs.has("default") {
ok!(kwargs.get::<Value>("default"))
} else {
Value::UNDEFINED
};
for value in ok!(state.undefined_behavior().try_iter(value)) {
let sub_val = match attr.as_str() {
Some(path) => value.get_path(path),
None => value.get_item(&attr),
};
rv.push(match (sub_val, &default) {
(Ok(attr), _) => {
if attr.is_undefined() {
default.clone()
} else {
attr
}
}
(Err(_), default) if !default.is_undefined() => default.clone(),
(Err(err), _) => return Err(err),
});
}
ok!(kwargs.assert_all_used());
return Ok(rv);
}
let filter_name = ok!(args
.first()
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "filter name is required")));
let filter_name = ok!(filter_name.as_str().ok_or_else(|| {
Error::new(ErrorKind::InvalidOperation, "filter name must be a string")
}));
let filter = ok!(state
.env()
.get_filter(filter_name)
.ok_or_else(|| Error::from(ErrorKind::UnknownFilter)));
for value in ok!(state.undefined_behavior().try_iter(value)) {
let new_args = Some(value.clone())
.into_iter()
.chain(args.iter().skip(1).cloned())
.collect::<Vec<_>>();
rv.push(ok!(filter.call(state, &new_args)));
}
Ok(rv)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn groupby(value: Value, attribute: Option<&str>, kwargs: Kwargs) -> Result<Value, Error> {
let default = ok!(kwargs.get::<Option<Value>>("default")).unwrap_or_default();
let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
let attr = match attribute {
Some(attr) => attr,
None => ok!(kwargs.get::<&str>("attribute")),
};
let mut items: Vec<Value> = ok!(value.try_iter()).collect();
safe_sort(&mut items, |a, b| {
let a = a.get_path_or_default(attr, &default);
let b = b.get_path_or_default(attr, &default);
cmp_helper(&a, &b, case_sensitive, false)
})?;
ok!(kwargs.assert_all_used());
#[derive(Debug)]
pub struct GroupTuple {
grouper: Value,
list: Vec<Value>,
}
impl Object for GroupTuple {
fn repr(self: &Arc<Self>) -> ObjectRepr {
ObjectRepr::Seq
}
fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
match (key.as_usize(), key.as_str()) {
(Some(0), None) | (None, Some("grouper")) => Some(self.grouper.clone()),
(Some(1), None) | (None, Some("list")) => {
Some(Value::make_object_iterable(self.clone(), |this| {
Box::new(this.list.iter().cloned())
as Box<dyn Iterator<Item = _> + Send + Sync>
}))
}
_ => None,
}
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
Enumerator::Seq(2)
}
}
let mut rv = Vec::new();
let mut grouper = None::<Value>;
let mut list = Vec::new();
for item in items {
let group_by = item.get_path_or_default(attr, &default);
if let Some(ref last_grouper) = grouper {
if cmp_helper(last_grouper, &group_by, case_sensitive, false) != Ordering::Equal {
rv.push(Value::from_object(GroupTuple {
grouper: last_grouper.clone(),
list: std::mem::take(&mut list),
}));
}
}
grouper = Some(group_by);
list.push(item);
}
if !list.is_empty() {
rv.push(Value::from_object(GroupTuple {
grouper: grouper.unwrap(),
list,
}));
}
Ok(Value::from_object(rv))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn unique(state: &State, values: Value, kwargs: Kwargs) -> Result<Value, Error> {
use std::collections::BTreeSet;
let attr = ok!(kwargs.get::<Option<&str>>("attribute"));
let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
ok!(kwargs.assert_all_used());
let mut rv = Vec::new();
let mut seen = BTreeSet::new();
let iter = ok!(state.undefined_behavior().try_iter(values));
for item in iter {
let value_to_compare = if let Some(attr) = attr {
item.get_path_or_default(attr, &Value::UNDEFINED)
} else {
item.clone()
};
let memorized_value = if case_sensitive {
value_to_compare.clone()
} else if let Some(s) = value_to_compare.as_str() {
Value::from(s.to_lowercase())
} else {
value_to_compare.clone()
};
if !seen.contains(&memorized_value) {
rv.push(item);
seen.insert(memorized_value);
}
}
Ok(Value::from(rv))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn chain(
_state: &State,
value: Value,
others: crate::value::Rest<Value>,
) -> Result<Value, Error> {
let all_values = Some(value.clone())
.into_iter()
.chain(others.0.iter().cloned())
.collect::<Vec<_>>();
if all_values.iter().all(|v| v.kind() == ValueKind::Map) {
Ok(Value::from_object(MergeDict::new(all_values)))
} else if all_values
.iter()
.all(|v| matches!(v.kind(), ValueKind::Seq))
{
Ok(Value::from_object(MergeSeq::new(all_values)))
} else {
Ok(Value::make_object_iterable(all_values, |values| {
Box::new(values.iter().flat_map(|v| match v.try_iter() {
Ok(iter) => Box::new(iter) as Box<dyn Iterator<Item = Value> + Send + Sync>,
Err(err) => Box::new(Some(Value::from(err)).into_iter())
as Box<dyn Iterator<Item = Value> + Send + Sync>,
})) as Box<dyn Iterator<Item = Value> + Send + Sync>
}))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn zip(_state: &State, value: Value, others: Rest<Value>) -> Result<Value, Error> {
let all_values = Some(value).into_iter().chain(others.0).collect::<Vec<_>>();
let mut known_len: Option<usize> = None;
for val in &all_values {
match val.try_iter() {
Ok(_) => {
if let Some(len) = val.len() {
known_len = Some(match known_len {
None => len,
Some(current_min) => current_min.min(len),
});
} else {
known_len = None;
break;
}
}
Err(_) => {
return Err(Error::new(
ErrorKind::InvalidOperation,
format!("zip filter argument must be iterable, got {}", val.kind()),
));
}
}
}
Ok(Value::make_object_iterable(all_values, move |values| {
let iter = std::iter::from_fn({
let mut iters = values
.iter()
.map(|val| val.try_iter().ok())
.collect::<Option<Vec<_>>>()
.unwrap_or_default();
move || {
if iters.is_empty() {
return None;
}
let mut tuple = Vec::with_capacity(iters.len());
for iter in &mut iters {
match iter.next() {
Some(val) => tuple.push(val),
None => return None,
}
}
Some(Value::from(tuple))
}
});
if let Some(len) = known_len {
Box::new(LenIterWrap(len, iter)) as Box<dyn Iterator<Item = Value> + Send + Sync>
} else {
Box::new(iter) as Box<dyn Iterator<Item = Value> + Send + Sync>
}
}))
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn pprint(value: &Value) -> String {
format!("{value:#?}")
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn format(format_str: &str, format_args: Rest<Value>) -> Result<String, Error> {
format_filter(FormatStyle::Printf, format_str, &format_args)
}
}
#[cfg(feature = "builtins")]
pub use self::builtins::*;