diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e20bedc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 0.8 + - "0.10" + - 0.11 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb80a1d --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +#nth-check [![Build Status](https://travis-ci.org/fb55/nth-check.png)](https://travis-ci.org/fb55/nth-check) + +A performant nth-check parser & compiler. + +###About + +This module can be used to parse & compile nth-checks, as they are found in CSS 3's `nth-child()` and `nth-last-of-type()`. + +`nth-check` focusses on speed, providing optimized functions for different kinds of nth-child formulas, while still following the [spec](http://www.w3.org/TR/css3-selectors/#nth-child-pseudo). + +###API + +```js +var nthCheck = require("nth-check"); +``` + +#####`nthCheck(formula)` + +First parses, then compiles the formula. + +#####`nthCheck.parse(formula)` + +Parses the expression, throws a `SyntaxError` if it fails, otherwise returns an array containing two elements. + +__Example:__ + +```js +nthCheck.parse("2n+3") //[2, 3] +``` + +#####`nthCheck.compile([a, b])` + +Takes an array with two elements (as returned by `.parse`) and returns a highly optimized function. + +If the formula doesn't match any elements, it returns [`boolbase`](https://github.com/fb55/boolbase)'s `falseFunc`, otherwise, a function accepting an _index_ is returned, which returns whether or not a passed _index_ matches the formula. (Note: The spec starts counting at `1`, the returned function at `0`). + +__Example:__ +```js +var check = nthCheck.compile([2, 3]); + +check(0) //false +check(1) //false +check(2) //true +check(3) //false +check(4) //true +check(5) //false +check(6) //true +``` + +--- +License: BSD diff --git a/compile.js b/compile.js new file mode 100644 index 0000000..77f2436 --- /dev/null +++ b/compile.js @@ -0,0 +1,40 @@ +module.exports = compile; + +var BaseFuncs = require("boolbase"), + trueFunc = BaseFuncs.trueFunc, + falseFunc = BaseFuncs.falseFunc; + +/* + returns a function that checks if an elements index matches the given rule + highly optimized to return the fastest solution +*/ +function compile(parsed){ + var a = parsed[0], + b = parsed[1] - 1; + + //when b <= 0, a*n won't be possible for any matches when a < 0 + //besides, the specification says that no element is matched when a and b are 0 + if(b < 0 && a <= 0) return falseFunc; + + //when a is in the range -1..1, it matches any element (so only b is checked) + if(a ===-1) return function(pos){ return pos <= b; }; + if(a === 0) return function(pos){ return pos === b; }; + //when b <= 0 and a === 1, they match any element + if(a === 1) return b < 0 ? trueFunc : function(pos){ return pos >= b; }; + + //when a > 0, modulo can be used to check if there is a match + var bMod = b % a; + if(bMod < 0) bMod += a; + + if(a > 1){ + return function(pos){ + return pos >= b && pos % a === bMod; + }; + } + + a *= -1; //make `a` positive + + return function(pos){ + return pos <= b && pos % a === bMod; + }; +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..3253bbd --- /dev/null +++ b/index.js @@ -0,0 +1,9 @@ +var parse = require("./parse.js"), + compile = require("./compile.js"); + +module.exports = function nthCheck(formula){ + return compile(parse(formula)); +}; + +module.exports.parse = parse; +module.exports.compile = compile; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8c48e53 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "nth-check", + "version": "1.0.1", + "description": "performant nth-check parser & compiler", + "main": "index.js", + "scripts": { + "test": "node test" + }, + "repository": { + "type": "git", + "url": "https://github.com/fb55/nth-check" + }, + "keywords": [ + "nth-child", + "nth", + "css" + ], + "author": "Felix Boehm ", + "license": "BSD", + "bugs": { + "url": "https://github.com/fb55/nth-check/issues" + }, + "homepage": "https://github.com/fb55/nth-check", + "dependencies": { + "boolbase": "~1.0.0" + } +} diff --git a/parse.js b/parse.js new file mode 100644 index 0000000..5302951 --- /dev/null +++ b/parse.js @@ -0,0 +1,40 @@ +module.exports = parse; + +//following http://www.w3.org/TR/css3-selectors/#nth-child-pseudo + +//[ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? +var re_nthElement = /^([+\-]?\d*n)?\s*(?:([+\-]?)\s*(\d+))?$/; + +/* + parses a nth-check formula, returns an array of two numbers +*/ +function parse(formula){ + formula = formula.trim().toLowerCase(); + + if(formula === "even"){ + return [2, 0]; + } else if(formula === "odd"){ + return [2, 1]; + } else { + var parsed = formula.match(re_nthElement); + + if(!parsed){ + throw new SyntaxError("n-th rule couldn't be parsed ('" + formula + "')"); + } + + var a; + + if(parsed[1]){ + a = parseInt(parsed[1], 10); + if(isNaN(a)){ + if(parsed[1].charAt(0) === "-") a = -1; + else a = 1; + } + } else a = 0; + + return [ + a, + parsed[3] ? parseInt((parsed[2] || "") + parsed[3], 10) : 0 + ]; + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..0800701 --- /dev/null +++ b/test.js @@ -0,0 +1,102 @@ +var nthCheck = require("./"), + assert = require("assert"); + +var invalid = ["-", "asdf", "2n+-0", "2+0", "- 1n", "-1 n"]; + +function parseInvalid(){ + invalid.forEach(function(formula){ + assert.throws(function(){ + nthCheck.parse(formula); + }, + SyntaxError, + formula + ); + }); +} + +var valid = { + "1": [ 0, 1 ], + "2": [ 0, 2 ], + "3": [ 0, 3 ], + "5": [ 0, 5 ], + " 1 ": [ 0, 1 ], + " 5 ": [ 0, 5 ], + "+2n + 1": [ 2, 1 ], + "-1": [ 0, -1 ], + "-1n + 3": [ -1, 3 ], + "-1n+3": [ -1, 3 ], + "-n+2": [ -1, 2 ], + "-n+3": [ -1, 3 ], + "0n+3": [ 0, 3 ], + "1n": [ 1, 0 ], + "1n+0": [ 1, 0 ], + "2n": [ 2, 0 ], + "2n + 1": [ 2, 1 ], + "2n+1": [ 2, 1 ], + "3n": [ 3, 0 ], + "3n+0": [ 3, 0 ], + "3n+1": [ 3, 1 ], + "3n+2": [ 3, 2 ], + "3n+3": [ 3, 3 ], + "3n-1": [ 3, -1 ], + "3n-2": [ 3, -2 ], + "3n-3": [ 3, -3 ], + even: [ 2, 0 ], + n: [ 1, 0 ], + "n+2": [ 1, 2 ], + odd: [ 2, 1 ], + + //surprisingly, neither sizzle, qwery or nwmatcher cover these cases + "-4n+13": [-4, 13], + "-2n + 12": [-2, 12] +}; + +function parseValid(){ + Object.keys(valid).forEach(function(formula){ + assert.deepEqual(nthCheck.parse(formula), valid[formula], formula); + }); +} + +function testValid(){ + Object.keys(valid).forEach(function(formula){ + testFormula(valid[formula], formula); + }); +} + +var valArray = Array.apply(null, Array(2e3)).map(function(_, i){return i;}); + +function testFormula(formula, name){ + var filtered = valArray.filter(nthCheck.compile(formula)), + iterated = stupidNth(formula); + + try { + assert.deepEqual(filtered, iterated, name); + } catch(e){ + e.expected = JSON.stringify(iterated) + " " + name; + e.actual = JSON.stringify(filtered) + " " + name; + throw e; + } +} + +function stupidNth(formula, limit){ + var a = formula[0], + b = formula[1]; + + if(a === 0 && b > 0) return [b - 1]; + + //taken from qwery + return valArray.filter(function(val){ + for(var i = b, l = valArray.length; ((a > 0) ? (i <= l) : (i >= 1)); i += a){ + if(val === valArray[i - 1]) return true; + } + }); +} + +process.stdout.write("- parser"); +process.stdout.write("\n - parse invalid:\t"); +parseInvalid(); +process.stdout.write("X\n - parse valid:\t"); +parseValid(); +process.stdout.write("X\n- check values: \t"); +testValid(); +process.stdout.write("X\n");