diff --git a/.travis.yml b/.travis.yml index 5167f9931..012c2745e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,9 @@ matrix: before_install: - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - sudo apt-get update - - sudo apt-get install gcc-4.8 g++-4.8 - - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 20 - - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 20 + - sudo apt-get install gcc-4.7 g++-4.7 + - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.7 20 + - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.7 20 - g++ --version - sudo apt-get update -qq - git submodule update --init --recursive diff --git a/README.md b/README.md index 2bb4ff094..5a500f783 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,68 @@ When returning or calling `done()` with `{ contents: "String" }`, the string val Starting from v3.0.0, `this` refers to a contextual scope for the immediate run of `sass.render` or `sass.renderSync` +### functions +`functions` is an `Object` that holds a collection of custom functions that may be invoked by the sass files being compiled. They may take zero or more input parameters and must return a value either synchronously (`return ...;`) or asynchronously (`done();`). Those parameters will be instances of one of the constructors contained in the `require('node-sass').types` hash. The return value must be of one of these types as well. See the list of available types below: + +#### types.Number(value [, unit = ""]) +* `getValue()`/ `setValue(value)` : gets / sets the numerical portion of the number +* `getUnit()` / `setUnit(unit)` : gets / sets the unit portion of the number + +#### types.String(value) +* `getValue()` / `setValue(value)` : gets / sets the enclosed string + +#### types.Color(r, g, b [, a = 1.0]) or types.Color(argb) +* `getR()` / `setR(value)` : red component (integer from `0` to `255`) +* `getG()` / `setG(value)` : green component (integer from `0` to `255`) +* `getB()` / `setB(value)` : blue component (integer from `0` to `255`) +* `getA()` / `setA(value)` : alpha component (number from `0` to `1.0`) + +Example: + +```javascript +var Color = require('node-sass').types.Color, + c1 = new Color(255, 0, 0), + c2 = new Color(0xff0088cc); +``` + +#### types.Boolean(value) +* `getValue()` : gets the enclosed boolean +* `types.Boolean.TRUE` : Singleton instance of `types.Boolean` that holds "true" +* `types.Boolean.FALSE` : Singleton instance of `types.Boolean` that holds "false" + +#### types.List(length [, commaSeparator = true]) +* `getValue(index)` / `setValue(index, value)` : `value` must itself be an instance of one of the constructors in `sass.types`. +* `getSeparator()` / `setSeparator(isComma)` : whether to use commas as a separator +* `getLength()` + +#### types.Map(length) +* `getKey(index)` / `setKey(index, value)` +* `getValue(index)` / `setValue(index, value)` +* `getLength()` + +#### types.Null() +* `types.Null.NULL` : Singleton instance of `types.Null`. + +#### Example + +```javascript +sass.renderSync({ + data: '#{headings(2,5)} { color: #08c; }', + functions: { + 'headings($from: 0, $to: 6)': function(from, to) { + var i, f = from.getValue(), t = to.getValue(), + list = new sass.types.List(t - f + 1); + + for (i = f; i <= t; i++) { + list.setValue(i - f, new sass.types.String('h' + i)); + } + + return list; + } + } +}); +``` + ### includePaths Type: `Array` Default: `[]` diff --git a/binding.gyp b/binding.gyp index 5cd6af875..8e625e407 100644 --- a/binding.gyp +++ b/binding.gyp @@ -4,7 +4,19 @@ 'target_name': 'binding', 'sources': [ 'src/binding.cpp', - 'src/sass_context_wrapper.cpp' + 'src/create_string.cpp', + 'src/custom_function_bridge.cpp', + 'src/custom_importer_bridge.cpp', + 'src/sass_context_wrapper.cpp', + 'src/sass_types/boolean.cpp', + 'src/sass_types/color.cpp', + 'src/sass_types/error.cpp', + 'src/sass_types/factory.cpp', + 'src/sass_types/list.cpp', + 'src/sass_types/map.cpp', + 'src/sass_types/null.cpp', + 'src/sass_types/number.cpp', + 'src/sass_types/string.cpp' ], 'include_dirs': [ '= 0; i--) { + args.unshift(list.getValue(i)); + } + + return callback.apply(this, args); + } + }; + } + + return { + signature: signature, + callback: callback + }; +} + /** * Render * @@ -172,13 +233,9 @@ module.exports.render = function(options, cb) { var importer = options.importer; if (importer) { - options.importer = function(file, prev, key) { + options.importer = function(file, prev, bridge) { function done(data) { - console.log(data); // ugly hack - binding.importedCallback({ - index: key, - objectLiteral: data - }); + bridge.success(data); } var result = importer.call(options.context, file, prev, done); @@ -189,6 +246,31 @@ module.exports.render = function(options, cb) { }; } + var functions = options.functions; + + if (functions) { + options.functions = {}; + + Object.keys(functions).forEach(function(signature) { + var cb = normalizeFunctionSignature(signature, functions[signature]); + + options.functions[cb.signature] = function() { + var args = Array.prototype.slice.call(arguments), + bridge = args.pop(); + + function done(data) { + bridge.success(data); + } + + var result = tryCallback(cb.callback, args.concat(done)); + + if (result) { + done(result); + } + }; + }); + } + options.data ? binding.render(options) : binding.renderFile(options); }; @@ -206,10 +288,24 @@ module.exports.renderSync = function(options) { if (importer) { options.importer = function(file, prev) { - return { objectLiteral: importer.call(options.context, file, prev) }; + return importer.call(options.context, file, prev); }; } + var functions = options.functions; + + if (options.functions) { + options.functions = {}; + + Object.keys(functions).forEach(function(signature) { + var cb = normalizeFunctionSignature(signature, functions[signature]); + + options.functions[cb.signature] = function() { + return tryCallback(cb.callback, arguments); + }; + }); + } + var status = options.data ? binding.renderSync(options) : binding.renderFileSync(options); var result = options.result; @@ -228,3 +324,12 @@ module.exports.renderSync = function(options) { */ module.exports.info = process.sass.versionInfo; + +/** + * Expose sass types + */ + +module.exports.types = binding.types; +module.exports.TRUE = binding.types.Boolean.TRUE; +module.exports.FALSE = binding.types.Boolean.FALSE; +module.exports.NULL = binding.types.Null.NULL; \ No newline at end of file diff --git a/libsass.gyp b/libsass.gyp index 1a4b69b27..a08f25f3b 100644 --- a/libsass.gyp +++ b/libsass.gyp @@ -61,14 +61,12 @@ '-std=c++11', '-stdlib=libc++' ], - 'OTHER_LDFLAGS': [ - '-stdlib=libc++' - ], + 'OTHER_LDFLAGS': [], 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', 'GCC_ENABLE_CPP_RTTI': 'YES', 'MACOSX_DEPLOYMENT_TARGET': '10.7' } - }], + }], ['OS=="win"', { 'msvs_settings': { 'VCCLCompilerTool': { diff --git a/package.json b/package.json index 84e6195ac..aaa2d347a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "coverage": "node scripts/coverage.js", "install": "node scripts/install.js", "postinstall": "node scripts/build.js", - "pretest": "node_modules/.bin/jshint bin lib test", + "pretest": "node_modules/.bin/jshint bin lib scripts test", "test": "node_modules/.bin/mocha test" }, "files": [ diff --git a/scripts/build.js b/scripts/build.js index 66911602f..8d843f4de 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -53,7 +53,7 @@ function afterBuild(options) { */ function build(options) { - var arguments = [ + var args = [ path.join('node_modules', 'pangyp', 'bin', 'node-gyp'), 'rebuild', ].concat( @@ -63,9 +63,9 @@ function build(options) { }) ).concat(options.args); - console.log(['Building:', process.sass.runtime.execPath].concat(arguments).join(' ')); + console.log(['Building:', process.sass.runtime.execPath].concat(args).join(' ')); - var proc = spawn(process.sass.runtime.execPath, arguments, { + var proc = spawn(process.sass.runtime.execPath, args, { stdio: [0, 1, 2] }); diff --git a/src/binding.cpp b/src/binding.cpp index 0d8e7ca44..dde937157 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -1,112 +1,39 @@ #include #include #include "sass_context_wrapper.h" - -char* create_string(Local value) { - if (value->IsNull() || !value->IsString()) { - return 0; - } - - String::Utf8Value string(value); - char *str = (char *)malloc(string.length() + 1); - strcpy(str, *string); - return str; -} - -std::vector imports_collection; - -void prepare_import_results(Local returned_value, sass_context_wrapper* ctx_w) { - NanScope(); - - if (returned_value->IsArray()) { - Handle array = Handle::Cast(returned_value); - - ctx_w->imports = sass_make_import_list(array->Length()); - - for (size_t i = 0; i < array->Length(); ++i) { - Local value = array->Get(static_cast(i)); - - if (!value->IsObject()) - continue; - - Local object = Local::Cast(value); - char* path = create_string(object->Get(NanNew("file"))); - char* contents = create_string(object->Get(NanNew("contents"))); - - ctx_w->imports[i] = sass_make_import_entry(path, (!contents || contents[0] == '\0') ? 0 : strdup(contents), 0); - } - } - else if (returned_value->IsObject()) { - ctx_w->imports = sass_make_import_list(1); - Local object = Local::Cast(returned_value); - char* path = create_string(object->Get(NanNew("file"))); - char* contents = create_string(object->Get(NanNew("contents"))); - - ctx_w->imports[0] = sass_make_import_entry(path, (!contents || contents[0] == '\0') ? 0 : strdup(contents), 0); - } - else { - ctx_w->imports = sass_make_import_list(1); - ctx_w->imports[0] = sass_make_import_entry(ctx_w->file, 0, 0); - } -} - -void dispatched_async_uv_callback(uv_async_t *req) { - NanScope(); - sass_context_wrapper* ctx_w = static_cast(req->data); - - TryCatch try_catch; - - imports_collection.push_back(ctx_w); - - Handle argv[] = { - NanNew(strdup(ctx_w->file ? ctx_w->file : 0)), - NanNew(strdup(ctx_w->prev ? ctx_w->prev : 0)), - NanNew(imports_collection.size() - 1) - }; - - NanNew(ctx_w->importer_callback->Call(3, argv)); - - if (try_catch.HasCaught()) { - node::FatalException(try_catch); - } -} +#include "custom_function_bridge.h" +#include "create_string.h" +#include "sass_types/factory.h" struct Sass_Import** sass_importer(const char* file, const char* prev, void* cookie) { sass_context_wrapper* ctx_w = static_cast(cookie); + CustomImporterBridge& bridge = *(ctx_w->importer_bridge); - if (!ctx_w->is_sync) { - /* that is async: Render() or RenderFile(), - * the default even loop is unblocked so it - * can run uv_async_send without a push. - */ + std::vector argv; + argv.push_back((void*) file); + argv.push_back((void*) prev); - std::unique_lock lock(*ctx_w->importer_mutex); + return bridge(argv); +} - ctx_w->file = file ? strdup(file) : 0; - ctx_w->prev = prev ? strdup(prev) : 0; - ctx_w->async.data = (void*)ctx_w; +union Sass_Value* sass_custom_function(const union Sass_Value* s_args, void* cookie) +{ + CustomFunctionBridge& bridge = *(static_cast(cookie)); - uv_async_send(&ctx_w->async); - ctx_w->importer_condition_variable->wait(lock); + std::vector argv; + for (unsigned l = sass_list_get_length(s_args), i = 0; i < l; i++) { + argv.push_back((void*) sass_list_get_value(s_args, i)); } - else { - NanScope(); - - Handle argv[] = { - NanNew(file), - NanNew(prev) - }; - - Local returned_value = Local::Cast(NanNew(ctx_w->importer_callback->Call(2, argv))); - prepare_import_results(returned_value->Get(NanNew("objectLiteral")), ctx_w); + try { + return bridge(argv); + } catch (const std::exception& e) { + return sass_make_error(e.what()); } - - return ctx_w->imports; } -void extract_options(Local options, void* cptr, sass_context_wrapper* ctx_w, bool is_file, bool is_sync) { +void ExtractOptions(Local options, void* cptr, sass_context_wrapper* ctx_w, bool is_file, bool is_sync) { NanScope(); struct Sass_Context* ctx; @@ -124,7 +51,6 @@ void extract_options(Local options, void* cptr, sass_context_wrapper* ct struct Sass_Options* sass_options = sass_context_get_options(ctx); - ctx_w->importer_callback = NULL; ctx_w->is_sync = is_sync; if (!is_sync) { @@ -141,8 +67,7 @@ void extract_options(Local options, void* cptr, sass_context_wrapper* ct Local importer_callback = Local::Cast(options->Get(NanNew("importer"))); if (importer_callback->IsFunction()) { - ctx_w->importer_callback = new NanCallback(importer_callback); - uv_async_init(uv_default_loop(), &ctx_w->async, (uv_async_cb)dispatched_async_uv_callback); + ctx_w->importer_bridge = new CustomImporterBridge(new NanCallback(importer_callback), ctx_w->is_sync); sass_option_set_importer(sass_options, sass_make_importer(sass_importer, ctx_w)); } @@ -160,9 +85,34 @@ void extract_options(Local options, void* cptr, sass_context_wrapper* ct sass_option_set_source_map_file(sass_options, create_string(options->Get(NanNew("sourceMap")))); sass_option_set_include_path(sass_options, create_string(options->Get(NanNew("includePaths")))); sass_option_set_precision(sass_options, options->Get(NanNew("precision"))->Int32Value()); + + Local custom_functions = Local::Cast(options->Get(NanNew("functions"))); + + if (custom_functions->IsObject()) { + Local signatures = custom_functions->GetOwnPropertyNames(); + unsigned num_signatures = signatures->Length(); + Sass_C_Function_List fn_list = sass_make_function_list(num_signatures); + + for (unsigned i = 0; i < num_signatures; i++) { + Local signature = Local::Cast(signatures->Get(NanNew(i))); + Local callback = Local::Cast(custom_functions->Get(signature)); + + if (!signature->IsString() || !callback->IsFunction()) { + NanThrowError(NanNew("options.functions must be a (signature -> function) hash")); + } + + CustomFunctionBridge* bridge = new CustomFunctionBridge(new NanCallback(callback), ctx_w->is_sync); + ctx_w->function_bridges.push_back(bridge); + + Sass_C_Function_Callback fn = sass_make_function(create_string(signature), sass_custom_function, bridge); + sass_function_set_list_entry(fn_list, i, fn); + } + + sass_option_set_c_functions(sass_options, fn_list); + } } -void get_stats(sass_context_wrapper* ctx_w, Sass_Context* ctx) { +void GetStats(sass_context_wrapper* ctx_w, Sass_Context* ctx) { NanScope(); char** included_files = sass_context_get_included_files(ctx); @@ -177,7 +127,7 @@ void get_stats(sass_context_wrapper* ctx_w, Sass_Context* ctx) { NanNew(ctx_w->result)->Get(NanNew("stats"))->ToObject()->Set(NanNew("includedFiles"), arr); } -int get_result(sass_context_wrapper* ctx_w, Sass_Context* ctx, bool is_sync = false) { +int GetResult(sass_context_wrapper* ctx_w, Sass_Context* ctx, bool is_sync = false) { NanScope(); int status = sass_context_get_error_status(ctx); @@ -188,7 +138,7 @@ int get_result(sass_context_wrapper* ctx_w, Sass_Context* ctx, bool is_sync = fa NanNew(ctx_w->result)->Set(NanNew("css"), NanNewBufferHandle(css, static_cast(strlen(css)))); - get_stats(ctx_w, ctx); + GetStats(ctx_w, ctx); if (map) { NanNew(ctx_w->result)->Set(NanNew("map"), NanNewBufferHandle(map, static_cast(strlen(map)))); @@ -201,7 +151,7 @@ int get_result(sass_context_wrapper* ctx_w, Sass_Context* ctx, bool is_sync = fa return status; } -void make_callback(uv_work_t* req) { +void MakeCallback(uv_work_t* req) { NanScope(); TryCatch try_catch; @@ -215,7 +165,7 @@ void make_callback(uv_work_t* req) { ctx = sass_file_context_get_context(ctx_w->fctx); } - int status = get_result(ctx_w, ctx); + int status = GetResult(ctx_w, ctx); if (status == 0 && ctx_w->success_callback) { // if no error, do callback(null, result) @@ -233,10 +183,6 @@ void make_callback(uv_work_t* req) { node::FatalException(try_catch); } - if (ctx_w->importer_callback) { - uv_close((uv_handle_t*)&ctx_w->async, NULL); - } - sass_free_context_wrapper(ctx_w); } @@ -248,9 +194,9 @@ NAN_METHOD(render) { struct Sass_Data_Context* dctx = sass_make_data_context(source_string); sass_context_wrapper* ctx_w = sass_make_context_wrapper(); - extract_options(options, dctx, ctx_w, false, false); + ExtractOptions(options, dctx, ctx_w, false, false); - int status = uv_queue_work(uv_default_loop(), &ctx_w->request, compile_it, (uv_after_work_cb)make_callback); + int status = uv_queue_work(uv_default_loop(), &ctx_w->request, compile_it, (uv_after_work_cb)MakeCallback); assert(status == 0); @@ -266,11 +212,11 @@ NAN_METHOD(render_sync) { struct Sass_Context* ctx = sass_data_context_get_context(dctx); sass_context_wrapper* ctx_w = sass_make_context_wrapper(); - extract_options(options, dctx, ctx_w, false, true); + ExtractOptions(options, dctx, ctx_w, false, true); compile_data(dctx); - int result = get_result(ctx_w, ctx, true); + int result = GetResult(ctx_w, ctx, true); sass_free_context_wrapper(ctx_w); @@ -285,9 +231,9 @@ NAN_METHOD(render_file) { struct Sass_File_Context* fctx = sass_make_file_context(input_path); sass_context_wrapper* ctx_w = sass_make_context_wrapper(); - extract_options(options, fctx, ctx_w, true, false); + ExtractOptions(options, fctx, ctx_w, true, false); - int status = uv_queue_work(uv_default_loop(), &ctx_w->request, compile_it, (uv_after_work_cb)make_callback); + int status = uv_queue_work(uv_default_loop(), &ctx_w->request, compile_it, (uv_after_work_cb)MakeCallback); assert(status == 0); @@ -303,47 +249,22 @@ NAN_METHOD(render_file_sync) { struct Sass_Context* ctx = sass_file_context_get_context(fctx); sass_context_wrapper* ctx_w = sass_make_context_wrapper(); - extract_options(options, fctx, ctx_w, true, true); + ExtractOptions(options, fctx, ctx_w, true, true); compile_file(fctx); - int result = get_result(ctx_w, ctx, true); + int result = GetResult(ctx_w, ctx, true); sass_wrapper_dispose(ctx_w, input_path); NanReturnValue(NanNew(result == 0)); } -NAN_METHOD(imported_callback) { - NanScope(); - - TryCatch try_catch; - - Local options = args[0]->ToObject(); - Local returned_value = options->Get(NanNew("objectLiteral")); - size_t index = options->Get(NanNew("index"))->Int32Value(); - - if (index >= imports_collection.size()) { - NanReturnUndefined(); - } - - sass_context_wrapper* ctx_w = imports_collection[index]; - - prepare_import_results(returned_value, ctx_w); - ctx_w->importer_condition_variable->notify_all(); - - if (try_catch.HasCaught()) { - node::FatalException(try_catch); - } - - NanReturnValue(NanNew(0)); -} - void RegisterModule(v8::Handle target) { NODE_SET_METHOD(target, "render", render); NODE_SET_METHOD(target, "renderSync", render_sync); NODE_SET_METHOD(target, "renderFile", render_file); NODE_SET_METHOD(target, "renderFileSync", render_file_sync); - NODE_SET_METHOD(target, "importedCallback", imported_callback); + SassTypes::Factory::initExports(target); } NODE_MODULE(binding, RegisterModule); diff --git a/src/callback_bridge.h b/src/callback_bridge.h new file mode 100644 index 000000000..a83b9e1f1 --- /dev/null +++ b/src/callback_bridge.h @@ -0,0 +1,165 @@ +#ifndef CALLBACK_BRIDGE_H +#define CALLBACK_BRIDGE_H + +#include +#include +#include +#include + +#define COMMA , + +using namespace v8; + +template +class CallbackBridge { + public: + CallbackBridge(NanCallback*, bool); + virtual ~CallbackBridge(); + + // Executes the callback + T operator()(std::vector); + + protected: + // We will expose a bridge object to the JS callback that wraps this instance so we don't loose context. + // This is the V8 constructor for such objects. + static Handle get_wrapper_constructor(); + static NAN_METHOD(New); + static NAN_METHOD(ReturnCallback); + static Persistent wrapper_constructor; + Persistent wrapper; + + // The callback that will get called in the main thread after the worker thread used for the sass + // compilation step makes a call to uv_async_send() + static void dispatched_async_uv_callback(uv_async_t*); + + // The V8 values sent to our ReturnCallback must be read on the main thread not the sass worker thread. + // This gives a chance to specialized subclasses to transform those values into whatever makes sense to + // sass before we resume the worker thread. + virtual T post_process_return_value(Handle) const =0; + + + virtual std::vector> pre_process_args(std::vector) const =0; + + NanCallback* callback; + bool is_sync; + + std::mutex cv_mutex; + std::condition_variable condition_variable; + uv_async_t async; + std::vector argv; + bool has_returned; + T return_value; +}; + +template +Persistent CallbackBridge::wrapper_constructor; + +template +CallbackBridge::CallbackBridge(NanCallback* callback, bool is_sync) : callback(callback), is_sync(is_sync) { + // This assumes the main thread will be the one instantiating the bridge + if (!is_sync) { + uv_async_init(uv_default_loop(), &this->async, (uv_async_cb) dispatched_async_uv_callback); + this->async.data = (void*) this; + } + + NanAssignPersistent(wrapper, NanNew(CallbackBridge::get_wrapper_constructor())->NewInstance()); + NanSetInternalFieldPointer(NanNew(wrapper), 0, this); +} + +template +CallbackBridge::~CallbackBridge() { + delete this->callback; + NanDisposePersistent(this->wrapper); + + if (!is_sync) { + uv_close((uv_handle_t*)&this->async, NULL); + } +} + +template +T CallbackBridge::operator()(std::vector argv) { + // argv.push_back(wrapper); + + if (this->is_sync) { + std::vector> argv_v8 = pre_process_args(argv); + argv_v8.push_back(NanNew(wrapper)); + + return this->post_process_return_value( + NanNew(this->callback->Call(argv_v8.size(), &argv_v8[0])) + ); + } + + this->argv = argv; + + std::unique_lock lock(this->cv_mutex); + this->has_returned = false; + uv_async_send(&this->async); + this->condition_variable.wait(lock, [this] { return this->has_returned; }); + + return this->return_value; +} + +template +void CallbackBridge::dispatched_async_uv_callback(uv_async_t *req) { + CallbackBridge* bridge = static_cast(req->data); + + NanScope(); + TryCatch try_catch; + + std::vector> argv_v8 = bridge->pre_process_args(bridge->argv); + argv_v8.push_back(NanNew(bridge->wrapper)); + + NanNew(bridge->callback->Call(argv_v8.size(), &argv_v8[0])); + + if (try_catch.HasCaught()) { + node::FatalException(try_catch); + } +} + +template +NAN_METHOD(CallbackBridge::ReturnCallback) { + NanScope(); + + CallbackBridge* bridge = static_cast*>(NanGetInternalFieldPointer(args.This(), 0)); + TryCatch try_catch; + + bridge->return_value = bridge->post_process_return_value(args[0]); + + { + std::lock_guard lock(bridge->cv_mutex); + bridge->has_returned = true; + } + + bridge->condition_variable.notify_all(); + + if (try_catch.HasCaught()) { + node::FatalException(try_catch); + } + + NanReturnUndefined(); +} + +template +Handle CallbackBridge::get_wrapper_constructor() { + if (wrapper_constructor.IsEmpty()) { + Local tpl = NanNew(New); + tpl->SetClassName(NanNew("CallbackBridge")); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + tpl->PrototypeTemplate()->Set( + NanNew("success"), + NanNew(ReturnCallback)->GetFunction() + ); + + NanAssignPersistent(wrapper_constructor, tpl->GetFunction()); + } + + return NanNew(wrapper_constructor); +} + +template +NAN_METHOD(CallbackBridge::New) { + NanScope(); + NanReturnValue(args.This()); +} + +#endif diff --git a/src/create_string.cpp b/src/create_string.cpp new file mode 100644 index 000000000..ca62b9d9c --- /dev/null +++ b/src/create_string.cpp @@ -0,0 +1,15 @@ +#include +#include +#include "create_string.h" + + +char* create_string(Local value) { + if (value->IsNull() || !value->IsString()) { + return 0; + } + + String::Utf8Value string(value); + char *str = (char *)malloc(string.length() + 1); + strcpy(str, *string); + return str; +} diff --git a/src/create_string.h b/src/create_string.h new file mode 100644 index 000000000..f8df3aee1 --- /dev/null +++ b/src/create_string.h @@ -0,0 +1,10 @@ +#ifndef CREATE_STRING_H +#define CREATE_STRING_H + +#include + +using namespace v8; + +char* create_string(Local); + +#endif \ No newline at end of file diff --git a/src/custom_function_bridge.cpp b/src/custom_function_bridge.cpp new file mode 100644 index 000000000..bf1499517 --- /dev/null +++ b/src/custom_function_bridge.cpp @@ -0,0 +1,25 @@ +#include +#include "custom_function_bridge.h" +#include +#include "sass_types/factory.h" + + +Sass_Value* CustomFunctionBridge::post_process_return_value(Handle val) const { + try { + return SassTypes::Factory::unwrap(val)->get_sass_value(); + } catch (const std::invalid_argument& e) { + return sass_make_error(e.what()); + } +} + +std::vector> CustomFunctionBridge::pre_process_args(std::vector in) const { + std::vector> argv = std::vector>(); + + for (void* value : in) { + argv.push_back( + SassTypes::Factory::create(static_cast(value))->get_js_object() + ); + } + + return argv; +} diff --git a/src/custom_function_bridge.h b/src/custom_function_bridge.h new file mode 100644 index 000000000..cdc94118b --- /dev/null +++ b/src/custom_function_bridge.h @@ -0,0 +1,21 @@ +#ifndef CUSTOM_FUNCTION_BRIDGE_H +#define CUSTOM_FUNCTION_BRIDGE_H + +#include +#include "callback_bridge.h" +#include + + +using namespace v8; + + +class CustomFunctionBridge : public CallbackBridge { + public: + CustomFunctionBridge(NanCallback* cb, bool is_sync) : CallbackBridge(cb, is_sync) {} + + private: + Sass_Value* post_process_return_value(Handle) const; + std::vector> pre_process_args(std::vector) const; +}; + +#endif diff --git a/src/custom_importer_bridge.cpp b/src/custom_importer_bridge.cpp new file mode 100644 index 000000000..e9b001c7a --- /dev/null +++ b/src/custom_importer_bridge.cpp @@ -0,0 +1,55 @@ +#include +#include +#include "custom_importer_bridge.h" +#include "create_string.h" + + +SassImportList CustomImporterBridge::post_process_return_value(Handle val) const { + SassImportList imports; + NanScope(); + + Local returned_value = NanNew(val); + + if (returned_value->IsArray()) { + Handle array = Handle::Cast(returned_value); + + imports = sass_make_import_list(array->Length()); + + for (size_t i = 0; i < array->Length(); ++i) { + Local value = array->Get(static_cast(i)); + + if (!value->IsObject()) + continue; + + Local object = Local::Cast(value); + char* path = create_string(object->Get(NanNew("file"))); + char* contents = create_string(object->Get(NanNew("contents"))); + + imports[i] = sass_make_import_entry(path, (!contents || contents[0] == '\0') ? 0 : strdup(contents), 0); + } + } + else if (returned_value->IsObject()) { + imports = sass_make_import_list(1); + Local object = Local::Cast(returned_value); + char* path = create_string(object->Get(NanNew("file"))); + char* contents = create_string(object->Get(NanNew("contents"))); + + imports[0] = sass_make_import_entry(path, (!contents || contents[0] == '\0') ? 0 : strdup(contents), 0); + } + else { + imports = sass_make_import_list(1); + imports[0] = sass_make_import_entry((char const*) this->argv[0], 0, 0); + } + + return imports; +} + +std::vector> CustomImporterBridge::pre_process_args(std::vector in) const { + std::vector> out; + + for (void* ptr : in) { + out.push_back(NanNew((char const*) ptr)); + } + + return out; +} diff --git a/src/custom_importer_bridge.h b/src/custom_importer_bridge.h new file mode 100644 index 000000000..7824f831a --- /dev/null +++ b/src/custom_importer_bridge.h @@ -0,0 +1,24 @@ +#ifndef CUSTOM_IMPORTER_BRIDGE_H +#define CUSTOM_IMPORTER_BRIDGE_H + +#include +#include +#include "callback_bridge.h" + + +using namespace v8; + + +typedef Sass_Import** SassImportList; + + +class CustomImporterBridge : public CallbackBridge { + public: + CustomImporterBridge(NanCallback* cb, bool is_sync) : CallbackBridge(cb, is_sync) {} + + private: + SassImportList post_process_return_value(Handle) const; + std::vector> pre_process_args(std::vector) const; +}; + +#endif diff --git a/src/sass_context_wrapper.cpp b/src/sass_context_wrapper.cpp index 411f4ae26..0bb846f69 100644 --- a/src/sass_context_wrapper.cpp +++ b/src/sass_context_wrapper.cpp @@ -23,12 +23,7 @@ extern "C" { } sass_context_wrapper* sass_make_context_wrapper() { - sass_context_wrapper* ctx_w = (sass_context_wrapper*)calloc(1, sizeof(sass_context_wrapper)); - - ctx_w->importer_mutex = new std::mutex(); - ctx_w->importer_condition_variable = new std::condition_variable(); - - return ctx_w; + return (sass_context_wrapper*)calloc(1, sizeof(sass_context_wrapper)); } void sass_wrapper_dispose(struct sass_context_wrapper* ctx_w, char* string = 0) { @@ -39,20 +34,24 @@ extern "C" { sass_delete_file_context(ctx_w->fctx); } - delete ctx_w->file; - delete ctx_w->prev; delete ctx_w->error_callback; delete ctx_w->success_callback; - delete ctx_w->importer_callback; - - delete ctx_w->importer_mutex; - delete ctx_w->importer_condition_variable; NanDisposePersistent(ctx_w->result); if(string) { free(string); } + + if (!ctx_w->function_bridges.empty()) { + for (CustomFunctionBridge* bridge : ctx_w->function_bridges) { + delete bridge; + } + } + + if (ctx_w->importer_bridge) { + delete ctx_w->importer_bridge; + } } void sass_free_context_wrapper(sass_context_wrapper* ctx_w) { diff --git a/src/sass_context_wrapper.h b/src/sass_context_wrapper.h index ba412fcb4..6986cb076 100644 --- a/src/sass_context_wrapper.h +++ b/src/sass_context_wrapper.h @@ -1,7 +1,13 @@ -#include +#ifndef SASS_CONTEXT_WRAPPER +#define SASS_CONTEXT_WRAPPER + +#include #include +#include #include #include +#include "custom_function_bridge.h" +#include "custom_importer_bridge.h" #ifdef __cplusplus extern "C" { @@ -17,13 +23,8 @@ extern "C" { // binding related bool is_sync; void* cookie; - const char* prev; - const char* file; - std::mutex* importer_mutex; - std::condition_variable* importer_condition_variable; // libsass related - Sass_Import** imports; Sass_Data_Context* dctx; Sass_File_Context* fctx; @@ -35,7 +36,9 @@ extern "C" { Persistent result; NanCallback* error_callback; NanCallback* success_callback; - NanCallback* importer_callback; + + std::vector function_bridges; + CustomImporterBridge* importer_bridge; }; struct sass_context_wrapper* sass_make_context_wrapper(void); @@ -45,3 +48,5 @@ extern "C" { #ifdef __cplusplus } #endif + +#endif diff --git a/src/sass_types/boolean.cpp b/src/sass_types/boolean.cpp new file mode 100644 index 000000000..288515cde --- /dev/null +++ b/src/sass_types/boolean.cpp @@ -0,0 +1,76 @@ +#include +#include +#include "boolean.h" +#include "sass_value_wrapper.h" + + +using namespace v8; + + +namespace SassTypes +{ + Persistent Boolean::constructor; + bool Boolean::constructor_locked = false; + + Boolean::Boolean(bool v) : value(v) {} + + Boolean& Boolean::get_singleton(bool v) { + static Boolean instance_false(false), instance_true(true); + return v ? instance_true : instance_false; + } + + Handle Boolean::get_constructor() { + if (constructor.IsEmpty()) { + Local tpl = NanNew(New); + + tpl->SetClassName(NanNew("SassBoolean")); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + tpl->PrototypeTemplate()->Set(NanNew("getValue"), NanNew(GetValue)->GetFunction()); + + NanAssignPersistent(constructor, tpl->GetFunction()); + + NanAssignPersistent(get_singleton(false).js_object, NanNew(constructor)->NewInstance()); + NanSetInternalFieldPointer(NanNew(get_singleton(false).js_object), 0, &get_singleton(false)); + NanNew(constructor)->Set(NanNew("FALSE"), NanNew(get_singleton(false).js_object)); + + NanAssignPersistent(get_singleton(true).js_object, NanNew(constructor)->NewInstance()); + NanSetInternalFieldPointer(NanNew(get_singleton(true).js_object), 0, &get_singleton(true)); + NanNew(constructor)->Set(NanNew("TRUE"), NanNew(get_singleton(true).js_object)); + + constructor_locked = true; + } + + return NanNew(constructor); + } + + Sass_Value* Boolean::get_sass_value() { + return sass_make_boolean(value); + } + + Local Boolean::get_js_object() { + return NanNew(this->js_object); + } + + NAN_METHOD(Boolean::New) { + NanScope(); + + if (args.IsConstructCall()) { + if (constructor_locked) { + return NanThrowError(NanNew("Cannot instantiate SassBoolean")); + } + } else { + if (args.Length() != 1 || !args[0]->IsBoolean()) { + return NanThrowError(NanNew("Expected one boolean argument")); + } + + NanReturnValue(NanNew(get_singleton(args[0]->ToBoolean()->Value()).get_js_object())); + } + + NanReturnUndefined(); + } + + NAN_METHOD(Boolean::GetValue) { + NanScope(); + NanReturnValue(NanNew(static_cast(Factory::unwrap(args.This()))->value)); + } +} diff --git a/src/sass_types/boolean.h b/src/sass_types/boolean.h new file mode 100644 index 000000000..3a1e8d94f --- /dev/null +++ b/src/sass_types/boolean.h @@ -0,0 +1,36 @@ +#ifndef SASS_TYPES_BOOLEAN_H +#define SASS_TYPES_BOOLEAN_H + +#include +#include +#include "value.h" + + +namespace SassTypes +{ + using namespace v8; + + class Boolean : public Value { + public: + static Boolean& get_singleton(bool); + static Handle get_constructor(); + + Sass_Value* get_sass_value(); + Local get_js_object(); + + static NAN_METHOD(New); + static NAN_METHOD(GetValue); + + private: + Boolean(bool); + + bool value; + Persistent js_object; + + static Persistent constructor; + static bool constructor_locked; + }; +} + + +#endif diff --git a/src/sass_types/color.cpp b/src/sass_types/color.cpp new file mode 100644 index 000000000..1707fdb6c --- /dev/null +++ b/src/sass_types/color.cpp @@ -0,0 +1,139 @@ +#include +#include +#include "color.h" +#include "sass_value_wrapper.h" + +using namespace v8; + +namespace SassTypes +{ + Color::Color(Sass_Value* v) : SassValueWrapper(v) {} + + Sass_Value* Color::construct(const std::vector> raw_val) { + double a = 1.0, r = 0, g = 0, b = 0; + unsigned argb; + + switch (raw_val.size()) { + case 1: + if (!raw_val[0]->IsNumber()) { + throw std::invalid_argument("Only argument should be an integer."); + } + + argb = raw_val[0]->ToInt32()->Value(); + a = (double) ((argb >> 030) & 0xff) / 0xff; + r = (double) ((argb >> 020) & 0xff); + g = (double) ((argb >> 010) & 0xff); + b = (double) (argb & 0xff); + break; + + case 4: + if (!raw_val[3]->IsNumber()) { + throw std::invalid_argument("Constructor arguments should be numbers exclusively."); + } + + a = raw_val[3]->ToNumber()->Value(); + // fall through vvv + + case 3: + if (!raw_val[0]->IsNumber() || !raw_val[1]->IsNumber() || !raw_val[2]->IsNumber()) { + throw std::invalid_argument("Constructor arguments should be numbers exclusively."); + } + + r = raw_val[0]->ToNumber()->Value(); + g = raw_val[1]->ToNumber()->Value(); + b = raw_val[2]->ToNumber()->Value(); + break; + + case 0: + break; + + default: + throw std::invalid_argument("Constructor should be invoked with either 0, 1, 3 or 4 arguments."); + } + + return sass_make_color(r, g, b, a); + } + + void Color::initPrototype(Handle proto) { + proto->Set(NanNew("getR"), NanNew(GetR)->GetFunction()); + proto->Set(NanNew("getG"), NanNew(GetG)->GetFunction()); + proto->Set(NanNew("getB"), NanNew(GetB)->GetFunction()); + proto->Set(NanNew("getA"), NanNew(GetA)->GetFunction()); + proto->Set(NanNew("setR"), NanNew(SetR)->GetFunction()); + proto->Set(NanNew("setG"), NanNew(SetG)->GetFunction()); + proto->Set(NanNew("setB"), NanNew(SetB)->GetFunction()); + proto->Set(NanNew("setA"), NanNew(SetA)->GetFunction()); + } + + NAN_METHOD(Color::GetR) { + NanScope(); + NanReturnValue(NanNew(sass_color_get_r(unwrap(args.This())->value))); + } + + NAN_METHOD(Color::GetG) { + NanScope(); + NanReturnValue(NanNew(sass_color_get_g(unwrap(args.This())->value))); + } + + NAN_METHOD(Color::GetB) { + NanScope(); + NanReturnValue(NanNew(sass_color_get_b(unwrap(args.This())->value))); + } + + NAN_METHOD(Color::GetA) { + NanScope(); + NanReturnValue(NanNew(sass_color_get_a(unwrap(args.This())->value))); + } + + NAN_METHOD(Color::SetR) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied value should be a number")); + } + + sass_color_set_r(unwrap(args.This())->value, args[0]->ToNumber()->Value()); + NanReturnUndefined(); + } + + NAN_METHOD(Color::SetG) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied value should be a number")); + } + + sass_color_set_g(unwrap(args.This())->value, args[0]->ToNumber()->Value()); + NanReturnUndefined(); + } + + NAN_METHOD(Color::SetB) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied value should be a number")); + } + + sass_color_set_b(unwrap(args.This())->value, args[0]->ToNumber()->Value()); + NanReturnUndefined(); + } + + NAN_METHOD(Color::SetA) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied value should be a number")); + } + + sass_color_set_a(unwrap(args.This())->value, args[0]->ToNumber()->Value()); + NanReturnUndefined(); + } +} diff --git a/src/sass_types/color.h b/src/sass_types/color.h new file mode 100644 index 000000000..558667a3b --- /dev/null +++ b/src/sass_types/color.h @@ -0,0 +1,33 @@ +#ifndef SASS_TYPES_COLOR_H +#define SASS_TYPES_COLOR_H + +#include +#include +#include "sass_value_wrapper.h" + + +namespace SassTypes +{ + using namespace v8; + + class Color : public SassValueWrapper { + public: + Color(Sass_Value*); + static char const* get_constructor_name() { return "SassColor"; } + static Sass_Value* construct(const std::vector>); + + static void initPrototype(Handle); + + static NAN_METHOD(GetR); + static NAN_METHOD(GetG); + static NAN_METHOD(GetB); + static NAN_METHOD(GetA); + static NAN_METHOD(SetR); + static NAN_METHOD(SetG); + static NAN_METHOD(SetB); + static NAN_METHOD(SetA); + }; +} + + +#endif diff --git a/src/sass_types/error.cpp b/src/sass_types/error.cpp new file mode 100644 index 000000000..de62145bf --- /dev/null +++ b/src/sass_types/error.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "error.h" +#include "../create_string.h" +#include "sass_value_wrapper.h" + +using namespace v8; + +namespace SassTypes +{ + Error::Error(Sass_Value* v) : SassValueWrapper(v) {} + + Sass_Value* Error::construct(const std::vector> raw_val) { + char const* value = ""; + + if (raw_val.size() >= 1) { + if (!raw_val[0]->IsString()) { + throw std::invalid_argument("Argument should be a string."); + } + + value = create_string(raw_val[0]); + } + + return sass_make_error(value); + } + + void Error::initPrototype(Handle) {} +} diff --git a/src/sass_types/error.h b/src/sass_types/error.h new file mode 100644 index 000000000..bd63b94d5 --- /dev/null +++ b/src/sass_types/error.h @@ -0,0 +1,23 @@ +#ifndef SASS_TYPES_ERROR_H +#define SASS_TYPES_ERROR_H + +#include +#include +#include "sass_value_wrapper.h" + + +namespace SassTypes +{ + using namespace v8; + + class Error : public SassValueWrapper { + public: + Error(Sass_Value*); + static char const* get_constructor_name() { return "SassError"; } + static Sass_Value* construct(const std::vector>); + + static void initPrototype(Handle); + }; +} + +#endif diff --git a/src/sass_types/factory.cpp b/src/sass_types/factory.cpp new file mode 100644 index 000000000..4f80dc07f --- /dev/null +++ b/src/sass_types/factory.cpp @@ -0,0 +1,71 @@ +#include +#include "factory.h" +#include +#include "value.h" +#include "number.h" +#include "string.h" +#include "color.h" +#include "boolean.h" +#include "list.h" +#include "map.h" +#include "null.h" +#include "error.h" + +using namespace v8; + +namespace SassTypes +{ + Value* Factory::create(Sass_Value* v) { + switch (sass_value_get_tag(v)) { + case SASS_NUMBER: + return new Number(v); + + case SASS_STRING: + return new String(v); + + case SASS_COLOR: + return new Color(v); + + case SASS_BOOLEAN: + return &Boolean::get_singleton(sass_boolean_get_value(v)); + + case SASS_LIST: + return new List(v); + + case SASS_MAP: + return new Map(v); + + case SASS_NULL: + return &Null::get_singleton(); + + case SASS_ERROR: + return new Error(v); + + default: + throw std::invalid_argument("Unknown type encountered."); + } + } + + void Factory::initExports(Handle exports) { + Local types = NanNew(); + exports->Set(NanNew("types"), types); + + types->Set(NanNew("Number"), Number::get_constructor()); + types->Set(NanNew("String"), String::get_constructor()); + types->Set(NanNew("Color"), Color::get_constructor()); + types->Set(NanNew("Boolean"), Boolean::get_constructor()); + types->Set(NanNew("List"), List::get_constructor()); + types->Set(NanNew("Map"), Map::get_constructor()); + types->Set(NanNew("Null"), Null::get_constructor()); + types->Set(NanNew("Error"), Error::get_constructor()); + } + + Value* Factory::unwrap(Handle obj) { + // Todo: non-SassValue objects could easily fall under that condition, need to be more specific. + if (!obj->IsObject() || obj->ToObject()->InternalFieldCount() != 1) { + throw std::invalid_argument("A SassValue object was expected."); + } + + return static_cast(NanGetInternalFieldPointer(obj->ToObject(), 0)); + } +} diff --git a/src/sass_types/factory.h b/src/sass_types/factory.h new file mode 100644 index 000000000..25ef15219 --- /dev/null +++ b/src/sass_types/factory.h @@ -0,0 +1,22 @@ +#ifndef SASS_TYPES_FACTORY_H +#define SASS_TYPES_FACTORY_H + +#include +#include +#include "value.h" + +namespace SassTypes +{ + using namespace v8; + + // This is the guru that knows everything about instantiating the right subclass of SassTypes::Value + // to wrap a given Sass_Value object. + class Factory { + public: + static void initExports(Handle); + static Value* create(Sass_Value*); + static Value* unwrap(Handle); + }; +} + +#endif diff --git a/src/sass_types/list.cpp b/src/sass_types/list.cpp new file mode 100644 index 000000000..9e8b93c73 --- /dev/null +++ b/src/sass_types/list.cpp @@ -0,0 +1,105 @@ +#include +#include +#include "list.h" +#include "sass_value_wrapper.h" + +using namespace v8; + +namespace SassTypes +{ + List::List(Sass_Value* v) : SassValueWrapper(v) {} + + Sass_Value* List::construct(const std::vector> raw_val) { + size_t length = 0; + bool comma = true; + + if (raw_val.size() >= 1) { + if (!raw_val[0]->IsNumber()) { + throw std::invalid_argument("First argument should be an integer."); + } + + length = raw_val[0]->ToInt32()->Value(); + + if (raw_val.size() >= 2) { + if (!raw_val[1]->IsBoolean()) { + throw std::invalid_argument("Second argument should be a boolean."); + } + + comma = raw_val[1]->ToBoolean()->Value(); + } + } + + return sass_make_list(length, comma ? SASS_COMMA : SASS_SPACE); + } + + void List::initPrototype(Handle proto) { + proto->Set(NanNew("getLength"), NanNew(GetLength)->GetFunction()); + proto->Set(NanNew("getSeparator"), NanNew(GetSeparator)->GetFunction()); + proto->Set(NanNew("setSeparator"), NanNew(SetSeparator)->GetFunction()); + proto->Set(NanNew("getValue"), NanNew(GetValue)->GetFunction()); + proto->Set(NanNew("setValue"), NanNew(SetValue)->GetFunction()); + } + + NAN_METHOD(List::GetValue) { + NanScope(); + + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied index should be an integer")); + } + + Sass_Value* list = unwrap(args.This())->value; + size_t index = args[0]->ToInt32()->Value(); + + + if (index >= sass_list_get_length(list)) { + return NanThrowError(NanNew("Out of bound index")); + } + + NanReturnValue(Factory::create(sass_list_get_value(list, args[0]->ToInt32()->Value()))->get_js_object()); + } + + NAN_METHOD(List::SetValue) { + if (args.Length() != 2) { + return NanThrowError(NanNew("Expected two arguments")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied index should be an integer")); + } + + if (!args[1]->IsObject()) { + return NanThrowError(NanNew("Supplied value should be a SassValue object")); + } + + Value* sass_value = Factory::unwrap(args[1]); + sass_list_set_value(unwrap(args.This())->value, args[0]->ToInt32()->Value(), sass_value->get_sass_value()); + NanReturnUndefined(); + } + + NAN_METHOD(List::GetSeparator) { + NanScope(); + NanReturnValue(NanNew(sass_list_get_separator(unwrap(args.This())->value) == SASS_COMMA)); + } + + NAN_METHOD(List::SetSeparator) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsBoolean()) { + return NanThrowError(NanNew("Supplied value should be a boolean")); + } + + sass_list_set_separator(unwrap(args.This())->value, args[0]->ToBoolean()->Value() ? SASS_COMMA : SASS_SPACE); + NanReturnUndefined(); + } + + NAN_METHOD(List::GetLength) { + NanScope(); + NanReturnValue(NanNew(sass_list_get_length(unwrap(args.This())->value))); + } +} diff --git a/src/sass_types/list.h b/src/sass_types/list.h new file mode 100644 index 000000000..55c60228b --- /dev/null +++ b/src/sass_types/list.h @@ -0,0 +1,29 @@ +#ifndef SASS_TYPES_LIST_H +#define SASS_TYPES_LIST_H + +#include +#include +#include "sass_value_wrapper.h" + + +namespace SassTypes +{ + using namespace v8; + + class List : public SassValueWrapper { + public: + List(Sass_Value*); + static char const* get_constructor_name() { return "SassList"; } + static Sass_Value* construct(const std::vector>); + + static void initPrototype(Handle); + + static NAN_METHOD(GetValue); + static NAN_METHOD(SetValue); + static NAN_METHOD(GetSeparator); + static NAN_METHOD(SetSeparator); + static NAN_METHOD(GetLength); + }; +} + +#endif diff --git a/src/sass_types/map.cpp b/src/sass_types/map.cpp new file mode 100644 index 000000000..046587431 --- /dev/null +++ b/src/sass_types/map.cpp @@ -0,0 +1,118 @@ +#include +#include +#include "map.h" +#include "sass_value_wrapper.h" + +using namespace v8; + +namespace SassTypes +{ + Map::Map(Sass_Value* v) : SassValueWrapper(v) {} + + Sass_Value* Map::construct(const std::vector> raw_val) { + size_t length = 0; + + if (raw_val.size() >= 1) { + if (!raw_val[0]->IsNumber()) { + throw std::invalid_argument("First argument should be an integer."); + } + + length = raw_val[0]->ToInt32()->Value(); + } + + return sass_make_map(length); + } + + void Map::initPrototype(Handle proto) { + proto->Set(NanNew("getLength"), NanNew(GetLength)->GetFunction()); + proto->Set(NanNew("getKey"), NanNew(GetKey)->GetFunction()); + proto->Set(NanNew("setKey"), NanNew(SetKey)->GetFunction()); + proto->Set(NanNew("getValue"), NanNew(GetValue)->GetFunction()); + proto->Set(NanNew("setValue"), NanNew(SetValue)->GetFunction()); + } + + NAN_METHOD(Map::GetValue) { + NanScope(); + + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied index should be an integer")); + } + + Sass_Value* map = unwrap(args.This())->value; + size_t index = args[0]->ToInt32()->Value(); + + + if (index >= sass_map_get_length(map)) { + return NanThrowError(NanNew("Out of bound index")); + } + + NanReturnValue(NanNew(Factory::create(sass_map_get_value(map, args[0]->ToInt32()->Value()))->get_js_object())); + } + + NAN_METHOD(Map::SetValue) { + if (args.Length() != 2) { + return NanThrowError(NanNew("Expected two arguments")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied index should be an integer")); + } + + if (!args[1]->IsObject()) { + return NanThrowError(NanNew("Supplied value should be a SassValue object")); + } + + Value* sass_value = Factory::unwrap(args[1]); + sass_map_set_value(unwrap(args.This())->value, args[0]->ToInt32()->Value(), sass_value->get_sass_value()); + NanReturnUndefined(); + } + + NAN_METHOD(Map::GetKey) { + NanScope(); + + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied index should be an integer")); + } + + Sass_Value* map = unwrap(args.This())->value; + size_t index = args[0]->ToInt32()->Value(); + + + if (index >= sass_map_get_length(map)) { + return NanThrowError(NanNew("Out of bound index")); + } + + NanReturnValue(Factory::create(sass_map_get_key(map, args[0]->ToInt32()->Value()))->get_js_object()); + } + + NAN_METHOD(Map::SetKey) { + if (args.Length() != 2) { + return NanThrowError(NanNew("Expected two arguments")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied index should be an integer")); + } + + if (!args[1]->IsObject()) { + return NanThrowError(NanNew("Supplied value should be a SassValue object")); + } + + Value* sass_value = Factory::unwrap(args[1]); + sass_map_set_key(unwrap(args.This())->value, args[0]->ToInt32()->Value(), sass_value->get_sass_value()); + NanReturnUndefined(); + } + + NAN_METHOD(Map::GetLength) { + NanScope(); + NanReturnValue(NanNew(sass_map_get_length(unwrap(args.This())->value))); + } +} diff --git a/src/sass_types/map.h b/src/sass_types/map.h new file mode 100644 index 000000000..1ec5bf88c --- /dev/null +++ b/src/sass_types/map.h @@ -0,0 +1,29 @@ +#ifndef SASS_TYPES_MAP_H +#define SASS_TYPES_MAP_H + +#include +#include +#include "sass_value_wrapper.h" + + +namespace SassTypes +{ + using namespace v8; + + class Map : public SassValueWrapper { + public: + Map(Sass_Value*); + static char const* get_constructor_name() { return "SassMap"; } + static Sass_Value* construct(const std::vector>); + + static void initPrototype(Handle); + + static NAN_METHOD(GetValue); + static NAN_METHOD(SetValue); + static NAN_METHOD(GetKey); + static NAN_METHOD(SetKey); + static NAN_METHOD(GetLength); + }; +} + +#endif diff --git a/src/sass_types/null.cpp b/src/sass_types/null.cpp new file mode 100644 index 000000000..b700c5d83 --- /dev/null +++ b/src/sass_types/null.cpp @@ -0,0 +1,68 @@ +#include +#include +#include "null.h" +#include "sass_value_wrapper.h" + + +using namespace v8; + + +namespace SassTypes +{ + Persistent Null::constructor; + bool Null::constructor_locked = false; + + + Null::Null() {} + + + Null& Null::get_singleton() { + static Null singleton_instance; + return singleton_instance; + } + + + Handle Null::get_constructor() { + if (constructor.IsEmpty()) { + Local tpl = NanNew(New); + + tpl->SetClassName(NanNew("SassNull")); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + NanAssignPersistent(constructor, tpl->GetFunction()); + + NanAssignPersistent(get_singleton().js_object, NanNew(constructor)->NewInstance()); + NanSetInternalFieldPointer(NanNew(get_singleton().js_object), 0, &get_singleton()); + NanNew(constructor)->Set(NanNew("NULL"), NanNew(get_singleton().js_object)); + + constructor_locked = true; + } + + return NanNew(constructor); + } + + + Sass_Value* Null::get_sass_value() { + return sass_make_null(); + } + + + Local Null::get_js_object() { + return NanNew(this->js_object); + } + + + NAN_METHOD(Null::New) { + NanScope(); + + if (args.IsConstructCall()) { + if (constructor_locked) { + return NanThrowError(NanNew("Cannot instantiate SassNull")); + } + } else { + NanReturnValue(NanNew(get_singleton().get_js_object())); + } + + NanReturnUndefined(); + } +} diff --git a/src/sass_types/null.h b/src/sass_types/null.h new file mode 100644 index 000000000..87199432f --- /dev/null +++ b/src/sass_types/null.h @@ -0,0 +1,34 @@ +#ifndef SASS_TYPES_NULL_H +#define SASS_TYPES_NULL_H + +#include +#include +#include "value.h" + + +namespace SassTypes +{ + using namespace v8; + + class Null : public Value { + public: + static Null& get_singleton(); + static Handle get_constructor(); + + Sass_Value* get_sass_value(); + Local get_js_object(); + + static NAN_METHOD(New); + + private: + Null(); + + Persistent js_object; + + static Persistent constructor; + static bool constructor_locked; + }; +} + + +#endif diff --git a/src/sass_types/number.cpp b/src/sass_types/number.cpp new file mode 100644 index 000000000..a8785874c --- /dev/null +++ b/src/sass_types/number.cpp @@ -0,0 +1,80 @@ +#include +#include +#include "number.h" +#include "../create_string.h" +#include "sass_value_wrapper.h" + +using namespace v8; + +namespace SassTypes +{ + Number::Number(Sass_Value* v) : SassValueWrapper(v) {} + + Sass_Value* Number::construct(const std::vector> raw_val) { + double value = 0; + char const* unit = ""; + + if (raw_val.size() >= 1) { + if (!raw_val[0]->IsNumber()) { + throw std::invalid_argument("First argument should be a number."); + } + + value = raw_val[0]->ToNumber()->Value(); + + if (raw_val.size() >= 2) { + if (!raw_val[1]->IsString()) { + throw std::invalid_argument("Second argument should be a string."); + } + + unit = create_string(raw_val[1]); + } + } + + return sass_make_number(value, unit); + } + + void Number::initPrototype(Handle proto) { + proto->Set(NanNew("getValue"), NanNew(GetValue)->GetFunction()); + proto->Set(NanNew("getUnit"), NanNew(GetUnit)->GetFunction()); + proto->Set(NanNew("setValue"), NanNew(SetValue)->GetFunction()); + proto->Set(NanNew("setUnit"), NanNew(SetUnit)->GetFunction()); + } + + NAN_METHOD(Number::GetValue) { + NanScope(); + NanReturnValue(NanNew(sass_number_get_value(unwrap(args.This())->value))); + } + + NAN_METHOD(Number::GetUnit) { + NanScope(); + NanReturnValue(NanNew(sass_number_get_unit(unwrap(args.This())->value))); + } + + NAN_METHOD(Number::SetValue) { + NanScope(); + + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsNumber()) { + return NanThrowError(NanNew("Supplied value should be a number")); + } + + sass_number_set_value(unwrap(args.This())->value, args[0]->ToNumber()->Value()); + NanReturnUndefined(); + } + + NAN_METHOD(Number::SetUnit) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsString()) { + return NanThrowError(NanNew("Supplied value should be a string")); + } + + sass_number_set_unit(unwrap(args.This())->value, create_string(args[0])); + NanReturnUndefined(); + } +} diff --git a/src/sass_types/number.h b/src/sass_types/number.h new file mode 100644 index 000000000..c83603146 --- /dev/null +++ b/src/sass_types/number.h @@ -0,0 +1,28 @@ +#ifndef SASS_TYPES_NUMBER_H +#define SASS_TYPES_NUMBER_H + +#include +#include +#include "sass_value_wrapper.h" + + +namespace SassTypes +{ + using namespace v8; + + class Number : public SassValueWrapper { + public: + Number(Sass_Value*); + static char const* get_constructor_name() { return "SassNumber"; } + static Sass_Value* construct(const std::vector>); + + static void initPrototype(Handle); + + static NAN_METHOD(GetValue); + static NAN_METHOD(GetUnit); + static NAN_METHOD(SetValue); + static NAN_METHOD(SetUnit); + }; +} + +#endif diff --git a/src/sass_types/sass_value_wrapper.h b/src/sass_types/sass_value_wrapper.h new file mode 100644 index 000000000..36b879fcb --- /dev/null +++ b/src/sass_types/sass_value_wrapper.h @@ -0,0 +1,134 @@ +#ifndef SASS_TYPES_SASS_VALUE_WRAPPER_H +#define SASS_TYPES_SASS_VALUE_WRAPPER_H + +#include +#include +#include +#include +#include "value.h" +#include "factory.h" + +namespace SassTypes +{ + using namespace v8; + + // Include this in any SassTypes::Value subclasses to handle all the heavy lifting of constructing JS + // objects and wrapping sass values inside them + template + class SassValueWrapper : public Value { + public: + static char const* get_constructor_name() { return "SassValue"; } + + SassValueWrapper(Sass_Value*); + virtual ~SassValueWrapper(); + + Sass_Value* get_sass_value(); + Local get_js_object(); + + static Handle get_constructor(); + static Local get_constructor_template(); + static NAN_METHOD(New); + + protected: + Sass_Value* value; + static T* unwrap(Local); + + private: + static Persistent constructor; + Persistent js_object; + }; + + template + Persistent SassValueWrapper::constructor; + + template + SassValueWrapper::SassValueWrapper(Sass_Value* v) { + this->value = sass_clone_value(v); + } + + template + SassValueWrapper::~SassValueWrapper() { + NanDisposePersistent(this->js_object); + sass_delete_value(this->value); + } + + template + Sass_Value* SassValueWrapper::get_sass_value() { + return sass_clone_value(this->value); + } + + template + Local SassValueWrapper::get_js_object() { + if (this->js_object.IsEmpty()) { + Local wrapper = NanNew(T::get_constructor())->NewInstance(); + delete static_cast(NanGetInternalFieldPointer(wrapper, 0)); + NanSetInternalFieldPointer(wrapper, 0, this); + NanAssignPersistent(this->js_object, wrapper); + } + + return NanNew(this->js_object); + } + + template + Local SassValueWrapper::get_constructor_template() { + Local tpl = NanNew(New); + tpl->SetClassName(NanNew(NanNew(T::get_constructor_name()))); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + T::initPrototype(tpl->PrototypeTemplate()); + + return tpl; + } + + template + Handle SassValueWrapper::get_constructor() { + if (constructor.IsEmpty()) { + NanAssignPersistent(constructor, T::get_constructor_template()->GetFunction()); + } + + return NanNew(constructor); + } + + template + NAN_METHOD(SassValueWrapper::New) { + NanScope(); + + if (!args.IsConstructCall()) { + unsigned argc = args.Length(); + std::vector> argv; + + argv.reserve(argc); + for (unsigned i = 0; i < argc; i++) { + argv.push_back(args[i]); + } + + NanReturnValue(NanNew(T::get_constructor())->NewInstance(argc, &argv[0])); + } + + std::vector> localArgs(args.Length()); + + for (auto i = 0; i < args.Length(); ++i) { + localArgs[i] = args[i]; + } + + try { + Sass_Value* value = T::construct(localArgs); + T* obj = new T(value); + sass_delete_value(value); + + NanSetInternalFieldPointer(args.This(), 0, obj); + NanAssignPersistent(obj->js_object, args.This()); + } catch (const std::exception& e) { + return NanThrowError(NanNew(e.what())); + } + + NanReturnUndefined(); + } + + template + T* SassValueWrapper::unwrap(Local obj) { + return static_cast(Factory::unwrap(obj)); + } +} + + +#endif diff --git a/src/sass_types/string.cpp b/src/sass_types/string.cpp new file mode 100644 index 000000000..0cdb9f339 --- /dev/null +++ b/src/sass_types/string.cpp @@ -0,0 +1,49 @@ +#include +#include +#include "string.h" +#include "../create_string.h" +#include "sass_value_wrapper.h" + +using namespace v8; + +namespace SassTypes +{ + String::String(Sass_Value* v) : SassValueWrapper(v) {} + + Sass_Value* String::construct(const std::vector> raw_val) { + char const* value = ""; + + if (raw_val.size() >= 1) { + if (!raw_val[0]->IsString()) { + throw std::invalid_argument("Argument should be a string."); + } + + value = create_string(raw_val[0]); + } + + return sass_make_string(value); + } + + void String::initPrototype(Handle proto) { + proto->Set(NanNew("getValue"), NanNew(GetValue)->GetFunction()); + proto->Set(NanNew("setValue"), NanNew(SetValue)->GetFunction()); + } + + NAN_METHOD(String::GetValue) { + NanScope(); + NanReturnValue(NanNew(sass_string_get_value(unwrap(args.This())->value))); + } + + NAN_METHOD(String::SetValue) { + if (args.Length() != 1) { + return NanThrowError(NanNew("Expected just one argument")); + } + + if (!args[0]->IsString()) { + return NanThrowError(NanNew("Supplied value should be a string")); + } + + sass_string_set_value(unwrap(args.This())->value, create_string(args[0])); + NanReturnUndefined(); + } +} diff --git a/src/sass_types/string.h b/src/sass_types/string.h new file mode 100644 index 000000000..49eab5335 --- /dev/null +++ b/src/sass_types/string.h @@ -0,0 +1,26 @@ +#ifndef SASS_TYPES_STRING_H +#define SASS_TYPES_STRING_H + +#include +#include +#include "sass_value_wrapper.h" + + +namespace SassTypes +{ + using namespace v8; + + class String : public SassValueWrapper { + public: + String(Sass_Value*); + static char const* get_constructor_name() { return "SassString"; } + static Sass_Value* construct(const std::vector>); + + static void initPrototype(Handle); + + static NAN_METHOD(GetValue); + static NAN_METHOD(SetValue); + }; +} + +#endif diff --git a/src/sass_types/value.h b/src/sass_types/value.h new file mode 100644 index 000000000..3053f53e7 --- /dev/null +++ b/src/sass_types/value.h @@ -0,0 +1,21 @@ +#ifndef SASS_TYPES_VALUE_H +#define SASS_TYPES_VALUE_H + +#include +#include + + +namespace SassTypes +{ + using namespace v8; + + // This is the interface that all sass values must comply with + class Value { + public: + virtual Sass_Value* get_sass_value() =0; + virtual Local get_js_object() =0; + }; +} + + +#endif diff --git a/test/api.js b/test/api.js index d96992f86..c0fab9cdc 100644 --- a/test/api.js +++ b/test/api.js @@ -356,6 +356,566 @@ describe('api', function() { }); }); + describe('.render(functions)', function() { + it('should call custom defined nullary function', function(done) { + sass.render({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + return new sass.types.Number(42, 'px'); + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: 42px; }'); + done(); + }); + }); + + it('should call custom function with multiple args', function(done) { + sass.render({ + data: 'div { color: foo(3, 42px); }', + functions: { + 'foo($a, $b)': function(factor, size) { + return new sass.types.Number(factor.getValue() * size.getValue(), size.getUnit()); + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: 126px; }'); + done(); + }); + }); + + it('should work with custom functions that return data asynchronously', function(done) { + sass.render({ + data: 'div { color: foo(42px); }', + functions: { + 'foo($a)': function(size, done) { + setTimeout(function() { + done(new sass.types.Number(66, 'em')); + }, 50); + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: 66em; }'); + done(); + }); + }); + + it('should let custom functions call setter methods on wrapped sass values (number)', function(done) { + sass.render({ + data: 'div { width: foo(42px); height: bar(42px); }', + functions: { + 'foo($a)': function(size) { + size.setUnit('rem'); + return size; + }, + 'bar($a)': function(size) { + size.setValue(size.getValue() * 2); + return size; + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n width: 42rem;\n height: 84px; }'); + done(); + }); + }); + + it('should properly convert strings when calling custom functions', function(done) { + sass.render({ + data: 'div { color: foo("bar"); }', + functions: { + 'foo($a)': function(str) { + str = str.getValue().replace(/['"]/g, ''); + return new sass.types.String('"' + str + str + '"'); + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: "barbar"; }'); + done(); + }); + }); + + it('should let custom functions call setter methods on wrapped sass values (string)', function(done) { + sass.render({ + data: 'div { width: foo("bar"); }', + functions: { + 'foo($a)': function(str) { + var unquoted = str.getValue().replace(/['"]/g, ''); + str.setValue('"' + unquoted + unquoted + '"'); + return str; + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n width: "barbar"; }'); + done(); + }); + }); + + it('should properly convert colors when calling custom functions', function(done) { + sass.render({ + data: 'div { color: foo(#f00); background-color: bar(); border-color: baz(); }', + functions: { + 'foo($a)': function(color) { + assert.equal(color.getR(), 255); + assert.equal(color.getG(), 0); + assert.equal(color.getB(), 0); + assert.equal(color.getA(), 1.0); + + return new sass.types.Color(255, 255, 0, 0.5); + }, + 'bar()': function() { + return new sass.types.Color(0x33ff00ff); + }, + 'baz()': function() { + return new sass.types.Color(0xffff0000); + } + } + }, function(error, result) { + assert.equal( + result.css.toString().trim(), + 'div {\n color: rgba(255, 255, 0, 0.5);' + + '\n background-color: rgba(255, 0, 255, 0.2);' + + '\n border-color: red; }' + ); + done(); + }); + }); + + it('should properly convert boolean when calling custom functions', function(done) { + sass.render({ + data: 'div { color: if(foo(true, false), #fff, #000);' + + '\n background-color: if(foo(true, true), #fff, #000); }', + functions: { + 'foo($a, $b)': function(a, b) { + return sass.types.Boolean(a.getValue() && b.getValue()); + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: #000;\n background-color: #fff; }'); + done(); + }); + }); + + it('should let custom functions call setter methods on wrapped sass values (boolean)', function(done) { + sass.render({ + data: 'div { color: if(foo(false), #fff, #000); background-color: if(foo(true), #fff, #000); }', + functions: { + 'foo($a)': function(a) { + return a.getValue() ? sass.types.Boolean.FALSE : sass.types.Boolean.TRUE; + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: #fff;\n background-color: #000; }'); + done(); + }); + }); + + it('should properly convert lists when calling custom functions', function(done) { + sass.render({ + data: '$test-list: (bar, #f00, 123em); @each $item in foo($test-list) { .#{$item} { color: #fff; } }', + functions: { + 'foo($l)': function(list) { + assert.equal(list.getLength(), 3); + assert.ok(list.getValue(0) instanceof sass.types.String); + assert.equal(list.getValue(0).getValue(), 'bar'); + assert.ok(list.getValue(1) instanceof sass.types.Color); + assert.equal(list.getValue(1).getR(), 0xff); + assert.equal(list.getValue(1).getG(), 0); + assert.equal(list.getValue(1).getB(), 0); + assert.ok(list.getValue(2) instanceof sass.types.Number); + assert.equal(list.getValue(2).getValue(), 123); + assert.equal(list.getValue(2).getUnit(), 'em'); + + var out = new sass.types.List(3); + out.setValue(0, new sass.types.String('foo')); + out.setValue(1, new sass.types.String('bar')); + out.setValue(2, new sass.types.String('baz')); + return out; + } + } + }, function(error, result) { + assert.equal( + result.css.toString().trim(), + '.foo {\n color: #fff; }\n\n.bar {\n color: #fff; }\n\n.baz {\n color: #fff; }' + ); + done(); + }); + }); + + it('should properly convert maps when calling custom functions', function(done) { + sass.render({ + data: '$test-map: foo((abc: 123, #def: true)); div { color: if(map-has-key($test-map, hello), #fff, #000); }' + + 'span { color: map-get($test-map, baz); }', + functions: { + 'foo($m)': function(map) { + assert.equal(map.getLength(), 2); + assert.ok(map.getKey(0) instanceof sass.types.String); + assert.ok(map.getKey(1) instanceof sass.types.Color); + assert.ok(map.getValue(0) instanceof sass.types.Number); + assert.ok(map.getValue(1) instanceof sass.types.Boolean); + assert.equal(map.getKey(0).getValue(), 'abc'); + assert.equal(map.getValue(0).getValue(), 123); + assert.equal(map.getKey(1).getR(), 0xdd); + assert.equal(map.getValue(1).getValue(), true); + + var out = new sass.types.Map(3); + out.setKey(0, new sass.types.String('hello')); + out.setValue(0, new sass.types.String('world')); + out.setKey(1, new sass.types.String('foo')); + out.setValue(1, new sass.types.String('bar')); + out.setKey(2, new sass.types.String('baz')); + out.setValue(2, new sass.types.String('qux')); + return out; + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: #fff; }\n\nspan {\n color: qux; }'); + done(); + }); + }); + + it('should properly convert null when calling custom functions', function(done) { + sass.render({ + data: 'div { color: if(foo("bar"), #fff, #000); } ' + + 'span { color: if(foo(null), #fff, #000); }' + + 'table { color: if(bar() == null, #fff, #000); }', + functions: { + 'foo($a)': function(a) { + return sass.types.Boolean(a instanceof sass.types.Null); + }, + 'bar()': function() { + return sass.NULL; + } + } + }, function(error, result) { + assert.equal( + result.css.toString().trim(), + 'div {\n color: #000; }\n\nspan {\n color: #fff; }\n\ntable {\n color: #fff; }' + ); + done(); + }); + }); + + it('should be possible to carry sass values across different renders', function(done) { + var persistentMap; + + sass.render({ + data: 'div { color: foo((abc: #112233, #ddeeff: true)); }', + functions: { + foo: function(m) { + persistentMap = m; + return sass.types.Color(0, 0, 0); + } + } + }, function() { + sass.render({ + data: 'div { color: map-get(bar(), abc); background-color: baz(); }', + functions: { + bar: function() { + return persistentMap; + }, + baz: function() { + return persistentMap.getKey(1); + } + } + }, function(errror, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: #112233;\n background-color: #ddeeff; }'); + done(); + }); + }); + }); + + it('should let us register custom functions without signatures', function(done) { + sass.render({ + data: 'div { color: foo(20, 22); }', + functions: { + foo: function(a, b) { + return new sass.types.Number(a.getValue() + b.getValue(), 'em'); + } + } + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }'); + done(); + }); + }); + + it('should fail when returning anything other than a sass value from a custom function', function(done) { + sass.render({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + return {}; + } + } + }, function(error) { + assert.ok(/A SassValue object was expected/.test(error.message)); + done(); + }); + }); + + it('should properly bubble up standard JS errors thrown by custom functions', function(done) { + sass.render({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + throw new RangeError('This is a test error'); + } + } + }, function(error) { + assert.ok(/This is a test error/.test(error.message)); + done(); + }); + }); + + it('should properly bubble up unknown errors thrown by custom functions', function(done) { + sass.render({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + throw {}; + } + } + }, function(error) { + assert.ok(/unexpected error/.test(error.message)); + done(); + }); + }); + + it('should properly bubble up errors from sass value constructors', function(done) { + sass.render({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + return sass.types.Boolean('foo'); + } + } + }, function(error) { + assert.ok(/Expected one boolean argument/.test(error.message)); + done(); + }); + }); + + it('should properly bubble up errors from sass value setters', function(done) { + sass.render({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + var ret = new sass.types.Number(42); + ret.setUnit(123); + return ret; + } + } + }, function(error) { + assert.ok(/Supplied value should be a string/.test(error.message)); + done(); + }); + }); + + it('should always map null, true and false to the same (immutable) object', function(done) { + var counter = 0; + + sass.render({ + data: 'div { color: foo(bar(null)); background-color: baz("foo" == "bar"); }', + functions: { + foo: function(a) { + assert.ok( + 'Supplied value should be the same instance as sass.TRUE', + a === sass.TRUE + ); + + assert.ok( + 'sass.types.Boolean(true) should return a singleton', + sass.types.Boolean(true) === sass.types.Boolean(true) && + sass.types.Boolean(true) === sass.TRUE + ); + + counter++; + + return sass.types.String('foo'); + }, + bar: function(a) { + assert.ok( + 'Supplied value should be the same instance as sass.NULL', + a === sass.NULL + ); + + assert.throws(function() { + return new sass.types.Null(); + }, /Cannot instantiate SassNull/); + + counter++; + + return sass.TRUE; + }, + baz: function(a) { + assert.ok( + 'Supplied value should be the same instance as sass.FALSE', + a === sass.FALSE + ); + + assert.throws(function() { + return new sass.types.Boolean(false); + }, /Cannot instantiate SassBoolean/); + + assert.ok( + 'sass.types.Boolean(false) should return a singleton', + sass.types.Boolean(false) === sass.types.Boolean(false) && + sass.types.Boolean(false) === sass.FALSE + ); + + counter++; + + return sass.types.String('baz'); + } + } + }, function() { + assert.ok(counter === 3); + done(); + }); + }); + }); + + describe('.renderSync(functions)', function() { + it('should call custom function in sync mode', function(done) { + var result = sass.renderSync({ + data: 'div { width: cos(0) * 50px; }', + functions: { + 'cos($a)': function(angle) { + if (!(angle instanceof sass.types.Number)) { + throw new TypeError('Unexpected type for "angle"'); + } + return new sass.types.Number(Math.cos(angle.getValue())); + } + } + }); + + assert.equal(result.css.toString().trim(), 'div {\n width: 50px; }'); + done(); + }); + + it('should return a list of selectors after calling the headings custom function', function(done) { + var result = sass.renderSync({ + data: '#{headings(2,5)} { color: #08c; }', + functions: { + 'headings($from: 0, $to: 6)': function(from, to) { + var i, f = from.getValue(), t = to.getValue(), + list = new sass.types.List(t - f + 1); + + for (i = f; i <= t; i++) { + list.setValue(i - f, new sass.types.String('h' + i)); + } + + return list; + } + } + }); + + assert.equal(result.css.toString().trim(), 'h2, h3, h4, h5 {\n color: #08c; }'); + done(); + }); + + it('should let custom function invoke sass types constructors without the `new` keyword', function(done) { + var result = sass.renderSync({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + return sass.types.Number(42, 'em'); + } + } + }); + + assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }'); + done(); + }); + + it('should let us register custom functions without signatures', function(done) { + var result = sass.renderSync({ + data: 'div { color: foo(20, 22); }', + functions: { + foo: function(a, b) { + return new sass.types.Number(a.getValue() + b.getValue(), 'em'); + } + } + }); + + assert.equal(result.css.toString().trim(), 'div {\n color: 42em; }'); + done(); + }); + + it('should fail when returning anything other than a sass value from a custom function', function(done) { + assert.throws(function() { + sass.renderSync({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + return {}; + } + } + }); + }, /A SassValue object was expected/); + + done(); + }); + + it('should properly bubble up standard JS errors thrown by custom functions', function(done) { + assert.throws(function() { + sass.renderSync({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + throw new RangeError('This is a test error'); + } + } + }); + }, /This is a test error/); + + done(); + }); + + it('should properly bubble up unknown errors thrown by custom functions', function(done) { + assert.throws(function() { + sass.renderSync({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + throw {}; + } + } + }); + }, /unexpected error/); + + done(); + }); + + it('should properly bubble up errors from sass value getters/setters/constructors', function(done) { + assert.throws(function() { + sass.renderSync({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + return sass.types.Boolean('foo'); + } + } + }); + }, /Expected one boolean argument/); + + assert.throws(function() { + sass.renderSync({ + data: 'div { color: foo(); }', + functions: { + 'foo()': function() { + var ret = new sass.types.Number(42); + ret.setUnit(123); + return ret; + } + } + }); + }, /Supplied value should be a string/); + + done(); + }); + }); + describe('.renderSync(options)', function() { it('should compile sass to css with file', function(done) { var expected = read(fixture('simple/expected.css'), 'utf8').trim();