Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Support nested function #119

Merged
merged 1 commit into from

3 participants

@bolasblack

No description provided.

lib/plugins/function.js
@@ -8,16 +8,25 @@ var utils = require('../utils');
var strip = utils.stripQuotes;
/**
+ * Store all defined functions
+ */
+
+var functions = {};

functions shouldn't be global. or why did you make them global?

@jonathanong When we using rework, we can call .functions with multiple time, then some time will appear this case:

JavaScript:

css.use(rework.function({
  func-in-first-call: function(arg){ return arg; },
  func2-in-first-call: function(arg){ return arg; }
}).use(rework.function({
  func-in-second-call: function(arg){ return arg; }
});

CSS:

func-in-first-call(func-in-second-call(func2-in-first-call(10px)))
/* output */
func-in-first-call(10px)

You can see the unit test:

bolasblack@5de6d82#diff-fdacade56a5f218dd71703fab45cbd7aR221

i'm not sure this is the best way to solve this problem, though, since with globals you can potentially create unintended bugs. why call the middleware twice? why not just call it once with a bigger object?

I think you are right. I made a stupid mistake.

hopefully you're just fixing that part then. i think this PR is still very useful!

@jonathanong thanks for your encourage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@bolasblack bolasblack closed this
@bolasblack bolasblack reopened this
lib/plugins/function.js
((15 lines not shown))
- args = args.split(/,\s*/).map(strip);
- return func.apply(decl, args);
- });
+ var generatedFuncs = [];
+
+ while (decl.value.match(matcher)) {
+ decl.value = decl.value.replace(matcher, function(_, name, args){
+ var result;
+ if (!parseArgs) {
+ result = funcs[name].call(decl, strip(args));
+ } else {
+ args = args.split(/\s*,\s*/).map(strip);
+ result = funcs[name].apply(decl, args);
+ }
+ // Prevent the case function `rgba` return string contain `rgba(...)`
+ generatedFunc = {from: name, to: name + Date.now() + ''};

not 100% sure what's going on here. maybe it just needs more comments. why would you need the current date for anything?

User may define a function like this:

var prefix = '/some/prefix/path'

return {
  url: function(path) {
    return 'url(' +prefix + path + ')'
  }
}

Then the program will fall into infinite loop...

OH. you should add a comment and a test case for that. that's a good catch.

OK :ok_hand:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/plugins/function.js
((6 lines not shown))
if (false !== parseArgs) parseArgs = true;
- var regexp = new RegExp(escape(name) + '\\(([^\)]+)\\)', 'g');
+ var matcher = matcherBuilder(Object.keys(funcs).join('|'));

this should be in module.exports instead of being defined on every declaration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/plugins/function.js
((10 lines not shown))
declarations.forEach(function(decl){
if ('comment' == decl.type) return;
- if (!~decl.value.indexOf(name + '(')) return;
- decl.value = decl.value.replace(regexp, function(_, args){
- if (!parseArgs) return func.call(decl, strip(args));
- args = args.split(/,\s*/).map(strip);
- return func.apply(decl, args);
- });
+ var generatedFuncs = [];
+
+ while (decl.value.match(matcher)) {
+ decl.value = decl.value.replace(matcher, function(_, name, args){
+ // Ensure result is string
+ var result = '';

why would result not be a string? i would just do var result; here and result = instead of += down below

@jonathanong �Because some function will return a number or other type value, but i need use result.replace in line 72.

So i need ensure result is a string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/plugins/function.js
((25 lines not shown))
+ if (!parseArgs) {
+ result += funcs[name].call(decl, strip(args));
+ } else {
+ args = args.split(/\s*,\s*/).map(strip);
+ result += funcs[name].apply(decl, args);
+ }
+
+ // Prevent fall into infinite loop like this:
+ //
+ // {
+ // url: function(path) {
+ // return 'url(' + '/some/prefix' + path + ')'
+ // }
+ // }
+ //
+ generatedFunc = {from: name, to: name + Date.now()};

not really comfortable using Date.now() here though since there's potential for conflicts. can we do a really silly uid or something? my favorite: Math.random().toString(36).slice(2)

It's a batter solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jonathanong

also, some sort of docs would be good, even if it's just a line of works with nested functions, too!

@bolasblack

@jonathanong :ok_hand: document added~

@jonathanong

ohhh noo! i just merged your other PR now i can't merge this one. i think everything looks good now. if anyone wants to add any input before i merge this...

@bolasblack

@jonathanong I will rebase this branch.

@bolasblack

@jonathanong All things done now :laughing:

Readme.md
((14 lines not shown))
+ subtract: makeArgsInt(function(a, b) { return a - b }),
+ multiply: makeArgsInt(function(a, b) { return a * b }),
+ divide: makeArgsInt(function(a, b) { return a / b }),
+ floor: makeArgsInt(Math.floor)
+}
+
+rework(fixture('function.nested'))
+ .use(rework.function(functions))
+ .should.equal(fixture('function.nested.out'));
+
+function makeArgsInt(cb) {
+ return function(a, b) {
+ a = parseInt(a, 10);
+ b = parseInt(b, 10);
+ return cb(a, b);
+ }
@tj Owner
tj added a note

I'd ditch this function since it complicates the example a bit, math on the strings should still work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/plugins/function.js
((14 lines not shown))
declarations.forEach(function(decl){
if ('comment' == decl.type) return;
- if (!~decl.value.indexOf(name + '(')) return;
- decl.value = decl.value.replace(regexp, function(_, args){
- if (!parseArgs) return func.call(decl, strip(args));
- args = args.split(/,\s*/).map(strip);
- return func.apply(decl, args);
- });
+ var generatedFuncs = [];
+
+ while (decl.value.match(functionMatcher)) {
+ decl.value = decl.value.replace(functionMatcher, function(_, name, args){
+ // Ensure result is string
+ var result = '';
+
+ if (!parseArgs) {
@tj Owner
tj added a note

negative with an else below could just be switched

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@bolasblack

@visionmedia I think it's ok now :ok_hand:

lib/plugins/function.js
((17 lines not shown))
- decl.value = decl.value.replace(regexp, function(_, args){
- if (!parseArgs) return func.call(decl, strip(args));
- args = args.split(/,\s*/).map(strip);
- return func.apply(decl, args);
- });
+ var generatedFuncs = [];
+
+ while (decl.value.match(functionMatcher)) {
+ decl.value = decl.value.replace(functionMatcher, function(_, name, args){
+ if (parseArgs) {
+ args = args.split(/\s*,\s*/).map(strip);
+ } else {
+ args = [strip(args)];
+ }
+ // Ensure result is string
+ var result = '' + functions[name].apply(decl, args);

much cleaner :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jonathanong jonathanong commented on the diff
lib/plugins/function.js
((27 lines not shown))
+ args = args.split(/\s*,\s*/).map(strip);
+ } else {
+ args = [strip(args)];
+ }
+ // Ensure result is string
+ var result = '' + functions[name].apply(decl, args);
+
+ // Prevent fall into infinite loop like this:
+ //
+ // {
+ // url: function(path) {
+ // return 'url(' + '/some/prefix' + path + ')'
+ // }
+ // }
+ //
+ generatedFunc = {from: name, to: name + getRandomString()};

where is the var for this? i think we have a leak.

Ohhhhh....I forgot declare generatedFunc...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Readme.md
((5 lines not shown))
+
+```css
+input {
+ top: divide(subtract(30, floor(multiply(20, 10))), 2);
+}
+```
+
+```javascript
+var functions = {
+ subtract: function(a, b) { return a - b },
+ multiply: function(a, b) { return a * b },
+ divide: function(a, b) { return a / b },
+ floor: makeArgsInt(Math.floor)
+}
+
+rework(fixture('function.nested'))

haha the test is unnecessary here. just .use(rework.function(functions)) should be fine. you can do it all in one code block: .use(rework.function({subtract: ...}))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@bolasblack

@jonathanong How about now~~

@jonathanong

yup! thank you!

@jonathanong jonathanong merged commit 5d7dc5d into from
@bolasblack bolasblack deleted the branch
@bolasblack

@jonathanong @visionmedia Thanks for all your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 5, 2013
  1. @bolasblack

    Support nested function

    bolasblack authored
This page is out of date. Refresh to see the latest.
View
29 Readme.md
@@ -392,6 +392,35 @@ h1 {
}
```
+ Nested functions works well too:
+
+```css
+input {
+ top: divide(subtract(30, floor(multiply(20, 10))), 2);
+}
+```
+
+```javascript
+var functions = {
+}
+
+rework(css)
+ .use(rework.function(
+ subtract: function(a, b) { return a - b },
+ multiply: function(a, b) { return a * b },
+ divide: function(a, b) { return a / b },
+ floor: Math.floor
+ )).toString()
+```
+
+ Would yield:
+
+```css
+input {
+ top: -85;
+}
+```
+
You may also return array values to expand to several definitions of the property:
```
View
70 lib/plugins/function.js
@@ -14,10 +14,10 @@ var strip = utils.stripQuotes;
module.exports = function(functions, args) {
if (!functions) throw new Error('functions object required');
return function(style, rework){
+ var functionMatcher = functionMatcherBuilder(Object.keys(functions).join('|'));
+
visit.declarations(style, function(declarations){
- for (var name in functions) {
- func(declarations, name, functions[name], args);
- }
+ func(declarations, functions, functionMatcher, args);
});
}
};
@@ -38,20 +38,68 @@ function escape(s) {
*
* @param {Array} declarations
* @param {Object} functions
+ * @param {RegExp} functionMatcher
* @param {Boolean} [parseArgs]
* @api private
*/
-function func(declarations, name, func, parseArgs) {
+function func(declarations, functions, functionMatcher, parseArgs) {
if (false !== parseArgs) parseArgs = true;
- var regexp = new RegExp(escape(name) + '\\(([^\)]+)\\)', 'g');
+
declarations.forEach(function(decl){
if ('comment' == decl.type) return;
- if (!~decl.value.indexOf(name + '(')) return;
- decl.value = decl.value.replace(regexp, function(_, args){
- if (!parseArgs) return func.call(decl, strip(args));
- args = args.split(/,\s*/).map(strip);
- return func.apply(decl, args);
- });
+ var generatedFuncs = [], result, generatedFunc;
+
+ while (decl.value.match(functionMatcher)) {
+ decl.value = decl.value.replace(functionMatcher, function(_, name, args){
+ if (parseArgs) {
+ args = args.split(/\s*,\s*/).map(strip);
+ } else {
+ args = [strip(args)];
+ }
+ // Ensure result is string
+ result = '' + functions[name].apply(decl, args);
+
+ // Prevent fall into infinite loop like this:
+ //
+ // {
+ // url: function(path) {
+ // return 'url(' + '/some/prefix' + path + ')'
+ // }
+ // }
+ //
+ generatedFunc = {from: name, to: name + getRandomString()};

where is the var for this? i think we have a leak.

Ohhhhh....I forgot declare generatedFunc...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ result = result.replace(functionMatcherBuilder(name), generatedFunc.to + '($2)');
+ generatedFuncs.push(generatedFunc);
+ return result;
+ });
+ }
+
+ generatedFuncs.forEach(function(func) {
+ decl.value = decl.value.replace(func.to, func.from);
+ })
});
}
+
+/**
+ * Build function regexp
+ *
+ * @param {String} name
+ * @api private
+ */
+
+function functionMatcherBuilder(name) {
+ // /(?!\W+)(\w+)\(([^()]+)\)/
+ return new RegExp("(?!\\W+)(" + name + ")\\(([^\(\)]+)\\)");
+}
+
+/**
+ * get random string
+ *
+ * @api private
+ */
+
+function getRandomString() {
+ return Math.random().toString(36).slice(2);
+}
+
View
4 test/fixtures/function.infinite-loop.css
@@ -0,0 +1,4 @@
+.selector {
+ background-image: url(/path/to/some/image.png);
+}
+
View
4 test/fixtures/function.infinite-loop.out.css
@@ -0,0 +1,4 @@
+.selector {
+ background-image: url(/some/prefix/path/to/some/image.png);
+}
+
View
8 test/fixtures/function.nested.css
@@ -0,0 +1,8 @@
+.rule-with-all-defined-functions {
+ attr: divide(subtract(30, floor(multiply(20, 10))), 2);
+}
+
+.rule-with-some-undefined-functions {
+ attr: subtract(30, user-need-handle-this-case-by-self(multiply(20, 10)))
+}
+
View
8 test/fixtures/function.nested.out.css
@@ -0,0 +1,8 @@
+.rule-with-all-defined-functions {
+ attr: -85;
+}
+
+.rule-with-some-undefined-functions {
+ attr: subtract(30, user-need-handle-this-case-by-self(200));
+}
+
View
25 test/rework.js
@@ -201,6 +201,31 @@ describe('rework', function(){
}).join(', ');
}
})
+
+ it('should support nested function', function() {
+ var functions = {
+ subtract: function(a, b) { return a - b },
+ multiply: function(a, b) { return a * b },
+ divide: function(a, b) { return a / b },
+ floor: Math.floor
+ }
+
+ rework(fixture('function.nested'))
+ .use(rework.function(functions))
+ .toString()
+ .should.equal(fixture('function.nested.out'));
+ })
+
+ it('should prevent infinite loop', function() {
+ rework(fixture('function.infinite-loop'))
+ .use(rework.function({url: prefixurl}))
+ .toString()
+ .should.equal(fixture('function.infinite-loop.out'));
+
+ function prefixurl(path) {
+ return 'url(' + '/some/prefix' + path + ')';
+ }
+ })
})
describe('.references()', function(){
Something went wrong with that request. Please try again.