///|
pub fn Value::to_display_string(self : Value) -> String {
value_to_string(self)
}
///|
pub fn Value::as_string(self : Value) -> String? {
match self {
StrValue(value) | SafeStr(value) => Some(value)
_ => None
}
}
///|
pub fn Value::as_int(self : Value) -> Int? {
match self {
IntValue(value) => Some(value)
_ => None
}
}
///|
fn first_arg(args : Array[Value], fallback : Value) -> Value {
match args {
[value, ..] => value
[] => fallback
}
}
///|
fn builtin_upper(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
StrValue(text) => StrValue(text.to_upper())
SafeStr(text) => SafeStr(text.to_upper())
_ => raise RenderError("upper expects a string")
}
}
///|
fn builtin_lower(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
StrValue(text) => StrValue(text.to_lower())
SafeStr(text) => SafeStr(text.to_lower())
_ => raise RenderError("lower expects a string")
}
}
///|
fn builtin_trim(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
StrValue(text) => StrValue(trim_ws(text))
SafeStr(text) => SafeStr(trim_ws(text))
_ => raise RenderError("trim expects a string")
}
}
///|
fn builtin_split(value : Value, args : Array[Value]) -> Value raise Error {
let separator = first_arg(args, StrValue(",")).to_display_string()
match value {
StrValue(text) | SafeStr(text) =>
ListValue(
text.split(separator).map(part => StrValue(part.to_owned())).to_array(),
)
_ => raise RenderError("split expects a string")
}
}
///|
fn builtin_safe(value : Value, _args : Array[Value]) -> Value {
match value {
SafeStr(text) => SafeStr(text)
_ => SafeStr(value.to_display_string())
}
}
///|
fn builtin_escape(value : Value, _args : Array[Value]) -> Value {
match value {
SafeStr(text) => SafeStr(text)
_ => SafeStr(escape_html(value.to_display_string()))
}
}
///|
fn builtin_length(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
StrValue(text) | SafeStr(text) => IntValue(text.char_length())
ListValue(items) => IntValue(items.length())
MapValue(entries) => IntValue(entries.length())
_ => raise RenderError("length expects a string, list, or map")
}
}
///|
fn builtin_default(value : Value, args : Array[Value]) -> Value {
let use_boolean = match args {
[_, BoolValue(flag), ..] => flag
_ => false
}
match value {
Undefined => first_arg(args, StrValue(""))
_ if use_boolean && !is_truthy(value) => first_arg(args, StrValue(""))
_ => value
}
}
///|
fn builtin_capitalize(value : Value, _args : Array[Value]) -> Value raise Error {
guard value.as_string() is Some(text) else {
raise RenderError("capitalize expects a string")
}
let chars = text.to_array()
if chars.is_empty() {
return StrValue("")
}
StrValue(
chars[0].to_string().to_upper() +
String::from_array(chars[1:].to_owned()).to_lower(),
)
}
///|
fn builtin_title(value : Value, _args : Array[Value]) -> Value raise Error {
guard value.as_string() is Some(text) else {
raise RenderError("title expects a string")
}
let output = StringBuilder()
let mut start_word = true
for char in text {
if char == ' ' || char == '\t' || char == '\n' || char == '-' || char == '_' {
output.write_char(char)
start_word = true
} else {
output.write_string(
if start_word {
char.to_string().to_upper()
} else {
char.to_string().to_lower()
},
)
start_word = false
}
}
StrValue(output.to_string())
}
///|
fn builtin_wordcount(value : Value, _args : Array[Value]) -> Value raise Error {
guard value.as_string() is Some(text) else {
raise RenderError("wordcount expects a string")
}
let mut count = 0
let mut in_word = false
for char in text {
if is_ws(char) {
in_word = false
} else if !in_word {
count += 1
in_word = true
}
}
IntValue(count)
}
///|
fn builtin_sum(value : Value, _args : Array[Value]) -> Value raise Error {
guard value is ListValue(items) else {
raise RenderError("sum expects a list")
}
let mut total : Value = IntValue(0)
for item in items {
total = apply_binop(total, BinOp::Add, item)
}
total
}
///|
fn builtin_extreme(value : Value, maximum : Bool) -> Value raise Error {
guard value is ListValue(items) else {
raise RenderError(
if maximum {
"max expects a list"
} else {
"min expects a list"
},
)
}
guard items is [first, .. rest] else { return Undefined }
let mut selected = first
for item in rest {
let comparison = apply_binop(
item,
if maximum {
BinOp::Gt
} else {
BinOp::Lt
},
selected,
)
if (match comparison {
BoolValue(true) => true
_ => false
}) {
selected = item
}
}
selected
}
///|
fn builtin_unique(value : Value, _args : Array[Value]) -> Value raise Error {
guard value is ListValue(items) else {
raise RenderError("unique expects a list")
}
let unique : Array[Value] = []
for item in items {
if !unique.any(existing => primitive_values_equal(existing, item)) {
unique.push(item)
}
}
ListValue(unique)
}
///|
fn builtin_sort(value : Value, _args : Array[Value]) -> Value raise Error {
guard value is ListValue(items) else {
raise RenderError("sort expects a list")
}
let sorted = items.copy()
for right in 1.. 0 &&
(match apply_binop(sorted[index - 1], BinOp::Gt, sorted[index]) {
BoolValue(true) => true
_ => false
}) {
let temporary = sorted[index - 1]
sorted[index - 1] = sorted[index]
sorted[index] = temporary
index -= 1
}
}
ListValue(sorted)
}
///|
fn builtin_map_entries(value : Value, mode : String) -> Value raise Error {
guard value is MapValue(entries) else {
raise RenderError(mode + " expects a map")
}
let result : Array[Value] = []
for key, item in entries {
match mode {
"keys" => result.push(StrValue(key))
"values" => result.push(item)
_ => result.push(ListValue([StrValue(key), item]))
}
}
ListValue(result)
}
///|
fn builtin_join(value : Value, args : Array[Value]) -> Value raise Error {
let separator = first_arg(args, StrValue("")).to_display_string()
match value {
ListValue(items) => StrValue(items.map(value_to_string).join(separator))
_ => raise RenderError("join expects a list")
}
}
///|
fn builtin_replace(value : Value, args : Array[Value]) -> Value raise Error {
guard args is [old, new, ..] else {
raise RenderError("replace expects old and new values")
}
match value {
StrValue(text) =>
StrValue(
text.replace_all(
old=old.to_display_string(),
new=new.to_display_string(),
),
)
SafeStr(text) =>
SafeStr(
text.replace_all(
old=old.to_display_string(),
new=new.to_display_string(),
),
)
_ => raise RenderError("replace expects a string")
}
}
///|
fn builtin_first(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
ListValue([first, ..]) => first
ListValue([]) => Undefined
_ => raise RenderError("first expects a list")
}
}
///|
fn builtin_last(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
ListValue(items) =>
match items.last() {
Some(last) => last
None => Undefined
}
_ => raise RenderError("last expects a list")
}
}
///|
fn builtin_reverse(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
ListValue(items) => ListValue(items.rev())
_ => raise RenderError("reverse expects a list")
}
}
///|
fn builtin_abs(value : Value, _args : Array[Value]) -> Value raise Error {
match value {
IntValue(number) => IntValue(if number < 0 { -number } else { number })
DoubleValue(number) =>
DoubleValue(if number < 0.0 { -number } else { number })
_ => raise RenderError("abs expects a number")
}
}
///|
fn builtin_string(value : Value, _args : Array[Value]) -> Value {
StrValue(value.to_display_string())
}
///|
fn builtin_range(args : Array[Value], max_items : Int) -> Value raise Error {
let (start, stop, step) = match args {
[IntValue(stop)] => (0, stop, 1)
[IntValue(start), IntValue(stop)] => (start, stop, 1)
[IntValue(start), IntValue(stop), IntValue(step)] => (start, stop, step)
_ => raise RenderError("range expects one to three integer arguments")
}
if step == 0 {
raise RenderError("range step cannot be zero")
}
let result : Array[Value] = []
if step > 0 {
for current = start; current < stop; current = current + step {
if result.length() >= max_items {
raise RenderError("range exceeds the configured item limit")
}
result.push(IntValue(current))
}
} else {
for current = start; current > stop; current = current + step {
if result.length() >= max_items {
raise RenderError("range exceeds the configured item limit")
}
result.push(IntValue(current))
}
}
ListValue(result)
}
///|
fn builtin_list(args : Array[Value]) -> Value {
ListValue(args)
}
///|
fn builtin_test_defined(value : Value, _args : Array[Value]) -> Bool {
match value {
Undefined => false
_ => true
}
}
///|
fn builtin_test_number(value : Value, _args : Array[Value]) -> Bool {
match value {
IntValue(_) | DoubleValue(_) => true
_ => false
}
}
///|
fn builtin_test_string(value : Value, _args : Array[Value]) -> Bool {
match value {
StrValue(_) | SafeStr(_) => true
_ => false
}
}
///|
fn builtin_test_iterable(value : Value, _args : Array[Value]) -> Bool {
match value {
StrValue(_) | SafeStr(_) | ListValue(_) | MapValue(_) => true
_ => false
}
}
///|
fn Environment::install_builtins(self : Environment) -> Unit {
self.add_filter("upper", (value, args) => {
Ok(builtin_upper(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("lower", (value, args) => {
Ok(builtin_lower(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("trim", (value, args) => {
Ok(builtin_trim(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("split", (value, args) => {
Ok(builtin_split(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("safe", (value, args) => Ok(builtin_safe(value, args)))
self.add_filter("escape", (value, args) => Ok(builtin_escape(value, args)))
self.add_filter("e", (value, args) => Ok(builtin_escape(value, args)))
self.add_filter("length", (value, args) => {
Ok(builtin_length(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("default", (value, args) => Ok(builtin_default(value, args)))
self.add_filter("capitalize", (value, args) => {
Ok(builtin_capitalize(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("title", (value, args) => {
Ok(builtin_title(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("wordcount", (value, args) => {
Ok(builtin_wordcount(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("sum", (value, args) => {
Ok(builtin_sum(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("min", (value, _args) => {
Ok(builtin_extreme(value, false)) catch {
error => Err("\{error}")
}
})
self.add_filter("max", (value, _args) => {
Ok(builtin_extreme(value, true)) catch {
error => Err("\{error}")
}
})
self.add_filter("unique", (value, args) => {
Ok(builtin_unique(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("sort", (value, args) => {
Ok(builtin_sort(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("keys", (value, _args) => {
Ok(builtin_map_entries(value, "keys")) catch {
error => Err("\{error}")
}
})
self.add_filter("values", (value, _args) => {
Ok(builtin_map_entries(value, "values")) catch {
error => Err("\{error}")
}
})
self.add_filter("items", (value, _args) => {
Ok(builtin_map_entries(value, "items")) catch {
error => Err("\{error}")
}
})
self.add_filter("join", (value, args) => {
Ok(builtin_join(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("replace", (value, args) => {
Ok(builtin_replace(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("first", (value, args) => {
Ok(builtin_first(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("last", (value, args) => {
Ok(builtin_last(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("reverse", (value, args) => {
Ok(builtin_reverse(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("abs", (value, args) => {
Ok(builtin_abs(value, args)) catch {
error => Err("\{error}")
}
})
self.add_filter("string", (value, args) => Ok(builtin_string(value, args)))
self.add_function("range", args => {
Ok(builtin_range(args, self.options().max_range_size)) catch {
error => Err("\{error}")
}
})
self.add_function("list", args => Ok(builtin_list(args)))
self.add_context_function("dict", (_, args, kwargs) => {
let result : Map[String, Value] = Map([])
match args {
[] => ()
[MapValue(entries)] =>
for key, value in entries {
result[key] = value
}
_ => return Err("dict expects zero arguments or one map")
}
for key, value in kwargs {
result[key] = value
}
Ok(MapValue(result))
})
self.add_context_function("namespace", (_, args, kwargs) => {
if !args.is_empty() {
Err("namespace only accepts keyword arguments")
} else {
Ok(MapValue(kwargs))
}
})
self.add_test("defined", (value, args) => {
Ok(builtin_test_defined(value, args))
})
self.add_test("undefined", (value, args) => {
Ok(!builtin_test_defined(value, args))
})
self.add_test("number", (value, args) => Ok(builtin_test_number(value, args)))
self.add_test("string", (value, args) => Ok(builtin_test_string(value, args)))
self.add_test("iterable", (value, args) => {
Ok(builtin_test_iterable(value, args))
})
self.add_test("mapping", (value, _args) => {
Ok(
match value {
MapValue(_) => true
_ => false
},
)
})
self.add_test("none", (value, _args) => {
Ok(
match value {
Null => true
_ => false
},
)
})
self.add_test("odd", (value, _args) => {
match value {
IntValue(number) => Ok(number % 2 != 0)
_ => Err("odd expects an integer")
}
})
self.add_test("even", (value, _args) => {
match value {
IntValue(number) => Ok(number % 2 == 0)
_ => Err("even expects an integer")
}
})
self.add_test("boolean", (value, _args) => {
Ok(
match value {
BoolValue(_) => true
_ => false
},
)
})
self.add_test("integer", (value, _args) => {
Ok(
match value {
IntValue(_) => true
_ => false
},
)
})
self.add_test("float", (value, _args) => {
Ok(
match value {
DoubleValue(_) => true
_ => false
},
)
})
self.add_test("sequence", (value, _args) => {
Ok(
match value {
StrValue(_) | SafeStr(_) | ListValue(_) => true
_ => false
},
)
})
self.add_test("equalto", (value, args) => {
Ok(
match args {
[other, ..] => primitive_values_equal(value, other)
[] => false
},
)
})
self.add_test("sameas", (value, args) => {
Ok(
match args {
[other, ..] => primitive_values_equal(value, other)
[] => false
},
)
})
self.add_test("divisibleby", (value, args) => {
match (value, args) {
(IntValue(number), [IntValue(divisor), ..]) if divisor != 0 =>
Ok(number % divisor == 0)
_ => Err("divisibleby expects an integer divisor")
}
})
}