Commit 6ff683eb by Aaron Leung

Pushing the refactor through the eval/apply functions.

parent bd841192
#include "prelexer.hpp"
#include "eval_apply.hpp"
#include "error.hpp"
#include <iostream>
#include <sstream>
#include <cstdlib>
namespace Sass {
using std::cerr; using std::endl;
static void throw_eval_error(string message, size_t line, const char* path)
{
string fn;
if (path) {
const char* end = Prelexer::string_constant(path);
if (end) fn = path.substr(1, path.size() - 1);
else fn = path;
}
throw Error(Error::evaluation, line, fn, message);
}
static void throw_eval_error(string message, string path, size_t line)
{
if (!path.empty() && Prelexer::string_constant(path.c_str()))
......@@ -27,7 +18,7 @@ namespace Sass {
Node eval(Node& expr, Environment& env, map<pair<string, size_t>, Function>& f_env, Node_Factory& new_Node)
{
switch (expr.type)
switch (expr.type())
{
case Node::mixin: {
env[expr[0].token()] = expr;
......@@ -37,46 +28,46 @@ namespace Sass {
case Node::expansion: {
Token name(expr[0].token());
Node args(expr[1]);
if (!env.query(name)) throw_eval_error("mixin " + name.to_string() + " is undefined", expr.line(), expr.path());
if (!env.query(name)) throw_eval_error("mixin " + name.to_string() + " is undefined", expr.path(), expr.line());
Node mixin(env[name]);
Node expansion(apply_mixin(mixin, args, env, f_env, registry));
expr.content.children->pop_back();
expr.content.children->pop_back();
Node expansion(apply_mixin(mixin, args, env, f_env, new_Node));
expr.pop_back();
expr.pop_back();
expr += expansion;
return expr;
} break;
case Node::propset:
case Node::ruleset: {
eval(expr[1], env, f_env, registry);
eval(expr[1], env, f_env, new_Node);
return expr;
} break;
case Node::root: {
for (size_t i = 0; i < expr.size(); ++i) {
eval(expr[i], env, f_env, registry);
for (size_t i = 0, S = expr.size(); i < S; ++i) {
eval(expr[i], env, f_env, new_Node);
}
return expr;
} break;
case Node::block: {
Environment current;
current.link(env);
for (size_t i = 0; i < expr.size(); ++i) {
eval(expr[i], current, f_env, registry);
Environment new_frame;
new_frame.link(env);
for (size_t i = 0, S = expr.size(); i < S; ++i) {
eval(expr[i], new_frame, f_env, new_Node);
}
return expr;
} break;
case Node::assignment: {
Node val(expr[1]);
if (val.type == Node::comma_list || val.type == Node::space_list) {
for (size_t i = 0; i < val.size(); ++i) {
if (val[i].eval_me) val[i] = eval(val[i], env, f_env, registry);
if (val.type() == Node::comma_list || val.type() == Node::space_list) {
for (size_t i = 0, S = val.size(); i < S; ++i) {
if (val[i].should_eval()) val[i] = eval(val[i], env, f_env, new_Node);
}
}
else {
val = eval(val, env, f_env, registry);
val = eval(val, env, f_env, new_Node);
}
Node var(expr[0]);
if (env.query(var.token())) {
......@@ -90,33 +81,31 @@ namespace Sass {
case Node::rule: {
Node rhs(expr[1]);
if (rhs.type == Node::comma_list || rhs.type == Node::space_list) {
for (size_t i = 0; i < rhs.size(); ++i) {
if (rhs[i].eval_me) rhs[i] = eval(rhs[i], env, f_env, registry);
if (rhs.type() == Node::comma_list || rhs.type() == Node::space_list) {
for (size_t i = 0, S = rhs.size(); i < S; ++i) {
if (rhs[i].should_eval()) rhs[i] = eval(rhs[i], env, f_env, new_Node);
}
}
else if (rhs.type == Node::value_schema || rhs.type == Node::string_schema) {
eval(rhs, env, f_env, registry);
else if (rhs.type() == Node::value_schema || rhs.type() == Node::string_schema) {
eval(rhs, env, f_env, new_Node);
}
else {
if (rhs.eval_me) expr[1] = eval(rhs, env, f_env, registry);
if (rhs.should_eval()) expr[1] = eval(rhs, env, f_env, new_Node);
}
return expr;
} break;
case Node::comma_list:
case Node::space_list: {
if (expr.eval_me) expr[0] = eval(expr[0], env, f_env, registry);
if (expr.should_eval()) expr[0] = eval(expr[0], env, f_env, new_Node);
return expr;
} break;
case Node::disjunction: {
Node result;
for (size_t i = 0; i < expr.size(); ++i) {
// if (expr[i].type == Node::relation ||
// expr[i].type == Node::function_call && expr[0].token().to_string() == "not") {
result = eval(expr[i], env, f_env, registry);
if (result.type == Node::boolean && result.content.boolean_value == false) continue;
for (size_t i = 0, S = expr.size(); i < S; ++i) {
result = eval(expr[i], env, f_env, new_Node);
if (result.type() == Node::boolean && result.boolean_value() == false) continue;
else return result;
}
return result;
......@@ -124,26 +113,23 @@ namespace Sass {
case Node::conjunction: {
Node result;
for (size_t i = 0; i < expr.size(); ++i) {
result = eval(expr[i], env, f_env, registry);
if (result.type == Node::boolean && result.content.boolean_value == false) return result;
for (size_t i = 0, S = expr.size(); i < S; ++i) {
result = eval(expr[i], env, f_env, new_Node);
if (result.type() == Node::boolean && result.boolean_value() == false) return result;
}
return result;
} break;
case Node::relation: {
Node lhs(eval(expr[0], env, f_env, registry));
Node lhs(eval(expr[0], env, f_env, new_Node));
Node op(expr[1]);
Node rhs(eval(expr[2], env, f_env, registry));
Node rhs(eval(expr[2], env, f_env, new_Node));
Node T(Node::boolean);
T.line() = lhs.line();
T.content.boolean_value = true;
Node F(T);
F.content.boolean_value = false;
Node T(new_Node(Node::boolean, lhs.path(), lhs.line(), true));
Node F(new_Node(Node::boolean, lhs.path(), lhs.line(), false));
switch (op.type) {
switch (op.type()) {
case Node::eq: return (lhs == rhs) ? T : F;
case Node::neq: return (lhs != rhs) ? T : F;
case Node::gt: return (lhs > rhs) ? T : F;
......@@ -151,32 +137,32 @@ namespace Sass {
case Node::lt: return (lhs < rhs) ? T : F;
case Node::lte: return (lhs <= rhs) ? T : F;
default:
eval_error("unknown comparison operator " + expr.token().to_string(), expr.line(), expr.path());
return Node(Node::none);
throw_eval_error("unknown comparison operator " + expr.token().to_string(), expr.path(), expr.line());
return Node();
}
} break;
case Node::expression: {
Node acc(Node::expression, registry, expr.line(), 1);
acc << eval(expr[0], env, f_env, registry);
Node rhs(eval(expr[2], env, f_env, registry));
accumulate(expr[1].type, acc, rhs, registry);
for (size_t i = 3; i < expr.size(); i += 2) {
Node rhs(eval(expr[i+1], env, f_env, registry));
accumulate(expr[i].type, acc, rhs, registry);
Node acc(new_Node(Node::expression, expr.path(), expr.line(), 1));
acc << eval(expr[0], env, f_env, new_Node);
Node rhs(eval(expr[2], env, f_env, new_Node));
accumulate(expr[1].type(), acc, rhs, new_Node);
for (size_t i = 3, S = expr.size(); i < S; i += 2) {
Node rhs(eval(expr[i+1], env, f_env, new_Node));
accumulate(expr[i].type(), acc, rhs, new_Node);
}
return acc.size() == 1 ? acc[0] : acc;
} break;
case Node::term: {
if (expr.eval_me) {
Node acc(Node::expression, registry, expr.line(), 1);
acc << eval(expr[0], env, f_env, registry);
Node rhs(eval(expr[2], env, f_env, registry));
accumulate(expr[1].type, acc, rhs, registry);
for (size_t i = 3; i < expr.size(); i += 2) {
Node rhs(eval(expr[i+1], env, f_env, registry));
accumulate(expr[i].type, acc, rhs, registry);
if (expr.should_eval()) {
Node acc(new_Node(Node::expression, expr.path(), expr.line(), 1));
acc << eval(expr[0], env, f_env, new_Node);
Node rhs(eval(expr[2], env, f_env, new_Node));
accumulate(expr[1].type(), acc, rhs, new_Node);
for (size_t i = 3, S = expr.size(); i < S; i += 2) {
Node rhs(eval(expr[i+1], env, f_env, new_Node));
accumulate(expr[i].type(), acc, rhs, new_Node);
}
return acc.size() == 1 ? acc[0] : acc;
}
......@@ -186,46 +172,44 @@ namespace Sass {
} break;
case Node::textual_percentage: {
Node pct(expr.line(), std::atof(expr.token().begin));
pct.type = Node::numeric_percentage;
return pct;
return new_Node(expr.path(), expr.line(), std::atof(expr.token().begin), Node::numeric_percentage);
} break;
case Node::textual_dimension: {
return Node(expr.line(),
return new_Node(expr.path(), expr.line(),
std::atof(expr.token().begin),
Token::make(Prelexer::number(expr.token().begin),
expr.token().end));
} break;
case Node::textual_number: {
return Node(expr.line(), std::atof(expr.token().begin));
return new_Node(expr.path(), expr.line(), std::atof(expr.token().begin));
} break;
case Node::textual_hex: {
Node triple(Node::numeric_color, registry, expr.line(), 4);
Node triple(new_Node(Node::numeric_color, expr.path(), expr.line(), 4));
Token hext(Token::make(expr.token().begin+1, expr.token().end));
if (hext.length() == 6) {
for (int i = 0; i < 6; i += 2) {
triple << Node(expr.line(), static_cast<double>(std::strtol(string(hext.begin+i, 2).c_str(), NULL, 16)));
triple << new_Node(expr.path(), expr.line(), static_cast<double>(std::strtol(string(hext.begin+i, 2).c_str(), NULL, 16)));
}
}
else {
for (int i = 0; i < 3; ++i) {
triple << Node(expr.line(), static_cast<double>(std::strtol(string(2, hext.begin[i]).c_str(), NULL, 16)));
triple << new_Node(expr.path(), expr.line(), static_cast<double>(std::strtol(string(2, hext.begin[i]).c_str(), NULL, 16)));
}
}
triple << Node(expr.line(), 1.0);
triple << new_Node(expr.path(), expr.line(), 1.0);
return triple;
} break;
case Node::variable: {
if (!env.query(expr.token())) eval_error("reference to unbound variable " + expr.token().to_string(), expr.line(), expr.path());
if (!env.query(expr.token())) throw_eval_error("reference to unbound variable " + expr.token().to_string(), expr.path(), expr.line());
return env[expr.token()];
} break;
case Node::function_call: {
// TO DO: default-constructed Function should be a generic callback
// TO DO: default-constructed Function should be a generic callback (maybe)
pair<string, size_t> sig(expr[0].token().to_string(), expr[1].size());
if (!f_env.count(sig)) {
// stringstream ss;
......@@ -233,12 +217,14 @@ namespace Sass {
// eval_error(ss.str(), expr.line(), expr.path());
return expr;
}
return apply_function(f_env[sig], expr[1], env, f_env, registry);
return apply_function(f_env[sig], expr[1], env, f_env, new_Node);
} break;
case Node::unary_plus: {
Node arg(eval(expr[0], env, f_env, registry));
if (arg.is_numeric()) return arg;
Node arg(eval(expr[0], env, f_env, new_Node));
if (arg.is_numeric()) {
return arg;
}
else {
expr[0] = arg;
return expr;
......@@ -246,9 +232,9 @@ namespace Sass {
} break;
case Node::unary_minus: {
Node arg(eval(expr[0], env, f_env, registry));
Node arg(eval(expr[0], env, f_env, new_Node));
if (arg.is_numeric()) {
arg.set_numeric_value(-arg.numeric_value());
return new_Node(expr.path(), expr.line(), -arg.numeric_value());
}
else {
expr[0] = arg;
......@@ -259,14 +245,14 @@ namespace Sass {
case Node::string_schema:
case Node::value_schema: {
// cerr << "evaluating schema of size " << expr.size() << endl;
for (size_t i = 0; i < expr.size(); ++i) {
expr[i] = eval(expr[i], env, f_env, registry);
for (size_t i = 0, S = expr.size(); i < S; ++i) {
expr[i] = eval(expr[i], env, f_env, new_Node);
}
return expr;
} break;
case Node::css_import: {
expr[0] = eval(expr[0], env, f_env, registry);
expr[0] = eval(expr[0], env, f_env, new_Node);
return expr;
} break;
......@@ -280,88 +266,79 @@ namespace Sass {
Node accumulate(Node::Type op, Node& acc, Node& rhs, Node_Factory& new_Node)
{
Node lhs(acc.content.children->back());
Node lhs(acc.back());
double lnum = lhs.numeric_value();
double rnum = rhs.numeric_value();
if (lhs.type == Node::number && rhs.type == Node::number) {
Node result(acc.line(), operate(op, lnum, rnum));
acc.content.children->pop_back();
acc.content.children->push_back(result);
if (lhs.type() == Node::number && rhs.type() == Node::number) {
Node result(new_Node(acc.path(), acc.line(), operate(op, lnum, rnum)));
acc.pop_back();
acc.push_back(result);
}
// TO DO: find a way to merge the following two clauses
else if (lhs.type == Node::number && rhs.type == Node::numeric_dimension) {
Node result(acc.line(), operate(op, lnum, rnum), Token::make(rhs.content.dimension.unit, Prelexer::identifier(rhs.content.dimension.unit)));
acc.content.children->pop_back();
acc.content.children->push_back(result);
else if (lhs.type() == Node::number && rhs.type() == Node::numeric_dimension) {
Node result(new_Node(acc.path(), acc.line(), operate(op, lnum, rnum), rhs.unit()));
acc.pop_back();
acc.push_back(result);
}
else if (lhs.type == Node::numeric_dimension && rhs.type == Node::number) {
Node result(acc.line(), operate(op, lnum, rnum), Token::make(lhs.content.dimension.unit, Prelexer::identifier(rhs.content.dimension.unit)));
acc.content.children->pop_back();
acc.content.children->push_back(result);
else if (lhs.type() == Node::numeric_dimension && rhs.type() == Node::number) {
Node result(new_Node(acc.path(), acc.line(), operate(op, lnum, rnum), lhs.unit()));
acc.pop_back();
acc.push_back(result);
}
else if (lhs.type == Node::numeric_dimension && rhs.type == Node::numeric_dimension) {
else if (lhs.type() == Node::numeric_dimension && rhs.type() == Node::numeric_dimension) {
// TO DO: CHECK FOR MISMATCHED UNITS HERE
Node result;
if (op == Node::div)
{ result = Node(acc.line(), operate(op, lnum, rnum)); }
{ result = new_Node(acc.path(), acc.line(), operate(op, lnum, rnum)); }
else
{ result = Node(acc.line(), operate(op, lnum, rnum), Token::make(lhs.content.dimension.unit, Prelexer::identifier(rhs.content.dimension.unit))); }
acc.content.children->pop_back();
acc.content.children->push_back(result);
{ result = new_Node(acc.path(), acc.line(), operate(op, lnum, rnum), lhs.unit()); }
acc.pop_back();
acc.push_back(result);
}
// TO DO: find a way to merge the following two clauses
else if (lhs.type == Node::number && rhs.type == Node::numeric_color) {
else if (lhs.type() == Node::number && rhs.type() == Node::numeric_color) {
if (op != Node::sub && op != Node::div) {
double r = operate(op, lhs.content.numeric_value, rhs[0].content.numeric_value);
double g = operate(op, lhs.content.numeric_value, rhs[1].content.numeric_value);
double b = operate(op, lhs.content.numeric_value, rhs[2].content.numeric_value);
double a = rhs[3].content.numeric_value;
acc.content.children->pop_back();
acc << Node(registry, acc.line(), r, g, b, a);
double r = operate(op, lhs.numeric_value(), rhs[0].numeric_value());
double g = operate(op, lhs.numeric_value(), rhs[1].numeric_value());
double b = operate(op, lhs.numeric_value(), rhs[2].numeric_value());
double a = rhs[3].numeric_value();
acc.pop_back();
acc << new_Node(acc.path(), acc.line(), r, g, b, a);
}
// trying to handle weird edge cases ... not sure if it's worth it
else if (op == Node::div) {
acc << Node(Node::div);
acc << new_Node(Node::div, acc.path(), acc.line(), 0);
acc << rhs;
}
else if (op == Node::sub) {
acc << Node(Node::sub);
acc << new_Node(Node::sub, acc.path(), acc.line(), 0);
acc << rhs;
}
else {
acc << rhs;
}
}
else if (lhs.type == Node::numeric_color && rhs.type == Node::number) {
double r = operate(op, lhs[0].content.numeric_value, rhs.content.numeric_value);
double g = operate(op, lhs[1].content.numeric_value, rhs.content.numeric_value);
double b = operate(op, lhs[2].content.numeric_value, rhs.content.numeric_value);
double a = lhs[3].content.numeric_value;
acc.content.children->pop_back();
acc << Node(registry, acc.line(), r, g, b, a);
}
else if (lhs.type == Node::numeric_color && rhs.type == Node::numeric_color) {
if (lhs[3].content.numeric_value != rhs[3].content.numeric_value) eval_error("alpha channels must be equal for " + lhs.to_string("") + " + " + rhs.to_string(""), lhs.line(), lhs.path());
double r = operate(op, lhs[0].content.numeric_value, rhs[0].content.numeric_value);
double g = operate(op, lhs[1].content.numeric_value, rhs[1].content.numeric_value);
double b = operate(op, lhs[2].content.numeric_value, rhs[2].content.numeric_value);
double a = lhs[3].content.numeric_value;
acc.content.children->pop_back();
acc << Node(registry, acc.line(), r, g, b, a);
}
// else if (lhs.type == Node::concatenation) {
// lhs << rhs;
// }
// else if (lhs.type == Node::string_constant || rhs.type == Node::string_constant) {
// acc.content.children->pop_back();
// Node cat(Node::concatenation, lhs.line(), 2);
// cat << lhs << rhs;
// acc << cat;
// }
else if (lhs.type() == Node::numeric_color && rhs.type() == Node::number) {
double r = operate(op, lhs[0].numeric_value(), rhs.numeric_value());
double g = operate(op, lhs[1].numeric_value(), rhs.numeric_value());
double b = operate(op, lhs[2].numeric_value(), rhs.numeric_value());
double a = lhs[3].numeric_value();
acc.pop_back();
acc << new_Node(acc.path(), acc.line(), r, g, b, a);
}
else if (lhs.type() == Node::numeric_color && rhs.type() == Node::numeric_color) {
if (lhs[3].numeric_value() != rhs[3].numeric_value()) throw_eval_error("alpha channels must be equal for " + lhs.to_string("") + " + " + rhs.to_string(""), lhs.line(), lhs.path());
double r = operate(op, lhs[0].numeric_value(), rhs[0].numeric_value());
double g = operate(op, lhs[1].numeric_value(), rhs[1].numeric_value());
double b = operate(op, lhs[2].numeric_value(), rhs[2].numeric_value());
double a = lhs[3].numeric_value();
acc.pop_back();
acc << new_Node(acc.path(), acc.line(), r, g, b, a);
}
else {
// TO DO: disallow division and multiplication on lists
acc.content.children->push_back(rhs);
acc.push_back(rhs);
}
return acc;
......@@ -382,26 +359,26 @@ namespace Sass {
Node apply_mixin(Node& mixin, const Node& args, Environment& env, map<pair<string, size_t>, Function>& f_env, Node_Factory& new_Node)
{
Node params(mixin[1]);
Node body(mixin[2].clone(registry));
Node body(new_Node(mixin[2])); // clone the body
Environment bindings;
// bind arguments
for (size_t i = 0, j = 0; i < args.size(); ++i) {
if (args[i].type == Node::assignment) {
for (size_t i = 0, j = 0, S = args.size(); i < S; ++i) {
if (args[i].type() == Node::assignment) {
Node arg(args[i]);
Token name(arg[0].token());
// check that the keyword arg actually names a formal parameter
bool valid_param = false;
for (size_t k = 0; k < params.size(); ++k) {
for (size_t k = 0, S = params.size(); k < S; ++k) {
Node param_k = params[k];
if (param_k.type == Node::assignment) param_k = param_k[0];
if (param_k.type() == Node::assignment) param_k = param_k[0];
if (arg[0] == param_k) {
valid_param = true;
break;
}
}
if (!valid_param) eval_error("mixin " + mixin[0].to_string("") + " has no parameter named " + name.to_string(), arg.line(), arg.path());
if (!valid_param) throw_eval_error("mixin " + mixin[0].to_string("") + " has no parameter named " + name.to_string(), arg.line(), arg.path());
if (!bindings.query(name)) {
bindings[name] = eval(arg[1], env, f_env, registry);
bindings[name] = eval(arg[1], env, f_env, new_Node);
}
}
else {
......@@ -409,28 +386,28 @@ namespace Sass {
if (j >= params.size()) {
stringstream ss;
ss << "mixin " << mixin[0].to_string("") << " only takes " << params.size() << ((params.size() == 1) ? " argument" : " arguments");
eval_error(ss.str(), args[i].line(), args[i].path());
throw_eval_error(ss.str(), args[i].path(), args[i].line());
}
Node param(params[j]);
Token name(param.type == Node::variable ? param.token() : param[0].token());
bindings[name] = eval(args[i], env, f_env, registry);
Token name(param.type() == Node::variable ? param.token() : param[0].token());
bindings[name] = eval(args[i], env, f_env, new_Node);
++j;
}
}
// plug the holes with default arguments if any
for (size_t i = 0; i < params.size(); ++i) {
if (params[i].type == Node::assignment) {
for (size_t i = 0, S = params.size(); i < S; ++i) {
if (params[i].type() == Node::assignment) {
Node param(params[i]);
Token name(param[0].token());
if (!bindings.query(name)) {
bindings[name] = eval(param[1], env, f_env, registry);
bindings[name] = eval(param[1], env, f_env, new_Node);
}
}
}
// lexically link the new environment and eval the mixin's body
bindings.link(env.global ? *env.global : env);
for (size_t i = 0; i < body.size(); ++i) {
body[i] = eval(body[i], bindings, f_env, registry);
for (size_t i = 0, S = body.size(); i < S; ++i) {
body[i] = eval(body[i], bindings, f_env, new_Node);
}
return body;
}
......@@ -439,19 +416,19 @@ namespace Sass {
{
map<Token, Node> bindings;
// bind arguments
for (size_t i = 0, j = 0; i < args.size(); ++i) {
if (args[i].type == Node::assignment) {
for (size_t i = 0, j = 0, S = args.size(); i < S; ++i) {
if (args[i].type() == Node::assignment) {
Node arg(args[i]);
Token name(arg[0].token());
bindings[name] = eval(arg[1], env, f_env, registry);
bindings[name] = eval(arg[1], env, f_env, new_Node);
}
else {
// TO DO: ensure (j < f.parameters.size())
bindings[f.parameters[j]] = eval(args[i], env, f_env, registry);
bindings[f.parameters[j]] = eval(args[i], env, f_env, new_Node);
++j;
}
}
return f(bindings, registry);
return f(bindings, new_Node);
}
}
......@@ -169,6 +169,7 @@ namespace Sass {
bool empty() const;
Node& at(size_t i) const;
Node& back() const;
Node& operator[](size_t i) const;
void pop_back();
Node& push_back(Node n);
......@@ -302,6 +303,7 @@ namespace Sass {
inline bool Node::empty() const { return ip_->empty(); }
inline Node& Node::at(size_t i) const { return ip_->at(i); }
inline Node& Node::back() const { return ip_->back(); }
inline Node& Node::operator[](size_t i) const { return at(i); }
inline void Node::pop_back() { ip_->pop_back(); }
inline Node& Node::push_back(Node n)
......
......@@ -61,9 +61,9 @@ namespace Sass {
// }
// for making nodes representing numbers
Node Node_Factory::operator()(string path, size_t line, double v)
Node Node_Factory::operator()(string path, size_t line, double v, Node::Type type)
{
Node_Impl* ip = alloc_Node_Impl(Node::number, path, line);
Node_Impl* ip = alloc_Node_Impl(type, path, line);
ip->value.numeric = v;
return Node(ip);
}
......
......@@ -25,7 +25,7 @@ namespace Sass {
// // for making nodes representing boolean values
// Node operator()(Node::Type type, string file, size_t line, bool b);
// for making nodes representing numbers
Node operator()(string file, size_t line, double v);
Node operator()(string file, size_t line, double v, Node::Type type = Node::number);
// for making nodes representing numeric dimensions (e.g. 5px, 3em)
Node operator()(string file, size_t line, double v, const Token& t);
// for making nodes representing rgba color quads
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment