-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Custom functions #644
Custom functions #644
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -136,6 +136,67 @@ function getOptions(options, cb) { | |
return options; | ||
} | ||
|
||
/** | ||
* Executes a callback and transforms any exception raised into a sass error | ||
* | ||
* @param {Function} callback | ||
* @param {Array} arguments | ||
* @api private | ||
*/ | ||
|
||
function tryCallback(callback, args) { | ||
try { | ||
return callback.apply(this, args); | ||
} catch (e) { | ||
if (typeof e === 'string') { | ||
return new binding.types.Error(e); | ||
} else if (e instanceof Error) { | ||
return new binding.types.Error(e.message); | ||
} else { | ||
return new binding.types.Error('An unexpected error occurred'); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Normalizes the signature of custom functions to make it possible to just supply the | ||
* function name and have the signature default to `fn(...)`. The callback is adjusted | ||
* to transform the input sass list into discrete arguments. | ||
* | ||
* @param {String} signature | ||
* @param {Function} callback | ||
* @return {Object} | ||
* @api private | ||
*/ | ||
|
||
function normalizeFunctionSignature(signature, callback) { | ||
if (!/^\*|@warn|@error|@debug|\w+\(.*\)$/.test(signature)) { | ||
if (!/\w+/.test(signature)) { | ||
throw new Error('Invalid function signature format "' + signature + '"'); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has history attached to it. Without this, it fails with CLI usage and manually calling render with importer. If you have discovered a way to fix it, then Kudos! But please make sure of it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, I must have removed this thinking this was part of my debug logs. But now I'm intrigued, is there an issue ticket that explains why this hack was introduced? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes here: #615
I was also intrigued about it. Took me sometime to devise this random hack. :D It could have fixed in this code overhaul of yours. Nonetheless, I am still very interested to know about what was causing it. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I see. Well I am glad to announce that yes this indeed got fixed along with my overhaul :D which I guess is a good thing since the previous hack would not play nice with the The reason this was happening is because of missed wakeup, i.e. the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brilliant work! and thanks for explaining it!! 💯 We were using Everyday learning new things on GitHub. :) |
||
return { | ||
signature: signature + '(...)', | ||
callback: function() { | ||
var args = Array.prototype.slice.call(arguments), | ||
list = args.shift(), | ||
i; | ||
|
||
for (i = list.getLength() - 1; i >= 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); | ||
} | ||
}; | ||
}); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, we need to reuse the same logic for CLI usage. Just note for self. Let me know if / whenever you are adding the CLI support (before that). We need to adapt and enhance on the way jscs, jshint and other linters support custom importers. I will then explain it. :) |
||
|
||
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is time to maintain a Wiki for node-sass. Lets refactor the README as well. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First off, thank you for working on this. It's super important functionality.
The property setters in these Sass value objects are very worrisome to me. Sass values are immutable. If this makes it possible to mutate the Sass values created from a sass file then, it's a violation of the language's design. If it doesn't then this makes it seem like you can, and would make it hard for them to debug what is going on when it doesn't work. My preference would be to make these objects completely immutable, but barring that, I think the objects passed into the function from the sass compiler should be marked as "readonly" and have any attempt to use the setters result in an error. After looking into the code more closely it seems that you're just binding libsass value objects to javascript here so this critique may be more towards libsass than the javascript binding layer. (ping @akhleung)
A few more things that we've done in the ruby api (can totally be added after this lands):
null
,true
andfalse
are always the same (immutable) object.null
andfalse
ever return false.toSass()
ortoString()
that returns a string representation for debugging.nth($index)
function that returns the key/value pair at the nth index. If they are mutable, setters, inserts, deletes by index make sense too.==
operation would return true.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the
Map
type should have akeys
method. How else can you iterate over the map?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chriseppstein This is great input! You are right, the
SassTypes::Value
objects exposed to the v8 runtime constitute a very thin binding layer to the correspondingSass_Value
objects fromlibsass
and as such expose all the same setters.That said, custom functions are invoked with their arguments passed by value (i.e.
SassTypes::Value
’s constructor performs a deep copy of the underlyingSass_Value
object viasass_clone_value
) and similarly the results are returned by value (seeSassTypes::Value::get_sass_value()
). As a consequence it would not be possible for a custom function to mutate sass values created from a sass file. This immutable virtue is thus preserved for all values from the perspective of the sass files.I understand that Sass as a language only exposes immutable values and as such it would make a lot of sense to hold onto this philosophy when it comes to custom functions especially if you guys think this would relieve ambiguities potentially perceived by the end developer.
However, If I can offer my two cents as a developer who spends a good deal of time working with JS: this is by no means a necessity. The fact that Sass’ design does not favor mutable values does not have to bleed onto the conventions established in Javascript. While I can totally make sense of immutable strings and numbers, I believe immutable maps and lists would feel awkward to a lot of JS developers out there. Additionally making these complex types immutable may lead to inferior performance results (and I said “may” because I have not looked at what’s behind the public libsass API, so I couldn't affirm that for sure).
In any case, I believe you are absolutely right in pushing for more user-friendly objects. And while it would make sense to me to leave all the subclasses of
SassTypes::Value
strictly as a translation layer, I can totally see a variety of facade objects in the Javascript realm that would expose all the goodies that you mentioned.As far as the
Map
type is concerned, you would iterate over the map the same way you would if you had to uselibsass
directly: