]> git.sesse.net Git - casparcg/blobdiff - core/producer/scene/expression_parser.cpp
[scene] #563 Added some string functions to expression language
[casparcg] / core / producer / scene / expression_parser.cpp
index 3d6c120966b7099fddb733c98d9628b089cccd82..27156db264b63574428e00283938487224ece864 100644 (file)
@@ -19,7 +19,7 @@
 * Author: Helge Norberg, helge.norberg@svt.se
 */
 
-#include "../../stdafx.h"
+#include "../../StdAfx.h"
 
 #include "expression_parser.h"
 
 #include <functional>
 #include <typeinfo>
 #include <cstdint>
+#include <cmath>
 
 #include <boost/any.hpp>
+#include <boost/locale.hpp>
 
 #include <common/log.h>
 #include <common/except.h>
 #include <common/utf.h>
+#include <common/tweener.h>
 
 namespace caspar { namespace core { namespace scene {
 
@@ -56,7 +59,7 @@ wchar_t next_non_whitespace(
                }
        }
 
-       CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+       CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                        L"Unexpected end of input (" + error_if_eof + L") in " + str));
 }
 
@@ -69,6 +72,21 @@ std::wstring at_position(
                        + L" in " + str;
 }
 
+boost::any as_binding(const boost::any& value);
+
+template<typename T>
+binding<T> require(const boost::any& value)
+{
+       auto b = as_binding(value);
+
+       if (is<binding<T>>(b))
+               return as<binding<T>>(b);
+       else
+               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
+                       L"Required binding of type " + u16(typeid(T).name())
+                       + L" but got " + u16(value.type().name())));
+}
+
 boost::any parse_expression(
                std::wstring::const_iterator& cursor,
                const std::wstring& str,
@@ -80,18 +98,176 @@ boost::any parse_parenthesis(
                const variable_repository& var_repo)
 {
        if (*cursor++ != L'(')
-               CASPAR_THROW_EXCEPTION(caspar_exception()
+               CASPAR_THROW_EXCEPTION(user_error()
                                << msg_info(L"Expected (" + at_position(cursor, str)));
 
        auto expr = parse_expression(cursor, str, var_repo);
 
-       if (*cursor++ != L')')
-               CASPAR_THROW_EXCEPTION(caspar_exception()
+       if (next_non_whitespace(cursor, str, L"Expected )") != L')')
+               CASPAR_THROW_EXCEPTION(user_error()
                                << msg_info(L"Expected )" + at_position(cursor, str)));
 
+       ++cursor;
+
        return expr;
 }
 
+boost::any create_animate_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 3)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"animate() function requires three parameters: to_animate, duration, tweener"));
+
+       auto to_animate         = require<double>(params.at(0));
+       auto frame_counter      = var_repo(L"frame").as<double>();
+       auto duration           = require<double>(params.at(1));
+       auto tw                         = require<std::wstring>(params.at(2)).transformed([](const std::wstring& s) { return tweener(s); });
+
+       return to_animate.animated(frame_counter, duration, tw);
+}
+
+boost::any create_sin_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"sin() function requires one parameters: angle"));
+
+       auto angle = require<double>(params.at(0));
+
+       return angle.transformed([](double a) { return std::sin(a); });
+}
+
+boost::any create_cos_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"cos() function requires one parameters: angle"));
+
+       auto angle = require<double>(params.at(0));
+
+       return angle.transformed([](double a) { return std::cos(a); });
+}
+
+boost::any create_abs_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"abs() function requires one parameters: value"));
+
+       auto val = require<double>(params.at(0));
+
+       return val.transformed([](double v) { return std::abs(v); });
+}
+
+boost::any create_floor_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"floor() function requires one parameters: value"));
+
+       auto val = require<double>(params.at(0));
+
+       return val.transformed([](double v) { return std::floor(v); });
+}
+
+std::locale create_utf_locale()
+{
+       boost::locale::generator gen;
+       gen.categories(boost::locale::codepage_facet);
+       gen.categories(boost::locale::convert_facet);
+
+       return gen("");
+}
+
+boost::any create_to_lower_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"to_lower() function requires one parameters: str"));
+
+       auto str        = require<std::wstring>(params.at(0));
+       auto locale     = create_utf_locale();
+
+       return str.transformed([=](std::wstring v) { return boost::locale::to_lower(v, locale); });
+}
+
+boost::any create_to_upper_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"to_upper() function requires one parameters: str"));
+
+       auto str        = require<std::wstring>(params.at(0));
+       auto locale     = create_utf_locale();
+
+       return str.transformed([=](std::wstring v) { return boost::locale::to_upper(v, locale); });
+}
+
+boost::any create_length_function(const std::vector<boost::any>& params, const variable_repository& var_repo)
+{
+       if (params.size() != 1)
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"length() function requires one parameters: str"));
+
+       auto str = require<std::wstring>(params.at(0));
+
+       return str.transformed([](std::wstring v) { return static_cast<double>(v.length()); });
+}
+
+boost::any parse_function(
+               const std::wstring& function_name,
+               std::wstring::const_iterator& cursor,
+               const std::wstring& str,
+               const variable_repository& var_repo)
+{
+       static std::map<std::wstring, std::function<boost::any (const std::vector<boost::any>& params, const variable_repository& var_repo)>> FUNCTIONS
+       {
+               { L"animate",   create_animate_function },
+               { L"sin",               create_sin_function },
+               { L"cos",               create_cos_function },
+               { L"abs",               create_abs_function },
+               { L"floor",             create_floor_function },
+               { L"to_lower",  create_to_lower_function },
+               { L"to_upper",  create_to_upper_function },
+               { L"length",    create_length_function }
+       };
+
+       auto function = FUNCTIONS.find(function_name);
+
+       if (function == FUNCTIONS.end())
+               CASPAR_THROW_EXCEPTION(user_error()
+                               << msg_info(function_name + L"() is an unknown function" + at_position(cursor, str)));
+
+       if (*cursor++ != L'(')
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"Expected (" + at_position(cursor, str)));
+
+       std::vector<boost::any> params;
+
+       while (cursor != str.end())
+       {
+               params.push_back(parse_expression(cursor, str, var_repo));
+
+               auto next = next_non_whitespace(cursor, str, L"Expected , or )");
+
+               if (next == L')')
+                       break;
+               else if (next != L',')
+                       CASPAR_THROW_EXCEPTION(user_error()
+                               << msg_info(L"Expected ) or ," + at_position(cursor, str)));
+
+               ++cursor;
+       }
+
+       if (next_non_whitespace(cursor, str, L"Expected , or )") != L')')
+               CASPAR_THROW_EXCEPTION(user_error()
+                       << msg_info(L"Expected ) " + at_position(cursor, str)));
+
+       ++cursor;
+
+       return function->second(params, var_repo);
+}
+
 double parse_constant(
                std::wstring::const_iterator& cursor, const std::wstring& str)
 {
@@ -118,7 +294,7 @@ std::wstring parse_string_literal(
        std::wstring literal;
 
        if (*cursor++ != L'"')
-               CASPAR_THROW_EXCEPTION(caspar_exception()
+               CASPAR_THROW_EXCEPTION(user_error()
                                << msg_info(L"Expected (" + at_position(cursor, str)));
 
        bool escaping = false;
@@ -166,7 +342,7 @@ std::wstring parse_string_literal(
                ++cursor;
        }
 
-       CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+       CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                        L"Unexpected end of input (Expected closing \") in " + str));
 }
 
@@ -184,7 +360,8 @@ boost::any parse_variable(
                if (ch == L'.'
                                || ch == L'_'
                                || (ch >= L'a' && ch <= L'z')
-                               || (ch >= L'A' && ch <= L'Z'))
+                               || (ch >= L'A' && ch <= L'Z')
+                               || (variable_name.length() > 0 && ch >= L'0' && ch <= L'9'))
                        variable_name += ch;
                else
                        break;
@@ -192,6 +369,9 @@ boost::any parse_variable(
                ++cursor;
        }
 
+       if (cursor != str.end() && *cursor == L'(')
+               return variable_name;
+
        if (variable_name == L"true")
                return true;
        else if (variable_name == L"false")
@@ -208,14 +388,14 @@ boost::any parse_variable(
        else if (var.is<bool>())
                return var.as<bool>();
 
-       CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+       CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                L"Unhandled variable type of " + variable_name
                                + at_position(cursor, str)));
 }
 
 struct op
 {
-       enum op_type
+       enum class op_type
        {
                UNARY,
                BINARY,
@@ -250,19 +430,19 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                {
                case L'+':
                        ++cursor;
-                       return op(ch, 6, op::BINARY);
+                       return op(ch, 6, op::op_type::BINARY);
                case L'*':
                case L'/':
                case L'%':
                        ++cursor;
-                       return op(ch, 5, op::BINARY);
+                       return op(ch, 5, op::op_type::BINARY);
                case L'?':
                case L':':
                        ++cursor;
-                       return op(ch, 15, op::TERNARY);
+                       return op(ch, 15, op::op_type::TERNARY);
                case L'-':
                        if (first == L'-')
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect -" + at_position(cursor, str)));
                        else
                                first = ch;
@@ -271,7 +451,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                        break;
                case L'!':
                        if (first == L'!')
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect !" + at_position(cursor, str)));
                        else
                                first = ch;
@@ -280,7 +460,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                        break;
                case L'<':
                        if (first == L'<')
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect <" + at_position(cursor, str)));
                        else
                                first = ch;
@@ -289,7 +469,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                        break;
                case L'>':
                        if (first == L'>')
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect >" + at_position(cursor, str)));
                        else
                                first = ch;
@@ -300,22 +480,22 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                        if (first == L'=')
                        {
                                ++cursor;
-                               return op(L"==", 9, op::BINARY);
+                               return op(L"==", 9, op::op_type::BINARY);
                        }
                        else if (first == L'!')
                        {
                                ++cursor;
-                               return op(L"!=", 9, op::BINARY);
+                               return op(L"!=", 9, op::op_type::BINARY);
                        }
                        else if (first == L'>')
                        {
                                ++cursor;
-                               return op(L">=", 8, op::BINARY);
+                               return op(L">=", 8, op::op_type::BINARY);
                        }
                        else if (first == L'<')
                        {
                                ++cursor;
-                               return op(L"<=", 8, op::BINARY);
+                               return op(L"<=", 8, op::op_type::BINARY);
                        }
                        else if (first == NONE)
                        {
@@ -323,7 +503,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                                first = L'=';
                        }
                        else
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect =" + at_position(cursor, str)));
 
                        break;
@@ -331,7 +511,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                        if (first == L'|')
                        {
                                ++cursor;
-                               return op(L"||", 14, op::BINARY);
+                               return op(L"||", 14, op::op_type::BINARY);
                        }
                        else if (first == NONE)
                        {
@@ -339,7 +519,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                                first = L'|';
                        }
                        else
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect =" + at_position(cursor, str)));
 
                        break;
@@ -347,7 +527,7 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                        if (first == L'&')
                        {
                                ++cursor;
-                               return op(L"&&", 13, op::BINARY);
+                               return op(L"&&", 13, op::op_type::BINARY);
                        }
                        else if (first == NONE)
                        {
@@ -355,42 +535,36 @@ op parse_operator(std::wstring::const_iterator& cursor, const std::wstring& str)
                                first = L'&';
                        }
                        else
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Did not expect =" + at_position(cursor, str)));
 
                        break;
                case L' ':
                case L'\t':
                        if (first == L'-')
-                               return op(L'-', 6, op::BINARY);
+                               return op(L'-', 6, op::op_type::BINARY);
                        else if (first == L'!')
-                               return op(L'!', 3, op::UNARY);
+                               return op(L'!', 3, op::op_type::UNARY);
                default:
                        if (first == L'<')
-                               return op(L'<', 8, op::BINARY);
+                               return op(L'<', 8, op::op_type::BINARY);
                        else if (first == L'>')
-                               return op(L'>', 8, op::BINARY);
+                               return op(L'>', 8, op::op_type::BINARY);
                        else if (first == L'-')
-                               return op(L"unary-", 3, op::UNARY);
+                               return op(L"unary-", 3, op::op_type::UNARY);
                        else if (first == L'!')
-                               return op(L'!', 3, op::UNARY);
+                               return op(L'!', 3, op::op_type::UNARY);
                        else
-                               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                L"Expected second character of operator"
                                                + at_position(cursor, str)));
                }
        }
 
-       CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+       CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                        L"Unexpected end of input (Expected operator) in " + str));
 }
 
-template<typename T>
-T as(const boost::any& value)
-{
-       return boost::any_cast<T>(value);
-}
-
 boost::any as_binding(const boost::any& value)
 {
        // Wrap supported constants as bindings
@@ -408,29 +582,16 @@ boost::any as_binding(const boost::any& value)
        else if (is<binding<std::wstring>>(value))
                return as<binding<std::wstring>>(value);
        else
-               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+               CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                L"Couldn't detect type of " + u16(value.type().name())));
 }
 
-template<typename T>
-binding<T> require(const boost::any& value)
-{
-       auto b = as_binding(value);
-
-       if (is<binding<T>>(b))
-               return as<binding<T>>(b);
-       else
-               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
-                               L"Required binding of type " + u16(typeid(T).name())
-                               + L" but got " + u16(value.type().name())));
-}
-
 boost::any negative(const boost::any& to_create_negative_of)
 {
        return -require<double>(to_create_negative_of);
 }
 
-boost::any not(const boost::any& to_create_not_of)
+boost::any not_(const boost::any& to_create_not_of)
 {
        return !require<bool>(to_create_not_of);
 }
@@ -465,7 +626,7 @@ binding<std::wstring> stringify(const boost::any& value)
        else if (is<binding<bool>>(b))
                return as<binding<bool>>(b).as<std::wstring>();
        else
-               CASPAR_THROW_EXCEPTION(caspar_exception()
+               CASPAR_THROW_EXCEPTION(user_error()
                                << msg_info(L"Couldn't stringify " + u16(value.type().name())));
 }
 
@@ -526,12 +687,12 @@ boost::any equal(const boost::any& lhs, boost::any& rhs)
                return require<bool>(l) == require<bool>(r);
 }
 
-boost::any and(const boost::any& lhs, boost::any& rhs)
+boost::any and_(const boost::any& lhs, boost::any& rhs)
 {
        return require<bool>(lhs) && require<bool>(rhs);
 }
 
-boost::any or(const boost::any& lhs, boost::any& rhs)
+boost::any or_(const boost::any& lhs, boost::any& rhs)
 {
        return require<bool>(lhs) || require<bool>(rhs);
 }
@@ -553,7 +714,7 @@ boost::any ternary(
        auto cond = require<bool>(condition);
        auto t = as_binding(true_value);
        auto f = as_binding(false_value);
-       
+
        // double
        if (is<binding<double>>(t) && is<binding<double>>(f))
                return ternary(cond, as<binding<double>>(t), as<binding<double>>(f));
@@ -587,20 +748,20 @@ void resolve_operators(int precedence, std::vector<boost::any>& tokens)
 
                switch (op_token.type)
                {
-               case op::UNARY:
+               case op::op_type::UNARY:
                        if (op_token.characters == L"unary-")
                        {
                                tokens.at(i) = negative(token_after);
                        }
                        else if (op_token.characters == L"!")
                        {
-                               tokens.at(i) = not(token_after);
+                               tokens.at(i) = not_(token_after);
                        }
 
                        tokens.erase(tokens.begin() + index_after);
 
                        break;
-               case op::BINARY:
+               case op::op_type::BINARY:
                        {
                                auto& token_before = tokens.at(i - 1);
 
@@ -625,25 +786,25 @@ void resolve_operators(int precedence, std::vector<boost::any>& tokens)
                                else if (op_token.characters == L"==")
                                        token_before = equal(token_before, token_after);
                                else if (op_token.characters == L"!=")
-                                       token_before = not(equal(token_before, token_after));
+                                       token_before = not_(equal(token_before, token_after));
                                else if (op_token.characters == L"&&")
-                                       token_before = and(token_before, token_after);
+                                       token_before = and_(token_before, token_after);
                                else if (op_token.characters == L"||")
-                                       token_before = or(token_before, token_after);
+                                       token_before = or_(token_before, token_after);
                        }
 
                        tokens.erase(tokens.begin() + i, tokens.begin() + i + 2);
                        --i;
 
                        break;
-               case op::TERNARY:
+               case op::op_type::TERNARY:
                        if (op_token.characters == L"?")
                        {
                                auto& token_before = tokens.at(i - 1);
                                auto& token_colon_operator = tokens.at(i + 2);
 
                                if (as<op>(token_colon_operator).characters != L":")
-                                       CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
+                                       CASPAR_THROW_EXCEPTION(user_error() << msg_info(
                                                        L"Expected : as part of ternary expression"));
 
                                auto& token_false_value = tokens.at(i + 3);
@@ -703,9 +864,17 @@ boost::any parse_expression(
                        tokens.push_back(parse_string_literal(cursor, str));
                        break;
                case L'(':
-                       tokens.push_back(parse_parenthesis(cursor, str, var_repo));
+                       if (!tokens.empty() && is<std::wstring>(tokens.back()))
+                       {
+                               auto function_name = as<std::wstring>(tokens.back());
+                               tokens.pop_back();
+                               tokens.push_back(parse_function(function_name, cursor, str, var_repo));
+                       }
+                       else
+                               tokens.push_back(parse_parenthesis(cursor, str, var_repo));
                        break;
                case L')':
+               case L',':
                        stop = true;
                        break;
                default:
@@ -718,8 +887,8 @@ boost::any parse_expression(
        }
 
        if (tokens.empty())
-               CASPAR_THROW_EXCEPTION(caspar_exception()
-                               << msg_info(L"Expected expression"));
+               CASPAR_THROW_EXCEPTION(user_error()
+                               << msg_info(L"Expected expression" + at_position(cursor, str)));
 
        int precedence = 1;
 
@@ -731,21 +900,4 @@ boost::any parse_expression(
        return as_binding(tokens.at(0));
 }
 
-template<>
-binding<std::wstring> parse_expression(
-               const std::wstring& str, const variable_repository& var_repo)
-{
-       auto cursor = str.cbegin();
-       auto expr = parse_expression(cursor, str, var_repo);
-
-       if (is<binding<std::wstring>>(expr))
-               return as<binding<std::wstring>>(expr);
-       else if (is<binding<double>>(expr))
-               return as<binding<double>>(expr).as<std::wstring>();
-       else
-               CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(
-                               L"parse_expression() Unsupported type "
-                               + u16(expr.type().name())));
-}
-
 }}}