ext/attribute_builder/attribute_builder.cc
#include <ruby.h>
#include <ruby/version.h>
#include "houdini.h"
#include <algorithm>
#include <map>
#include <string>
#include <vector>
/* faml requires Ruby >= 2.1 */
/* rb_utf8_str_new() is available since Ruby 2.2 */
#if RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR < 2
#include <ruby/encoding.h>
#define rb_utf8_str_new(ptr, len) rb_enc_str_new(ptr, len, rb_utf8_encoding())
#endif
#define FOREACH_FUNC(func) reinterpret_cast<int (*)(ANYARGS)>(func)
VALUE rb_mAttributeBuilder;
static ID id_flatten;
static inline std::string string_from_value(VALUE v) {
return std::string(RSTRING_PTR(v), RSTRING_LEN(v));
}
enum attribute_value_type {
ATTRIBUTE_TYPE_TRUE,
ATTRIBUTE_TYPE_FALSE,
ATTRIBUTE_TYPE_VALUE,
};
struct attribute_value {
attribute_value_type type_;
std::string str_;
attribute_value(attribute_value_type type) : type_(type) {}
attribute_value(const char* cstr, long len)
: type_(ATTRIBUTE_TYPE_VALUE), str_(cstr, len) {}
attribute_value(const std::string& str)
: type_(ATTRIBUTE_TYPE_VALUE), str_(str) {}
static attribute_value from_value(VALUE value) {
if (RB_TYPE_P(value, T_TRUE)) {
return attribute_value(ATTRIBUTE_TYPE_TRUE);
} else if (!RTEST(value)) {
return attribute_value(ATTRIBUTE_TYPE_FALSE);
} else {
value = rb_convert_type(value, T_STRING, "String", "to_s");
return attribute_value(string_from_value(value));
}
}
};
typedef std::map<std::string, attribute_value> attributes_type;
void upsert_attribute(attributes_type& m, const std::string& key,
const attribute_value& value) {
const std::pair<attributes_type::iterator, bool> r =
m.insert(std::make_pair(key, value));
if (!r.second) {
r.first->second = value;
}
}
struct attribute_holder {
std::vector<attribute_value> ids_, classes_;
attributes_type m_;
inline void upsert(const std::string& key, const attribute_value& value) {
return upsert_attribute(m_, key, value);
}
};
static VALUE to_value(const attributes_type& m) {
VALUE h = rb_hash_new();
for (attributes_type::const_iterator it = m.begin(); it != m.end(); ++it) {
VALUE k = rb_utf8_str_new(it->first.data(), it->first.size());
VALUE v = Qnil;
switch (it->second.type_) {
case ATTRIBUTE_TYPE_TRUE:
v = Qtrue;
break;
case ATTRIBUTE_TYPE_FALSE:
v = Qnil;
break;
case ATTRIBUTE_TYPE_VALUE:
v = rb_utf8_str_new(it->second.str_.data(), it->second.str_.size());
break;
}
rb_hash_aset(h, k, v);
}
return h;
}
static void concat_array_attribute(std::vector<attribute_value>& ary,
VALUE value) {
if (RB_TYPE_P(value, T_ARRAY)) {
value = rb_funcall(value, id_flatten, 0);
} else {
value = rb_Array(value);
}
const long len = RARRAY_LEN(value);
for (long i = 0; i < len; i++) {
ary.push_back(attribute_value::from_value(RARRAY_AREF(value, i)));
}
}
static attributes_type normalize_data(VALUE data);
static int normalize_data_i(VALUE key, VALUE value, VALUE arg) {
attributes_type* normalized = reinterpret_cast<attributes_type*>(arg);
key = rb_convert_type(key, T_STRING, "String", "to_s");
std::string key_ = string_from_value(key);
std::replace(key_.begin(), key_.end(), '_', '-');
if (RB_TYPE_P(value, T_HASH)) {
const attributes_type sub = normalize_data(value);
for (attributes_type::const_iterator it = sub.begin(); it != sub.end();
++it) {
upsert_attribute(*normalized, key_ + "-" + it->first, it->second);
}
} else {
upsert_attribute(*normalized, key_, attribute_value::from_value(value));
}
return ST_CONTINUE;
}
static attributes_type normalize_data(VALUE data) {
Check_Type(data, T_HASH);
attributes_type m;
rb_hash_foreach(data, FOREACH_FUNC(normalize_data_i),
reinterpret_cast<VALUE>(&m));
return m;
}
static int merge_one_i(VALUE key, VALUE value, VALUE arg) {
attribute_holder* attributes = reinterpret_cast<attribute_holder*>(arg);
key = rb_convert_type(key, T_STRING, "String", "to_s");
const std::string key_ = string_from_value(key);
if (key_ == "class") {
concat_array_attribute(attributes->classes_, value);
} else if (key_ == "id") {
concat_array_attribute(attributes->ids_, value);
} else if (key_ == "data" && RB_TYPE_P(value, T_HASH)) {
const attributes_type data = normalize_data(value);
for (attributes_type::const_iterator it = data.begin(); it != data.end();
++it) {
attributes->upsert("data-" + it->first, it->second);
}
} else {
attributes->upsert(key_, attribute_value::from_value(value));
}
return ST_CONTINUE;
}
static void merge_one(attribute_holder& attributes, VALUE h) {
Check_Type(h, T_HASH);
rb_hash_foreach(h, FOREACH_FUNC(merge_one_i),
reinterpret_cast<VALUE>(&attributes));
}
static void join_class_attribute(attribute_holder& attributes) {
const std::vector<attribute_value>& classes = attributes.classes_;
std::vector<std::string> ary;
for (std::vector<attribute_value>::const_iterator it = classes.begin();
it != classes.end(); ++it) {
switch (it->type_) {
case ATTRIBUTE_TYPE_FALSE:
break;
case ATTRIBUTE_TYPE_TRUE:
ary.push_back("true");
break;
case ATTRIBUTE_TYPE_VALUE:
size_t prev = 0, pos;
while ((pos = it->str_.find_first_of(' ', prev)) != std::string::npos) {
if (pos != prev) {
ary.push_back(std::string(it->str_, prev, pos - prev));
}
prev = pos + 1;
}
ary.push_back(std::string(it->str_, prev, it->str_.size() - prev));
break;
}
}
if (ary.empty()) {
return;
}
std::sort(ary.begin(), ary.end());
ary.erase(std::unique(ary.begin(), ary.end()), ary.end());
std::string buf;
for (std::vector<std::string>::const_iterator it = ary.begin();
it != ary.end(); ++it) {
if (it != ary.begin()) {
buf.push_back(' ');
}
buf.append(*it);
}
attributes.upsert("class", attribute_value(buf));
}
static void join_id_attribute(attribute_holder& attributes) {
const std::vector<attribute_value>& ids = attributes.ids_;
std::string buf;
bool first = true;
for (std::vector<attribute_value>::const_iterator it = ids.begin();
it != ids.end(); ++it) {
switch (it->type_) {
case ATTRIBUTE_TYPE_FALSE:
break;
case ATTRIBUTE_TYPE_TRUE:
if (!first) {
buf.push_back('_');
}
buf.append("true");
first = false;
break;
case ATTRIBUTE_TYPE_VALUE:
if (!first) {
buf.push_back('_');
}
buf.append(it->str_);
first = false;
break;
}
}
if (first) {
return;
}
attributes.upsert("id", attribute_value(buf));
}
static void delete_falsey_values(attributes_type& m) {
for (attributes_type::iterator it = m.begin(); it != m.end();) {
if (it->second.type_ == ATTRIBUTE_TYPE_FALSE) {
attributes_type::iterator jt = it;
++it;
m.erase(jt);
} else {
++it;
}
}
}
static attributes_type merge(VALUE object_ref, int argc, VALUE* argv) {
int i;
attribute_holder attributes;
for (i = 0; i < argc; i++) {
merge_one(attributes, argv[i]);
}
if (!NIL_P(object_ref)) {
merge_one(attributes, object_ref);
}
join_class_attribute(attributes);
join_id_attribute(attributes);
delete_falsey_values(attributes.m_);
return attributes.m_;
}
static void put_attribute(std::string& buf,
const std::string& attr_quote, const std::string& key,
const std::string& value) {
buf.append(" ").append(key).append("=").append(attr_quote);
gh_buf ob = GH_BUF_INIT;
if (houdini_escape_html0(&ob, (const uint8_t*)value.data(), value.size(),
0)) {
buf.append(std::string(ob.ptr, ob.size));
gh_buf_free(&ob);
} else {
buf.append(value);
}
buf.append(attr_quote);
}
static void build_attribute(std::string& buf,
const std::string& attr_quote, int is_html,
const std::string& key,
const attribute_value& value) {
if (value.type_ == ATTRIBUTE_TYPE_TRUE) {
if (is_html) {
buf.push_back(' ');
buf.append(key);
} else {
put_attribute(buf, attr_quote, key, key);
}
} else {
put_attribute(buf, attr_quote, key, value.str_);
}
}
static VALUE m_build(int argc, VALUE* argv, RB_UNUSED_VAR(VALUE self)) {
VALUE object_ref;
int is_html;
rb_check_arity(argc, 3, UNLIMITED_ARGUMENTS);
Check_Type(argv[0], T_STRING);
const std::string attr_quote = string_from_value(argv[0]);
is_html = RTEST(argv[1]);
object_ref = argv[2];
const attributes_type attributes = merge(object_ref, argc - 3, argv + 3);
std::string buf;
for (attributes_type::const_iterator it = attributes.begin();
it != attributes.end(); ++it) {
build_attribute(buf, attr_quote, is_html, it->first, it->second);
}
return rb_utf8_str_new(buf.data(), buf.size());
}
static VALUE m_normalize_data(RB_UNUSED_VAR(VALUE self), VALUE data) {
return to_value(normalize_data(data));
}
static VALUE m_merge(int argc, VALUE* argv, RB_UNUSED_VAR(VALUE self)) {
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
return to_value(merge(argv[0], argc - 1, argv + 1));
}
extern "C" {
void Init_attribute_builder(void) {
VALUE mFaml = rb_define_module("Faml");
rb_mAttributeBuilder = rb_define_module_under(mFaml, "AttributeBuilder");
rb_define_singleton_method(rb_mAttributeBuilder, "build",
RUBY_METHOD_FUNC(m_build), -1);
rb_define_singleton_method(rb_mAttributeBuilder, "normalize_data",
RUBY_METHOD_FUNC(m_normalize_data), 1);
rb_define_singleton_method(rb_mAttributeBuilder, "merge",
RUBY_METHOD_FUNC(m_merge), -1);
id_flatten = rb_intern("flatten");
}
};