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, format_printf_with, FormatConversion, 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, StringInput, 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(value: StringInput<'_>) -> Value {
let output = value.as_str().to_uppercase();
value.preserve_safety(output)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn lower(value: StringInput<'_>) -> Value {
let output = value.as_str().to_lowercase();
value.preserve_safety(output)
}
#[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(value: StringInput<'_>) -> Value {
let mut chars = value.as_str().chars();
let output = match chars.next() {
None => String::new(),
Some(first) => {
first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
}
};
value.preserve_safety(output)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn replace(
state: &State,
value: StringInput<'_>,
from: StringInput<'_>,
to: StringInput<'_>,
) -> Result<Value, Error> {
let safety_aware = !matches!(state.auto_escape(), AutoEscape::None)
&& (value.is_safe() || from.is_safe() || to.is_safe());
if safety_aware {
let output = value
.format(state)?
.replace(from.as_str(), &to.format(state)?);
Ok(Value::from_safe_string(output))
} else {
Ok(Value::from(
value.as_str().replace(from.as_str(), to.as_str()),
))
}
}
#[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<_> = if let Some(iter) = v.as_object().and_then(|v| v.try_iter_pairs()) {
iter.collect()
} else {
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([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([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(value: &Value) -> Result<Value, Error> {
if value.kind() == ValueKind::String {
let string = value.as_str().unwrap();
let output = string.chars().rev().collect::<String>();
if value.is_safe() {
Ok(Value::from_safe_string(output))
} else {
Ok(Value::from(output))
}
} else {
value.reverse()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn trim(value: StringInput<'_>, chars: Option<Cow<'_, str>>) -> Value {
let output = match chars {
Some(chars) => {
let chars = chars.chars().collect::<Vec<_>>();
value.as_str().trim_matches(&chars[..]).to_string()
}
None => value.as_str().trim().to_string(),
};
value.preserve_safety(output)
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn join(
state: &State,
value: &Value,
joiner: Option<StringInput<'_>>,
) -> Result<Value, Error> {
fn join_plain(iter: impl Iterator<Item = Value>, joiner: &str) -> String {
let mut output = String::new();
for (idx, item) in iter.enumerate() {
if idx > 0 {
output.push_str(joiner);
}
if let Some(string) = item.as_str() {
output.push_str(string);
} else {
write!(output, "{item}").ok();
}
}
output
}
fn join_safe(
state: &State,
iter: impl Iterator<Item = Value>,
joiner: &str,
) -> Result<String, Error> {
let mut output = String::new();
for (idx, item) in iter.enumerate() {
if idx > 0 {
output.push_str(joiner);
}
if item.is_safe() {
output.push_str(item.as_str().unwrap());
} else {
output.push_str(&ok!(state.format(item)));
}
}
Ok(output)
}
let joiner_str = joiner.as_ref().map(StringInput::as_str).unwrap_or_default();
let iter = ok!(value.try_iter().map_err(|err| {
Error::new(
ErrorKind::InvalidOperation,
format!("cannot join value of type {}", value.kind()),
)
.with_source(err)
}));
if matches!(state.auto_escape(), AutoEscape::None) {
return Ok(Value::from(join_plain(iter, joiner_str)));
}
if joiner.as_ref().is_some_and(StringInput::is_safe) {
return Ok(Value::from_safe_string(ok!(join_safe(
state, iter, joiner_str
))));
}
let items = iter.collect::<Vec<_>>();
if items.iter().any(Value::is_safe) {
let joiner = match joiner.as_ref() {
Some(joiner) => ok!(joiner.format(state)),
None => Cow::Borrowed(""),
};
Ok(Value::from_safe_string(ok!(join_safe(
state,
items.into_iter(),
&joiner
))))
} else {
Ok(Value::from(join_plain(items.into_iter(), joiner_str)))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn split(
value: &Value,
split: Option<Arc<str>>,
maxsplits: Option<i64>,
) -> Result<Value, Error> {
let string = Arc::<str>::try_from(value.clone())?;
let maxsplits = maxsplits.and_then(|x| if x >= 0 { Some(x as usize + 1) } else { None });
let preserve_safety = value.kind() == ValueKind::String && value.is_safe();
let wrap = |item: &str| {
if preserve_safety {
Value::from_safe_string(item.to_string())
} else {
Value::from(item)
}
};
Ok(match (split, maxsplits) {
(None, None) => Value::from_iter(string.split_whitespace().map(wrap)),
(Some(sep), None) => Value::from_iter(string.split(sep.as_ref()).map(wrap)),
(None, Some(n)) => Value::from_iter(splitn_whitespace(&string, n).map(wrap)),
(Some(sep), Some(n)) => Value::from_iter(string.splitn(n, sep.as_ref()).map(wrap)),
})
}
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn lines(value: &Value) -> Result<Value, Error> {
let string = Arc::<str>::try_from(value.clone())?;
let preserve_safety = value.kind() == ValueKind::String && value.is_safe();
Ok(Value::from_iter(string.lines().map(|line| {
if preserve_safety {
Value::from_safe_string(line.to_string())
} else {
Value::from(line)
}
})))
}
#[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(string) = value.as_str() {
Ok(string.chars().next_back().map_or(Value::UNDEFINED, |ch| {
if value.is_safe() {
Value::from_safe_string(ch.to_string())
} else {
Value::from(ch)
}
}))
} 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(
value: StringInput<'_>,
width: Option<usize>,
indent_first_line: Option<bool>,
indent_blank_lines: Option<bool>,
kwargs: Kwargs,
) -> Result<Value, Error> {
fn strip_trailing_newline(mut input: &str) -> &str {
if let Some(stripped) = input.strip_suffix('\n') {
input = stripped;
}
if let Some(stripped) = input.strip_suffix('\r') {
input = stripped;
}
input
}
let width = match width {
Some(width) => width,
None => ok!(kwargs.get::<Option<usize>>("width")).unwrap_or(4),
};
let indent_first_line = match indent_first_line {
Some(value) => value,
None => ok!(kwargs.get::<Option<bool>>("first")).unwrap_or(false),
};
let indent_blank_lines = match indent_blank_lines {
Some(value) => value,
None => ok!(kwargs.get::<Option<bool>>("blank")).unwrap_or(false),
};
ok!(kwargs.assert_all_used());
let input = strip_trailing_newline(value.as_str());
let indent_with = " ".repeat(width);
let mut output = String::new();
let mut iterator = input.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');
}
output.truncate(strip_trailing_newline(&output).len());
Ok(value.preserve_safety(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(
state: &State,
format_str: &Value,
format_args: Rest<Value>,
) -> Result<Value, Error> {
let string = format_str
.as_str()
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "value is not a string"))?;
if format_str.is_safe() {
let output = ok!(format_printf_with(
string,
&format_args,
|value, conversion| {
if conversion == FormatConversion::Character {
return Err(Error::new(
ErrorKind::InvalidOperation,
"character formatting is not supported for safe format strings",
));
}
if value.is_safe()
|| matches!(value.kind(), ValueKind::Bool | ValueKind::Number)
{
Ok(None)
} else {
escape(state, value).map(|value| Some(Value::from(value.as_str().unwrap())))
}
},
));
Ok(Value::from_safe_string(output))
} else {
format_filter(FormatStyle::Printf, string, &format_args).map(Value::from)
}
}
}
#[cfg(feature = "builtins")]
pub use self::builtins::*;