Merge branch 'helper_math' into render

This commit is contained in:
Tom Alexander 2020-06-13 23:04:36 -04:00
commit c3ebf4c66c
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
14 changed files with 1299 additions and 147 deletions

View File

@ -27,3 +27,7 @@ Do objects with different paths referencing the same variable match?{~n}
{#int renamed=some_obj}
{@eq key=some_obj value=renamed}some_obj equals renamed{:else}some_obj does not equal renamed{/eq}{~n}
{/int}
Floating point equality{~n}
======================={~n}
{@eq key=int value=7.0}int is equal to 7.0{:else}int is not equal to 7.0{/eq}

View File

@ -0,0 +1,9 @@
Early termination
-----------------
If the math helper has a body then it stops rendering conditionals after the first conditional that returns true. Rendering an else block does not cause early termination, but else blocks are rendered.
Non-number values
-----------------
If the math operation involves non-numbers, NaN is returned.

View File

@ -0,0 +1,7 @@
{
"number_7": 7,
"string_7": "7",
"string_cat": "cat",
"add_operation": "add",
"a_operation": "a"
}

View File

@ -0,0 +1,111 @@
Bodiless math{~n}
============={~n}
{@math key="7" method="add" operand="4" /}{~n}
{@math key=number_7 method="add" operand="4" /}{~n}
{@math key=string_7 method="add" operand="4" /}{~n}
{@math key=string_cat method="add" operand="4" /}{~n}
{@math key=string_cat method="add" operand="foo" /}{~n}
Math with body: dot reference{~n}
============================={~n}
{@math key="7" method="add" operand="4"}
{.}{~n}
{/math}
Math with body: eq literal{~n}
=========================={~n}
{@math key="7" method="add" operand="4"}
{@eq value=11}math result is 11{:else}math result is not 11{/eq}{~n}
{/math}
Math with body: eq string{~n}
========================={~n}
{@math key="7" method="add" operand="4"}
{@eq value="11"}math result is "11"{:else}math result is not "11"{/eq}{~n}
{/math}
Math with body: eq else block{~n}
============================={~n}
{@math key="7" method="add" operand="4"}
{@eq value=12}math result is 12{:else}math result is not 12{/eq}{~n}
{/math}
Math with body: standalone eq{~n}
============================={~n}
{@math key="7" method="add" operand="4"}
{@eq key=12 value=12}12 is 12{:else}12 is not 12{/eq}{~n}
{/math}
Math with body: standalone else block{~n}
====================================={~n}
{@math key="7" method="add" operand="4"}
{@eq key=11 value=12}11 is 12{:else}11 is not 12{/eq}{~n}
{/math}
Math with body: early termination{~n}
============================={~n}
{@math key="7" method="add" operand="4"}
{@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq}
{@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq}
{/math}
Math with body: early termination else block{~n}
============================================{~n}
{@math key="7" method="add" operand="4"}
{@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq}
{@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq}
{@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq}
{/math}
Math with body: any{~n}
==================={~n}
{@math key="7" method="add" operand="4"}
{@eq value=10}math result is 10{~n}{:else}math result is not 10{~n}{/eq}
{@eq value=11}math result is 11{~n}{:else}math result is not 11{~n}{/eq}
{@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq}
{@any}We found the value{~n}{/any}
{@none}We did not find the value{~n}{/none}
{/math}
Math with body: none{~n}
===================={~n}
{@math key="7" method="add" operand="4"}
{@eq value=10}math result is 10{~n}{:else}math result is not 10{~n}{/eq}
{@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq}
{@any}We found the value{~n}{/any}
{@none}We did not find the value{~n}{/none}
{/math}
Math where method is a reference{~n}
================================{~n}
{@math key="7" method=add_operation operand="4" /}{~n}
Math where method is a template{~n}
==============================={~n}
{@math key="7" method="{a_operation}dd" operand="4" /}{~n}
Math where key is a template{~n}
============================{~n}
{@math key="{string_7}1" method="add" operand="4" /}{~n}
Math where method is absent{~n}
============================{~n}
{@math key="7" operand="4" /}{~n}
Math where method is missing{~n}
============================{~n}
{@math key="7" method=foobar operand="4" /}{~n}
Math with unknown method{~n}
========================{~n}
{@math key="7" method="twirl" operand="4" /}{~n}
Math with body where method is absent{~n}
===================================={~n}
{@math key="7" operand="4"}
{@eq value=12}math result is 12{~n}{:else}math result is not 12{~n}{/eq}
{@any}We found the value{~n}{/any}
{@none}We did not find the value{~n}{/none}
plain text not inside a tag{~n}
{/math}

View File

@ -7,6 +7,10 @@ Early termination
Comparisons are done in-order and only the first matching comparison (eq/ne/gt/gte/lt/lte) is evaluated. All other non-comparison elements inside the select tag are still rendered normally.
Matching is terminated even if the first matching comparison has its own key.
Else blocks are rendered until the first matching comparison but else blocks after that are not rendered.
Default vs none
---------------
Default was deprecated as of dust 1.6.0 and no longer does anything. It was deprecated because it was essentially the same as none except that there could only be one per select block and it had to be placed after all the other conditions to make sure they were all false. None is more flexible.

View File

@ -101,6 +101,20 @@ Early termination stand-alone comparison{~n}
{/pet_names}
{/select}{~n}
Early termination else block{~n}
============================{~n}
{@select key=pet}
{@eq value="dog"}Lets name your pet rover{~n}{:else}Lets not name your pet rover{~n}{/eq}
{@eq value="cat"}Lets name your pet fluffy{~n}{:else}Lets not name your pet fluffy{~n}{/eq}
{@eq value="cat"}Lets name your pet whiskers{~n}{:else}Lets not name your pet whiskers{~n}{/eq}
{@eq value="lizard"}Lets name your pet dave{~n}{:else}Lets not name your pet dave{~n}{/eq}
{@any}{person} has a pet!{~n}{/any}
text not inside a comparison{~n}
{#pet_names}
If your pet was a {type} we'd name it {pet_name}{~n}
{/pet_names}
{/select}{~n}
@any alone{~n}
=========={~n}
{@any}{person} has a pet!{~n}{/any}

View File

@ -5,17 +5,21 @@ use parser::Filter;
use parser::OwnedLiteral;
use parser::Template;
use renderer::compile_template;
use renderer::Castable;
use renderer::CompileError;
use renderer::ContextElement;
use renderer::DustRenderer;
use renderer::IceResult;
use renderer::IntoContextElement;
use renderer::Loopable;
use renderer::MathNumber;
use renderer::RenderError;
use renderer::Renderable;
use renderer::Truthiness;
use renderer::WalkError;
use renderer::Walkable;
use std::cmp::Ordering;
use std::convert::TryInto;
use std::env;
use std::fs;
use std::io::{self, Read};
@ -274,7 +278,6 @@ impl Renderable for serde_json::Value {
serde_json::Value::Number(num) => Ok(num.to_string()),
serde_json::Value::String(string) => Ok(string.to_string()),
serde_json::Value::Array(arr) => {
// TODO: Handle the filters instead of passing a Vec::new()
let rendered: Result<Vec<String>, RenderError> =
arr.iter().map(|val| val.render(&Vec::new())).collect();
let rendered_slice: &[String] = &rendered?;
@ -310,8 +313,30 @@ impl Loopable for serde_json::Value {
}
}
impl Castable for serde_json::Value {
fn cast_to_type<'a>(&'a self, target: &str) -> Option<IceResult<'a>> {
match (self, target) {
(serde_json::Value::String(text), "number") => text
.parse::<u64>()
.map(|num| IceResult::from_owned(OwnedLiteral::LPositiveInteger(num)))
.or_else(|_| {
text.parse::<i64>()
.map(|num| IceResult::from_owned(OwnedLiteral::LNegativeInteger(num)))
})
.or_else(|_| {
text.parse::<f64>()
.map(|num| IceResult::from_owned(OwnedLiteral::LFloat(num)))
})
.ok(),
(serde_json::Value::Number(_), "number") => Some(IceResult::from_borrowed(self)),
(_, _) => panic!("Unimplemented cast"),
}
}
}
impl CompareContextElement for serde_json::Value {
fn equals(&self, other: &dyn ContextElement) -> bool {
// println!("Json equality check {:?} == {:?}", self, other);
// Handle other serde_json::Value
match other.to_any().downcast_ref::<Self>() {
None => (),
@ -332,8 +357,19 @@ impl CompareContextElement for serde_json::Value {
return self.as_str().map_or(false, |s| s == other_string)
}
Some(OwnedLiteral::LPositiveInteger(other_num)) => {
return self.as_u64().map_or(false, |n| n == *other_num)
let other_json_num: serde_json::Number = std::convert::From::from(*other_num);
return self
.equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement);
}
Some(OwnedLiteral::LNegativeInteger(other_num)) => {
let other_json_num: serde_json::Number = std::convert::From::from(*other_num);
return self
.equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement);
}
Some(OwnedLiteral::LFloat(other_num)) => match self.as_f64() {
None => return false,
Some(self_float) => return self_float == *other_num,
},
}
false
}
@ -355,138 +391,261 @@ impl CompareContextElement for serde_json::Value {
.partial_compare(other);
}
// Handle other serde_json::Value
match other.to_any().downcast_ref::<Self>() {
None => (),
Some(other_json_value) => {
return match (self, other_json_value) {
(
serde_json::Value::Bool(self_boolean),
serde_json::Value::Bool(other_boolean),
) => self_boolean.partial_cmp(other_boolean),
(
serde_json::Value::Number(self_number),
serde_json::Value::Number(other_number),
) => return compare_json_numbers(self_number, other_number),
(
serde_json::Value::String(self_string),
serde_json::Value::Number(other_number),
) => return compare_json_numbers(self_string, other_number),
(
serde_json::Value::Number(self_number),
serde_json::Value::String(other_string),
) => return compare_json_numbers(self_number, other_string),
(
serde_json::Value::String(self_string),
serde_json::Value::String(other_string),
) => self_string.partial_cmp(other_string),
(
serde_json::Value::Array(_self_array),
serde_json::Value::Array(_other_array),
) => {
// TODO: is this reachable given the early convert to string before this block?
return self
.render(&Vec::new())
.unwrap_or("".to_owned())
.partial_cmp(
&other_json_value
.render(&Vec::new())
.unwrap_or("".to_owned()),
);
}
_ => None,
};
}
let maybe_json_other = other.to_any().downcast_ref::<Self>();
let maybe_literal_other = other.to_any().downcast_ref::<OwnedLiteral>();
// If they're both strings, compare them directly
match (self, maybe_json_other, maybe_literal_other) {
// If they're both strings, compare them directly
(
serde_json::Value::String(self_string),
Some(serde_json::Value::String(other_string)),
_,
) => return self_string.partial_cmp(&other_string),
(
serde_json::Value::String(self_string),
_,
Some(OwnedLiteral::LString(other_string)),
) => return self_string.partial_cmp(&other_string),
// Otherwise, convert to numbers are compare them that way
(_, Some(json_other), _) => return compare_json_numbers(self, json_other),
(_, _, Some(literal_other)) => return compare_json_numbers(self, literal_other),
_ => panic!("Unimplemented comparison type."),
}
// Handle literals
match other.to_any().downcast_ref::<OwnedLiteral>() {
None => (),
Some(other_literal) => match (self, other_literal) {
(serde_json::Value::String(self_string), OwnedLiteral::LString(other_string)) => {
return self_string.partial_cmp(other_string)
}
(
serde_json::Value::String(self_string),
OwnedLiteral::LPositiveInteger(_other_num),
) => return compare_json_numbers(self_string, other_literal),
(serde_json::Value::Number(self_num), OwnedLiteral::LString(other_string)) => {
return compare_json_numbers(self_num, other_string)
}
(
serde_json::Value::Number(self_num),
OwnedLiteral::LPositiveInteger(_other_num),
) => return compare_json_numbers(self_num, other_literal),
(serde_json::Value::Array(_), _) => {
// TODO
todo!()
}
(serde_json::Value::Object(_), _) => {
// TODO
todo!()
}
(serde_json::Value::Bool(_), _) => {
// TODO
todo!()
}
(serde_json::Value::Null, _) => {
// TODO
todo!()
}
},
}
fn math_add<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
let other_json = other.to_any().downcast_ref::<Self>();
let other_literal = other.to_any().downcast_ref::<OwnedLiteral>();
match (self, other_json, other_literal) {
// If its neither of those types, then it is unimplemented
(_, None, None) => panic!("Math operation on unimplemented type"),
// Since this is specifically for the math helper, non-primitives are not supported
(serde_json::Value::Array(_), _, _)
| (serde_json::Value::Object(_), _, _)
| (_, Some(serde_json::Value::Array(_)), _)
| (_, Some(serde_json::Value::Object(_)), _) => None,
// Strings also are ignored because this is specifically a math function
(serde_json::Value::String(_), _, _)
| (_, Some(serde_json::Value::String(_)), _)
| (_, _, Some(OwnedLiteral::LString(_))) => None,
// Handle other serde_json::Value
(_, Some(other_json_value), _) => (std::convert::Into::<MathNumber>::into(self)
+ std::convert::Into::<MathNumber>::into(other_json_value))
.map(IceResult::from_owned),
// Handle literals
(_, _, Some(other_literal)) => (std::convert::Into::<MathNumber>::into(self)
+ std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
}
None
}
fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
let other_json = other.to_any().downcast_ref::<Self>();
let other_literal = other.to_any().downcast_ref::<OwnedLiteral>();
match (self, other_json, other_literal) {
// If its neither of those types, then it is unimplemented
(_, None, None) => panic!("Math operation on unimplemented type"),
// Since this is specifically for the math helper, non-primitives are not supported
(serde_json::Value::Array(_), _, _)
| (serde_json::Value::Object(_), _, _)
| (_, Some(serde_json::Value::Array(_)), _)
| (_, Some(serde_json::Value::Object(_)), _) => None,
// Strings also are ignored because this is specifically a math function
(serde_json::Value::String(_), _, _)
| (_, Some(serde_json::Value::String(_)), _)
| (_, _, Some(OwnedLiteral::LString(_))) => None,
// Handle other serde_json::Value
(_, Some(other_json_value), _) => (std::convert::Into::<MathNumber>::into(self)
- std::convert::Into::<MathNumber>::into(other_json_value))
.map(IceResult::from_owned),
// Handle literals
(_, _, Some(other_literal)) => (std::convert::Into::<MathNumber>::into(self)
- std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
}
}
fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
let other_json = other.to_any().downcast_ref::<Self>();
let other_literal = other.to_any().downcast_ref::<OwnedLiteral>();
match (self, other_json, other_literal) {
// If its neither of those types, then it is unimplemented
(_, None, None) => panic!("Math operation on unimplemented type"),
// Since this is specifically for the math helper, non-primitives are not supported
(serde_json::Value::Array(_), _, _)
| (serde_json::Value::Object(_), _, _)
| (_, Some(serde_json::Value::Array(_)), _)
| (_, Some(serde_json::Value::Object(_)), _) => None,
// Strings also are ignored because this is specifically a math function
(serde_json::Value::String(_), _, _)
| (_, Some(serde_json::Value::String(_)), _)
| (_, _, Some(OwnedLiteral::LString(_))) => None,
// Handle other serde_json::Value
(_, Some(other_json_value), _) => (std::convert::Into::<MathNumber>::into(self)
* std::convert::Into::<MathNumber>::into(other_json_value))
.map(IceResult::from_owned),
// Handle literals
(_, _, Some(other_literal)) => (std::convert::Into::<MathNumber>::into(self)
* std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
}
}
fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
let other_json = other.to_any().downcast_ref::<Self>();
let other_literal = other.to_any().downcast_ref::<OwnedLiteral>();
match (self, other_json, other_literal) {
// If its neither of those types, then it is unimplemented
(_, None, None) => panic!("Math operation on unimplemented type"),
// Since this is specifically for the math helper, non-primitives are not supported
(serde_json::Value::Array(_), _, _)
| (serde_json::Value::Object(_), _, _)
| (_, Some(serde_json::Value::Array(_)), _)
| (_, Some(serde_json::Value::Object(_)), _) => None,
// Strings also are ignored because this is specifically a math function
(serde_json::Value::String(_), _, _)
| (_, Some(serde_json::Value::String(_)), _)
| (_, _, Some(OwnedLiteral::LString(_))) => None,
// Handle other serde_json::Value
(_, Some(other_json_value), _) => (std::convert::Into::<MathNumber>::into(self)
/ std::convert::Into::<MathNumber>::into(other_json_value))
.map(IceResult::from_owned),
// Handle literals
(_, _, Some(other_literal)) => (std::convert::Into::<MathNumber>::into(self)
/ std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
}
}
fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
let other_json = other.to_any().downcast_ref::<Self>();
let other_literal = other.to_any().downcast_ref::<OwnedLiteral>();
match (self, other_json, other_literal) {
// If its neither of those types, then it is unimplemented
(_, None, None) => panic!("Math operation on unimplemented type"),
// Since this is specifically for the math helper, non-primitives are not supported
(serde_json::Value::Array(_), _, _)
| (serde_json::Value::Object(_), _, _)
| (_, Some(serde_json::Value::Array(_)), _)
| (_, Some(serde_json::Value::Object(_)), _) => None,
// Strings also are ignored because this is specifically a math function
(serde_json::Value::String(_), _, _)
| (_, Some(serde_json::Value::String(_)), _)
| (_, _, Some(OwnedLiteral::LString(_))) => None,
// Handle other serde_json::Value
(_, Some(other_json_value), _) => (std::convert::Into::<MathNumber>::into(self)
% std::convert::Into::<MathNumber>::into(other_json_value))
.map(IceResult::from_owned),
// Handle literals
(_, _, Some(other_literal)) => (std::convert::Into::<MathNumber>::into(self)
% std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
}
}
fn math_abs<'a>(&self) -> Option<IceResult<'a>> {
std::convert::Into::<MathNumber>::into(self)
.math_abs()
.map(IceResult::from_owned)
}
fn math_floor<'a>(&self) -> Option<IceResult<'a>> {
std::convert::Into::<MathNumber>::into(self)
.math_floor()
.map(IceResult::from_owned)
}
fn math_ceil<'a>(&self) -> Option<IceResult<'a>> {
std::convert::Into::<MathNumber>::into(self)
.math_ceil()
.map(IceResult::from_owned)
}
}
#[derive(Debug)]
enum JsonNumber {
enum ComparisonNumber {
UnsignedInteger(u64),
SignedInteger(i64),
Decimal(f64),
Failure,
}
impl From<&String> for JsonNumber {
impl From<&serde_json::Value> for ComparisonNumber {
/// Convert from a JSON value to a number for comparison based on
/// the logic described at
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than
fn from(original: &serde_json::Value) -> Self {
match original {
serde_json::Value::Null => ComparisonNumber::UnsignedInteger(0),
serde_json::Value::Bool(boolean) => {
if *boolean {
ComparisonNumber::UnsignedInteger(1)
} else {
ComparisonNumber::UnsignedInteger(0)
}
}
serde_json::Value::Number(num) => num.into(),
serde_json::Value::String(text) => text.into(),
serde_json::Value::Array(_) => {
panic!("Only primitives should be cast to numbers for comparisons")
}
serde_json::Value::Object(_) => {
panic!("Only primitives should be cast to numbers for comparisons")
}
}
}
}
impl From<&String> for ComparisonNumber {
fn from(original: &String) -> Self {
match original.parse::<u64>() {
Ok(num) => return JsonNumber::UnsignedInteger(num),
Ok(num) => return ComparisonNumber::UnsignedInteger(num),
Err(_) => (),
};
match original.parse::<i64>() {
Ok(num) => return JsonNumber::SignedInteger(num),
Ok(num) => return ComparisonNumber::SignedInteger(num),
Err(_) => (),
};
match original.parse::<f64>() {
Ok(num) => return JsonNumber::Decimal(num),
Ok(num) => return ComparisonNumber::Decimal(num),
Err(_) => (),
};
JsonNumber::Failure
ComparisonNumber::Failure
}
}
impl From<&serde_json::Number> for JsonNumber {
impl From<&serde_json::Number> for ComparisonNumber {
fn from(original: &serde_json::Number) -> Self {
match original.as_u64() {
Some(num) => return JsonNumber::UnsignedInteger(num),
Some(num) => return ComparisonNumber::UnsignedInteger(num),
None => (),
};
match original.as_i64() {
Some(num) => return JsonNumber::SignedInteger(num),
Some(num) => return ComparisonNumber::SignedInteger(num),
None => (),
};
match original.as_f64() {
Some(num) => return JsonNumber::Decimal(num),
Some(num) => return ComparisonNumber::Decimal(num),
None => (),
};
JsonNumber::Failure
ComparisonNumber::Failure
}
}
impl From<&OwnedLiteral> for JsonNumber {
impl From<&OwnedLiteral> for ComparisonNumber {
fn from(original: &OwnedLiteral) -> Self {
match original {
OwnedLiteral::LPositiveInteger(num) => JsonNumber::UnsignedInteger(*num),
OwnedLiteral::LPositiveInteger(num) => ComparisonNumber::UnsignedInteger(*num),
OwnedLiteral::LNegativeInteger(num) => ComparisonNumber::SignedInteger(*num),
OwnedLiteral::LString(text) => text.into(),
OwnedLiteral::LFloat(num) => {
if num.is_nan() {
ComparisonNumber::Failure
} else {
ComparisonNumber::Decimal(*num)
}
}
}
}
}
@ -498,39 +657,104 @@ impl From<&OwnedLiteral> for JsonNumber {
/// strings
fn compare_json_numbers<S, O>(self_input: S, other_input: O) -> Option<Ordering>
where
S: Into<JsonNumber>,
O: Into<JsonNumber>,
S: Into<ComparisonNumber>,
O: Into<ComparisonNumber>,
{
let self_number: JsonNumber = self_input.into();
let other_number: JsonNumber = other_input.into();
// TODO: Figure out how javascript compares floats and ints
let self_number: ComparisonNumber = self_input.into();
let other_number: ComparisonNumber = other_input.into();
match (self_number, other_number) {
(JsonNumber::Failure, _) => return None,
(_, JsonNumber::Failure) => return None,
(JsonNumber::UnsignedInteger(self_num), JsonNumber::UnsignedInteger(other_num)) => {
return self_num.partial_cmp(&other_num)
(ComparisonNumber::Failure, _) => return None,
(_, ComparisonNumber::Failure) => return None,
(
ComparisonNumber::UnsignedInteger(self_num),
ComparisonNumber::UnsignedInteger(other_num),
) => return self_num.partial_cmp(&other_num),
(
ComparisonNumber::UnsignedInteger(self_num),
ComparisonNumber::SignedInteger(other_num),
) => {
if self_num < std::i64::MAX as u64 {
return (self_num as i64).partial_cmp(&other_num);
} else {
return Some(Ordering::Greater);
}
}
(JsonNumber::UnsignedInteger(_self_num), JsonNumber::SignedInteger(_other_num)) => {
return Some(Ordering::Greater)
(ComparisonNumber::UnsignedInteger(self_num), ComparisonNumber::Decimal(other_num)) => {
return (self_num as f64).partial_cmp(&other_num)
}
(JsonNumber::UnsignedInteger(_self_num), JsonNumber::Decimal(_other_num)) => return None,
(JsonNumber::SignedInteger(_self_num), JsonNumber::UnsignedInteger(_other_num)) => {
return Some(Ordering::Less)
(
ComparisonNumber::SignedInteger(self_num),
ComparisonNumber::UnsignedInteger(other_num),
) => {
if other_num < std::i64::MAX as u64 {
return self_num.partial_cmp(&(other_num as i64));
} else {
return Some(Ordering::Less);
}
}
(JsonNumber::SignedInteger(self_num), JsonNumber::SignedInteger(other_num)) => {
(ComparisonNumber::SignedInteger(self_num), ComparisonNumber::SignedInteger(other_num)) => {
return self_num.partial_cmp(&other_num)
}
(JsonNumber::SignedInteger(_self_num), JsonNumber::Decimal(_other_num)) => return None,
(ComparisonNumber::SignedInteger(self_num), ComparisonNumber::Decimal(other_num)) => {
return (self_num as f64).partial_cmp(&other_num)
}
(JsonNumber::Decimal(_self_num), JsonNumber::UnsignedInteger(_other_num)) => return None,
(JsonNumber::Decimal(_self_num), JsonNumber::SignedInteger(_other_num)) => return None,
(JsonNumber::Decimal(self_num), JsonNumber::Decimal(other_num)) => {
(ComparisonNumber::Decimal(self_num), ComparisonNumber::UnsignedInteger(other_num)) => {
return self_num.partial_cmp(&(other_num as f64))
}
(ComparisonNumber::Decimal(self_num), ComparisonNumber::SignedInteger(other_num)) => {
return self_num.partial_cmp(&(other_num as f64))
}
(ComparisonNumber::Decimal(self_num), ComparisonNumber::Decimal(other_num)) => {
return self_num.partial_cmp(&other_num)
}
}
}
impl From<&serde_json::Value> for MathNumber {
fn from(original: &serde_json::Value) -> Self {
match original {
serde_json::Value::Null => MathNumber::Integer(0),
serde_json::Value::Bool(boolean) => {
if *boolean {
MathNumber::Integer(1)
} else {
MathNumber::Integer(0)
}
}
serde_json::Value::Number(num) => num.into(),
serde_json::Value::String(_) => {
panic!("Strings should not be cast to numbers for math")
}
serde_json::Value::Array(_) => {
panic!("Only primitives should be cast to numbers for comparisons")
}
serde_json::Value::Object(_) => {
panic!("Only primitives should be cast to numbers for comparisons")
}
}
}
}
impl From<&serde_json::Number> for MathNumber {
fn from(original: &serde_json::Number) -> Self {
match original.as_u64() {
Some(num) => return MathNumber::Integer(num.try_into().unwrap()),
None => (),
};
match original.as_i64() {
Some(num) => return MathNumber::Integer(num.into()),
None => (),
};
match original.as_f64() {
Some(num) => return MathNumber::Decimal(num),
None => (),
};
MathNumber::Failure
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -47,6 +47,7 @@ pub enum DustTag<'a> {
DTHelperSelect(ParameterizedBlock<'a>),
DTHelperAny(ParameterizedBlock<'a>),
DTHelperNone(ParameterizedBlock<'a>),
DTHelperMath(ParameterizedBlock<'a>),
}
#[derive(Clone, Debug, PartialEq)]
@ -118,6 +119,8 @@ pub struct Partial<'a> {
pub enum OwnedLiteral {
LString(String),
LPositiveInteger(u64),
LNegativeInteger(i64),
LFloat(f64),
}
#[derive(Debug, PartialEq)]
@ -279,6 +282,10 @@ fn dust_tag_helper(i: &str) -> IResult<&str, DustTag> {
parameterized_block("{@", &tag_to_path("none")),
DustTag::DTHelperNone,
),
map(
parameterized_block("{@", &tag_to_path("math")),
DustTag::DTHelperMath,
),
))(i)
}
@ -348,6 +355,34 @@ fn postitive_integer_literal(i: &str) -> IResult<&str, u64> {
)(i)
}
/// No decimals, just the sign and digits
fn negative_integer_literal(i: &str) -> IResult<&str, i64> {
map(
verify(
map(
recognize(tuple((tag("-"), digit1))),
|number_string: &str| number_string.parse::<i64>(),
),
|parse_result| parse_result.is_ok(),
),
|parsed_number| parsed_number.unwrap(),
)(i)
}
/// A non-scientific notation float (sign, digits, decimal, and more digits)
fn float_literal(i: &str) -> IResult<&str, f64> {
map(
verify(
map(
recognize(tuple((opt(one_of("+-")), digit1, tag("."), digit1))),
|number_string: &str| number_string.parse::<f64>(),
),
|parse_result| parse_result.is_ok(),
),
|parsed_number| parsed_number.unwrap(),
)(i)
}
fn template_string_rvalue(i: &str) -> IResult<&str, Vec<PartialNameElement>> {
let (i, template_string) = verify(quoted_string, |s: &String| {
partial_quoted_tag(s.as_str()).is_ok()
@ -367,6 +402,12 @@ fn rvalue(i: &str) -> IResult<&str, RValue> {
alt((
map(path, RValue::RVPath),
map(template_string_rvalue, RValue::RVTemplate),
map(float_literal, |num| {
RValue::RVLiteral(OwnedLiteral::LFloat(num))
}),
map(negative_integer_literal, |num| {
RValue::RVLiteral(OwnedLiteral::LNegativeInteger(num))
}),
map(postitive_integer_literal, |num| {
RValue::RVLiteral(OwnedLiteral::LPositiveInteger(num))
}),
@ -698,6 +739,13 @@ mod tests {
use nom::error::ErrorKind;
use nom::Err::Error;
#[test]
fn test_direct_literal() {
assert_eq!(super::float_literal("-17.4"), Ok(("", -17.4)));
assert_eq!(super::float_literal("17.1"), Ok(("", 17.1)));
assert_eq!(super::negative_integer_literal("-12"), Ok(("", -12)));
}
#[test]
fn test_reference() {
assert_eq!(
@ -1163,7 +1211,9 @@ mod tests {
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
]
})
@ -1189,7 +1239,9 @@ mod tests {
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
]
})
@ -1224,7 +1276,9 @@ mod tests {
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
]
})
@ -1252,7 +1306,9 @@ mod tests {
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
]
})
@ -1282,7 +1338,9 @@ mod tests {
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
]
})
@ -1293,7 +1351,7 @@ mod tests {
#[test]
fn test_literals() {
assert_eq!(
dust_tag(r#"{>foo a="foo" b=179/}"#),
dust_tag(r#"{>foo a="foo" b=179 c=17.1 d=-12 e=-17.4/}"#),
Ok((
"",
DustTag::DTPartial(Partial {
@ -1304,11 +1362,25 @@ mod tests {
params: vec![
KVPair {
key: "a",
value: RValue::RVLiteral(OwnedLiteral::LString("foo".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "foo".to_owned()
}])
},
KVPair {
key: "b",
value: RValue::RVLiteral(OwnedLiteral::LPositiveInteger(179))
},
KVPair {
key: "c",
value: RValue::RVLiteral(OwnedLiteral::LFloat(17.1))
},
KVPair {
key: "d",
value: RValue::RVLiteral(OwnedLiteral::LNegativeInteger(-12))
},
KVPair {
key: "e",
value: RValue::RVLiteral(OwnedLiteral::LFloat(-17.4))
}
]
})
@ -1332,7 +1404,9 @@ mod tests {
},
KVPair {
key: "value",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
],
contents: Some(Body {
@ -1369,7 +1443,9 @@ mod tests {
},
KVPair {
key: "value",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
],
contents: None,
@ -1397,7 +1473,9 @@ mod tests {
},
KVPair {
key: "value",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
],
contents: Some(Body {
@ -1436,7 +1514,9 @@ mod tests {
},
KVPair {
key: "value",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
value: RValue::RVTemplate(vec![PartialNameElement::PNSpan {
contents: "cat".to_owned()
}])
}
],
contents: None,
@ -1552,33 +1632,43 @@ mod tests {
params: vec![
KVPair {
key: "v1",
value: RValue::RVLiteral(
OwnedLiteral::LString("b".to_owned())
)
value: RValue::RVTemplate(vec![
PartialNameElement::PNSpan {
contents: "b".to_owned()
}
])
},
KVPair {
key: "v2",
value: RValue::RVLiteral(
OwnedLiteral::LString("b".to_owned())
)
value: RValue::RVTemplate(vec![
PartialNameElement::PNSpan {
contents: "b".to_owned()
}
])
},
KVPair {
key: "v3",
value: RValue::RVLiteral(
OwnedLiteral::LString("b".to_owned())
)
value: RValue::RVTemplate(vec![
PartialNameElement::PNSpan {
contents: "b".to_owned()
}
])
},
KVPair {
key: "v4",
value: RValue::RVLiteral(
OwnedLiteral::LString("b".to_owned())
)
value: RValue::RVTemplate(vec![
PartialNameElement::PNSpan {
contents: "b".to_owned()
}
])
},
KVPair {
key: "v5",
value: RValue::RVLiteral(
OwnedLiteral::LString("b".to_owned())
)
value: RValue::RVTemplate(vec![
PartialNameElement::PNSpan {
contents: "b".to_owned()
}
])
}
]
}

View File

@ -4,6 +4,11 @@ use crate::renderer::errors::RenderError;
use crate::renderer::errors::WalkError;
use crate::renderer::DustRenderer;
use std::any::Any;
use std::ops::Add;
use std::ops::Div;
use std::ops::Mul;
use std::ops::Rem;
use std::ops::Sub;
use std::rc::Rc;
use std::{cmp::Ordering, fmt::Debug};
@ -16,6 +21,7 @@ pub trait ContextElement:
+ CompareContextElement
+ FromContextElement
+ IntoRcIce
+ Castable
{
}
@ -53,6 +59,10 @@ pub trait Loopable {
fn get_loop_elements(&self) -> Vec<&dyn ContextElement>;
}
pub trait Castable {
fn cast_to_type<'a>(&'a self, target: &str) -> Option<IceResult<'a>>;
}
pub trait CastToAny {
fn to_any(&self) -> &dyn Any;
}
@ -61,6 +71,15 @@ pub trait CompareContextElement: CastToAny {
fn equals(&self, other: &dyn ContextElement) -> bool;
fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering>;
fn math_add<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>>;
fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>>;
fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>>;
fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>>;
fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>>;
fn math_abs<'a>(&self) -> Option<IceResult<'a>>;
fn math_floor<'a>(&self) -> Option<IceResult<'a>>;
fn math_ceil<'a>(&self) -> Option<IceResult<'a>>;
}
impl<C: 'static + ContextElement> CastToAny for C {
@ -81,6 +100,46 @@ impl<'a, 'b> PartialOrd<&'b dyn ContextElement> for &'a dyn ContextElement {
}
}
impl<'a> Add<&'a dyn ContextElement> for &'a dyn ContextElement {
type Output = Option<IceResult<'a>>;
fn add(self, other: &'a dyn ContextElement) -> Self::Output {
self.math_add(other)
}
}
impl<'a> Sub<&'a dyn ContextElement> for &'a dyn ContextElement {
type Output = Option<IceResult<'a>>;
fn sub(self, other: &'a dyn ContextElement) -> Self::Output {
self.math_subtract(other)
}
}
impl<'a> Mul<&'a dyn ContextElement> for &'a dyn ContextElement {
type Output = Option<IceResult<'a>>;
fn mul(self, other: &'a dyn ContextElement) -> Self::Output {
self.math_multiply(other)
}
}
impl<'a> Div<&'a dyn ContextElement> for &'a dyn ContextElement {
type Output = Option<IceResult<'a>>;
fn div(self, other: &'a dyn ContextElement) -> Self::Output {
self.math_divide(other)
}
}
impl<'a> Rem<&'a dyn ContextElement> for &'a dyn ContextElement {
type Output = Option<IceResult<'a>>;
fn rem(self, other: &'a dyn ContextElement) -> Self::Output {
self.math_modulus(other)
}
}
pub trait FromContextElement {
fn from_context_element(&self) -> &dyn IntoContextElement;
}
@ -91,7 +150,7 @@ impl<C: ContextElement> FromContextElement for C {
}
}
pub trait IntoContextElement: Debug + Walkable /* + CloneIntoBoxedContextElement*/ {
pub trait IntoContextElement: Debug + Walkable {
fn into_context_element<'a>(
&'a self,
renderer: &DustRenderer,

View File

@ -111,7 +111,8 @@ fn extract_inline_partials_from_tag<'a, 'b>(
| DustTag::DTHelperLast(parameterized_block)
| DustTag::DTHelperSelect(parameterized_block)
| DustTag::DTHelperAny(parameterized_block)
| DustTag::DTHelperNone(parameterized_block) => {
| DustTag::DTHelperNone(parameterized_block)
| DustTag::DTHelperMath(parameterized_block) => {
match &parameterized_block.contents {
None => (),
Some(body) => extract_inline_partials_from_body(blocks, &body),

187
src/renderer/math.rs Normal file
View File

@ -0,0 +1,187 @@
use crate::parser::OwnedLiteral;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::ops::Add;
use std::ops::Div;
use std::ops::Mul;
use std::ops::Rem;
use std::ops::Sub;
#[derive(Debug)]
pub enum MathNumber {
Integer(i128),
Decimal(f64),
Failure,
}
impl From<&OwnedLiteral> for MathNumber {
fn from(original: &OwnedLiteral) -> Self {
match original {
OwnedLiteral::LString(_) => panic!("Strings should not be cast to numbers for math"),
OwnedLiteral::LPositiveInteger(num) => {
return MathNumber::Integer((*num).try_into().unwrap())
}
OwnedLiteral::LNegativeInteger(num) => return MathNumber::Integer((*num).into()),
OwnedLiteral::LFloat(num) => return MathNumber::Decimal(*num),
}
}
}
impl Add<MathNumber> for MathNumber {
type Output = Option<OwnedLiteral>;
fn add(self, other: MathNumber) -> Self::Output {
match (self, other) {
(MathNumber::Failure, _) | (_, MathNumber::Failure) => None,
(MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => {
math_ints(self_num, other_num, std::ops::Add::add)
}
(MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat(self_num + other_num))
}
(MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat((self_num as f64) + other_num))
}
(MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => {
Some(OwnedLiteral::LFloat(self_num + (other_num as f64)))
}
}
}
}
impl Sub<MathNumber> for MathNumber {
type Output = Option<OwnedLiteral>;
fn sub(self, other: MathNumber) -> Self::Output {
match (self, other) {
(MathNumber::Failure, _) | (_, MathNumber::Failure) => None,
(MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => {
math_ints(self_num, other_num, std::ops::Sub::sub)
}
(MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat(self_num - other_num))
}
(MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat((self_num as f64) - other_num))
}
(MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => {
Some(OwnedLiteral::LFloat(self_num - (other_num as f64)))
}
}
}
}
impl Mul<MathNumber> for MathNumber {
type Output = Option<OwnedLiteral>;
fn mul(self, other: MathNumber) -> Self::Output {
match (self, other) {
(MathNumber::Failure, _) | (_, MathNumber::Failure) => None,
(MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => {
math_ints(self_num, other_num, std::ops::Mul::mul)
}
(MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat(self_num * other_num))
}
(MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat((self_num as f64) * other_num))
}
(MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => {
Some(OwnedLiteral::LFloat(self_num * (other_num as f64)))
}
}
}
}
impl Div<MathNumber> for MathNumber {
type Output = Option<OwnedLiteral>;
fn div(self, other: MathNumber) -> Self::Output {
match (self, other) {
(MathNumber::Failure, _) | (_, MathNumber::Failure) => None,
(MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => {
Some(OwnedLiteral::LFloat((self_num as f64) / (other_num as f64)))
}
(MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat(self_num / other_num))
}
(MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat((self_num as f64) / other_num))
}
(MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => {
Some(OwnedLiteral::LFloat(self_num / (other_num as f64)))
}
}
}
}
impl Rem<MathNumber> for MathNumber {
type Output = Option<OwnedLiteral>;
fn rem(self, other: MathNumber) -> Self::Output {
match (self, other) {
(MathNumber::Failure, _) | (_, MathNumber::Failure) => None,
(MathNumber::Integer(self_num), MathNumber::Integer(other_num)) => {
math_ints(self_num, other_num, std::ops::Rem::rem)
}
(MathNumber::Decimal(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat(self_num % other_num))
}
(MathNumber::Integer(self_num), MathNumber::Decimal(other_num)) => {
Some(OwnedLiteral::LFloat((self_num as f64) % other_num))
}
(MathNumber::Decimal(self_num), MathNumber::Integer(other_num)) => {
Some(OwnedLiteral::LFloat(self_num % (other_num as f64)))
}
}
}
}
impl MathNumber {
pub fn math_abs(&self) -> Option<OwnedLiteral> {
match self {
MathNumber::Failure => None,
MathNumber::Integer(num) => num.abs().try_into().ok(),
MathNumber::Decimal(num) => Some(OwnedLiteral::LFloat(num.abs())),
}
}
pub fn math_floor(&self) -> Option<OwnedLiteral> {
match self {
MathNumber::Failure => None,
MathNumber::Integer(num) => (*num).try_into().ok(),
MathNumber::Decimal(num) => Some(OwnedLiteral::LFloat(num.floor())),
}
}
pub fn math_ceil(&self) -> Option<OwnedLiteral> {
match self {
MathNumber::Failure => None,
MathNumber::Integer(num) => (*num).try_into().ok(),
MathNumber::Decimal(num) => Some(OwnedLiteral::LFloat(num.ceil())),
}
}
}
/// For math operations that take in integers and return integers
/// (add, subtract, multiply)
pub fn math_ints<L, R, F>(left: L, right: R, operation: F) -> Option<OwnedLiteral>
where
L: Into<i128>,
R: Into<i128>,
F: Fn(i128, i128) -> i128,
{
operation(left.into(), right.into()).try_into().ok()
}
impl TryFrom<i128> for OwnedLiteral {
type Error = &'static str;
fn try_from(original: i128) -> Result<Self, Self::Error> {
std::convert::TryInto::<u64>::try_into(original)
.map(OwnedLiteral::LPositiveInteger)
.ok()
.or(std::convert::TryInto::<i64>::try_into(original)
.map(OwnedLiteral::LNegativeInteger)
.ok())
.ok_or("Value does not fit into either u64 or i64")
}
}

View File

@ -5,13 +5,16 @@ mod context_element;
mod errors;
mod inline_partial_tree;
mod iteration_context;
mod math;
mod parameters_context;
mod renderer;
mod select_context;
mod walking;
pub use context_element::Castable;
pub use context_element::CompareContextElement;
pub use context_element::ContextElement;
pub use context_element::IceResult;
pub use context_element::IntoContextElement;
pub use context_element::Loopable;
pub use context_element::Renderable;
@ -20,6 +23,7 @@ pub use context_element::Walkable;
pub use errors::CompileError;
pub use errors::RenderError;
pub use errors::WalkError;
pub use math::MathNumber;
pub use renderer::compile_template;
pub use renderer::DustRenderer;
pub use select_context::SelectContext;

View File

@ -7,7 +7,9 @@ use crate::renderer::context_element::CompareContextElement;
use crate::renderer::context_element::ContextElement;
use crate::renderer::context_element::IceResult;
use crate::renderer::context_element::IntoContextElement;
use crate::renderer::math::MathNumber;
use crate::renderer::walking::walk_path;
use crate::renderer::Castable;
use crate::renderer::DustRenderer;
use crate::renderer::Loopable;
use crate::renderer::RenderError;
@ -67,6 +69,16 @@ impl<'a> ParametersContext<'a> {
}
}
pub fn from_values(
parent: Option<&'a ParametersContext<'a>>,
params: HashMap<&'a str, (&'a RValue<'a>, Option<BreadcrumbTreeElement<'a>>)>,
) -> Self {
ParametersContext {
parent: parent,
params: params,
}
}
pub fn contains_key(&self, segment: &str) -> bool {
self.params.contains_key(segment)
|| self
@ -74,6 +86,17 @@ impl<'a> ParametersContext<'a> {
.map(|p| p.contains_key(segment))
.unwrap_or(false)
}
pub fn get_original_rvalue(&self, segment: &str) -> Option<&'a RValue<'a>> {
self.params
.get(segment)
.map(|(rvalue, _bte)| *rvalue)
.or_else(|| {
self.parent
.map(|p| p.get_original_rvalue(segment))
.flatten()
})
}
}
impl<'a> IntoContextElement for ParametersContext<'a> {
@ -137,6 +160,8 @@ impl Truthiness for OwnedLiteral {
match self {
OwnedLiteral::LString(text) => !text.is_empty(),
OwnedLiteral::LPositiveInteger(_num) => true,
OwnedLiteral::LNegativeInteger(_num) => true,
OwnedLiteral::LFloat(_num) => true,
}
}
}
@ -146,6 +171,8 @@ impl Renderable for OwnedLiteral {
match self {
OwnedLiteral::LString(text) => Ok(text.clone()),
OwnedLiteral::LPositiveInteger(num) => Ok(num.to_string()),
OwnedLiteral::LNegativeInteger(num) => Ok(num.to_string()),
OwnedLiteral::LFloat(num) => Ok(num.to_string()),
}
}
}
@ -162,8 +189,32 @@ impl Walkable for OwnedLiteral {
}
}
impl Castable for OwnedLiteral {
fn cast_to_type<'a>(&'a self, target: &str) -> Option<IceResult<'a>> {
match (self, target) {
(OwnedLiteral::LString(text), "number") => text
.parse::<u64>()
.map(|num| IceResult::from_owned(OwnedLiteral::LPositiveInteger(num)))
.or_else(|_| {
text.parse::<i64>()
.map(|num| IceResult::from_owned(OwnedLiteral::LNegativeInteger(num)))
})
.or_else(|_| {
text.parse::<f64>()
.map(|num| IceResult::from_owned(OwnedLiteral::LFloat(num)))
})
.ok(),
(OwnedLiteral::LPositiveInteger(_), "number") => Some(IceResult::from_borrowed(self)),
(OwnedLiteral::LNegativeInteger(_), "number") => Some(IceResult::from_borrowed(self)),
(OwnedLiteral::LFloat(_), "number") => Some(IceResult::from_borrowed(self)),
(_, _) => panic!("Unimplemented cast"),
}
}
}
impl CompareContextElement for OwnedLiteral {
fn equals(&self, other: &dyn ContextElement) -> bool {
// println!("Literal equality check {:?} == {:?}", self, other);
// If its an OwnedLiteral then compare them directly,
// otherwise defer to the other type's implementation of
// CompareContextElement since the end user could add any
@ -174,16 +225,52 @@ impl CompareContextElement for OwnedLiteral {
(OwnedLiteral::LString(self_text), OwnedLiteral::LString(other_text)) => {
self_text == other_text
}
(OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LString(other_text)) => {
&self_num.to_string() == other_text
}
(OwnedLiteral::LString(self_text), OwnedLiteral::LPositiveInteger(other_num)) => {
self_text == &other_num.to_string()
}
(
OwnedLiteral::LPositiveInteger(self_num),
OwnedLiteral::LPositiveInteger(other_num),
) => self_num == other_num,
(
OwnedLiteral::LNegativeInteger(self_num),
OwnedLiteral::LNegativeInteger(other_num),
) => self_num == other_num,
(OwnedLiteral::LString(_self_text), _) | (_, OwnedLiteral::LString(_self_text)) => {
false
}
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LFloat(other_num)) => {
self_num == other_num
}
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LPositiveInteger(other_num)) => {
*self_num == (*other_num as f64)
}
(OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LFloat(other_num)) => {
(*self_num as f64) == *other_num
}
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LNegativeInteger(other_num)) => {
*self_num == (*other_num as f64)
}
(OwnedLiteral::LNegativeInteger(self_num), OwnedLiteral::LFloat(other_num)) => {
(*self_num as f64) == *other_num
}
(
OwnedLiteral::LPositiveInteger(self_num),
OwnedLiteral::LNegativeInteger(other_num),
) => {
if *self_num < std::i64::MAX as u64 {
(*self_num as i64) == *other_num
} else {
false
}
}
(
OwnedLiteral::LNegativeInteger(self_num),
OwnedLiteral::LPositiveInteger(other_num),
) => {
if *other_num < std::i64::MAX as u64 {
*self_num == (*other_num as i64)
} else {
false
}
}
},
}
}
@ -206,17 +293,166 @@ impl CompareContextElement for OwnedLiteral {
(OwnedLiteral::LString(self_text), OwnedLiteral::LString(other_text)) => {
self_text.partial_cmp(other_text)
}
(
OwnedLiteral::LPositiveInteger(self_num),
OwnedLiteral::LPositiveInteger(other_num),
) => self_num.partial_cmp(other_num),
(
OwnedLiteral::LNegativeInteger(self_num),
OwnedLiteral::LNegativeInteger(other_num),
) => self_num.partial_cmp(other_num),
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LFloat(other_num)) => {
self_num.partial_cmp(other_num)
}
(OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LString(other_text)) => {
self_num.to_string().partial_cmp(other_text)
}
(OwnedLiteral::LString(self_text), OwnedLiteral::LPositiveInteger(other_num)) => {
self_text.partial_cmp(&other_num.to_string())
}
(OwnedLiteral::LNegativeInteger(self_num), OwnedLiteral::LString(other_text)) => {
self_num.to_string().partial_cmp(other_text)
}
(OwnedLiteral::LString(self_text), OwnedLiteral::LNegativeInteger(other_num)) => {
self_text.partial_cmp(&other_num.to_string())
}
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LString(other_text)) => {
self_num.to_string().partial_cmp(other_text)
}
(OwnedLiteral::LString(self_text), OwnedLiteral::LFloat(other_num)) => {
self_text.partial_cmp(&other_num.to_string())
}
(OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LFloat(other_num)) => {
(*self_num as f64).partial_cmp(other_num)
}
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LPositiveInteger(other_num)) => {
self_num.partial_cmp(&(*other_num as f64))
}
(OwnedLiteral::LNegativeInteger(self_num), OwnedLiteral::LFloat(other_num)) => {
(*self_num as f64).partial_cmp(other_num)
}
(OwnedLiteral::LFloat(self_num), OwnedLiteral::LNegativeInteger(other_num)) => {
self_num.partial_cmp(&(*other_num as f64))
}
(
OwnedLiteral::LPositiveInteger(self_num),
OwnedLiteral::LNegativeInteger(other_num),
) => {
if *self_num < std::i64::MAX as u64 {
(*self_num as i64).partial_cmp(other_num)
} else {
Some(Ordering::Greater)
}
}
(
OwnedLiteral::LNegativeInteger(self_num),
OwnedLiteral::LPositiveInteger(other_num),
) => self_num.partial_cmp(other_num),
) => {
if *other_num < std::i64::MAX as u64 {
self_num.partial_cmp(&(*other_num as i64))
} else {
Some(Ordering::Less)
}
}
},
}
}
fn math_add<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
// If its an OwnedLiteral then add them directly, otherwise
// defer to the other type's implementation of
// CompareContextElement since the end user could add any
// type.
match other.to_any().downcast_ref::<Self>() {
None => other.math_add(self),
Some(other_literal) => match (self, other_literal) {
(OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None,
(_, _) => (std::convert::Into::<MathNumber>::into(self)
+ std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
},
}
}
fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
// If its an OwnedLiteral then subtract them directly,
// otherwise defer to the other type's implementation of
// CompareContextElement since the end user could add any
// type.
match other.to_any().downcast_ref::<Self>() {
None => other.math_subtract(self),
Some(other_literal) => match (self, other_literal) {
(OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None,
(_, _) => (std::convert::Into::<MathNumber>::into(self)
- std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
},
}
}
fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
// If its an OwnedLiteral then multiply them directly,
// otherwise defer to the other type's implementation of
// CompareContextElement since the end user could add any
// type.
match other.to_any().downcast_ref::<Self>() {
None => other.math_multiply(self),
Some(other_literal) => match (self, other_literal) {
(OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None,
(_, _) => (std::convert::Into::<MathNumber>::into(self)
* std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
},
}
}
fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
// If its an OwnedLiteral then divide them directly, otherwise
// defer to the other type's implementation of
// CompareContextElement since the end user could add any
// type.
match other.to_any().downcast_ref::<Self>() {
None => other.math_divide(self),
Some(other_literal) => match (self, other_literal) {
(OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None,
(_, _) => (std::convert::Into::<MathNumber>::into(self)
/ std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
},
}
}
fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option<IceResult<'a>> {
// If its an OwnedLiteral then modulus them directly,
// otherwise defer to the other type's implementation of
// CompareContextElement since the end user could add any
// type.
match other.to_any().downcast_ref::<Self>() {
None => other.math_modulus(self),
Some(other_literal) => match (self, other_literal) {
(OwnedLiteral::LString(_), _) | (_, OwnedLiteral::LString(_)) => None,
(_, _) => (std::convert::Into::<MathNumber>::into(self)
% std::convert::Into::<MathNumber>::into(other_literal))
.map(IceResult::from_owned),
},
}
}
fn math_abs<'a>(&self) -> Option<IceResult<'a>> {
std::convert::Into::<MathNumber>::into(self)
.math_abs()
.map(IceResult::from_owned)
}
fn math_floor<'a>(&self) -> Option<IceResult<'a>> {
std::convert::Into::<MathNumber>::into(self)
.math_floor()
.map(IceResult::from_owned)
}
fn math_ceil<'a>(&self) -> Option<IceResult<'a>> {
std::convert::Into::<MathNumber>::into(self)
.math_ceil()
.map(IceResult::from_owned)
}
}

View File

@ -5,6 +5,7 @@ use crate::parser::Filter;
use crate::parser::OwnedLiteral;
use crate::parser::PartialNameElement;
use crate::parser::Path;
use crate::parser::RValue;
use crate::parser::Special;
use crate::parser::Template;
use crate::parser::TemplateElement;
@ -644,6 +645,83 @@ impl<'a> DustRenderer<'a> {
}
_ => return Ok("".to_owned()),
},
DustTag::DTHelperMath(parameterized_block) => {
let new_breadcrumbs = self.new_breadcrumbs_partial(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
);
let new_breadcrumbs_ref = new_breadcrumbs.as_ref().unwrap_or(breadcrumbs);
let param_map =
ParametersContext::new(self, breadcrumbs, &parameterized_block.params, None);
match &parameterized_block.contents {
None => {
return self
.perform_math_operation(breadcrumbs, &param_map)
.map(|final_val| {
final_val
.get_context_element_reference()
.render(&Vec::new())
})
.unwrap_or(Ok("".to_owned()));
}
Some(body) => {
// Calculate the value
let calculated_value =
match self.perform_math_operation(breadcrumbs, &param_map) {
None => return Ok("".to_owned()),
Some(val) => val,
};
// Generate a ParametersContext with the result of the math operation as key
let converted_value: BreadcrumbTreeElement<'_> = calculated_value.into();
let dummy_rvalue = RValue::RVLiteral(OwnedLiteral::LPositiveInteger(0));
let calculated_param_map: HashMap<
&str,
(&RValue<'_>, Option<BreadcrumbTreeElement<'_>>),
> = vec![("key", (&dummy_rvalue, Some(converted_value)))]
.into_iter()
.collect();
let calculated_context =
ParametersContext::from_values(None, calculated_param_map);
// calculate are_any_checks_true
let are_any_checks_true = body
.elements
.iter()
.filter_map(|te| match te {
TemplateElement::TETag(dt) => match dt {
DustTag::DTHelperEquals(_)
| DustTag::DTHelperNotEquals(_)
| DustTag::DTHelperGreaterThan(_)
| DustTag::DTHelperLessThan(_)
| DustTag::DTHelperGreaterThanOrEquals(_)
| DustTag::DTHelperLessThanOrEquals(_) => Some(dt),
_ => None,
},
_ => None,
})
.map(|dt| {
self.perform_comparison_check(
dt,
new_breadcrumbs_ref,
Some(&calculated_context),
)
})
.any(|check_result| check_result.unwrap_or(false));
// Generate a SelectContext
let select_context =
SelectContext::new(&calculated_context, are_any_checks_true);
// render_maybe_body
return self.render_maybe_body(
&parameterized_block.contents,
new_breadcrumbs_ref,
blocks,
&mut Some(select_context),
);
}
}
}
}
Ok("".to_owned())
@ -901,6 +979,130 @@ impl<'a> DustRenderer<'a> {
_ => panic!("perform_comparison_check only implemented for comparison helpers (eq, ne, gt, gte, lt, lte)")
}
}
/// Performs a math operation (add, subtract, multiply, divide,
/// mod, abs, floor, ceil) and returns the result or None if
/// nothing should be rendered.
fn perform_math_operation(
&'a self,
breadcrumbs: &'a Vec<BreadcrumbTreeElement<'a>>,
math_parameters: &'a ParametersContext<'a>,
) -> Option<IceResult<'a>> {
// Special case: if method is a template then do not render
// anything. This is to match the behavior of dustjs even
// though it works fine.
match math_parameters.get_original_rvalue("method") {
Some(RValue::RVTemplate(template)) => {
if template.iter().any(|pne| match pne {
PartialNameElement::PNReference { .. } => true,
PartialNameElement::PNSpan { .. } => false,
}) {
return None;
}
}
_ => (),
}
let method = match self.tap(breadcrumbs, math_parameters, "method") {
None | Some(Err(_)) => return None,
Some(Ok(ice_result)) => ice_result,
};
let method_rendered = match method.get_context_element_reference().render(&Vec::new()) {
Ok(text) => text,
Err(_) => return None,
};
let left_side = self.tap(breadcrumbs, math_parameters, "key");
let right_side = self.tap(breadcrumbs, math_parameters, "operand");
let left_side_ce = left_side.as_ref().map(|maybe_ice| {
maybe_ice
.as_ref()
.map(|ice| ice.get_context_element_reference())
.map(|ce| ce.cast_to_type("number"))
});
let right_side_ce = right_side.as_ref().map(|maybe_ice| {
maybe_ice
.as_ref()
.map(|ice| ice.get_context_element_reference())
.map(|ce| ce.cast_to_type("number"))
});
// println!(
// "Doing {:?} {:?} {:?}",
// left_side, method_rendered, right_side
// );
return match method_rendered.as_str() {
"add" => match (left_side_ce, right_side_ce) {
(None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None,
(Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned(
OwnedLiteral::LString("NaN".to_owned()),
)),
(Some(Ok(Some(l))), Some(Ok(Some(r)))) => l
.get_context_element_reference()
.math_add(r.get_context_element_reference()),
},
"subtract" => match (left_side_ce, right_side_ce) {
(None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None,
(Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned(
OwnedLiteral::LString("NaN".to_owned()),
)),
(Some(Ok(Some(l))), Some(Ok(Some(r)))) => l
.get_context_element_reference()
.math_subtract(r.get_context_element_reference()),
},
"multiply" => match (left_side_ce, right_side_ce) {
(None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None,
(Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned(
OwnedLiteral::LString("NaN".to_owned()),
)),
(Some(Ok(Some(l))), Some(Ok(Some(r)))) => l
.get_context_element_reference()
.math_multiply(r.get_context_element_reference()),
},
"divide" => match (left_side_ce, right_side_ce) {
(None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None,
(Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned(
OwnedLiteral::LString("NaN".to_owned()),
)),
(Some(Ok(Some(l))), Some(Ok(Some(r)))) => l
.get_context_element_reference()
.math_divide(r.get_context_element_reference()),
},
"mod" => match (left_side_ce, right_side_ce) {
(None, _) | (Some(Err(_)), _) | (_, None) | (_, Some(Err(_))) => None,
(Some(Ok(None)), _) | (_, Some(Ok(None))) => Some(IceResult::from_owned(
OwnedLiteral::LString("NaN".to_owned()),
)),
(Some(Ok(Some(l))), Some(Ok(Some(r)))) => l
.get_context_element_reference()
.math_modulus(r.get_context_element_reference()),
},
"abs" => match left_side_ce {
None | Some(Err(_)) => None,
Some(Ok(None)) => Some(IceResult::from_owned(OwnedLiteral::LString(
"NaN".to_owned(),
))),
Some(Ok(Some(num))) => num.get_context_element_reference().math_abs(),
},
"floor" => match left_side_ce {
None | Some(Err(_)) => None,
Some(Ok(None)) => Some(IceResult::from_owned(OwnedLiteral::LString(
"NaN".to_owned(),
))),
Some(Ok(Some(num))) => num.get_context_element_reference().math_floor(),
},
"ceil" => match left_side_ce {
None | Some(Err(_)) => None,
Some(Ok(None)) => Some(IceResult::from_owned(OwnedLiteral::LString(
"NaN".to_owned(),
))),
Some(Ok(Some(num))) => num.get_context_element_reference().math_ceil(),
},
_ => None,
};
}
}
struct BlockContext<'a> {