diff --git a/README.md b/README.md index 73da0a789..cf206447c 100644 --- a/README.md +++ b/README.md @@ -57,21 +57,21 @@ var result = sass.renderSync({ ## Options ### file -Type: `String | null` +Type: `String` Default: `null` **Special**: `file` or `data` must be specified Path to a file for [libsass] to render. ### data -Type: `String | null` +Type: `String` Default: `null` **Special**: `file` or `data` must be specified A string to pass to [libsass] to render. It is recommended that you use `includePaths` in conjunction with this so that [libsass] can find files when using the `@import` directive. ### importer (>= v2.0.0) -Type: `Function` signature `function(url, prev, done)` +Type: `Function | Function[]` signature `function(url, prev, done)` Default: `undefined` Function Parameters and Information: @@ -89,6 +89,8 @@ 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` +Starting from v3.0.0, importer can be an array of functions, which will be called by libsass in the order of their occurance in array. This helps user specify special importer for particular kind of path (filesystem, http). If the importer does not handle particular path, it should return `sass.NULL`. See [functions section](#functions) for more details on Sass types. + ### functions (>= v3.0.0) `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: diff --git a/lib/index.js b/lib/index.js index 782abfe5b..cf1158cd2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -282,17 +282,33 @@ module.exports.render = function(options, cb) { var importer = options.importer; if (importer) { - options.importer = function(file, prev, bridge) { - function done(data) { - bridge.success(data); - } + if (Array.isArray(importer)) { + importer.forEach(function(subject, index) { + options.importer[index] = function(file, prev, bridge) { + function done(data) { + bridge.success(data); + } + + var result = subject.call(options.context, file, prev, done); + + if (result) { + done(result === module.exports.NULL ? null : result); + } + }; + }); + } else { + options.importer = function(file, prev, bridge) { + function done(data) { + bridge.success(data); + } - var result = importer.call(options.context, file, prev, done); + var result = importer.call(options.context, file, prev, done); - if (result) { - done(result); - } - }; + if (result) { + done(result === module.exports.NULL ? null : result); + } + }; + } } var functions = options.functions; @@ -300,8 +316,8 @@ module.exports.render = function(options, cb) { if (functions) { options.functions = {}; - Object.keys(functions).forEach(function(signature) { - var cb = normalizeFunctionSignature(signature, functions[signature]); + Object.keys(functions).forEach(function(subject) { + var cb = normalizeFunctionSignature(subject, functions[subject]); options.functions[cb.signature] = function() { var args = Array.prototype.slice.call(arguments), @@ -336,9 +352,21 @@ module.exports.renderSync = function(options) { var importer = options.importer; if (importer) { - options.importer = function(file, prev) { - return importer.call(options.context, file, prev); - }; + if (Array.isArray(importer)) { + importer.forEach(function(subject, index) { + options.importer[index] = function(file, prev) { + var result = subject.call(options.context, file, prev); + + return result === module.exports.NULL ? null : result; + }; + }); + } else { + options.importer = function(file, prev) { + var result = importer.call(options.context, file, prev); + + return result === module.exports.NULL ? null : result; + }; + } } var functions = options.functions; diff --git a/src/binding.cpp b/src/binding.cpp index 5c7158716..022871e1f 100644 --- a/src/binding.cpp +++ b/src/binding.cpp @@ -5,20 +5,24 @@ #include "create_string.h" #include "sass_types/factory.h" -struct Sass_Import** sass_importer(const char* file, const char* prev, void* cookie) +Sass_Import_List sass_importer(const char* cur_path, Sass_Importer_Entry cb, struct Sass_Compiler* comp) { + void* cookie = sass_importer_get_cookie(cb); + struct Sass_Import* previous = sass_compiler_get_last_import(comp); + const char* prev_path = sass_import_get_path(previous); sass_context_wrapper* ctx_w = static_cast(cookie); - CustomImporterBridge& bridge = *(ctx_w->importer_bridge); + CustomImporterBridge& bridge = *(static_cast(cookie)); std::vector argv; - argv.push_back((void*)file); - argv.push_back((void*)prev); + argv.push_back((void*)cur_path); + argv.push_back((void*)prev_path); return bridge(argv); } -union Sass_Value* sass_custom_function(const union Sass_Value* s_args, void* cookie) +union Sass_Value* sass_custom_function(const union Sass_Value* s_args, Sass_Function_Entry cb, struct Sass_Options* opts) { + void* cookie = sass_function_get_cookie(cb); CustomFunctionBridge& bridge = *(static_cast(cookie)); std::vector argv; @@ -65,13 +69,6 @@ void ExtractOptions(Local options, void* cptr, sass_context_wrapper* ctx ctx_w->error_callback = new NanCallback(error_callback); } - Local importer_callback = Local::Cast(options->Get(NanNew("importer"))); - - if (importer_callback->IsFunction()) { - 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)); - } - if (!is_file) { ctx_w->file = create_string(options->Get(NanNew("file"))); sass_option_set_input_path(sass_options, ctx_w->file); @@ -106,25 +103,58 @@ void ExtractOptions(Local options, void* cptr, sass_context_wrapper* ctx sass_option_set_indent(sass_options, ctx_w->indent); sass_option_set_linefeed(sass_options, ctx_w->linefeed); - Local custom_functions = Local::Cast(options->Get(NanNew("functions"))); + Local importer_callback = options->Get(NanNew("importer")); + + if (importer_callback->IsFunction()) { + Local importer = Local::Cast(importer_callback); + auto bridge = std::make_shared(new NanCallback(importer), ctx_w->is_sync); + ctx_w->importer_bridges.push_back(bridge); + + Sass_Importer_List c_importers = sass_make_importer_list(1); + c_importers[0] = sass_make_importer(sass_importer, 0, bridge.get()); + + sass_option_set_c_importers(sass_options, c_importers); + } + else if (importer_callback->IsArray()) { + Handle importers = Handle::Cast(importer_callback); + Sass_Importer_List c_importers = sass_make_importer_list(importers->Length()); + + for (size_t i = 0; i < importers->Length(); ++i) { + Local callback = Local::Cast(importers->Get(static_cast(i))); + + if (!callback->IsFunction()) { + NanThrowError(NanNew("options.importer must be set to a function or array of functions")); + } + + auto bridge = std::make_shared(new NanCallback(callback), ctx_w->is_sync); + ctx_w->importer_bridges.push_back(bridge); + + c_importers[i] = sass_make_importer(sass_importer, importers->Length() - i - 1, bridge.get()); + } + + sass_option_set_c_importers(sass_options, c_importers); + } + + Local custom_functions = options->Get(NanNew("functions")); if (custom_functions->IsObject()) { - Local signatures = custom_functions->GetOwnPropertyNames(); + Local functions = Local::Cast(custom_functions); + Local signatures = functions->GetOwnPropertyNames(); unsigned num_signatures = signatures->Length(); - Sass_C_Function_List fn_list = sass_make_function_list(num_signatures); + Sass_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)); + Local callback = Local::Cast(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); + auto bridge = std::make_shared(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_Entry fn = sass_make_function(create_string(signature), sass_custom_function, bridge.get()); sass_function_set_list_entry(fn_list, i, fn); } @@ -274,7 +304,8 @@ NAN_METHOD(render_file_sync) { int result = GetResult(ctx_w, ctx, true); - sass_wrapper_dispose(ctx_w, input_path); + free(input_path); + sass_free_context_wrapper(ctx_w); NanReturnValue(NanNew(result == 0)); } diff --git a/src/custom_function_bridge.cpp b/src/custom_function_bridge.cpp index 96cebfbc8..0729f0ee7 100644 --- a/src/custom_function_bridge.cpp +++ b/src/custom_function_bridge.cpp @@ -5,7 +5,8 @@ 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) { + } + catch (const std::invalid_argument& e) { return sass_make_error(e.what()); } } @@ -14,9 +15,7 @@ std::vector> CustomFunctionBridge::pre_process_args(std::vector> argv = std::vector>(); for (void* value : in) { - argv.push_back( - SassTypes::Factory::create(static_cast(value))->get_js_object() - ); + argv.push_back(SassTypes::Factory::create(static_cast(value))->get_js_object()); } return argv; diff --git a/src/custom_importer_bridge.cpp b/src/custom_importer_bridge.cpp index 885221999..7d921d529 100644 --- a/src/custom_importer_bridge.cpp +++ b/src/custom_importer_bridge.cpp @@ -3,7 +3,7 @@ #include "create_string.h" SassImportList CustomImporterBridge::post_process_return_value(Handle val) const { - SassImportList imports; + SassImportList imports = 0; NanScope(); Local returned_value = NanNew(val); @@ -32,8 +32,9 @@ SassImportList CustomImporterBridge::post_process_return_value(Handle val else { char* path = create_string(object->Get(NanNew("file"))); char* contents = create_string(object->Get(NanNew("contents"))); + char* srcmap = create_string(object->Get(NanNew("map"))); - imports[i] = sass_make_import_entry(path, (!contents || contents[0] == '\0') ? 0 : strdup(contents), 0); + imports[i] = sass_make_import_entry(path, contents, srcmap); } } } @@ -51,12 +52,9 @@ SassImportList CustomImporterBridge::post_process_return_value(Handle val Local object = Local::Cast(returned_value); char* path = create_string(object->Get(NanNew("file"))); char* contents = create_string(object->Get(NanNew("contents"))); + char* srcmap = create_string(object->Get(NanNew("map"))); - 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); + imports[0] = sass_make_import_entry(path, contents, srcmap); } return imports; diff --git a/src/custom_importer_bridge.h b/src/custom_importer_bridge.h index 7af35b5e6..89b23767b 100644 --- a/src/custom_importer_bridge.h +++ b/src/custom_importer_bridge.h @@ -7,7 +7,7 @@ using namespace v8; -typedef Sass_Import** SassImportList; +typedef Sass_Import_List SassImportList; class CustomImporterBridge : public CallbackBridge { public: diff --git a/src/libsass.gyp b/src/libsass.gyp index d1037249b..c3fb7caf2 100644 --- a/src/libsass.gyp +++ b/src/libsass.gyp @@ -11,6 +11,7 @@ 'libsass/constants.cpp', 'libsass/context.cpp', 'libsass/contextualize.cpp', + 'libsass/contextualize_eval.cpp', 'libsass/cssize.cpp', 'libsass/emitter.cpp', 'libsass/error_handling.cpp', @@ -21,6 +22,8 @@ 'libsass/functions.cpp', 'libsass/inspect.cpp', 'libsass/json.cpp', + 'libsass/lexer.cpp', + 'libsass/listize.cpp', 'libsass/node.cpp', 'libsass/output.cpp', 'libsass/parser.cpp', @@ -72,7 +75,7 @@ 'VCCLCompilerTool': { 'AdditionalOptions': [ '/GR', - '/EHsc' + '/EHs' ] } } diff --git a/src/sass_context_wrapper.cpp b/src/sass_context_wrapper.cpp index 7df6f63d5..bfa3bf943 100644 --- a/src/sass_context_wrapper.cpp +++ b/src/sass_context_wrapper.cpp @@ -26,7 +26,7 @@ extern "C" { return (sass_context_wrapper*)calloc(1, sizeof(sass_context_wrapper)); } - void sass_wrapper_dispose(struct sass_context_wrapper* ctx_w, char* string = 0) { + void sass_free_context_wrapper(sass_context_wrapper* ctx_w) { if (ctx_w->dctx) { sass_delete_data_context(ctx_w->dctx); } @@ -46,23 +46,8 @@ extern "C" { free(ctx_w->source_map_root); free(ctx_w->indent); - 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) { - sass_wrapper_dispose(ctx_w); + ctx_w->importer_bridges.resize(0); + ctx_w->function_bridges.resize(0); free(ctx_w); } diff --git a/src/sass_context_wrapper.h b/src/sass_context_wrapper.h index bdbab8bff..800635013 100644 --- a/src/sass_context_wrapper.h +++ b/src/sass_context_wrapper.h @@ -2,6 +2,7 @@ #define SASS_CONTEXT_WRAPPER #include +#include #include #include #include @@ -44,12 +45,11 @@ extern "C" { NanCallback* error_callback; NanCallback* success_callback; - std::vector function_bridges; - CustomImporterBridge* importer_bridge; + std::vector> function_bridges; + std::vector> importer_bridges; }; struct sass_context_wrapper* sass_make_context_wrapper(void); - void sass_wrapper_dispose(struct sass_context_wrapper*, char*); void sass_free_context_wrapper(struct sass_context_wrapper*); #ifdef __cplusplus diff --git a/test/api.js b/test/api.js index 9bc240fa5..04787d118 100644 --- a/test/api.js +++ b/test/api.js @@ -341,6 +341,25 @@ describe('api', function() { }); }); + it('should accept arrays of importers and return respect the order', function(done) { + sass.render({ + file: fixture('include-files/index.scss'), + importer: [ + function() { + return sass.NULL; + }, + function() { + return { + contents: 'div {color: yellow;}' + }; + } + ] + }, function(error, result) { + assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }'); + done(); + }); + }); + it('should be able to see its options in this.options', function(done) { var fxt = fixture('include-files/index.scss'); sass.render({ @@ -1146,6 +1165,25 @@ describe('api', function() { done(); }); + it('should accept arrays of importers and return respect the order', function(done) { + var result = sass.renderSync({ + file: fixture('include-files/index.scss'), + importer: [ + function() { + return sass.NULL; + }, + function() { + return { + contents: 'div {color: yellow;}' + }; + } + ] + }); + + assert.equal(result.css.toString().trim(), 'div {\n color: yellow; }\n\ndiv {\n color: yellow; }'); + done(); + }); + it('should be able to see its options in this.options', function(done) { var fxt = fixture('include-files/index.scss'); var sync = false; @@ -1161,7 +1199,6 @@ describe('api', function() { done(); }); - it('should throw user-defined error', function(done) { assert.throws(function() { sass.renderSync({ diff --git a/test/cli.js b/test/cli.js index 82681c0dd..de8e130f0 100644 --- a/test/cli.js +++ b/test/cli.js @@ -226,7 +226,7 @@ describe('cli', function() { }); setTimeout(function() { - fs.appendFileSync(foo, 'body{background:white}'); + fs.appendFileSync(foo, 'body{background:white}\n'); }, 500); }); }); @@ -410,6 +410,19 @@ describe('cli', function() { }); }); + it('should accept arrays of importers and return respect the order', function(done) { + var bin = spawn(cli, [ + src, '--output', path.dirname(dest), + '--importer', fixture('extras/my_custom_arrays_of_importers.js') + ]); + + bin.once('close', function() { + assert.equal(read(dest, 'utf8').trim(), expected); + fs.unlinkSync(dest); + done(); + }); + }); + it('should return error for invalid importer file path', function(done) { var bin = spawn(cli, [ src, '--output', path.dirname(dest), diff --git a/test/fixtures/extras/my_custom_arrays_of_importers.js b/test/fixtures/extras/my_custom_arrays_of_importers.js new file mode 100644 index 000000000..38c1c0844 --- /dev/null +++ b/test/fixtures/extras/my_custom_arrays_of_importers.js @@ -0,0 +1,12 @@ +var sass = require('../../..'); + +module.exports = [ + function() { + return sass.NULL; + }, + function() { + return { + contents: 'div {color: yellow;}' + }; + } +]; diff --git a/test/fixtures/source-map/expected.map b/test/fixtures/source-map/expected.map index 590b2eba4..b43854b7d 100644 --- a/test/fixtures/source-map/expected.map +++ b/test/fixtures/source-map/expected.map @@ -5,6 +5,6 @@ "index.scss" ], "sourcesContent": [], - "mappings": "AAAA,OAAO,CAAC;EACN,KAAK,EAAE,GAAI;EACX,MAAM,EAAE,IAAI,GAFL;;AAKD,OAAO,CAAC,EAAE,CAAP;EACT,eAAe,EAAE,IAAK,GADZ;;AAIJ,OAAO,CAAC,EAAE,CAAP;EACT,KAAK,EAAE,IAAK,GADF;EAGV,OAAO,CAAC,EAAE,CAAC,CAAC,CAAV;IACA,WAAW,EAAE,IAAK,GADjB", + "mappings": "AAAO,OAAO,CAAN;EACN,KAAK,EAAE,GAAI;EACX,MAAM,EAAE,IAAI,GAFL;;AAKC,OAAO,CAAC,EAAE,CAAT;EACT,eAAe,EAAE,IAAK,GADZ;;AAIF,OAAO,CAAC,EAAE,CAAT;EACT,KAAK,EAAE,IAAK,GADF;EAGT,OAAO,CAAC,EAAE,CAAC,CAAC,CAAX;IACA,WAAW,EAAE,IAAK,GADjB", "names": [] }