<commit_before>'use strict';
// var debug = require('debug')('metalsmith-navigation');
var merge = require('merge');
var path = require('path');

// var util = require('util');
// var inspect = function(obj){
//     console.log(util.inspect(obj, {
//         depth: null,
//         showHidden: true,
//         colors: true
//     }));
// };

var SETTINGS_DEFAULT = {

    /*
    * metadata key all navs will be assigned to metatdata[navListProperty]
    * not set if false
    */
    navListProperty: 'navs',

    /*
    * if true, paths will be transformed to use metalsmith-permalinks
    * metalsmith-permalinks must be run before metalsmith-navigation
    */
    permalinks: false,
};

var NAV_CONFIG_DEFAULT = {

    /*
    * sortby function or property name
    * function example: function(navNode){ return navNode.getValueToSortBy(); }
    */
    sortBy: false,

    /*
    * if true nodes will be sorted by path before sortBy
    * if false the sorting will not be stable unless ALL nodes have a unique sort value
    */
    sortByNameFirst: true,

    /*
    * to be included in this nav config, a file's metadata[filterProperty] must equal filterValue
    * ex:
    *   navConfigs key = 'footer'
    *   filterProperty = 'nav_group'
    *   files only added if files[path].nav_group == 'footer'
    */
    filterProperty: false,

    /*
    * if false nav name (navConfigs key) used instead
    * if files[path][filterProperty] equals or contains (string or array containing) filterValue
    * it will be included
    */
    filterValue: false,

    /*
    * the file object property that breadcrumb array is assigned to on each file object
    * breadcrumbs not generated or set if false
    * typically only one navConfig should generate breadcrumbs, often one specifically for them
    */
    breadcrumbProperty: 'breadcrumb_path',

    /**
    * the file object property that the nav path is assigned to on each file object, not set if false
    */
    pathProperty: 'nav_path',

    /**
    * the file object property that an array of nav child nodes will be assigned to
    */
    childrenProperty: 'nav_children',

    /*
    * if a file and sibling dir have matching names the file will be used as the parent in the nav tree
    * ex: /foo /foo.html
    */
    mergeMatchingFilesAndDirs: true,

    /*
    * if dirs should be included as nav nodes
    * has no effect if mergeMatchingFilesAndDirs = true
    */
    includeDirs: false,

};

/**
    This should be replaced by a module such as a custom lodash.sortBy build. A suitable
    module (allowing property key or simple callback) could not be found at the time of this writting
**/

/**
* Returns a (stably) sorted copy of list, ranked in ascending order by the
* results of running each value through sortBy. (derrived from underscore.js _.sortBy())
* @method arraySortBy
* @param {Array} list - array to be sorted
* @param {Function|String} sortBy - ( function(item){ return item.sort_property; } )
*    If string used as the name of the property to sort by (eg. length).
* @return {Array} copy of sorted array
*/
var arraySortBy = function(list, sortBy) {

    if(typeof sortBy !== 'function'){
        var sortByKey = sortBy;
        sortBy = function(obj){
            return obj[sortByKey];
        };
    }

    var mapped = list.map(function(value, index, list) {
        return {
            value: value,
            index: index,
            criteria: sortBy(value, index, list)
        };
    });

    var compareFunction = function(left, right) {
        var a = left.criteria,
            b = right.criteria;
        if (a !== b) {
            if (a > b || a === void 0) return 1;
            if (a < b || b === void 0) return -1;
        }
        return left.index < right.index ? -1 : 1;
    };

    mapped.sort(compareFunction);

    return mapped.map(function(item){
        return item.value;
    });
};

/**
* @method eachTreeNode
* @param {Array} tree - nav node tree
* @param {Function} callback - function(node, parent, depth), if callback returns false the node will be removed
* @return {Array} tree
*/
var eachTreeNode = function(tree, callback){
    var iterate = function(nodes, parent, depth){
        depth = (depth || 0) + 1;
        for(var i = nodes.length - 1; i >= 0; i--){
            var node = nodes[i];
            var result = callback(node, parent, depth);
            if(result === false){
                nodes.splice(i, 1);
            }
            iterate(node.children, node, depth);
        }
    };
    iterate(tree);
    return tree;
};

/**
* Convert list of files to tree structure
* @method pathsToTree
* @param {Array} paths - list of file paths to act on.
* @return Array
*/
var pathsToTree = function(paths){
    var items = [];
    for(var i = 0, l = paths.length; i < l; i++) {
        var path = paths[i];
        var name = path[0];
        var rest = path.slice(1);
        var item = null;
        for(var j = 0, m = items.length; j < m; j++) {
            if(items[j].name === name) {
                item = items[j];
                break;
            }
        }

        if(item === null) {
            item = {
                name: name,
                path: null,
                type: null,
                sort: 0,
                children: []
            };
            items.push(item);
        }
        if(rest.length > 0) {
            item.children.push(rest);
        }
    }
    for(i = 0, l = items.length; i < l; i++) {
        var node = items[i];
        node.children = pathsToTree(node.children);
    }
    return items;
};

/**
* Loop over each node and do the following:
*     set node.path - full path to file.
*     set node.type - 'file' or 'dir'.
*
* @method addNodeMetadata
* @param {Array} tree - tree of nodes to act on
* @param {Array} files - List of files being processed by metalsmith.
* @param {String} sortBy - Sort by property key.
* @return {Array} converted array (same obj as tree param)
*/
var addNodeMetadata = function(tree, files, sortBy){
    var fileList = Object.keys(files);

    eachTreeNode(tree, function(node, parent, depth){
        var path = node.name,
            type = 'dir';

        if(parent){
            path = parent.path + '/' + node.name;
        }

        if(fileList.indexOf(path) !== -1){
            type = 'file';
            var file = files[path];
            node.file = file;
            if(sortBy){
                node.sort = file[sortBy];
            }
        }

        node.path = path;
        node.type = type;
        node.depth = depth;
    });

    return tree;
};

/**
* Sort nodes recursively
* @method sortNodes
* @param {Array} nodes - tree of nodes to act on
* @return {Array} sorted node tree
*/
var sortNodes = function(tree){
    tree = arraySortBy(tree, 'sort');
    for (var i = 0; i < tree.length; i++) {
        var node = tree[i];
        if(node.children){
            node.children = sortNodes(node.children);
        }
    }
    return tree;
};

/**
* Find dirs with a sibling with afile name matching the dir name,
* remove the dir and move child nodes from it to the matching file node
* @method mergeFileDirs
* @param {Array} tree - Tree of nodes to act on.
* @return {Array} converted array (same obj as tree param)
*/
var mergeFileDirs = function(tree){

    var merge = function(siblings){
        for (var i = siblings.length - 1; i >= 0; i--){
            var node = siblings[i];
            if(node.type === 'dir'){
                for(var j = siblings.length - 1; j >= 0; j--){
                    var sibling = siblings[j];
                    if(node !== sibling && sibling.type !== 'dir'){
                        // remove file extension
                        var siblingBaseName = sibling.path.replace(/\.[^/.]+$/, "");
                        // if dir name matches node without file extension
                        if(siblingBaseName === node.path){
                            // copy children from dir to file node and mark the dir to be removed

                            // prevent copying duplicates
                            for(var k = node.children.length - 1; k >= 0; k--){
                                var nodeChild = node.children[k];
                                if(sibling.children.indexOf(nodeChild) === -1){
                                    sibling.children.push(nodeChild);
                                }
                            }
                            var index = siblings.indexOf(node);
                                siblings.splice(index, 1);
                            break;
                        }
                    }
                }
            }
        }

        // recurse children
        for(var i = siblings.length - 1; i >= 0; i--){
            var node = siblings[i];
            if(node.children){
                merge(node.children);
            }
        }
    };

    merge(tree);
    return tree;
};

/**
* @method setFileObjectReferences
* @param {Array} tree - nav node tree
* @param {Object} files - Metalsmith files object
* @param {String} pathProperty - the file object property that the nav path is assigned to on each file object, not set if false
* @param {String} navName - the name of the current nav
* @return {Array} tree
*/
var setFileObjectReferences = function(tree, files, pathProperty, childrenProperty, navName){
    eachTreeNode(tree, function(node, parent, depth){
        var path = node.path;
        if(files[path]){
            if(pathProperty){
                node.file[pathProperty] = path;
            }
            if(childrenProperty){
                node.file[childrenProperty] = node.file[childrenProperty] || {};
                node.file[childrenProperty][navName] = node.children;
            }
        }
        node.parent = parent;
    });
    return tree;
};

/**
* @method setBreadcrumbs
* @param {Array} tree - nav node tree
* @param {String} breadcrumbProperty -
* @return {Array} tree
*/
var setBreadcrumbs = function(tree, breadcrumbProperty){
    eachTreeNode(tree, function(node, parent, depth){
        var breadcrumbPath = [];
        if(parent){
            if(parent[breadcrumbProperty]){
                breadcrumbPath = parent[breadcrumbProperty].concat(parent);
            } else {
                breadcrumbPath = [parent];
            }
        }
        node[breadcrumbProperty] = breadcrumbPath;
        if(node.file){
            node.file[breadcrumbProperty] = breadcrumbPath;
        }
    });
    return tree;
};

/**
* @method filterFiles
* @param {Object} files - Metalsmith files object
* @param {String} key - file metadata key
* @param {String} val - file metadata key value to check for
* @return {Object} filtered files object
*/
var filterFiles = function(files, key, val){
    var out = {};
    for(var fileKey in files){
        var file = files[fileKey],
            groups = file[key];

        if(typeof groups === 'string'){
            if(groups === val){
                out[fileKey] = file;
            }
        } else if(groups && groups.indexOf(val) !== -1){
            out[fileKey] = file;
        }
    }
    return out;
};

/**
* @method transformPermalinks
* @param {Array} tree - nav nodes
* @return {Array} tree
*/
var transformPermalinks = function(tree){
    eachTreeNode(tree, function(node, parent, depth){
        if(node.file){
            if(node.depth > 1){
                if(!node.children || !node.children.length){
                    node.path = path.dirname(node.path);
                    node.parent.path = node.path;
                    node.parent.file = node.file;
                    node.parent.add_trailing = true;
                }
                return false;
            }
        }
    });
    return tree;
};

/*
* @method removeDirs
* @param {Array} tree - nav nodes
* @return {Array} tree
*/
var removeDirs = function(tree){
    eachTreeNode(tree, function(node, parent, depth){
        if(node.type === 'dir'){
            return false;
        }
    });
    return tree;
};

/**
* @method getNav
* @param {String} navName - name of this nav config
* @param {Object} config - A nav config object
* @param {Object} files - metalsmith files plugin param
* @return {Array} nav tree
*/
var getNav = function(navName, config, files){
    config = merge(NAV_CONFIG_DEFAULT, config);

    var sortByNameFirst             = config.sortByNameFirst,
        sortBy                      = config.sortBy,
        breadcrumbProperty          = config.breadcrumbProperty,
        pathProperty                = config.pathProperty,
        mergeMatchingFilesAndDirs   = config.mergeMatchingFilesAndDirs,
        includeDirs                 = config.includeDirs,
        childrenProperty            = config.childrenProperty,
        filterProperty              = config.filterProperty,
        filterValue                 = config.filterValue || navName;

    var navFiles = files;

    // filter to files that match the filterProperty and filterValue
    if(filterProperty){
        navFiles = filterFiles(files, filterProperty, filterValue);
    }

    var paths = Object.keys(navFiles).map(function(path) {
        return path.split('/');
    });

    var nodes = pathsToTree(paths);

    nodes = addNodeMetadata(nodes, files, sortBy);

    if(mergeMatchingFilesAndDirs){
        nodes = mergeFileDirs(nodes);
        nodes = removeDirs(nodes);
    }
    // ignored if mergeMatchingFilesAndDirs == true
    else if(!includeDirs){
        nodes = removeDirs(nodes);
    }

    if(sortByNameFirst){
        nodes = sortNodes(nodes, 'path');
    }

    if(sortBy){
        nodes = sortNodes(nodes);
    }

    nodes = setFileObjectReferences(nodes, files, pathProperty, childrenProperty, navName);

    if(breadcrumbProperty){
        nodes = setBreadcrumbs(nodes, breadcrumbProperty);
    }

    return nodes;
};

/**
* @method plugin
* @param {Object} navConfigs - list of nav config objects indexed by name
* @param {Object} settings - list of non nav specific plugin settings
* @return {Function}
*/
var plugin = function plugin(navConfigs, settings){
    navConfigs = navConfigs || {};
    settings = settings || {};

    settings = merge(SETTINGS_DEFAULT, settings);

    var navListProperty = settings.navListProperty;
    var permalinks      = settings.permalinks;

    return function(files, metalsmith, done){

        var metadata = metalsmith.metadata();

        if(navListProperty){
            metadata[navListProperty] = {};
        }

        // key is the name of the nav
        for(var key in navConfigs){

            var config = navConfigs[key];

            // get processed nav
            var nav = getNav(key, config, files);

            // apply permalinks if set in settings
            if(permalinks){
                nav = transformPermalinks(nav);
            }

            // add to global metadata
            if(navListProperty){
                metadata[navListProperty][key] = nav;
            }
        }
        done();
    };
};

module.exports = plugin;<commit_msg>sort by name fix<commit_after>'use strict';
// var debug = require('debug')('metalsmith-navigation');
var merge = require('merge');
var path = require('path');

// var util = require('util');
// var inspect = function(obj){
//     console.log(util.inspect(obj, {
//         depth: null,
//         showHidden: true,
//         colors: true
//     }));
// };

var SETTINGS_DEFAULT = {

    /*
    * metadata key all navs will be assigned to metatdata[navListProperty]
    * not set if false
    */
    navListProperty: 'navs',

    /*
    * if true, paths will be transformed to use metalsmith-permalinks
    * metalsmith-permalinks must be run before metalsmith-navigation
    */
    permalinks: false,
};

var NAV_CONFIG_DEFAULT = {

    /*
    * sortby function or property name
    * function example: function(navNode){ return navNode.getValueToSortBy(); }
    */
    sortBy: false,

    /*
    * if true nodes will be sorted by path before sortBy
    * if false the sorting will not be stable unless ALL nodes have a unique sort value
    */
    sortByNameFirst: true,

    /*
    * to be included in this nav config, a file's metadata[filterProperty] must equal filterValue
    * ex:
    *   navConfigs key = 'footer'
    *   filterProperty = 'nav_group'
    *   files only added if files[path].nav_group == 'footer'
    */
    filterProperty: false,

    /*
    * if false nav name (navConfigs key) used instead
    * if files[path][filterProperty] equals or contains (string or array containing) filterValue
    * it will be included
    */
    filterValue: false,

    /*
    * the file object property that breadcrumb array is assigned to on each file object
    * breadcrumbs not generated or set if false
    * typically only one navConfig should generate breadcrumbs, often one specifically for them
    */
    breadcrumbProperty: 'breadcrumb_path',

    /**
    * the file object property that the nav path is assigned to on each file object, not set if false
    */
    pathProperty: 'nav_path',

    /**
    * the file object property that an array of nav child nodes will be assigned to
    */
    childrenProperty: 'nav_children',

    /*
    * if a file and sibling dir have matching names the file will be used as the parent in the nav tree
    * ex: /foo /foo.html
    */
    mergeMatchingFilesAndDirs: true,

    /*
    * if ALL dirs should be included as nav nodes
    */
    includeDirs: false,

};

/**
    This should be replaced by a module such as a custom lodash.sortBy build. A suitable
    module (allowing property key or simple callback) could not be found at the time of this writting
**/

/**
* Returns a (stably) sorted copy of list, ranked in ascending order by the
* results of running each value through sortBy. (derrived from underscore.js _.sortBy())
* @method arraySortBy
* @param {Array} list - array to be sorted
* @param {Function|String} sortBy - ( function(item){ return item.sort_property; } )
*    If string used as the name of the property to sort by (eg. length).
* @return {Array} copy of sorted array
*/
var arraySortBy = function(list, sortBy) {

    if(typeof sortBy !== 'function'){
        var sortByKey = sortBy;
        sortBy = function(obj){
            return obj[sortByKey];
        };
    }

    var mapped = list.map(function(value, index, list) {
        return {
            value: value,
            index: index,
            criteria: sortBy(value, index, list)
        };
    });

    var compareFunction = function(left, right) {
        var a = left.criteria,
            b = right.criteria;
        if (a !== b) {
            if (a > b || a === void 0) return 1;
            if (a < b || b === void 0) return -1;
        }
        return left.index < right.index ? -1 : 1;
    };

    mapped.sort(compareFunction);

    return mapped.map(function(item){
        return item.value;
    });
};

/**
* @method eachTreeNode
* @param {Array} tree - nav node tree
* @param {Function} callback - function(node, parent, depth), if callback returns false the node will be removed
* @return {Array} tree
*/
var eachTreeNode = function(tree, callback){
    var iterate = function(nodes, parent, depth){
        depth = (depth || 0) + 1;
        for(var i = nodes.length - 1; i >= 0; i--){
            var node = nodes[i];
            var result = callback(node, parent, depth);
            if(result === false){
                nodes.splice(i, 1);
            }
            iterate(node.children, node, depth);
        }
    };
    iterate(tree);
    return tree;
};

/**
* Convert list of files to tree structure
* @method pathsToTree
* @param {Array} paths - list of file paths to act on.
* @return Array
*/
var pathsToTree = function(paths){
    var items = [];
    for(var i = 0, l = paths.length; i < l; i++) {
        var path = paths[i];
        var name = path[0];
        var rest = path.slice(1);
        var item = null;
        for(var j = 0, m = items.length; j < m; j++) {
            if(items[j].name === name) {
                item = items[j];
                break;
            }
        }

        if(item === null) {
            item = {
                name: name,
                path: null,
                type: null,
                sort: 0,
                children: []
            };
            items.push(item);
        }
        if(rest.length > 0) {
            item.children.push(rest);
        }
    }
    for(i = 0, l = items.length; i < l; i++) {
        var node = items[i];
        node.children = pathsToTree(node.children);
    }
    return items;
};

/**
* Loop over each node and do the following:
*     set node.path - full path to file.
*     set node.type - 'file' or 'dir'.
*
* @method addNodeMetadata
* @param {Array} tree - tree of nodes to act on
* @param {Array} files - List of files being processed by metalsmith.
* @param {String} sortBy - Sort by property key.
* @return {Array} converted array (same obj as tree param)
*/
var addNodeMetadata = function(tree, files, sortBy){
    var fileList = Object.keys(files);

    eachTreeNode(tree, function(node, parent, depth){
        var path = node.name,
            type = 'dir';

        if(parent){
            path = parent.path + '/' + node.name;
        }

        if(fileList.indexOf(path) !== -1){
            type = 'file';
            var file = files[path];
            node.file = file;
            if(sortBy){
                node.sort = file[sortBy];
            }
        }

        node.path = path;
        node.type = type;
        node.depth = depth;
    });

    return tree;
};

/**
* Sort nodes recursively
* @method sortNodes
* @param {Array} nodes - tree of nodes to act on
* @return {Array} sorted node tree
*/
var sortNodes = function(tree, sortBy){
    sortBy = sortBy || 'sort';
    tree = arraySortBy(tree, sortBy);
    for (var i = 0; i < tree.length; i++) {
        var node = tree[i];
        if(node.children){
            node.children = sortNodes(node.children);
        }
    }
    return tree;
};

/**
* Find dirs with a sibling with afile name matching the dir name,
* remove the dir and move child nodes from it to the matching file node
* @method mergeFileDirs
* @param {Array} tree - Tree of nodes to act on.
* @return {Array} converted array (same obj as tree param)
*/
var mergeFileDirs = function(tree){

    var merge = function(siblings){
        for (var i = siblings.length - 1; i >= 0; i--){
            var node = siblings[i];
            if(node.type === 'dir'){
                for(var j = siblings.length - 1; j >= 0; j--){
                    var sibling = siblings[j];
                    if(node !== sibling && sibling.type !== 'dir'){
                        // remove file extension
                        var siblingBaseName = sibling.path.replace(/\.[^/.]+$/, "");
                        // if dir name matches node without file extension
                        if(siblingBaseName === node.path){
                            // copy children from dir to file node and mark the dir to be removed

                            // prevent copying duplicates
                            for(var k = node.children.length - 1; k >= 0; k--){
                                var nodeChild = node.children[k];
                                if(sibling.children.indexOf(nodeChild) === -1){
                                    sibling.children.push(nodeChild);
                                }
                            }
                            var index = siblings.indexOf(node);
                                siblings.splice(index, 1);
                            break;
                        }
                    }
                }
            }
        }

        // recurse children
        for(var i = siblings.length - 1; i >= 0; i--){
            var node = siblings[i];
            if(node.children){
                merge(node.children);
            }
        }
    };

    merge(tree);
    return tree;
};

/**
* @method setFileObjectReferences
* @param {Array} tree - nav node tree
* @param {Object} files - Metalsmith files object
* @param {String} pathProperty - the file object property that the nav path is assigned to on each file object, not set if false
* @param {String} navName - the name of the current nav
* @return {Array} tree
*/
var setFileObjectReferences = function(tree, files, pathProperty, childrenProperty, navName){
    eachTreeNode(tree, function(node, parent, depth){
        var path = node.path;
        if(files[path]){
            if(pathProperty){
                node.file[pathProperty] = path;
            }
            if(childrenProperty){
                node.file[childrenProperty] = node.file[childrenProperty] || {};
                node.file[childrenProperty][navName] = node.children;
            }
        }
        node.parent = parent;
    });
    return tree;
};

/**
* @method setBreadcrumbs
* @param {Array} tree - nav node tree
* @param {String} breadcrumbProperty -
* @return {Array} tree
*/
var setBreadcrumbs = function(tree, breadcrumbProperty){
    eachTreeNode(tree, function(node, parent, depth){
        var breadcrumbPath = [];
        if(parent){
            if(parent[breadcrumbProperty]){
                breadcrumbPath = parent[breadcrumbProperty].concat(parent);
            } else {
                breadcrumbPath = [parent];
            }
        }
        node[breadcrumbProperty] = breadcrumbPath;
        if(node.file){
            node.file[breadcrumbProperty] = breadcrumbPath;
        }
    });
    return tree;
};

/**
* @method filterFiles
* @param {Object} files - Metalsmith files object
* @param {String} key - file metadata key
* @param {String} val - file metadata key value to check for
* @return {Object} filtered files object
*/
var filterFiles = function(files, key, val){
    var out = {};
    for(var fileKey in files){
        var file = files[fileKey],
            groups = file[key];

        if(typeof groups === 'string'){
            if(groups === val){
                out[fileKey] = file;
            }
        } else if(groups && groups.indexOf(val) !== -1){
            out[fileKey] = file;
        }
    }
    return out;
};

/**
* @method transformPermalinks
* @param {Array} tree - nav nodes
* @return {Array} tree
*/
var transformPermalinks = function(tree){
    eachTreeNode(tree, function(node, parent, depth){
        if(node.file){
            if(node.depth > 1){
                if(!node.children || !node.children.length){
                    node.path = path.dirname(node.path);
                    node.parent.path = node.path;
                    node.parent.file = node.file;
                    node.parent.add_trailing = true;
                }
                return false;
            }
        }
    });
    return tree;
};

/*
* @method removeDirs
* @param {Array} tree - nav nodes
* @return {Array} tree
*/
var removeDirs = function(tree){
    eachTreeNode(tree, function(node, parent, depth){
        if(node.type === 'dir'){
            return false;
        }
    });
    return tree;
};

/**
* @method getNav
* @param {String} navName - name of this nav config
* @param {Object} config - A nav config object
* @param {Object} files - metalsmith files plugin param
* @return {Array} nav tree
*/
var getNav = function(navName, config, files){
    config = merge(NAV_CONFIG_DEFAULT, config);

    var sortByNameFirst             = config.sortByNameFirst,
        sortBy                      = config.sortBy,
        breadcrumbProperty          = config.breadcrumbProperty,
        pathProperty                = config.pathProperty,
        mergeMatchingFilesAndDirs   = config.mergeMatchingFilesAndDirs,
        includeDirs                 = config.includeDirs,
        childrenProperty            = config.childrenProperty,
        filterProperty              = config.filterProperty,
        filterValue                 = config.filterValue || navName;

    var navFiles = files;

    // filter to files that match the filterProperty and filterValue
    if(filterProperty){
        navFiles = filterFiles(files, filterProperty, filterValue);
    }

    var paths = Object.keys(navFiles).map(function(path) {
        return path.split('/');
    });

    var nodes = pathsToTree(paths);

    nodes = addNodeMetadata(nodes, files, sortBy);

    if(mergeMatchingFilesAndDirs){
        nodes = mergeFileDirs(nodes);
    }

    if(sortByNameFirst){
        nodes = sortNodes(nodes, 'path');
    }

    if(sortBy){
        nodes = sortNodes(nodes);
    }

    nodes = setFileObjectReferences(nodes, files, pathProperty, childrenProperty, navName);

    if(breadcrumbProperty){
        nodes = setBreadcrumbs(nodes, breadcrumbProperty);
    }

    if(!includeDirs){
        nodes = removeDirs(nodes);
    }

    return nodes;
};

/**
* @method plugin
* @param {Object} navConfigs - list of nav config objects indexed by name
* @param {Object} settings - list of non nav specific plugin settings
* @return {Function}
*/
var plugin = function plugin(navConfigs, settings){
    navConfigs = navConfigs || {};
    settings = settings || {};

    settings = merge(SETTINGS_DEFAULT, settings);

    var navListProperty = settings.navListProperty;
    var permalinks      = settings.permalinks;

    return function(files, metalsmith, done){

        var metadata = metalsmith.metadata();

        if(navListProperty){
            metadata[navListProperty] = {};
        }

        // key is the name of the nav
        for(var key in navConfigs){

            var config = navConfigs[key];

            // get processed nav
            var nav = getNav(key, config, files);

            // apply permalinks if set in settings
            if(permalinks){
                nav = transformPermalinks(nav);
            }

            // add to global metadata
            if(navListProperty){
                metadata[navListProperty][key] = nav;
            }
        }
        done();
    };
};

module.exports = plugin;<|endoftext|>


<commit_before>import React from 'react';

import { Zerotorescue, HolySchmidt, Abelito75 } from 'CONTRIBUTORS';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';

export default [
  {
    date: new Date('2019-06-02'),
    changes: <>Enabled the QElive auto import link for stat values.</>,
    contributors: [Abelito75],
  },
  {
    date: new Date('2019-05-19'),
    changes: <><SpellLink id={SPELLS.AVENGING_CRUSADER_TALENT.id} /> statistic added showing total healing, healing transfered to beacons and tracks triggers as healing abilities.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-03-21'),
    changes: <><SpellLink id={SPELLS.GRACE_OF_THE_JUSTICAR.id} /> statistic added showing total healing, players hit per cast and healing transfered to beacons.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-03-19'),
    changes: <><SpellLink id={SPELLS.RADIANT_INCANDESCENCE.id} /> statistic tracker added showing healing, crit rate, damage and healing transfered to beacons.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-02-24'),
    changes: <><SpellLink id={SPELLS.GLIMMER_OF_LIGHT.id} /> has been marked as scaling with Mastery and Haste and should now affect the stat values.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-30'),
    changes: <><SpellLink id={SPELLS.GLIMMER_OF_LIGHT.id} /> has been added to the list of spells that beacon transfer (50% efficiency).</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-05'),
    changes: <><SpellLink id={SPELLS.LIGHT_OF_THE_MARTYR.id} /> that did negative healing are now highlighted in the timeline.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-05'),
    changes: <><SpellLink id={SPELLS.LIGHT_OF_THE_MARTYR.id} /> cast while <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} /> was available will now be highlighted in the timeline.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-05'),
    changes: <>Fixed a bug in the analysis of <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> that caused the active damage reduction to sometimes not properly be accounted for.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-12-13'),
    changes: <>Fixed a bug in the analysis of <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> that caused the passive damage reduction to not be properly accounted for.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-11-26'),
    changes: <>Improved accuracy of mastery effectiveness by using an advanced algorithm to determine if a player would actually have had an increased chance of survival from a potential increase in mastery effectiveness.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-11-10'),
    changes: <>Fixed a crash when using <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} />.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-10-10'),
    changes: <>Fixed a rare bug in the <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> damage reduction analysis where an immunity at the exact right time could throw off the analysis.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-16'),
    changes: <>Added <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> healing lost from missing beacons as well as beacon line of sighting.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Improved <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> transfer tracking accuracy when line of sighting beacon targets.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Fixed a bug in <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> transfer tracking where line of sighting a beacon targets could lead to inaccurate results.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Added a <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> transfer sources breakdown tab.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Fixed a bug in the analysis of <SpellLink id={SPELLS.HOLY_AVENGER_TALENT.id} /> where beacon healing was included twice.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-11'),
    changes: 'Shuffled the checklist to put higher importance things higher in the list.',
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-08-03'),
    changes: <><SpellLink id={SPELLS.DEVOTION_AURA_TALENT.id} /> analysis will no longer crash when the combatlog is corrupt.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-24'),
    changes: <>Improved the accuracy of the <SpellLink id={SPELLS.DEVOTION_AURA_TALENT.id} /> statistic by correctly scaling the passive via the new formula (more info).</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-24'),
    changes: <>Improved the accuracy of the <SpellLink id={SPELLS.DEVOTION_AURA_TALENT.id} /> statistic by excluding false positives such as <SpellLink id={SPELLS.STAGGER.id} /> from the <SpellLink id={SPELLS.AURA_MASTERY.id} /> effect. Added a link to WCL to view the details of Aura Mastery.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-22'),
    changes: 'Changed the "FoL/HL on beacons" statistic to "Direct beacon healing", so it now includes all beacon transfering spells.',
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-22'),
    changes: <>Reworked the <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> analyzer to get a much more accurate result. The old method had a lot of false positive damage included due to bugs in the logs coming out of the game.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-22'),
    changes: <>Changed the <SpellLink id={SPELLS.DIVINE_PURPOSE_TALENT_HOLY.id} /> analyzer to include back to back procs.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-21'),
    changes: <>Healing increases such as Ilterendi, Crown Jewel of Silvermoon now correctly include boosted healing caused by <SpellLink id={SPELLS.AVENGING_CRUSADER_TALENT.id} />.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-19'),
    changes: <>Added <SpellLink id={SPELLS.AVENGING_CRUSADER_TALENT.id} /> to the cooldown tracker.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-19'),
    changes: 'Removed Tyr\'s Deliverance.',
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-18'),
    changes: <>Added an accurate <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> stat for the reworked version.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-03'),
    changes: <>Fixed a bug where the mana reduction by <SpellLink id={SPELLS.DIVINE_PURPOSE_TALENT_HOLY.id} /> of <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} /> and <SpellLink id={SPELLS.LIGHT_OF_DAWN_CAST.id} /> was not correctly accounted for in the cooldowns tab.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-06-29'),
    changes: <>Updated the crit bonus for <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} /> to 30% (up from 25%).</>,
    contributors: [Zerotorescue],
  },
];
<commit_msg>Added description to the change log.<commit_after>import React from 'react';

import { Zerotorescue, HolySchmidt, Abelito75 } from 'CONTRIBUTORS';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';

export default [
  {
    date: new Date('2019-06-13'),
    changes: <><SpellLink id={SPELLS.GLIMMER_OF_LIGHT.id} /> statistic added showing healing, damage, beacon healing, <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} />s/min and refresh utilization.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-06-02'),
    changes: <>Enabled the QElive auto import link for stat values.</>,
    contributors: [Abelito75],
  },
  {
    date: new Date('2019-05-19'),
    changes: <><SpellLink id={SPELLS.AVENGING_CRUSADER_TALENT.id} /> statistic added showing total healing, healing transfered to beacons and tracks triggers as healing abilities.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-03-21'),
    changes: <><SpellLink id={SPELLS.GRACE_OF_THE_JUSTICAR.id} /> statistic added showing total healing, players hit per cast and healing transfered to beacons.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-03-19'),
    changes: <><SpellLink id={SPELLS.RADIANT_INCANDESCENCE.id} /> statistic tracker added showing healing, crit rate, damage and healing transfered to beacons.</>,
    contributors: [HolySchmidt],
  },
  {
    date: new Date('2019-02-24'),
    changes: <><SpellLink id={SPELLS.GLIMMER_OF_LIGHT.id} /> has been marked as scaling with Mastery and Haste and should now affect the stat values.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-30'),
    changes: <><SpellLink id={SPELLS.GLIMMER_OF_LIGHT.id} /> has been added to the list of spells that beacon transfer (50% efficiency).</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-05'),
    changes: <><SpellLink id={SPELLS.LIGHT_OF_THE_MARTYR.id} /> that did negative healing are now highlighted in the timeline.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-05'),
    changes: <><SpellLink id={SPELLS.LIGHT_OF_THE_MARTYR.id} /> cast while <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} /> was available will now be highlighted in the timeline.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2019-01-05'),
    changes: <>Fixed a bug in the analysis of <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> that caused the active damage reduction to sometimes not properly be accounted for.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-12-13'),
    changes: <>Fixed a bug in the analysis of <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> that caused the passive damage reduction to not be properly accounted for.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-11-26'),
    changes: <>Improved accuracy of mastery effectiveness by using an advanced algorithm to determine if a player would actually have had an increased chance of survival from a potential increase in mastery effectiveness.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-11-10'),
    changes: <>Fixed a crash when using <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} />.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-10-10'),
    changes: <>Fixed a rare bug in the <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> damage reduction analysis where an immunity at the exact right time could throw off the analysis.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-16'),
    changes: <>Added <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> healing lost from missing beacons as well as beacon line of sighting.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Improved <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> transfer tracking accuracy when line of sighting beacon targets.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Fixed a bug in <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> transfer tracking where line of sighting a beacon targets could lead to inaccurate results.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Added a <SpellLink id={SPELLS.BEACON_OF_LIGHT_HEAL.id} /> transfer sources breakdown tab.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-15'),
    changes: <>Fixed a bug in the analysis of <SpellLink id={SPELLS.HOLY_AVENGER_TALENT.id} /> where beacon healing was included twice.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-09-11'),
    changes: 'Shuffled the checklist to put higher importance things higher in the list.',
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-08-03'),
    changes: <><SpellLink id={SPELLS.DEVOTION_AURA_TALENT.id} /> analysis will no longer crash when the combatlog is corrupt.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-24'),
    changes: <>Improved the accuracy of the <SpellLink id={SPELLS.DEVOTION_AURA_TALENT.id} /> statistic by correctly scaling the passive via the new formula (more info).</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-24'),
    changes: <>Improved the accuracy of the <SpellLink id={SPELLS.DEVOTION_AURA_TALENT.id} /> statistic by excluding false positives such as <SpellLink id={SPELLS.STAGGER.id} /> from the <SpellLink id={SPELLS.AURA_MASTERY.id} /> effect. Added a link to WCL to view the details of Aura Mastery.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-22'),
    changes: 'Changed the "FoL/HL on beacons" statistic to "Direct beacon healing", so it now includes all beacon transfering spells.',
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-22'),
    changes: <>Reworked the <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> analyzer to get a much more accurate result. The old method had a lot of false positive damage included due to bugs in the logs coming out of the game.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-22'),
    changes: <>Changed the <SpellLink id={SPELLS.DIVINE_PURPOSE_TALENT_HOLY.id} /> analyzer to include back to back procs.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-21'),
    changes: <>Healing increases such as Ilterendi, Crown Jewel of Silvermoon now correctly include boosted healing caused by <SpellLink id={SPELLS.AVENGING_CRUSADER_TALENT.id} />.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-19'),
    changes: <>Added <SpellLink id={SPELLS.AVENGING_CRUSADER_TALENT.id} /> to the cooldown tracker.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-19'),
    changes: 'Removed Tyr\'s Deliverance.',
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-18'),
    changes: <>Added an accurate <SpellLink id={SPELLS.AURA_OF_SACRIFICE_TALENT.id} /> stat for the reworked version.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-07-03'),
    changes: <>Fixed a bug where the mana reduction by <SpellLink id={SPELLS.DIVINE_PURPOSE_TALENT_HOLY.id} /> of <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} /> and <SpellLink id={SPELLS.LIGHT_OF_DAWN_CAST.id} /> was not correctly accounted for in the cooldowns tab.</>,
    contributors: [Zerotorescue],
  },
  {
    date: new Date('2018-06-29'),
    changes: <>Updated the crit bonus for <SpellLink id={SPELLS.HOLY_SHOCK_CAST.id} /> to 30% (up from 25%).</>,
    contributors: [Zerotorescue],
  },
];
<|endoftext|>


<commit_before>'use strict';

var GridFS = require('gridfs-stream');
var Stream = require('stream');
var is = require('is_js');
var fs = require('fs');

/**
 * FiGrid constructor.
 */
function FiGrid() {
  this.mongo = null;
  this.db = null;
}

/**
 * FiGrid initialization method.
 *
 * @param {mongo.Db} db An open mongo.Db instance.
 * @param {mongo} mongo The native driver you are using.
 * @throws {Error} If any of this arguments is invalid.
 */
FiGrid.prototype.init = function init(db, mongo) {
  GridFS.mongo = mongo;

  this.mongo = mongo;
  this.db = db;
};

/**
 * Write a new file to GridFS.
 *
 * @param {Stream.Readable|Buffer|String} source The input read stream.
 * @param {Object} options Any valid gridfs-stream options.
 * @param {Function} callback The callback function.
 */
FiGrid.prototype.write = function write() {
  /* Last argument should be a callback function */
  var callback = arguments[arguments.length - 1];
  /* Set empty object as initial options */
  var options = {};

  /* Check for callback function */
  if (is.not.function(callback)) {
    throw new TypeError("Must provide a callback function!");
  }

  /* Check if options where passed */
  if (is.object(arguments[1])) {
    options = arguments[1];
  }

  var Grid = new GridFS(this.db);
  var source = arguments[0];

  options.mode = options.mode || 'w';
  options.filename = options.filename || 'unnamed_file';

  var ws = Grid.createWriteStream(options);

  ws.on('close', function (fsfile) {
    callback(null, fsfile);
  });

  ws.on('error', function (err) {
    callback(err);
  });

  if (source instanceof Stream.Readable) {
    source.pipe(ws);
  } else if (is.string(source)) {
    try {
      fs.createReadStream(source).pipe(ws);
    } catch (ex) {
      callback(ex);
    }
  } else if (source instanceof Buffer) {
    ws.write(source);
    ws.end();
  } else {
    callback(new Error("Source can only be a Readable Stream, Buffer or String!"));
  }
};

/**
 * Read a file from GridFS.
 *
 * @param {String} filter Either a valid ObjectID string or a filename.
 * @param {Function} callback The callback function.
 */
FiGrid.prototype.read = function read(filter, callback) {
  var Grid = new GridFS(this.db);
  var query = {};
  var _id;

  /* Check for callback function */
  if (is.not.function(callback)) {
    throw new TypeError("Must provide a callback function! E.g.: function (err, fsfile, stream) {...}");
  }

  /* Check if the filter is an ObjectID, filename or something else */
  _id = Grid.tryParseObjectId(filter);

  if (_id) {
    query._id = _id;
  } else if (is.string(filter)) {
    query.filename = filter;
  } else {
    throw new TypeError("Invalid query filter value! Must be either a valid ObjectID or a filename.");
  }

  /* Fetch the file information if any */
  Grid.collection().findOne(query, function (err, fsfile) {
    if (err) {
      return callback(err);
    }

    if (!fsfile) {
      return callback();
    }

    /* Check if the file data exists */
    Grid.exist(query, function (err, found) {
      if (err) {
        return callback(err);
      }

      if (!found) {
        return callback();
      }

      /* Return the file information and the read stream */
      callback(null, fsfile, Grid.createReadStream(query));
    });
  });
};

/**
 * Removes a file by passing any options, at least an _id or filename
 *
 * @param {Object} options
 * @param {Function} callback
 */
FiGrid.prototype.remove = function remove(param, callback) {
  var Grid = new GridFS(this.db);
  var options = {};
  var _id;

  /* Check for callback function */
  if (is.not.function(callback)) {
    throw new TypeError("Must provide a callback [Function]!");
  }

  if (is.not.string(param) && is.not.object(param)) {
    throw new Error("The first argument must be a [String] or an [ObjectId]!");
  }

  /* Check if the filter is an ObjectID, filename or something else */
  _id = Grid.tryParseObjectId(param);

  if (_id) {
    options._id = _id;
  } else if (is.string(param)) {
    options.filename = param;
  } else {
    throw new TypeError("Invalid query filter value! Must be either a valid ObjectID or a filename.");
  }

  return Grid.remove(options, callback);
};

module.exports = exports = new FiGrid();
<commit_msg>Added Promises implementation<commit_after>'use strict';

const GridFS = require('gridfs-stream');
const Stream = require('stream');
const is = require('fi-is');
const fs = require('fs');

const ERR_INVALID_SELECTOR = 'Invalid query selector! It must be either a valid ObjectID or a filename.';
const ERR_WRONG_SOURCE = 'Source can only be a Readable Stream, Buffer or String!';
const ERR_NOT_FOUND = 'Not Found!';
const ERR_NO_MATCH = 'No match!';

/**
 * FiGrid constructor.
 */
class FiGrid {
  constructor() {
    this.mongo = null;
    this.db = null;
  }

  /**
   * FiGrid initialization method.
   *
   * @param {mongo.Db} db An open mongo.Db instance.
   * @param {mongo} mongo The native driver you are using.
   * @throws {Error} If any of this arguments is invalid.
   */
  init(db, mongo) {
    GridFS.mongo = mongo;

    this.mongo = mongo;
    this.db = db;
  }

  /**
   * Write a new file to GridFS.
   *
   * @param {Stream.Readable|Buffer|String} source The input read stream.
   * @param {Object} options Any valid gridfs-stream options.
   * @param {Function} callback The callback function.
   *
   * @returns {Promise}
   */
  write() {
    /* Last argument should be a callback function */
    var callback = arguments[arguments.length - 1];

    /* Set empty object as initial options */
    var options = {};

    /* Check for callback function or behave as promise */
    if (is.not.function(callback)) {
      callback = function () {};
    }

    /* Check if options where passed */
    if (is.object(arguments[1])) {
      options = arguments[1];
    }

    var Grid = new GridFS(this.db);
    var source = arguments[0];

    options.content_type = options.contentType || options.content_type || null;
    options.filename = options.filename || 'unnamed_file';
    options.mode = options.mode || 'w';

    return new Promise((resolve, reject) => {
      var ws = Grid.createWriteStream(options);

      ws.once('close', (fsfile) => {
        callback(null, fsfile);
        resolve(fsfile);
      });

      ws.once('error', (err) => {
        callback(err);
        reject(err);
      });

      if (source instanceof Stream.Readable) {
        source.pipe(ws);
      } else if (is.string(source)) {
        try {
          fs.createReadStream(source).pipe(ws);
        } catch (ex) {
          callback(ex);
          reject(err);
        }
      } else if (source instanceof Buffer) {
        ws.write(source);
        ws.end();
      } else {
        var err = new Error(ERR_WRONG_SOURCE);

        callback(err);
        reject(err);
      }
    });
  }

  /**
   * Read a file from GridFS.
   *
   * @param {String} filter Either a valid ObjectID string or a filename.
   * @param {Function} callback The callback function.
   */
  read(selector, callback) {
    var Grid = new GridFS(this.db);
    var query = {};
    var _id;

    /* Check for callback function */
    if (is.not.function(callback)) {
      callback = function () {};
    }

    return new Promise((resolve, reject) => {
      /* Check if the filter is an ObjectID, filename or something else */
      _id = Grid.tryParseObjectId(selector);

      if (_id) {
        query._id = _id;
      } else if (is.string(selector)) {
        query.filename = selector;
      } else {
        var err = new TypeError(ERR_INVALID_SELECTOR);

        callback(err);
        reject(err);
      }

      /* Fetch the file information if any */
      Grid.collection().findOne(query, (err, fsfile) => {
        if (err) {
          callback(err);
          reject(err);

          return;
        }

        if (!fsfile) {
          err = new Error(ERR_NO_MATCH);

          callback(err);
          reject(err);

          return;
        }

        /* Check if the file data exists */
        Grid.exist(query, (err, found) => {
          if (err) {
            callback(err);
            reject(err);

            return;
          }

          if (!found) {
            err = new Error(ERR_NOT_FOUND);

            callback(err);
            reject(err);

            return;
          }

          /* Return the file information and the read stream */
          fsfile.stream = Grid.createReadStream(query);

          callback(null, fsfile, fsfile.stream);
          resolve(fsfile);
        });
      });
    });
  }

  /**
   * Removes a file by passing any options, at least an _id or filename
   *
   * @param {Object} options
   * @param {Function} callback
   */
  remove(param, callback) {
    var Grid = new GridFS(this.db);
    var options = {};
    var _id;

    /* Check for callback function */
    if (is.not.function(callback)) {
      callback = function () {};
    }

    return new Promise((resolve, reject) => {
      var err;

      if (is.not.string(param) && is.not.object(param)) {
        err = new Error(ERR_INVALID_SELECTOR);

        callback(err);
        reject(err);

        return;
      }

      /* Check if the filter is an ObjectID, filename or something else */
      _id = Grid.tryParseObjectId(param);

      if (_id) {
        options._id = _id;
      } else if (is.string(param)) {
        options.filename = param;
      } else {
        err = new Error(ERR_INVALID_SELECTOR);

        callback(err);
        reject(err);

        return;
      }

      return Grid.remove(options, (err) => {
        if (err) {
          callback(err);
          reject(err);

          return;
        }

        callback();
        reject();
      });
    });
  }
}

module.exports = exports = new FiGrid();
<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';

// application config
var config = require('./config.json');

var app         = require('express')(),
    http        = require('http').Server(app),
    cors        = require('cors'),
    bodyParser  = require('body-parser');

var request         = require('request'),
    extend          = require('util')._extend,
    cliOptions 	    = require('commander'),
    fs              = require('fs'),
    pathUtil        = require('path'),
    execSync        = require('child_process').execSync,
    when            = require("promised-io/promise").when;

var validateToken 	= require('./lib/validateToken.js');

cliOptions.version('0.0.1')
           .option('-d, --dev', 'Developer mode; this option attempts to use a token in the file: dev-user-token')
           .parse(process.argv);

// if --dev option is used, set token to token in file "./dev-user-token"
// otherwise, pass on token, if there is one
if (cliOptions.dev) {
    let token = fs.readFileSync('dev-user-token', 'utf8').trim();
    console.log('\n\x1b[36m'+'using development token:'+'\x1b[0m', token, '\n')

    app.all('/', (req, res, next) => {
        req.headers = {"Authorization": token};
        next();
    }).use((req, res, next) => {
        req.headers = {"Authorization": token};
        next();
    })
}

// Configure CORs and body parser.
app.use( cors() )
   .use( bodyParser.urlencoded({extended: false, limit: '50mb'}) );

// Configure Logging
app.use( (req, res, next) => {
    console.log('%s %s', req.method, req.url);
    next();
});

/**
 * @api {get} /list/:path list files/folders in path
 * @apiName list
 *
 * @apiParam {string} path path to directory
 * @apiParam {string} ?type=(folders|files) only fetch folders or files
 *
 * @apiSampleRequest /list/my/models/
 *
 * @apiSuccess {json} meta metadata for listed objects
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *      [
 *       {
 *           name: "blue-panda",
 *           mtime: 1459822597000,
 *           size: 476
 *       }, {
 *           name: "blue-zebra",
  *          mtime: 1458347601000,
 *           size: 170,
 *           isFolder: true
 *       }
 *     ]
 */
app.get('/v0/list/*', AuthRequired, (req, res) => {
    console.log('req.user.id:', req.user.id);
    const user = req.user.id;
    const opts = req.query;

    const rootDir = config.ftpRoot,
          path = '/'+req.params[0],
          fullPath = rootDir+path;

    // check if user has home
    let hasHome;
    try {
        hasHome = fs.statSync(rootDir+'/'+user).isDirectory();
    } catch(e) {
        hasHome = false;
        console.log("User's home not found:", user);
    }

    // if needed, create user's directory and enable acl's if it doesn't exist
    if (!hasHome) {
        let scriptPath = '/root/add_acl_dolson.py';
        try {
            let scriptRes = execSync(scriptPath+' --share-dir="'+rootDir+'/'+
                user+'/" --share-name="'+user+'"');
        } catch(e) {
            let msg = 'User ('+user+')'+
                ' does not have a directory and server could not run share script: '
                + scriptPath
            console.log(msg);

            res.status(500).send({error: msg});
        }
    }

    // generate list of objects describing contents
    let files = [];
    fs.readdirSync(fullPath).forEach( (file) => {
        let filePath = pathUtil.join(fullPath, file),
            stats = fs.statSync(filePath),
            isDir = stats.isDirectory();

        if (opts.type === 'file' && isDir) return;
        if (opts.type === 'folder' && !isDir) return;

        let fileObj = {
            name: file,
            path: path+'/'+file,
            mtime: stats.mtime.getTime(),
            size: stats.size
        }

        // additional info if is directory
        if (isDir) {
            fileObj.isFolder = true;
            //fileObj.folderCount = parseInt( execSync('find "' +
            //    filePath+'" -maxdepth 1 -type d | wc -l').toString() ) - 1;
        }

        files.push(fileObj);
    });

    res.send(files);
})

/**
 * @api {get} /quota get spaced used and total allowed for user (not in use!)
 * @apiName quota
 *
 *
 * @apiSampleRequest /quota/
 *
 * @apiSuccess {json} meta give used/available space for user
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *        used: used,
 *        available: 1000000
 *     }
 */
/*
.get('/v0/quota', AuthRequired, (req, res) => {
    const opts = req.query;
    const userDir = '/Users/nc/';

    let str = execSync("du -s "+userDir).toString(),
        e = str.indexOf('\t');
    let used = parseInt(str.slice(0, e));

    let quota = {
        used: used,
        available: 1000000
    }

    res.send(quota);
})
*/

/**
 * @api {get} /test-service/  Way to test simple, unauthenticated GET request.
 *  Note: no version "v0", "v1", etc, in the endpoint.
 *
 * @apiName test-service
 *
 * @apiSampleRequest /test-server/
 *
 * @apiSuccess {json} string Should return code 200 with string
 *  "This is just a test. This is only a test."
 *
 */
.get('/test-service', (req, res) => {
    res.status(200).send( 'This is just a test. This is only a test.' );
})

function AuthRequired(req, res, next) {
    // if no token at all, return 401
    if (!('authorization' in req.headers)) {
        res.status(401).send( {error: 'Auth is required!'} );
    }

    when(validateToken(req.headers.authorization),
        userObj => {
            if (!(userObj && 'id' in userObj)) {
                res.status(401).send( {error: 'Invalid token!'} );
                return;
            }

            // Pass user id along
            req.user = userObj;

            // safe to move along.
            next();
        })
}

var server = http.listen(3000, () => {
    var host = server.address().address;
    var port = server.address().port;

    console.log('Example app listening at http://%s:%s', host, port);
});
<commit_msg>directory restrictions and error handling<commit_after>#!/usr/bin/env node
'use strict';

// application config
var config = require('./config.json');

var app         = require('express')(),
    http        = require('http').Server(app),
    cors        = require('cors'),
    bodyParser  = require('body-parser');

var request         = require('request'),
    extend          = require('util')._extend,
    cliOptions 	    = require('commander'),
    fs              = require('fs'),
    pathUtil        = require('path'),
    execSync        = require('child_process').execSync,
    when            = require("promised-io/promise").when;

var validateToken 	= require('./lib/validateToken.js');

cliOptions.version('0.0.1')
           .option('-d, --dev', 'Developer mode; this option attempts to use a token in the file: dev-user-token')
           .parse(process.argv);

// if --dev option is used, set token to token in file "./dev-user-token"
// otherwise, pass on token, if there is one
if (cliOptions.dev) {
    let token = fs.readFileSync('dev-user-token', 'utf8').trim();
    console.log('\n\x1b[36m'+'using development token:'+'\x1b[0m', token, '\n')

    app.all('/', (req, res, next) => {
        req.headers = {"Authorization": token};
        next();
    }).use((req, res, next) => {
        req.headers = {"Authorization": token};
        next();
    })
}

// Configure CORs and body parser.
app.use( cors() )
   .use( bodyParser.urlencoded({extended: false, limit: '50mb'}) );

// Configure Logging
app.use( (req, res, next) => {
    console.log('%s %s', req.method, req.url);
    next();
});

/**
 * @api {get} /list/:path list files/folders in path
 * @apiName list
 *
 * @apiParam {string} path path to directory
 * @apiParam {string} ?type=(folders|files) only fetch folders or files
 *
 * @apiSampleRequest /list/my/models/
 *
 * @apiSuccess {json} meta metadata for listed objects
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *      [
 *       {
 *           name: "blue-panda",
 *           mtime: 1459822597000,
 *           size: 476
 *       }, {
 *           name: "blue-zebra",
  *          mtime: 1458347601000,
 *           size: 170,
 *           isFolder: true
 *       }
 *     ]
 */
app.get('/v0/list/*', AuthRequired, (req, res) => {
    console.log('req.user.id:', req.user.id);
    const user = req.user.id,
          opts = req.query;

    const requestedPath = req.params[0];
    let pathList = requestedPath.split('/').filter(s => {
        if (s !== '') return true;
    })
    const requestedHome = pathList.shift();

    // replace user with authenticated user
    const path = '/'+user+'/'+pathList.join('/');

    // throw error if user doesn't have access
    if (user != requestedHome) {
        let msg = 'User ('+user+')'+
                ' does have permission to access: '
                + requestedPath
        res.status(403).send({error: msg});
        return;
    }

    const rootDir = config.ftpRoot,
          fullPath = rootDir+path;

    // check if user has home
    let hasHome = false;
    try {
        hasHome = fs.statSync(rootDir+'/'+user).isDirectory();
    } catch(e) {
        console.log("User's home not found:", user);
    }

    // if needed, create user's directory and enable acl's
    if (!hasHome) {
        let scriptPath = '/root/add_acl_dolson.py';
        try {
            let scriptRes = execSync(scriptPath+' --share-dir="'+rootDir+'/'+
                user+'/" --share-name="'+user+'"');
        } catch(e) {
            let msg = 'User ('+user+')'+
                ' does not have a directory and server could not run share script: '
                + scriptPath
            console.log(msg);

            res.status(500).send({error: msg});
            return;
        }
    }

    // generate list of objects describing contents
    let files = [];
    fs.readdirSync(fullPath).forEach( (file) => {
        let filePath = pathUtil.join(fullPath, file),
            stats = fs.statSync(filePath),
            isDir = stats.isDirectory();

        if (opts.type === 'file' && isDir) return;
        if (opts.type === 'folder' && !isDir) return;

        let fileObj = {
            name: file,
            path: path+'/'+file,
            mtime: stats.mtime.getTime(),
            size: stats.size
        }

        // additional info if is directory
        if (isDir) {
            fileObj.isFolder = true;
            //fileObj.folderCount = parseInt( execSync('find "' +
            //    filePath+'" -maxdepth 1 -type d | wc -l').toString() ) - 1;
        }

        files.push(fileObj);
    });

    res.send(files);
})

/**
 * @api {get} /quota get spaced used and total allowed for user (not in use!)
 * @apiName quota
 *
 *
 * @apiSampleRequest /quota/
 *
 * @apiSuccess {json} meta give used/available space for user
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *        used: used,
 *        available: 1000000
 *     }
 */
/*
.get('/v0/quota', AuthRequired, (req, res) => {
    const opts = req.query;
    const userDir = '/Users/nc/';

    let str = execSync("du -s "+userDir).toString(),
        e = str.indexOf('\t');
    let used = parseInt(str.slice(0, e));

    let quota = {
        used: used,
        available: 1000000
    }

    res.send(quota);
})
*/

/**
 * @api {get} /test-service/  Way to test simple, unauthenticated GET request.
 *  Note: no version "v0", "v1", etc, in the endpoint.
 *
 * @apiName test-service
 *
 * @apiSampleRequest /test-server/
 *
 * @apiSuccess {json} string Should return code 200 with string
 *  "This is just a test. This is only a test."
 *
 */
.get('/test-service', (req, res) => {
    res.status(200).send( 'This is just a test. This is only a test.' );
})

function AuthRequired(req, res, next) {
    // if no token at all, return 401
    if (!('authorization' in req.headers)) {
        res.status(401).send( {error: 'Auth is required!'} );
    }

    when(validateToken(req.headers.authorization),
        userObj => {
            if (!(userObj && 'id' in userObj)) {
                res.status(401).send( {error: 'Invalid token!'} );
                return;
            }

            // Pass user id along
            req.user = userObj;

            // safe to move along.
            next();
        })
}

var server = http.listen(3000, () => {
    var host = server.address().address;
    var port = server.address().port;

    console.log('Example app listening at http://%s:%s', host, port);
});
<|endoftext|>


<commit_before>// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License.  You may obtain a copy
// of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
// License for the specific language governing permissions and limitations under
// the License.

couchTests.list_views = function(debug) {
  var db = new CouchDB("test_suite_db");
  db.deleteDb();
  db.createDb();
  if (debug) debugger;

  var designDoc = {
    _id:"_design/lists",
    language: "javascript",
    views : {
      basicView : {
        map : stringFun(function(doc) {
          emit(doc.integer, doc.string);
        })
      },
      withReduce : {
        map : stringFun(function(doc) {
          emit(doc.integer, doc.string);
        }),
        reduce : stringFun(function(keys, values, rereduce) {
          if (rereduce) {
            return sum(values);
          } else {
            return values.length;
          }
        })
      }
    },
    lists: {
      simpleForm: stringFun(function(head, row, req, row_info) {
        if (row) {
          // we ignore headers on rows and tail
          return {
                  body : '\n
Key: '+row.key
                  +' Value: '+row.value
                  +' LineNo: '+row_info.row_number+'
'
          };
        } else if (head) {
          // we return an object (like those used by external and show)
          // so that we can specify headers
          return {
            body : '
Total Rows: '
              + head.total_rows
              + ' Offset: ' + head.offset
              + '

'
          };
        } else {
          // tail
          return {body : '
'+
              '
FirstKey: '+(row_info ? row_info.first_key : '')+
              ' LastKey: '+(row_info ? row_info.prev_key : '')+'
'};
        }
      }),
      acceptSwitch: stringFun(function(head, row, req, row_info) {
        return respondWith(req, {
          html : function() {
            // If you're outputting text and you're not setting
            // any headers, you can just return a string.
            if (head) {
              return "HTML
";
            } else if (row) {
              return '\n
Key: '
                +row.key+' Value: '+row.value
                +' LineNo: '+row_info.row_number+'
';
            } else { // tail
              return '
';

            }
          },
          xml : function() {
            if (head) {
              return '<feed xmlns="http://www.w3.org/2005/Atom">'
                +'<title>Test XML Feed</title>';
            } else if (row) {
              // Becase Safari can't stand to see that dastardly
              // E4X outside of a string. Outside of tests you
              // can just use E4X literals.
              var entry = new XML('<entry/>');
              entry.id = row.id;
              entry.title = row.key;
              entry.content = row.value;
              // We'll also let you return just an E4X object
              // if you aren't setting headers.
              return entry;
            } else {
              return "</feed>";
            }
          }
        })
      }),
      qsParams: stringFun(function(head, row, req, row_info) {
        if(head) return {body: req.query.foo};
        else return {body: "\n"};
      }),
      stopIter: stringFun(function(head, row, req, row_info) {
        if(head) {
          return {body: "head"};
        } else if(row) {
          if(row_info.row_number > 2) return {stop: true};
          return {body: " " + row_info.row_number};
        } else {
          return {body: " tail"};
        }
      }),
      stopIter2: stringFun(function(head, row, req, row_info) {
        return respondWith(req, {
          html: function() {
            if(head) {
              return "head";
            } else if(row) {
              if(row_info.row_number > 2) return {stop: true};
              return " " + row_info.row_number;
            } else {
              return " tail";
            }
          }
        });
      }),
      emptyList: stringFun(function(head, row, req, row_info) {
        return { body: "" };
      })
    }
  };

  T(db.save(designDoc).ok);

  var docs = makeDocs(0, 10);
  var saveResult = db.bulkSave(docs);
  T(saveResult.ok);

  var view = db.view('lists/basicView');
  T(view.total_rows == 10);

  // standard get
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Key: 1/.test(xhr.responseText));
  T(/LineNo: 0/.test(xhr.responseText));
  T(/LineNo: 5/.test(xhr.responseText));
  T(/FirstKey: 0/.test(xhr.responseText));
  T(/LastKey: 9/.test(xhr.responseText));

  var lines = xhr.responseText.split('\n');
  T(/LineNo: 5/.test(lines[6]));

  // test that etags are available
  var etag = xhr.getResponseHeader("etag");
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView", {
    headers: {"if-none-match": etag}
  });
  T(xhr.status == 304);

  // get with query params
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView?startkey=3");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(!(/Key: 1/.test(xhr.responseText)));
  T(/FirstKey: 3/.test(xhr.responseText));
  T(/LastKey: 9/.test(xhr.responseText));

  // with 0 rows
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView?startkey=30");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Offset: null/.test(xhr.responseText));

  // reduce with 0 rows
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?startkey=30");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Offset: undefined/.test(xhr.responseText));

  // when there is a reduce present, but not used
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?reduce=false");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Key: 1/.test(xhr.responseText));

  // when there is a reduce present, and used
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true");
  T(xhr.status == 200);
  T(/Key: 1/.test(xhr.responseText));

  // there should be etags on reduce as well
  var etag = xhr.getResponseHeader("etag");
  T(etag, "Etags should be served with reduce lists");
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true", {
    headers: {"if-none-match": etag}
  });
  T(xhr.status == 304);

  // verify the etags expire correctly
  var docs = makeDocs(11, 12);
  var saveResult = db.bulkSave(docs);
  T(saveResult.ok);

  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true", {
    headers: {"if-none-match": etag}
  });
  T(xhr.status == 200);

  // with accept headers for HTML
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/acceptSwitch/basicView", {
    headers: {
      "Accept": 'text/html'
    }
  });
  T(xhr.getResponseHeader("Content-Type") == "text/html");
  T(xhr.responseText.match(/HTML/));
  T(xhr.responseText.match(/Value/));

  // now with xml
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/acceptSwitch/basicView", {
    headers: {
      "Accept": 'application/xml'
    }
  });
  T(xhr.getResponseHeader("Content-Type") == "application/xml");
  T(xhr.responseText.match(/XML/));
  T(xhr.responseText.match(/entry/));

  // now with extra qs params
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/qsParams/basicView?foo=blam");
  T(xhr.responseText.match(/blam/));

  // aborting iteration
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter/basicView");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter2/basicView");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));

  // aborting iteration with reduce
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter/withReduce?group=true");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter2/withReduce?group=true");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));

  // empty list
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/emptyList/basicView");
  T(xhr.responseText.match(/^$/));
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/emptyList/withReduce?group=true");
  T(xhr.responseText.match(/^$/));

};
<commit_msg>allow multi-key POST requests for _list.  closes COUCHDB-269. thanks Jason Davies<commit_after>// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License.  You may obtain a copy
// of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
// License for the specific language governing permissions and limitations under
// the License.

couchTests.list_views = function(debug) {
  var db = new CouchDB("test_suite_db");
  db.deleteDb();
  db.createDb();
  if (debug) debugger;

  var designDoc = {
    _id:"_design/lists",
    language: "javascript",
    views : {
      basicView : {
        map : stringFun(function(doc) {
          emit(doc.integer, doc.string);
        })
      },
      withReduce : {
        map : stringFun(function(doc) {
          emit(doc.integer, doc.string);
        }),
        reduce : stringFun(function(keys, values, rereduce) {
          if (rereduce) {
            return sum(values);
          } else {
            return values.length;
          }
        })
      }
    },
    lists: {
      simpleForm: stringFun(function(head, row, req, row_info) {
        if (row) {
          // we ignore headers on rows and tail
          return {
                  body : '\n
Key: '+row.key
                  +' Value: '+row.value
                  +' LineNo: '+row_info.row_number+'
'
          };
        } else if (head) {
          // we return an object (like those used by external and show)
          // so that we can specify headers
          return {
            body : '
Total Rows: '
              + head.total_rows
              + ' Offset: ' + head.offset
              + '

'
          };
        } else {
          // tail
          return {body : '
'+
              '
FirstKey: '+(row_info ? row_info.first_key : '')+
              ' LastKey: '+(row_info ? row_info.prev_key : '')+'
'};
        }
      }),
      acceptSwitch: stringFun(function(head, row, req, row_info) {
        return respondWith(req, {
          html : function() {
            // If you're outputting text and you're not setting
            // any headers, you can just return a string.
            if (head) {
              return "HTML
";
            } else if (row) {
              return '\n
Key: '
                +row.key+' Value: '+row.value
                +' LineNo: '+row_info.row_number+'
';
            } else { // tail
              return '
';

            }
          },
          xml : function() {
            if (head) {
              return '<feed xmlns="http://www.w3.org/2005/Atom">'
                +'<title>Test XML Feed</title>';
            } else if (row) {
              // Becase Safari can't stand to see that dastardly
              // E4X outside of a string. Outside of tests you
              // can just use E4X literals.
              var entry = new XML('<entry/>');
              entry.id = row.id;
              entry.title = row.key;
              entry.content = row.value;
              // We'll also let you return just an E4X object
              // if you aren't setting headers.
              return entry;
            } else {
              return "</feed>";
            }
          }
        })
      }),
      qsParams: stringFun(function(head, row, req, row_info) {
        if(head) return {body: req.query.foo};
        else return {body: "\n"};
      }),
      stopIter: stringFun(function(head, row, req, row_info) {
        if(head) {
          return {body: "head"};
        } else if(row) {
          if(row_info.row_number > 2) return {stop: true};
          return {body: " " + row_info.row_number};
        } else {
          return {body: " tail"};
        }
      }),
      stopIter2: stringFun(function(head, row, req, row_info) {
        return respondWith(req, {
          html: function() {
            if(head) {
              return "head";
            } else if(row) {
              if(row_info.row_number > 2) return {stop: true};
              return " " + row_info.row_number;
            } else {
              return " tail";
            }
          }
        });
      }),
      emptyList: stringFun(function(head, row, req, row_info) {
        return { body: "" };
      })
    }
  };

  T(db.save(designDoc).ok);

  var docs = makeDocs(0, 10);
  var saveResult = db.bulkSave(docs);
  T(saveResult.ok);

  var view = db.view('lists/basicView');
  T(view.total_rows == 10);

  // standard get
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Key: 1/.test(xhr.responseText));
  T(/LineNo: 0/.test(xhr.responseText));
  T(/LineNo: 5/.test(xhr.responseText));
  T(/FirstKey: 0/.test(xhr.responseText));
  T(/LastKey: 9/.test(xhr.responseText));

  var lines = xhr.responseText.split('\n');
  T(/LineNo: 5/.test(lines[6]));

  // test that etags are available
  var etag = xhr.getResponseHeader("etag");
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView", {
    headers: {"if-none-match": etag}
  });
  T(xhr.status == 304);

  // get with query params
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView?startkey=3");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(!(/Key: 1/.test(xhr.responseText)));
  T(/FirstKey: 3/.test(xhr.responseText));
  T(/LastKey: 9/.test(xhr.responseText));

  // with 0 rows
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView?startkey=30");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Offset: null/.test(xhr.responseText));

  // reduce with 0 rows
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?startkey=30");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Offset: undefined/.test(xhr.responseText));

  // when there is a reduce present, but not used
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?reduce=false");
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(/Key: 1/.test(xhr.responseText));

  // when there is a reduce present, and used
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true");
  T(xhr.status == 200);
  T(/Key: 1/.test(xhr.responseText));

  // there should be etags on reduce as well
  var etag = xhr.getResponseHeader("etag");
  T(etag, "Etags should be served with reduce lists");
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true", {
    headers: {"if-none-match": etag}
  });
  T(xhr.status == 304);

  // verify the etags expire correctly
  var docs = makeDocs(11, 12);
  var saveResult = db.bulkSave(docs);
  T(saveResult.ok);

  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true", {
    headers: {"if-none-match": etag}
  });
  T(xhr.status == 200);

  // with accept headers for HTML
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/acceptSwitch/basicView", {
    headers: {
      "Accept": 'text/html'
    }
  });
  T(xhr.getResponseHeader("Content-Type") == "text/html");
  T(xhr.responseText.match(/HTML/));
  T(xhr.responseText.match(/Value/));

  // now with xml
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/acceptSwitch/basicView", {
    headers: {
      "Accept": 'application/xml'
    }
  });
  T(xhr.getResponseHeader("Content-Type") == "application/xml");
  T(xhr.responseText.match(/XML/));
  T(xhr.responseText.match(/entry/));

  // now with extra qs params
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/qsParams/basicView?foo=blam");
  T(xhr.responseText.match(/blam/));

  // aborting iteration
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter/basicView");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter2/basicView");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));

  // aborting iteration with reduce
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter/withReduce?group=true");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/stopIter2/withReduce?group=true");
  T(xhr.responseText.match(/^head 0 1 2 tail$/));

  // empty list
  var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/emptyList/basicView");
  T(xhr.responseText.match(/^$/));
  xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/emptyList/withReduce?group=true");
  T(xhr.responseText.match(/^$/));

  // multi-key fetch
  var xhr = CouchDB.request("POST", "/test_suite_db/_list/lists/simpleForm/basicView", {
    body: '{"keys":[2,4,5,7]}'
  });
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(!(/Key: 1/.test(xhr.responseText)));
  T(/Key: 2/.test(xhr.responseText));
  T(/FirstKey: 2/.test(xhr.responseText));
  T(/LastKey: 7/.test(xhr.responseText));
  xhr = CouchDB.request("POST", "/test_suite_db/_list/lists/simpleForm/withReduce?group=true", {
    body: '{"keys":[2,4,5,7]}'
  });
  T(xhr.status == 200);
  T(/Total Rows/.test(xhr.responseText));
  T(!(/Key: 1/.test(xhr.responseText)));
  T(/Key: 2/.test(xhr.responseText));
  T(/FirstKey: 2/.test(xhr.responseText));
  T(/LastKey: 7/.test(xhr.responseText));
};
<|endoftext|>


<commit_before>/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

(function() {
    // special patch to correctly work on Ripple emulator (CB-9760)
    if (window.parent && !!window.parent.ripple) { // https://gist.github.com/triceam/4658021
        module.exports = window.open.bind(window); // fallback to default window.open behaviour
        return;
    }

    var exec = require('cordova/exec');
    var channel = require('cordova/channel');
    var modulemapper = require('cordova/modulemapper');
    var urlutil = require('cordova/urlutil');

    function InAppBrowser() {
       this.channels = {
            'loadstart': channel.create('loadstart'),
            'loadstop' : channel.create('loadstop'),
            'loaderror' : channel.create('loaderror'),
            'pollresult' : channel.create('pollresult'),
            'exit' : channel.create('exit')
       };
    }

    InAppBrowser.prototype = {
        _eventHandler: function (event) {
            if(event){
                console.log('Event of type' + event.type);
            }
            if (event && (event.type in this.channels)) {
                console.log('Firirng' + event.type);
                this.channels[event.type].fire(event);
            }
        },
        close: function (eventname) {
            exec(null, null, "InAppBrowser", "close", []);
        },
        show: function (eventname) {
          exec(null, null, "InAppBrowser", "show", []);
        },
        addEventListener: function (eventname,f) {
            console.log('Subscribing ' + event.type);
            if (eventname in this.channels) {
                this.channels[eventname].subscribe(f);
                console.log('Subscribed to ' + event.type);
            }
        },
        removeEventListener: function(eventname, f) {
            if (eventname in this.channels) {
                this.channels[eventname].unsubscribe(f);
            }
        },

        executeScript: function(injectDetails, cb) {
            if (injectDetails.code) {
                exec(cb, null, "InAppBrowser", "injectScriptCode", [injectDetails.code, !!cb]);
            } else if (injectDetails.file) {
                exec(cb, null, "InAppBrowser", "injectScriptFile", [injectDetails.file, !!cb]);
            } else {
                throw new Error('executeScript requires exactly one of code or file to be specified');
            }
        },

        insertCSS: function(injectDetails, cb) {
            if (injectDetails.code) {
                exec(cb, null, "InAppBrowser", "injectStyleCode", [injectDetails.code, !!cb]);
            } else if (injectDetails.file) {
                exec(cb, null, "InAppBrowser", "injectStyleFile", [injectDetails.file, !!cb]);
            } else {
                throw new Error('insertCSS requires exactly one of code or file to be specified');
            }
        }
    };

    module.exports = function(strUrl, strWindowName, strWindowFeatures, callbacks) {
        // Don't catch calls that write to existing frames (e.g. named iframes).
        if (window.frames && window.frames[strWindowName]) {
            var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open');
            return origOpenFunc.apply(window, arguments);
        }

        strUrl = urlutil.makeAbsolute(strUrl);
        var iab = new InAppBrowser();

        callbacks = callbacks || {};
        for (var callbackName in callbacks) {
            iab.addEventListener(callbackName, callbacks[callbackName]);
        }

        var cb = function(eventname) {
           iab._eventHandler(eventname);
        };

        strWindowFeatures = strWindowFeatures || "";

        exec(cb, cb, "InAppBrowser", "open", [strUrl, strWindowName, strWindowFeatures]);
        return iab;
    };
})();
<commit_msg>removed tracing code<commit_after>/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

(function() {
    // special patch to correctly work on Ripple emulator (CB-9760)
    if (window.parent && !!window.parent.ripple) { // https://gist.github.com/triceam/4658021
        module.exports = window.open.bind(window); // fallback to default window.open behaviour
        return;
    }

    var exec = require('cordova/exec');
    var channel = require('cordova/channel');
    var modulemapper = require('cordova/modulemapper');
    var urlutil = require('cordova/urlutil');

    function InAppBrowser() {
       this.channels = {
            'loadstart': channel.create('loadstart'),
            'loadstop' : channel.create('loadstop'),
            'loaderror' : channel.create('loaderror'),
            'pollresult' : channel.create('pollresult'),
            'exit' : channel.create('exit')
       };
    }

    InAppBrowser.prototype = {
        _eventHandler: function (event) {
            if (event && (event.type in this.channels)) {
                this.channels[event.type].fire(event);
            }
        },
        close: function (eventname) {
            exec(null, null, "InAppBrowser", "close", []);
        },
        show: function (eventname) {
          exec(null, null, "InAppBrowser", "show", []);
        },
        addEventListener: function (eventname,f) {
            if (eventname in this.channels) {
                this.channels[eventname].subscribe(f);
            }
        },
        removeEventListener: function(eventname, f) {
            if (eventname in this.channels) {
                this.channels[eventname].unsubscribe(f);
            }
        },

        executeScript: function(injectDetails, cb) {
            if (injectDetails.code) {
                exec(cb, null, "InAppBrowser", "injectScriptCode", [injectDetails.code, !!cb]);
            } else if (injectDetails.file) {
                exec(cb, null, "InAppBrowser", "injectScriptFile", [injectDetails.file, !!cb]);
            } else {
                throw new Error('executeScript requires exactly one of code or file to be specified');
            }
        },

        insertCSS: function(injectDetails, cb) {
            if (injectDetails.code) {
                exec(cb, null, "InAppBrowser", "injectStyleCode", [injectDetails.code, !!cb]);
            } else if (injectDetails.file) {
                exec(cb, null, "InAppBrowser", "injectStyleFile", [injectDetails.file, !!cb]);
            } else {
                throw new Error('insertCSS requires exactly one of code or file to be specified');
            }
        }
    };

    module.exports = function(strUrl, strWindowName, strWindowFeatures, callbacks) {
        // Don't catch calls that write to existing frames (e.g. named iframes).
        if (window.frames && window.frames[strWindowName]) {
            var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open');
            return origOpenFunc.apply(window, arguments);
        }

        strUrl = urlutil.makeAbsolute(strUrl);
        var iab = new InAppBrowser();

        callbacks = callbacks || {};
        for (var callbackName in callbacks) {
            iab.addEventListener(callbackName, callbacks[callbackName]);
        }

        var cb = function(eventname) {
           iab._eventHandler(eventname);
        };

        strWindowFeatures = strWindowFeatures || "";

        exec(cb, cb, "InAppBrowser", "open", [strUrl, strWindowName, strWindowFeatures]);
        return iab;
    };
})();
<|endoftext|>


<commit_before>var gulp = require('gulp');
var less = require('gulp-less');
var browserSync = require('browser-sync').create();
var header = require('gulp-header');
var cleanCSS = require('gulp-clean-css');
var rename = require("gulp-rename");
var uglify = require('gulp-uglify');
var pkg = require('./package.json');

// Set the banner content
var banner = ['/*!\n',
    ' * Start Bootstrap - <%= pkg.title %> v<%= pkg.version %> (<%= pkg.homepage %>)\n',
    ' * Copyright 2013-' + (new Date()).getFullYear(), ' <%= pkg.author %>\n',
    ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n',
    ' */\n',
    ''
].join('');

// Compile LESS files from /less into /css
gulp.task('less', function() {
    return gulp.src('less/new-age.less')
        .pipe(less())
        .pipe(header(banner, { pkg: pkg }))
        .pipe(gulp.dest('css'))
        .pipe(browserSync.reload({
            stream: true
        }))
});

// Minify compiled CSS
gulp.task('minify-css', ['less'], function() {
    return gulp.src('css/new-age.css')
        .pipe(cleanCSS({ compatibility: 'ie8' }))
        .pipe(rename({ suffix: '.min' }))
        .pipe(gulp.dest('css'))
        .pipe(browserSync.reload({
            stream: true
        }))
});

// Minify JS
gulp.task('minify-js', function() {
    return gulp.src('js/new-age.js')
        .pipe(uglify())
        .pipe(header(banner, { pkg: pkg }))
        .pipe(rename({ suffix: '.min' }))
        .pipe(gulp.dest('js'))
        .pipe(browserSync.reload({
            stream: true
        }))
});

// Copy vendor libraries from /node_modules into /vendor
gulp.task('copy', function() {
    gulp.src(['node_modules/bootstrap/dist/**/*', '!**/npm.js', '!**/bootstrap-theme.*', '!**/*.map'])
        .pipe(gulp.dest('vendor/bootstrap'))

    gulp.src(['node_modules/jquery/dist/jquery.js', 'node_modules/jquery/dist/jquery.min.js'])
        .pipe(gulp.dest('vendor/jquery'))

    gulp.src(['node_modules/simple-line-icons/*/*'])
        .pipe(gulp.dest('vendor/simple-line-icons'))

    gulp.src([
            'node_modules/font-awesome/**',
            '!node_modules/font-awesome/**/*.map',
            '!node_modules/font-awesome/.npmignore',
            '!node_modules/font-awesome/*.txt',
            '!node_modules/font-awesome/*.md',
            '!node_modules/font-awesome/*.json'
        ])
        .pipe(gulp.dest('vendor/font-awesome'))
})

// Run everything
gulp.task('default', ['less', 'minify-css', 'minify-js', 'copy']);

// Configure the browserSync task
gulp.task('browserSync', function() {
    browserSync.init({
        server: {
            baseDir: ''
        },
    })
})

// Dev task with browserSync
gulp.task('dev', ['browserSync', 'less', 'minify-css', 'minify-js'], function() {
    gulp.watch('less/*.less', ['less']);
    gulp.watch('css/*.css', ['minify-css']);
    gulp.watch('js/*.js', ['minify-js']);
    // Reloads the browser whenever HTML or JS files change
    gulp.watch('*.html', browserSync.reload);
    gulp.watch('js/**/*.js', browserSync.reload);
});
<commit_msg>Delete gulpfile.js<commit_after><|endoftext|>