toloframework
Version: 
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
249 lines (223 loc) • 6.16 kB
JavaScript
/**
 * @module tree-walker
 *
 * @description
 * Add hooks on an object's tree.
 *
 * @example
 * var TW = require('tree-walker');
 * var data = {
 *     students: [
 *         { gender: "M", name: "John" },
 *         { gender: "F", name: "Arya" },
 *         { gender: "F", name: "Shae" },
 *         { gender: "M", name: "Tyron" },
 *         { gender: "M", name: "Jammy" }
 *     ]
 * };
 *
 * var a = new TW({
 *     "students/2": display
 * });
 * a.walk( data );
 *
 * var b = new TW({
 *     "students/[gender=M]": display
 * });
 * b.walk( data );
 */
// Matches this kind of strings:
// * `[Var]`
// * ` [Toto]`
// * ` [Toto]  `
// * ` [Toto  ]  `
// * ` [ Toto]  `
// * `[Var=bob]`
// * `[titi= Fla ga da Johnes ]`
var rxTest = /^[ \t]*\[[ \t]*([a-z_][a-z_0-9]*)[ \t]*(=[^\]]+)?[ \t]*\]/i;
/**
 * @example
 * var TreeWalker = require("tree-walker");
 * var instance = new TreeWalker(opts);
 * @class TreeWalker
 */
var TreeWalker = function( actions ) {
    this._actions = compile.call( this, actions );
};
/**
 * @return void
 */
TreeWalker.prototype.walk = function( data ) {
    this._actions.forEach(function ( item ) {
        var action = item.action;
        var path = item.path;
        var tester = path[0];
        tester( data, action, path, 1 );
    });
};
function compile( actions ) {
    var out = [];
    var path, action;
    var compiledActions;
    var pathItems;
    for( path in actions ) {
        compiledActions = [];
        action = actions[path];
        pathItems = explodePath( path );
        out.push({ path: pathItems, action: action });
    }
    return out;
}
/**
 * Transform `"a/*"` into `[["att", "a"], ["any"]]`.
 * Transform `"bob/** /[youp]"` into `[["att", "bob"], ["all"], ["tst", {youp: undefined}]]`.
 * Transform `"a/b"` into `[["att", "a"], ["att", "b"]]`.
 * Transform `"a/4"` into `[[att: "a"], [idx: 4]]`.
 * Transform `"[x=toto]"` into `[["tst", {x: 'toto'}]]`.
 * Transform `"[x=toto][y=foo]"` into `[["tst", {x: 'toto', y: 'foo'}]]`.
 */
function explodePath( path ) {
    var items = path.split( '/' );
    return items.map(function( itm ) {
        itm = itm.trim();
        if( itm == '*' ) return buildAny();
        if( itm == '**' ) return buildAll();
        if( itm.charAt(0) != '[' ) {
            // Not a test.
            var idx = parseInt( itm );
            if( isNaN( idx ) ) {
                return buildAtt( itm );
            }
            return buildIdx( idx );
        }
        var tst = {};
        while( itm.length > 0 ) {
            var m = itm.match(rxTest);
            if (!m) break;
            var key = m[1];
            var val = m[2];
            if (val) {
                val = val.substr(1);
            }
            tst[key] = val;
            // Go to next test, if any.
            itm = itm.substr( m[0].length );
        }
        return buildTst( tst );
    });
}
function buildAtt( att ) {
    return function( node, action, path, idx ) {
        node = node[att];
        if( typeof node === 'undefined' ) return false;
        if( idx < path.length ) {
            return path[idx]( node, action, path, idx + 1 );
        }
        action( node );
        return false;
    };
}
function buildIdx( index ) {
    return function( node, action, path, idx ) {
        node = node[index];
        if( typeof node === 'undefined' ) return false;
        if( idx < path.length ) {
            return path[idx]( node, action, path, idx + 1 );
        }
        action( node );
        return false;
    };
}
function buildTst( tst ) {
    return function( node, action, path, idx ) {
        var arr = node;
        if( !Array.isArray( arr ) ) arr = [arr];
        arr.forEach(function ( child ) {
            var success = true;
            var key, val;
            for( key in tst ) {
                val = tst[key];
                if( typeof child[key] === 'undefined' ) {
                    success = false;
                    break;
                }
                if( typeof val !== 'undefined' ) {
                    if( child[key] != val ) {
                        success = false;
                        break;
                    }
                }
            }
            if( success ) {
                if( idx < path.length ) {
                    return path[idx]( child, action, path, idx + 1 );
                }
                action( child );
            }
        });
    };
}
function actionAny( node, action, path, idx ) {
    if( typeof node === 'undefined' ) return;
    if( Array.isArray( node ) ) {
        node.forEach(function ( child ) {
            if( idx < path.length ) {
                path[idx]( child, action, path, idx + 1 );
            } else {
                action( child );
            }
        });
    } else {
        var key, child;
        for( key in node ) {
            child = node[key];
            if( idx < path.length ) {
                path[idx]( child, action, path, idx + 1 );
            } else {
                action( child );
            }
        }
    }
}
function buildAny() {
    return actionAny;
}
function actionAll( node, action, path, idx ) {
    if( typeof node === 'undefined' ) return;
    var test = path[idx];
    var noMoreTests = (typeof test !== 'function');
    if( isLeaf( node ) ) {
        if( noMoreTests ) action( node );
        return;
    }
    if( Array.isArray( node ) ) {
        node.forEach(function ( child ) {
            if( !noMoreTests ) test( child, action, path, idx + 1 );
            actionAll( child, action, path, idx );
        });
    } else {
        var key, child;
        for( key in node ) {
            child = node[key];
            if( !noMoreTests ) test( child, action, path, idx + 1 );
            actionAll( child, action, path, idx );
        }
    }
}
function buildAll() {
    return actionAll;
}
function isLeaf( node ) {
    switch( typeof node ) {
    case 'function':
    case 'string':
    case 'number':
    case 'boolean':
        return true;
    }
    return false;
}
TreeWalker.create = function( actions ) {
    return new TreeWalker( actions );
};
module.exports = TreeWalker;