<commit_before>dojo.declare( 'JBrowse.Model.TrackMetaData', null,
/**
 * @lends JBrowse.Model.TrackMetaData.prototype
 */
{
    /**
     * Data store for track metadata, supporting faceted
     * (parameterized) searching.  Keeps all of the track metadata,
     * and the indexes thereof, in memory.
     * @constructs
     * @param args.trackConfigs {Array} array of track configuration
     */
    constructor: function( args ) {

        // set up our facet name discrimination: what facets we will
        // actually provide search on
        var non_facet_attrs = this.getIdentityAttributes();
        non_facet_attrs.push('conf');
        this._filterFacet = function( facetName ) {
            var userfilter = args.filterFacets || function() {return true;};
            return userfilter(facetName)
                   && ! dojo.some( non_facet_attrs, function(a) { return a == facetName;});
        };

        // set up our onReady callbacks to fire once the data is
        // loaded
        if( ! dojo.isArray( args.onReady ) ){
            this.onReadyFuncs = args.onReady ? [ args.onReady ] : [];
        } else {
            this.onReadyFuncs = dojo.clone(args.onReady);
        }

        // interpret the track configurations as a metadata store
        this._indexItems(
            {
                store: this,
                items: dojo.map( args.trackConfigs, function(conf) {
                    var metarecord = dojo.clone( conf.metadata || {} );
                    metarecord.label = conf.label;
                    metarecord.key = conf.key;
                    metarecord.conf = conf;
                    if( conf.category )
                        metarecord.category = conf.category;
                    return metarecord;
                },this)
            }
        );

        // fetch and index all the items from each of the stores
        var stores_fetched_count = 0;
        if( ! args.metadataStores || ! args.metadataStores.length ) {
            // if we don't actually have any stores besides the track
            // confs, we're ready now.
            this._ready();
        } else  {
            // index the track metadata from each of the stores

            var storeFetchFinished = dojo.hitch( this, function() {
                if( ++stores_fetched_count == args.metadataStores.length )
                    this._ready();
            });
            dojo.forEach( args.metadataStores, function(store) {
                store.fetch({
                    scope: this,
                    onComplete: Util.debugHandler( this, function(items) {
                        // build our indexes
                        this._indexItems({ store: store, items: items, supplementalOnly: true });

                        // if this is the last store to be fetched, call
                        // our onReady callbacks
                        storeFetchFinished();
                    }),
                    onError: storeFetchFinished
                });
            },this);
        }
     },

    /**
     * Set the store's state to be ready (i.e. loaded), and calls all
     * our onReady callbacks.
     * @private
     */
    _ready: function() {
        this.ready = true;
        dojo.forEach( this.onReadyFuncs, function(f) {
                          f.call( this, this );
                      }, this );
        this.onReadyFuncs = [];
    },

    _indexItems: function( args ) {
        // get our (filtered) list of facets we will index for
        var store = args.store,
            items = args.items;

        var storeAttributes = {};

        // convert the items to a uniform format
        items = dojo.map( items, function( item ) {
                              var itemattrs = store.getAttributes(item);

                              //convert the item into a uniform data format of plain objects
                              var newitem = {};
                              dojo.forEach( itemattrs, function(attr) {
                                                storeAttributes[attr] = 1;
                                                newitem[attr] = store.getValue(item,attr);
                                            });
                              return newitem;
                          },
                          this
                        );

        // merge them with any existing records, filtering out ones
        // that should be ignored if we were passed
        // 'supplementalOnly', and update the identity index
        this.identIndex = this.identIndex || {};
        items = dojo.map( items, function(item) {
                                 // merge the new item attributes with any existing
                                 // record for this item
                                 var ident = this.getIdentity(item);
                                 var existingItem = this.identIndex[ ident ];
                                 if( args.supplementalOnly && !existingItem) {
                                     // skip this item if we are supplementalOnly and it
                                     // does not already exist
                                     return null;
                                 }
                                 return this.identIndex[ ident ] = dojo.mixin( existingItem || {}, item );
                             },
                             this
                           );
        // filter out nulls
        items = dojo.filter( items, function(i) { return i;});

        // update our facet list to include any new attrs these
        // items have
        var old_facets = this.facets || [];
        this.facets = (function() {
            var seen = {};
            return dojo.filter( old_facets.concat( dojof.keys( storeAttributes ) ),
                                function(facetName) {
                                    var take = this._filterFacet(facetName) && !seen[facetName];
                                    seen[facetName] = true;
                                    return take;
                                },
                                this
                              );
        }).call(this);
        var new_facets = this.facets.slice( old_facets.length );

        // initialize indexes for any new facets
        this.facetIndexes = this.facetIndexes || { itemCount: 0, bucketCount: 0, byName: {} };
        dojo.forEach( new_facets, function(facet) {
            if( ! this.facetIndexes.byName[facet] ) {
                this.facetIndexes.bucketCount++;
                this.facetIndexes.byName[facet] = { itemCount: 0, bucketCount: 0, byValue: {} };
            }
        }, this);

        // now update the indexes with the new facets
        if( new_facets.length ) {
            dojo.forEach( items, function( item ) {
                this.facetIndexes.itemCount++;
                dojo.forEach( new_facets, function( facet ) {

                    var value = this.getValue( item, facet, undefined );
                    if( typeof value == 'undefined' )
                        return;
                    var facetValues = this.facetIndexes.byName[facet];
                    var bucket = facetValues.byValue[value];
                    if( !bucket ) {
                        bucket = facetValues.byValue[value] = { itemCount: 0, items: [] };
                        facetValues.bucketCount++;
                    }
                    bucket.itemCount++;
                    bucket.items.push(item);
                },this);
            }, this);

            // calculate the rank of the facets: make an array of
            // facet names sorted by smallest average bucket size,
            // descending
            this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
                a = this.facetIndexes.byName[a];
                b = this.facetIndexes.byName[b];
                return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
            }));
        }
    },

    /**
     * Get an array of the text names of the facets that are defined
     * in this track metadata.
     * @param callback {Function} called as callback( [facet,facet,...] )
     */
    getFacets: function( callback ) {
        return this.facets;
    },

    /**
     * Get an Array of the distinct values for a given facet name.
     * @param facetName {String} the facet name
     * @returns {Array} distinct values for that facet
     */
    getFacetValues: function( facetName ) {
        var index = this.facetIndexes.byName[facetName];
        if( !index )
            return [];

        return dojof.keys( index.byValue );
    },

    /**
     * Add a callback to be called when this store is ready (i.e. loaded).
     */
    onReady: function( callback ) {
        if( this.ready ) {
            callback.call( this, this );
        }
        else {
            this.onReadyFuncs.push( callback );
        }
    },

    // dojo.data.api.Read support

    getValue: function( i, attr, defaultValue ) {
        var v = i[attr];
        return typeof v == 'undefined' ? defaultValue : v;
    },
    getValues: function( i, attr ) {
        var a = [ i[attr] ];
        return typeof a[0] == 'undefined' ? [] : a;
    },

    getAttributes: function(item)  {
        return dojof.keys( item );
    },

    hasAttribute: function(item,attr) {
        return item.hasOwnProperty(attr);
    },

    containsValue: function(item, attribute, value) {
        return item[attribute] == value;
    },

    isItem: function(item) {
        return typeof item == 'object' && typeof item[name] == 'string';
    },

    isItemLoaded: function() {
        return true;
    },

    loadItem: function( args ) {
    },

    // used by the dojo.data.util.simpleFetch mixin to implement fetch()
    _fetchItems: function( keywordArgs, findCallback, errorCallback ) {
        if( ! this.ready ) {
            this.onReady( dojo.hitch( this, '_fetchItems', keywordArgs, findCallback, errorCallback ) );
            return;
        }

        var query = dojo.clone( keywordArgs.query || {} );
        var textFilter = query.text;
        delete query.text;

        var results;

        // if we don't actually have any facets specified in the
        // query, the results are just all the items
        if( ! dojo.some( dojof.values(query), function(q){ return q.length > 0;}) ) {
            results = dojof.values( this.identIndex );
        }
        else {
            // start with an initial set of items that have the desired
            // value for the most specific (smallest avg index bucket
            // size) facet that was specified
            results = (function() {
                           var highestRankedFacet,
                               queryValues = [],
                               index;
                           dojo.some( this.facetIndexes.facetRank, function(facetName) {
                                          if( query[facetName] ) {
                                              highestRankedFacet = facetName;
                                              queryValues = query[highestRankedFacet];
                                              index = this.facetIndexes.byName[highestRankedFacet];
                                              return queryValues.length > 0;
                                          }
                                          return false;
                                      },this);

                           delete query[highestRankedFacet];

                           var set = [];
                           dojo.forEach( queryValues, function(val) {
                              var bucket = index.byValue[val];
                              if( bucket )
                                  set.push.apply( set, bucket.items );
                           },this);
                           return set;
                       }).call(this);

            // and filter this starting set for the other facets
            dojo.forEach( dojof.keys(query), function(facetName) {
                              var desired_values = query[facetName] || [];
                              if( ! desired_values.length )
                                  return;
                              results = dojo.filter(results, function(item) {
                                                        var value = this.getValue(item,facetName);
                                                        return dojo.some( desired_values, function(desired) {
                                                                              return desired == value;
                                                                          },this);
                                                    },this);
                          },this);
        }

        // filter with the text filter, if we have it
	if( typeof textFilter != 'undefined' ) {
            var filter = this._compileTextFilter( textFilter );
            results = dojo.filter( results, function(item) {
                return dojo.some( this.facets, function(facetName) {
                           return filter( this.getValue( item, facetName ) );
                       },this);
            },this);
        }

        // and finally, hand them to the finding callback
        findCallback(results,keywordArgs);
    },

    /**
     * Compile a text search string into a function that tests whether
     * a given piece of text matches that search string.
     * @private
     */
    _compileTextFilter: function( textString ) {
        // escape regex control chars, and convert glob-like chars to
        // their regex equivalents
        textString =
            dojo.regexp.escapeString( textString, '*?' )
            .replace(/\*/g,'.+')
            .replace(/\?/g,'.');

        // split into words, and convert each word into a regexp
        var wordREs = dojo.map( textString.split(/\s+/), function(w) { return new RegExp(w,'i'); });

        // return a function that returns true if all of the words
        // match the string, but in any order
        return function( text ) {
            return dojof.every( wordREs, function(re) { return re.test(text); } );
        };
    },

    getFeatures: function() {
        return {
	    'dojo.data.api.Read': true,
	    'dojo.data.api.Identity': true
	};
    },
    close: function() {},

    getLabel: function(i) {
        return this.getValue(i,'key',undefined);
    },
    getLabelAttributes: function(i) {
        return ['key']; },

    // dojo.data.api.Identity support
    getIdentityAttributes: function() {
        return ['label'];
    },
    getIdentity: function(i) {
        return this.getValue(i, 'label', undefined);
    },
    fetchItemByIdentity: function(id) {
        return this.identIndex[id];
    }
});
dojo.require('dojo.data.util.simpleFetch');
dojo.extend( JBrowse.Model.TrackMetaData, dojo.data.util.simpleFetch );<commit_msg>refactor and clean up TrackMetaData.js a little, index items with no data for a given facet, allowing people to filter a facet for tracks that have no data in that facet<commit_after>dojo.declare( 'JBrowse.Model.TrackMetaData', null,
/**
 * @lends JBrowse.Model.TrackMetaData.prototype
 */
{
    /**
     * Data store for track metadata, supporting faceted
     * (parameterized) searching.  Keeps all of the track metadata,
     * and the indexes thereof, in memory.
     * @constructs
     * @param args.trackConfigs {Array} array of track configuration
     */
    constructor: function( args ) {

        // set up our facet name discrimination: what facets we will
        // actually provide search on
        var non_facet_attrs = this.getIdentityAttributes();
        non_facet_attrs.push('conf');
        this._filterFacet = function( facetName ) {
            var userfilter = args.filterFacets || function() {return true;};
            return userfilter(facetName)
                   && ! dojo.some( non_facet_attrs, function(a) { return a == facetName;});
        };

        // set up our onReady callbacks to fire once the data is
        // loaded
        if( ! dojo.isArray( args.onReady ) ){
            this.onReadyFuncs = args.onReady ? [ args.onReady ] : [];
        } else {
            this.onReadyFuncs = dojo.clone(args.onReady);
        }

        // interpret the track configurations as a metadata store
        this._indexItems(
            {
                store: this,
                items: dojo.map( args.trackConfigs, function(conf) {
                    var metarecord = dojo.clone( conf.metadata || {} );
                    metarecord.label = conf.label;
                    metarecord.key = conf.key;
                    metarecord.conf = conf;
                    if( conf.category )
                        metarecord.category = conf.category;
                    return metarecord;
                },this)
            }
        );

        // fetch and index all the items from each of the stores
        var stores_fetched_count = 0;
        if( ! args.metadataStores || ! args.metadataStores.length ) {
            // if we don't actually have any stores besides the track
            // confs, we're ready now.
            this._ready();
        } else  {
            // index the track metadata from each of the stores

            var storeFetchFinished = dojo.hitch( this, function() {
                if( ++stores_fetched_count == args.metadataStores.length )
                    this._ready();
            });
            dojo.forEach( args.metadataStores, function(store) {
                store.fetch({
                    scope: this,
                    onComplete: Util.debugHandler( this, function(items) {
                        // build our indexes
                        this._indexItems({ store: store, items: items, supplementalOnly: true });

                        // if this is the last store to be fetched, call
                        // our onReady callbacks
                        storeFetchFinished();
                    }),
                    onError: storeFetchFinished
                });
            },this);
        }
     },

    /**
     * Set the store's state to be ready (i.e. loaded), and calls all
     * our onReady callbacks.
     * @private
     */
    _ready: function() {
        this.ready = true;
        dojo.forEach( this.onReadyFuncs, function(f) {
                          f.call( this, this );
                      }, this );
        this.onReadyFuncs = [];
    },

    _indexItems: function( args ) {
        // get our (filtered) list of facets we will index for
        var store = args.store,
            items = args.items;

        var storeAttributes = {};

        // convert the items to a uniform format
        items = dojo.map( items, function( item ) {
                              var itemattrs = store.getAttributes(item);

                              //convert the item into a uniform data format of plain objects
                              var newitem = {};
                              dojo.forEach( itemattrs, function(attr) {
                                                storeAttributes[attr] = 1;
                                                newitem[attr] = store.getValue(item,attr);
                                            });
                              return newitem;
                          },
                          this
                        );

        // merge them with any existing records, filtering out ones
        // that should be ignored if we were passed
        // 'supplementalOnly', and update the identity index
        this.identIndex = this.identIndex || {};
        items = dojo.map( items, function(item) {
                                 // merge the new item attributes with any existing
                                 // record for this item
                                 var ident = this.getIdentity(item);
                                 var existingItem = this.identIndex[ ident ];
                                 if( args.supplementalOnly && !existingItem) {
                                     // skip this item if we are supplementalOnly and it
                                     // does not already exist
                                     return null;
                                 }
                                 return this.identIndex[ ident ] = dojo.mixin( existingItem || {}, item );
                             },
                             this
                           );
        // filter out nulls
        items = dojo.filter( items, function(i) { return i;});

        // update our facet list to include any new attrs these
        // items have
        var new_facets = this._addFacets( dojof.keys( storeAttributes ) );

        // initialize indexes for any new facets
        this.facetIndexes = this.facetIndexes || { itemCount: 0, bucketCount: 0, byName: {} };
        dojo.forEach( new_facets, function(facet) {
            if( ! this.facetIndexes.byName[facet] ) {
                this.facetIndexes.bucketCount++;
                this.facetIndexes.byName[facet] = { itemCount: 0, bucketCount: 0, byValue: {} };
            }
        }, this);

        // now update the indexes with the new facets
        if( new_facets.length ) {
            var gotDataForItem = {};
            dojo.forEach( new_facets, function(f){ gotDataForItem[f] = {};});

            dojo.forEach( items, function( item ) {
                this.facetIndexes.itemCount++;
                dojo.forEach( new_facets, function( facet ) {
                    var value = this.getValue( item, facet, undefined );
                    if( typeof value == 'undefined' )
                        return;
                    gotDataForItem[facet][this.getIdentity(item)] = 1;
                    this._indexItem( facet, value, item );
                },this);
            }, this);

            // index the items that do not have data for this facet
            var noDataValue = '(no data)';
            dojo.forEach( new_facets, function(facet) {
                var gotSomeWithNoData = false;
                dojo.forEach( dojof.values( this.identIndex ), function(item) {
                    if( ! gotDataForItem[facet][this.getIdentity(item)] ) {
                        gotSomeWithNoData = true;
                        this._indexItem( facet, noDataValue, item );
                    }
                },this);
            },this);

            // calculate the rank of the facets: make an array of
            // facet names sorted by smallest average bucket size,
            // descending
            this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
                a = this.facetIndexes.byName[a];
                b = this.facetIndexes.byName[b];
                return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
            }));
        }
    },

    /**
     * Add an item to the indexes for the given facet name and value.
     * @private
     */
    _indexItem: function( facet, value, item ) {
        var facetValues = this.facetIndexes.byName[facet];
        var bucket = facetValues.byValue[value];
        if( !bucket ) {
            bucket = facetValues.byValue[value] = { itemCount: 0, items: [] };
            facetValues.bucketCount++;
        }
        bucket.itemCount++;
        bucket.items.push(item);
    },

    /**
     * Given an array of string facet names, add records for them,
     * initializing the necessary data structures.
     * @private
     * @returns {Array[String]} facet names that did not already exist
     */
    _addFacets: function( facetNames ) {
        var old_facets = this.facets || [];
        var seen = {};
        this.facets = dojo.filter(
            old_facets.concat( facetNames ),
            function(facetName) {
                var take = this._filterFacet(facetName) && !seen[facetName];
                seen[facetName] = true;
                return take;
            },
            this
        );
        return this.facets.slice( old_facets.length );
    },

    /**
     * Get an array of the text names of the facets that are defined
     * in this track metadata.
     * @param callback {Function} called as callback( [facet,facet,...] )
     */
    getFacets: function( callback ) {
        return this.facets;
    },

    /**
     * Get an Array of the distinct values for a given facet name.
     * @param facetName {String} the facet name
     * @returns {Array} distinct values for that facet
     */
    getFacetValues: function( facetName ) {
        var index = this.facetIndexes.byName[facetName];
        if( !index )
            return [];

        return dojof.keys( index.byValue );
    },

    /**
     * Add a callback to be called when this store is ready (i.e. loaded).
     */
    onReady: function( callback ) {
        if( this.ready ) {
            callback.call( this, this );
        }
        else {
            this.onReadyFuncs.push( callback );
        }
    },

    // dojo.data.api.Read support

    getValue: function( i, attr, defaultValue ) {
        var v = i[attr];
        return typeof v == 'undefined' ? defaultValue : v;
    },
    getValues: function( i, attr ) {
        var a = [ i[attr] ];
        return typeof a[0] == 'undefined' ? [] : a;
    },

    getAttributes: function(item)  {
        return dojof.keys( item );
    },

    hasAttribute: function(item,attr) {
        return item.hasOwnProperty(attr);
    },

    containsValue: function(item, attribute, value) {
        return item[attribute] == value;
    },

    isItem: function(item) {
        return typeof item == 'object' && typeof item[name] == 'string';
    },

    isItemLoaded: function() {
        return true;
    },

    loadItem: function( args ) {
    },

    // used by the dojo.data.util.simpleFetch mixin to implement fetch()
    _fetchItems: function( keywordArgs, findCallback, errorCallback ) {
        if( ! this.ready ) {
            this.onReady( dojo.hitch( this, '_fetchItems', keywordArgs, findCallback, errorCallback ) );
            return;
        }

        var query = dojo.clone( keywordArgs.query || {} );
        var textFilter = query.text;
        delete query.text;

        var results;

        // if we don't actually have any facets specified in the
        // query, the results are just all the items
        if( ! dojo.some( dojof.values(query), function(q){ return q.length > 0;}) ) {
            results = dojof.values( this.identIndex );
        }
        else {
            // start with an initial set of items that have the desired
            // value for the most specific (smallest avg index bucket
            // size) facet that was specified
            results = (function() {
                           var highestRankedFacet,
                               queryValues = [],
                               index;
                           dojo.some( this.facetIndexes.facetRank, function(facetName) {
                                          if( query[facetName] ) {
                                              highestRankedFacet = facetName;
                                              queryValues = query[highestRankedFacet];
                                              index = this.facetIndexes.byName[highestRankedFacet];
                                              return queryValues.length > 0;
                                          }
                                          return false;
                                      },this);

                           delete query[highestRankedFacet];

                           var set = [];
                           dojo.forEach( queryValues, function(val) {
                              var bucket = index.byValue[val];
                              if( bucket )
                                  set.push.apply( set, bucket.items );
                           },this);
                           return set;
                       }).call(this);

            // and filter this starting set for the other facets
            dojo.forEach( dojof.keys(query), function(facetName) {
                              var desired_values = query[facetName] || [];
                              if( ! desired_values.length )
                                  return;
                              results = dojo.filter(results, function(item) {
                                                        var value = this.getValue(item,facetName);
                                                        return dojo.some( desired_values, function(desired) {
                                                                              return desired == value;
                                                                          },this);
                                                    },this);
                          },this);
        }

        // filter with the text filter, if we have it
	if( typeof textFilter != 'undefined' ) {
            var filter = this._compileTextFilter( textFilter );
            results = dojo.filter( results, function(item) {
                return dojo.some( this.facets, function(facetName) {
                           return filter( this.getValue( item, facetName ) );
                       },this);
            },this);
        }

        // and finally, hand them to the finding callback
        findCallback(results,keywordArgs);
    },

    /**
     * Compile a text search string into a function that tests whether
     * a given piece of text matches that search string.
     * @private
     */
    _compileTextFilter: function( textString ) {
        // escape regex control chars, and convert glob-like chars to
        // their regex equivalents
        textString =
            dojo.regexp.escapeString( textString, '*?' )
            .replace(/\*/g,'.+')
            .replace(/\?/g,'.');

        // split into words, and convert each word into a regexp
        var wordREs = dojo.map( textString.split(/\s+/), function(w) { return new RegExp(w,'i'); });

        // return a function that returns true if all of the words
        // match the string, but in any order
        return function( text ) {
            return dojof.every( wordREs, function(re) { return re.test(text); } );
        };
    },

    getFeatures: function() {
        return {
	    'dojo.data.api.Read': true,
	    'dojo.data.api.Identity': true
	};
    },
    close: function() {},

    getLabel: function(i) {
        return this.getValue(i,'key',undefined);
    },
    getLabelAttributes: function(i) {
        return ['key']; },

    // dojo.data.api.Identity support
    getIdentityAttributes: function() {
        return ['label'];
    },
    getIdentity: function(i) {
        return this.getValue(i, 'label', undefined);
    },
    fetchItemByIdentity: function(id) {
        return this.identIndex[id];
    }
});
dojo.require('dojo.data.util.simpleFetch');
dojo.extend( JBrowse.Model.TrackMetaData, dojo.data.util.simpleFetch );<|endoftext|>


<commit_before>var Fs = require('fs');
var Path = require('path');

var internals = {};

/* $lab:coverage:off$ */
// Coverage disabled for this method because we use a child process to test it due to the process.exit() call
internals.throwWarn = function (warning) {

    console.error('WARNING: ' + warning + ', installation aborted.');
    process.exit(0);
};
/* $lab:coverage:on$ */

// Find the topmost parent of the given module.
internals.findParent = function (mod) {

    return mod.parent ? internals.findParent(mod.parent) : mod;
};

// Similar to mkdir -p, recursively creates directories until `path` exists
internals.mkdir = function (path) {

    var mode = ~process.umask() & parseInt('777', 8);

    if (exports.isDir(path)) {
        return;
    }

    try {
        Fs.mkdirSync(path, mode);
    }
    catch (err) {
        if (err.code !== 'ENOENT') {
            throw err;
        }

        internals.mkdir(Path.dirname(path));
        internals.mkdir(path);
    }
};

// Expands source and target to absolute paths, then calls internals.copy
exports.copy = function (source, target, options) {

    if (typeof target === 'object') {
        options = target;
        target = undefined;
    }

    options = options || {};

    var root = Path.dirname(internals.findParent(module).filename);
    var projectRoot = exports.findProjectRoot(root);

    var sourcePath = Path.resolve(root, source);
    var targetPath = Path.resolve(projectRoot, target || source);

    if (targetPath.indexOf(projectRoot) !== 0) {
        throw new Error('Destination must be within project root');
    }

    return internals.copy(sourcePath, targetPath, options);
};

// Determine if source is a directory or a file and call the appropriate method
internals.copy = function (source, target, options) {

    if (exports.isDir(source)) {
        internals.copyDirectory(source, target, options);
    }
    else {
        return internals.copyFile(source, target, options);
    }
};

// Recursively copy a directory
internals.copyDirectory = function (source, target, options) {

    internals.mkdir(target);

    var sources = Fs.readdirSync(source);
    for (var i = 0, l = sources.length; i < l; ++i) {
        var sourcePath = Path.join(source, sources[i]);
        var targetPath = Path.join(target, sources[i]);

        internals.copy(sourcePath, targetPath, options);
    }
};

// Copy a single file
internals.copyFile = function (source, target, options) {

    internals.mkdir(Path.dirname(target));

    var mode = ~process.umask() & parseInt('666', 8);

    if (Fs.existsSync(target) &&
        !options.overwrite) {

        return new Error(target + ' already exists');
    }

    var sourceContent = '';
    try {
        sourceContent = Fs.readFileSync(source);
    } catch (e) {
        /* no source to copy */
    }

    Fs.writeFileSync(target, sourceContent, { flag: 'w', mode: mode });
};

// Given a path, determine if the path is a directory
exports.isDir = function (path) {

    try {
        var stat = Fs.statSync(path);
        return stat.isDirectory();
    }
    catch (e) {
        return false;
    }
};

// Given a starting directory, find the root of a git repository.
// In this case, the root is defined as the first directory that contains
// a directory named ".git"
//
// Returns a string if found, otherwise undefined
exports.findGitRoot = function (start) {

    start = start || Path.dirname(internals.findParent(module).filename);
    var root;

    if (exports.isDir(Path.join(start, '.git'))) {
        root = start;
    }
    /* $lab:coverage:off$ */
    // Coverage disabled here due to false positive on else if, since we have to trap the throwWarn method
    else if (Path.dirname(start) !== start) {
        root = exports.findGitRoot(Path.dirname(start));
    }
    else {
        return internals.throwWarn('Unable to find a .git directory for this project');
    }
    /* $lab:coverage:on$ */

    return root;
};

// Given a starting directory, find the root of the current project.
// The root of the project is defined as the topmost directory that is
// *not* contained within a directory named "node_modules" that also
// contains a file named "package.json"
//
// Returns a string
exports.findProjectRoot = function (start) {

    start = start || Path.dirname(internals.findParent(module).filename);
    var position = start.indexOf('node_modules');
    var root = start.slice(0, position === -1 ? undefined : position - Path.sep.length);
    /* $lab:coverage:off$ */
    // Coverage disabled here due to having to trap the throwWarn method
    if (root === Path.resolve(root, '..')) {
        return internals.throwWarn('Unable to find a package.json for this project');
    }
    /* $lab:coverage:on$ */

    while (!Fs.existsSync(Path.join(root, 'package.json'))) {
        root = exports.findProjectRoot(Path.dirname(root));
    }

    return root;
};

// Given a root path, find a list of projects.
// A project is defined as any directory within 4 levels of the starting
// directory that contains a file named "package.json"
//
// Returns an array
exports.findProjects = function (start, depth) {

    start = start || exports.findGitRoot(internals.findParent(module).filename);
    depth = depth || 0;
    ++depth;

    if (depth > 4 ||
        !exports.isDir(start) ||
        start.indexOf('node_modules') !== -1) {

        return [];
    }

    var dirs = Fs.readdirSync(start);
    var projects = [];

    if (Fs.existsSync(Path.join(start, 'package.json'))) {
        projects.push(start);
    }

    for (var i = 0, il = dirs.length; i < il; ++i) {
        var dir = dirs[i];
        var path = Path.join(start, dir);

        if (Fs.existsSync(Path.join(path, 'package.json'))) {
            projects.push(path);
        }
        else {
            projects = projects.concat(exports.findProjects(path, depth));
        }
    }

    return projects;
};

// Install the git hook as specified by `hook`.
// For example, Validate.installHook('pre-commit');
exports.installHooks = function (hooks, root) {

    hooks = Array.isArray(hooks) ? hooks : [hooks];
    var gitRoot = exports.findGitRoot(root);
    var hookRoot = Path.join(gitRoot, '.git', 'hooks');
    var source = Path.resolve(__dirname, '..', 'bin', 'validate.sh');

    if (!exports.isDir(hookRoot)) {
        internals.mkdir(hookRoot);
    }

    for (var i = 0, il = hooks.length; i < il; ++i) {
        var hook = hooks[i];
        var dest = Path.join(hookRoot, hook);

        if (Fs.existsSync(dest)) {
            Fs.renameSync(dest, dest + '.backup');
        }

        Fs.writeFileSync(dest, Fs.readFileSync(source), { mode: 511 });
    }
};

// Provide a default configuration for a git hook as specified by `hook`.
// For example, Validate.configureHook('pre-commit', ['test', 'lint']);
exports.configureHook = function (hook, defaults, overwrite, root) {

    var packagePath = Path.join(exports.findProjectRoot(root), 'package.json');
    var package = JSON.parse(Fs.readFileSync(packagePath, { encoding: 'utf8' }));

    if (!package.hasOwnProperty(hook) || overwrite) {
        package[hook] = Array.isArray(defaults) ? defaults : [defaults];
        Fs.writeFileSync(packagePath, JSON.stringify(package, null, 2), { encoding: 'utf8' });
    }
};

// Configure a default script by name and content
// For example, Validate.installScript('test', 'lab -a code -L');
// Or Validate.installScript('test', 'lab -a code -L', {overwrite: true}); to force updating
exports.installScript = function (name, script, options, root) {

    if (typeof options === 'string') {
        root = options;
        options = null;
    }

    options = options || {};

    var packagePath = Path.join(exports.findProjectRoot(root), 'package.json');
    var package = JSON.parse(Fs.readFileSync(packagePath, { encoding: 'utf8' }));
    if (!package.hasOwnProperty('scripts') ||
        !package.scripts.hasOwnProperty(name) ||
        options.overwrite) {

        package.scripts = package.scripts || {};
        package.scripts[name] = script;
        Fs.writeFileSync(packagePath, JSON.stringify(package, null, 2), { encoding: 'utf8' });
    }
};
<commit_msg>Remove old hook if it's a broken symlink<commit_after>var Fs = require('fs');
var Path = require('path');

var internals = {};

/* $lab:coverage:off$ */
// Coverage disabled for this method because we use a child process to test it due to the process.exit() call
internals.throwWarn = function (warning) {

    console.error('WARNING: ' + warning + ', installation aborted.');
    process.exit(0);
};
/* $lab:coverage:on$ */

// Find the topmost parent of the given module.
internals.findParent = function (mod) {

    return mod.parent ? internals.findParent(mod.parent) : mod;
};

// Similar to mkdir -p, recursively creates directories until `path` exists
internals.mkdir = function (path) {

    var mode = ~process.umask() & parseInt('777', 8);

    if (exports.isDir(path)) {
        return;
    }

    try {
        Fs.mkdirSync(path, mode);
    }
    catch (err) {
        if (err.code !== 'ENOENT') {
            throw err;
        }

        internals.mkdir(Path.dirname(path));
        internals.mkdir(path);
    }
};

// Expands source and target to absolute paths, then calls internals.copy
exports.copy = function (source, target, options) {

    if (typeof target === 'object') {
        options = target;
        target = undefined;
    }

    options = options || {};

    var root = Path.dirname(internals.findParent(module).filename);
    var projectRoot = exports.findProjectRoot(root);

    var sourcePath = Path.resolve(root, source);
    var targetPath = Path.resolve(projectRoot, target || source);

    if (targetPath.indexOf(projectRoot) !== 0) {
        throw new Error('Destination must be within project root');
    }

    return internals.copy(sourcePath, targetPath, options);
};

// Determine if source is a directory or a file and call the appropriate method
internals.copy = function (source, target, options) {

    if (exports.isDir(source)) {
        internals.copyDirectory(source, target, options);
    }
    else {
        return internals.copyFile(source, target, options);
    }
};

// Recursively copy a directory
internals.copyDirectory = function (source, target, options) {

    internals.mkdir(target);

    var sources = Fs.readdirSync(source);
    for (var i = 0, l = sources.length; i < l; ++i) {
        var sourcePath = Path.join(source, sources[i]);
        var targetPath = Path.join(target, sources[i]);

        internals.copy(sourcePath, targetPath, options);
    }
};

// Copy a single file
internals.copyFile = function (source, target, options) {

    internals.mkdir(Path.dirname(target));

    var mode = ~process.umask() & parseInt('666', 8);

    if (Fs.existsSync(target) &&
        !options.overwrite) {

        return new Error(target + ' already exists');
    }

    var sourceContent = '';
    try {
        sourceContent = Fs.readFileSync(source);
    } catch (e) {
        /* no source to copy */
    }

    Fs.writeFileSync(target, sourceContent, { flag: 'w', mode: mode });
};

// Given a path, determine if the path is a directory
exports.isDir = function (path) {

    try {
        var stat = Fs.statSync(path);
        return stat.isDirectory();
    }
    catch (e) {
        return false;
    }
};

// Given a starting directory, find the root of a git repository.
// In this case, the root is defined as the first directory that contains
// a directory named ".git"
//
// Returns a string if found, otherwise undefined
exports.findGitRoot = function (start) {

    start = start || Path.dirname(internals.findParent(module).filename);
    var root;

    if (exports.isDir(Path.join(start, '.git'))) {
        root = start;
    }
    /* $lab:coverage:off$ */
    // Coverage disabled here due to false positive on else if, since we have to trap the throwWarn method
    else if (Path.dirname(start) !== start) {
        root = exports.findGitRoot(Path.dirname(start));
    }
    else {
        return internals.throwWarn('Unable to find a .git directory for this project');
    }
    /* $lab:coverage:on$ */

    return root;
};

// Given a starting directory, find the root of the current project.
// The root of the project is defined as the topmost directory that is
// *not* contained within a directory named "node_modules" that also
// contains a file named "package.json"
//
// Returns a string
exports.findProjectRoot = function (start) {

    start = start || Path.dirname(internals.findParent(module).filename);
    var position = start.indexOf('node_modules');
    var root = start.slice(0, position === -1 ? undefined : position - Path.sep.length);
    /* $lab:coverage:off$ */
    // Coverage disabled here due to having to trap the throwWarn method
    if (root === Path.resolve(root, '..')) {
        return internals.throwWarn('Unable to find a package.json for this project');
    }
    /* $lab:coverage:on$ */

    while (!Fs.existsSync(Path.join(root, 'package.json'))) {
        root = exports.findProjectRoot(Path.dirname(root));
    }

    return root;
};

// Given a root path, find a list of projects.
// A project is defined as any directory within 4 levels of the starting
// directory that contains a file named "package.json"
//
// Returns an array
exports.findProjects = function (start, depth) {

    start = start || exports.findGitRoot(internals.findParent(module).filename);
    depth = depth || 0;
    ++depth;

    if (depth > 4 ||
        !exports.isDir(start) ||
        start.indexOf('node_modules') !== -1) {

        return [];
    }

    var dirs = Fs.readdirSync(start);
    var projects = [];

    if (Fs.existsSync(Path.join(start, 'package.json'))) {
        projects.push(start);
    }

    for (var i = 0, il = dirs.length; i < il; ++i) {
        var dir = dirs[i];
        var path = Path.join(start, dir);

        if (Fs.existsSync(Path.join(path, 'package.json'))) {
            projects.push(path);
        }
        else {
            projects = projects.concat(exports.findProjects(path, depth));
        }
    }

    return projects;
};

// Install the git hook as specified by `hook`.
// For example, Validate.installHook('pre-commit');
exports.installHooks = function (hooks, root) {

    hooks = Array.isArray(hooks) ? hooks : [hooks];
    var gitRoot = exports.findGitRoot(root);
    var hookRoot = Path.join(gitRoot, '.git', 'hooks');
    var source = Path.resolve(__dirname, '..', 'bin', 'validate.sh');

    if (!exports.isDir(hookRoot)) {
        internals.mkdir(hookRoot);
    }

    for (var i = 0, il = hooks.length; i < il; ++i) {
        var hook = hooks[i];
        var dest = Path.join(hookRoot, hook);

        if (Fs.existsSync(dest)) {
            Fs.renameSync(dest, dest + '.backup');
        } else {
          // There is no harm in removing a non-existant file.
          // It could also be a broken symlink, which is also fine to be
          // removed. If it wasn't, writing the new hook would fail.
          Fs.unlinkSync(dest)
        }

        Fs.writeFileSync(dest, Fs.readFileSync(source), { mode: 511 });
    }
};

// Provide a default configuration for a git hook as specified by `hook`.
// For example, Validate.configureHook('pre-commit', ['test', 'lint']);
exports.configureHook = function (hook, defaults, overwrite, root) {

    var packagePath = Path.join(exports.findProjectRoot(root), 'package.json');
    var package = JSON.parse(Fs.readFileSync(packagePath, { encoding: 'utf8' }));

    if (!package.hasOwnProperty(hook) || overwrite) {
        package[hook] = Array.isArray(defaults) ? defaults : [defaults];
        Fs.writeFileSync(packagePath, JSON.stringify(package, null, 2), { encoding: 'utf8' });
    }
};

// Configure a default script by name and content
// For example, Validate.installScript('test', 'lab -a code -L');
// Or Validate.installScript('test', 'lab -a code -L', {overwrite: true}); to force updating
exports.installScript = function (name, script, options, root) {

    if (typeof options === 'string') {
        root = options;
        options = null;
    }

    options = options || {};

    var packagePath = Path.join(exports.findProjectRoot(root), 'package.json');
    var package = JSON.parse(Fs.readFileSync(packagePath, { encoding: 'utf8' }));
    if (!package.hasOwnProperty('scripts') ||
        !package.scripts.hasOwnProperty(name) ||
        options.overwrite) {

        package.scripts = package.scripts || {};
        package.scripts[name] = script;
        Fs.writeFileSync(packagePath, JSON.stringify(package, null, 2), { encoding: 'utf8' });
    }
};
<|endoftext|>


<commit_before>dojo.declare( 'JBrowse.Model.TrackMetaData', null,
/**
 * @lends JBrowse.Model.TrackMetaData.prototype
 */
{
    /**
     * Data store for track metadata, supporting faceted
     * (parameterized) searching.  Keeps all of the track metadata,
     * and the indexes thereof, in memory.
     * @constructs
     * @param args.trackConfigs {Array} array of track configuration
     */
    constructor: function( args ) {

        // set up our facet name discrimination: what facets we will
        // actually provide search on
        var non_facet_attrs = this.getIdentityAttributes();
        non_facet_attrs.push('conf');
        this._filterFacet = function( facetName ) {
            var userfilter = args.filterFacets || function() {return true;};
            return userfilter(facetName)
                   && ! dojo.some( non_facet_attrs, function(a) { return a == facetName;});
        };

        // set up our onReady callbacks to fire once the data is
        // loaded
        if( ! dojo.isArray( args.onReady ) ){
            this.onReadyFuncs = args.onReady ? [ args.onReady ] : [];
        } else {
            this.onReadyFuncs = dojo.clone(args.onReady);
        }

        // interpret the track configurations as a metadata store
        this._indexItems(
            {
                store: this,
                items: dojo.map( args.trackConfigs, function(conf) {
                    var metarecord = dojo.clone( conf.metadata || {} );
                    metarecord.label = conf.label;
                    metarecord.key = conf.key;
                    metarecord.conf = conf;
                    return metarecord;
                },this)
            }
        );

        // fetch and index all the items from each of the stores
        var stores_fetched_count = 0;
        if( ! args.metadataStores || ! args.metadataStores.length ) {
            // if we don't actually have any stores besides the track
            // confs, we're ready now.
            this._ready();
        } else  {
            // index the track metadata from each of the stores
            dojo.forEach( args.metadataStores, function(store) {
                store.fetch({
                    scope: this,
                    onComplete: Util.debugHandler( this, function(items) {
                        // build our indexes
                        this._indexItems({ store: store, items: items, supplementalOnly: true });

                        // if this is the last store to be fetched, call
                        // our onReady callbacks
                        if( ++stores_fetched_count == args.metadataStores.length )
                            this._ready();
                    })
                });
            },this);
        }
     },

    /**
     * Set the store's state to be ready (i.e. loaded), and calls all
     * our onReady callbacks.
     * @private
     */
    _ready: function() {
        this.ready = true;
        dojo.forEach( this.onReadyFuncs, function(f) {
                          f.call( this, this );
                      }, this );
        this.onReadyFuncs = [];
    },

    _indexItems: function( args ) {
        // get our (filtered) list of facets we will index for
        var store = args.store,
            items = args.items;

        // convert the items to a uniform format
        items = dojo.map( items, function( item ) {
                              var attributes = store.getAttributes(item);

                              //convert the item into a uniform data format of plain objects
                              var newitem = {};
                              dojo.forEach( attributes, function(attr) {
                                                newitem[attr] = store.getValue(item,attr);
                                            });
                              return newitem;
                          },
                          this
                        );

        // merge them with any existing records, filtering out ones
        // that should be ignored if we were passed
        // 'supplementalOnly', and update the identity index
        this.identIndex = this.identIndex || {};
        items = dojo.map( items, function(item) {
                                 // merge the new item attributes with any existing
                                 // record for this item
                                 var ident = this.getIdentity(item);
                                 var existingItem = this.identIndex[ ident ];
                                 if( args.supplementalOnly && !existingItem) {
                                     // skip this item if we are supplementalOnly and it
                                     // does not already exist
                                     return null;
                                 }
                                 return this.identIndex[ ident ] = dojo.mixin( existingItem || {}, item );
                             },
                             this
                           );
        // filter out nulls
        items = dojo.filter( items, function(i) { return i;});

        // update our facet list to include any new attrs these
        // items have
        var old_facets = this.facets || [];
        this.facets = (function() {
            var seen = {};
            return dojo.filter( old_facets.concat( this.getAttributes( items[0] ) ),
                                function(facetName) {
                                    var take = this._filterFacet(facetName) && !seen[facetName];
                                    seen[facetName] = true;
                                    return take;
                                },
                                this
                              );
        }).call(this);
        var new_facets = this.facets.slice( old_facets.length );

        // initialize indexes for any new facets
        this.facetIndexes = this.facetIndexes || { itemCount: 0, bucketCount: 0, byName: {} };
        dojo.forEach( new_facets, function(facet) {
            if( ! this.facetIndexes.byName[facet] ) {
                this.facetIndexes.bucketCount++;
                this.facetIndexes.byName[facet] = { itemCount: 0, bucketCount: 0, byValue: {} };
            }
        }, this);

        // now update the indexes with the new facets
        if( new_facets.length ) {
            dojo.forEach( items, function( item ) {
                this.facetIndexes.itemCount++;
                dojo.forEach( new_facets, function( facet ) {

                    var value = this.getValue( item, facet, undefined );
                    if( typeof value == 'undefined' )
                        return;
                    var facetValues = this.facetIndexes.byName[facet];
                    var bucket = facetValues.byValue[value];
                    if( !bucket ) {
                        bucket = facetValues.byValue[value] = { itemCount: 0, items: [] };
                        facetValues.bucketCount++;
                    }
                    bucket.itemCount++;
                    bucket.items.push(item);
                },this);
            }, this);

            // calculate the rank of the facets: make an array of
            // facet names sorted by smallest average bucket size,
            // descending
            this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
                a = this.facetIndexes.byName[a];
                b = this.facetIndexes.byName[b];
                return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
            }));
        }
    },

    /**
     * Get an array of the text names of the facets that are defined
     * in this track metadata.
     * @param callback {Function} called as callback( [facet,facet,...] )
     */
    getFacets: function( callback ) {
        return this.facets;
    },

    /**
     * Get an Array of the distinct values for a given facet name.
     * @param facetName {String} the facet name
     * @returns {Array} distinct values for that facet
     */
    getFacetValues: function( facetName ) {
        var index = this.facetIndexes.byName[facetName];
        if( !index )
            return [];

        return dojof.keys( index.byValue );
    },

    /**
     * Add a callback to be called when this store is ready (i.e. loaded).
     */
    onReady: function( callback ) {
        if( this.ready ) {
            callback.call( this, this );
        }
        else {
            this.onReadyFuncs.push( callback );
        }
    },

    // dojo.data.api.Read support

    getValue: function( i, attr, defaultValue ) {
        var v = i[attr];
        return typeof v == 'undefined' ? defaultValue : v;
    },
    getValues: function( i, attr ) {
        var a = [ i[attr] ];
        return typeof a[0] == 'undefined' ? [] : a;
    },

    getAttributes: function(item)  {
        return dojof.keys( item );
    },

    hasAttribute: function(item,attr) {
        return item.hasOwnProperty(attr);
    },

    containsValue: function(item, attribute, value) {
        return item[attribute] == value;
    },

    isItem: function(item) {
        return typeof item == 'object' && typeof item[name] == 'string';
    },

    isItemLoaded: function() {
        return true;
    },

    loadItem: function( args ) {
    },

    // used by the dojo.data.util.simpleFetch mixin to implement fetch()
    _fetchItems: function( keywordArgs, findCallback, errorCallback ) {
        if( ! this.ready ) {
            this.onReady( dojo.hitch( this, '_fetchItems', keywordArgs, findCallback, errorCallback ) );
            return;
        }

        var query = dojo.clone( keywordArgs.query || {} );
        var textFilter = query.text;
        delete query.text;

        var results;

        // if we don't actually have any facets specified in the
        // query, the results are just all the items
        if( ! dojo.some( dojof.values(query), function(q){ return q.length > 0;}) ) {
            results = dojof.values( this.identIndex );
        }
        else {
            // start with an initial set of items that have the desired
            // value for the most specific (smallest avg index bucket
            // size) facet that was specified
            results = (function() {
                           var highestRankedFacet,
                               queryValues = [],
                               index;
                           dojo.some( this.facetIndexes.facetRank, function(facetName) {
                                          if( query[facetName] ) {
                                              highestRankedFacet = facetName;
                                              queryValues = query[highestRankedFacet];
                                              index = this.facetIndexes.byName[highestRankedFacet];
                                              return queryValues.length > 0;
                                          }
                                          return false;
                                      },this);

                           delete query[highestRankedFacet];

                           var set = [];
                           dojo.forEach( queryValues, function(val) {
                              var bucket = index.byValue[val];
                              if( bucket )
                                  set.push.apply( set, bucket.items );
                           },this);
                           return set;
                       }).call(this);

            // and filter this starting set for the other facets
            dojo.forEach( dojof.keys(query), function(facetName) {
                              var desired_values = query[facetName] || [];
                              if( ! desired_values.length )
                                  return;
                              results = dojo.filter(results, function(item) {
                                                        var value = this.getValue(item,facetName);
                                                        return dojo.some( desired_values, function(desired) {
                                                                              return desired == value;
                                                                          },this);
                                                    },this);
                          },this);
        }

        // TODO: filter with the text filter, if we have it

        // and finally, hand them to the finding callback
        findCallback(results,keywordArgs);
    },

    getFeatures: function() {
        return {
	    'dojo.data.api.Read': true,
	    'dojo.data.api.Identity': true
	};
    },
    close: function() {},

    getLabel: function(i) {
        return this.getValue(i,'key',undefined);
    },
    getLabelAttributes: function(i) {
        return ['key']; },

    // dojo.data.api.Identity support
    getIdentityAttributes: function() {
        return ['label'];
    },
    getIdentity: function(i) {
        return this.getValue(i, 'label', undefined);
    },
    fetchItemByIdentity: function(id) {
        return this.identIndex[id];
    }
});
dojo.require('dojo.data.util.simpleFetch');
dojo.extend( JBrowse.Model.TrackMetaData, dojo.data.util.simpleFetch );<commit_msg>make TrackMetaData able to tolerate an error fetching one of its metadata sources<commit_after>dojo.declare( 'JBrowse.Model.TrackMetaData', null,
/**
 * @lends JBrowse.Model.TrackMetaData.prototype
 */
{
    /**
     * Data store for track metadata, supporting faceted
     * (parameterized) searching.  Keeps all of the track metadata,
     * and the indexes thereof, in memory.
     * @constructs
     * @param args.trackConfigs {Array} array of track configuration
     */
    constructor: function( args ) {

        // set up our facet name discrimination: what facets we will
        // actually provide search on
        var non_facet_attrs = this.getIdentityAttributes();
        non_facet_attrs.push('conf');
        this._filterFacet = function( facetName ) {
            var userfilter = args.filterFacets || function() {return true;};
            return userfilter(facetName)
                   && ! dojo.some( non_facet_attrs, function(a) { return a == facetName;});
        };

        // set up our onReady callbacks to fire once the data is
        // loaded
        if( ! dojo.isArray( args.onReady ) ){
            this.onReadyFuncs = args.onReady ? [ args.onReady ] : [];
        } else {
            this.onReadyFuncs = dojo.clone(args.onReady);
        }

        // interpret the track configurations as a metadata store
        this._indexItems(
            {
                store: this,
                items: dojo.map( args.trackConfigs, function(conf) {
                    var metarecord = dojo.clone( conf.metadata || {} );
                    metarecord.label = conf.label;
                    metarecord.key = conf.key;
                    metarecord.conf = conf;
                    return metarecord;
                },this)
            }
        );

        // fetch and index all the items from each of the stores
        var stores_fetched_count = 0;
        if( ! args.metadataStores || ! args.metadataStores.length ) {
            // if we don't actually have any stores besides the track
            // confs, we're ready now.
            this._ready();
        } else  {
            // index the track metadata from each of the stores

            var storeFetchFinished = dojo.hitch( this, function() {
                if( ++stores_fetched_count == args.metadataStores.length )
                    this._ready();
            });
            dojo.forEach( args.metadataStores, function(store) {
                store.fetch({
                    scope: this,
                    onComplete: Util.debugHandler( this, function(items) {
                        // build our indexes
                        this._indexItems({ store: store, items: items, supplementalOnly: true });

                        // if this is the last store to be fetched, call
                        // our onReady callbacks
                        storeFetchFinished();
                    }),
                    onError: storeFetchFinished
                });
            },this);
        }
     },

    /**
     * Set the store's state to be ready (i.e. loaded), and calls all
     * our onReady callbacks.
     * @private
     */
    _ready: function() {
        this.ready = true;
        dojo.forEach( this.onReadyFuncs, function(f) {
                          f.call( this, this );
                      }, this );
        this.onReadyFuncs = [];
    },

    _indexItems: function( args ) {
        // get our (filtered) list of facets we will index for
        var store = args.store,
            items = args.items;

        // convert the items to a uniform format
        items = dojo.map( items, function( item ) {
                              var attributes = store.getAttributes(item);

                              //convert the item into a uniform data format of plain objects
                              var newitem = {};
                              dojo.forEach( attributes, function(attr) {
                                                newitem[attr] = store.getValue(item,attr);
                                            });
                              return newitem;
                          },
                          this
                        );

        // merge them with any existing records, filtering out ones
        // that should be ignored if we were passed
        // 'supplementalOnly', and update the identity index
        this.identIndex = this.identIndex || {};
        items = dojo.map( items, function(item) {
                                 // merge the new item attributes with any existing
                                 // record for this item
                                 var ident = this.getIdentity(item);
                                 var existingItem = this.identIndex[ ident ];
                                 if( args.supplementalOnly && !existingItem) {
                                     // skip this item if we are supplementalOnly and it
                                     // does not already exist
                                     return null;
                                 }
                                 return this.identIndex[ ident ] = dojo.mixin( existingItem || {}, item );
                             },
                             this
                           );
        // filter out nulls
        items = dojo.filter( items, function(i) { return i;});

        // update our facet list to include any new attrs these
        // items have
        var old_facets = this.facets || [];
        this.facets = (function() {
            var seen = {};
            return dojo.filter( old_facets.concat( this.getAttributes( items[0] ) ),
                                function(facetName) {
                                    var take = this._filterFacet(facetName) && !seen[facetName];
                                    seen[facetName] = true;
                                    return take;
                                },
                                this
                              );
        }).call(this);
        var new_facets = this.facets.slice( old_facets.length );

        // initialize indexes for any new facets
        this.facetIndexes = this.facetIndexes || { itemCount: 0, bucketCount: 0, byName: {} };
        dojo.forEach( new_facets, function(facet) {
            if( ! this.facetIndexes.byName[facet] ) {
                this.facetIndexes.bucketCount++;
                this.facetIndexes.byName[facet] = { itemCount: 0, bucketCount: 0, byValue: {} };
            }
        }, this);

        // now update the indexes with the new facets
        if( new_facets.length ) {
            dojo.forEach( items, function( item ) {
                this.facetIndexes.itemCount++;
                dojo.forEach( new_facets, function( facet ) {

                    var value = this.getValue( item, facet, undefined );
                    if( typeof value == 'undefined' )
                        return;
                    var facetValues = this.facetIndexes.byName[facet];
                    var bucket = facetValues.byValue[value];
                    if( !bucket ) {
                        bucket = facetValues.byValue[value] = { itemCount: 0, items: [] };
                        facetValues.bucketCount++;
                    }
                    bucket.itemCount++;
                    bucket.items.push(item);
                },this);
            }, this);

            // calculate the rank of the facets: make an array of
            // facet names sorted by smallest average bucket size,
            // descending
            this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
                a = this.facetIndexes.byName[a];
                b = this.facetIndexes.byName[b];
                return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
            }));
        }
    },

    /**
     * Get an array of the text names of the facets that are defined
     * in this track metadata.
     * @param callback {Function} called as callback( [facet,facet,...] )
     */
    getFacets: function( callback ) {
        return this.facets;
    },

    /**
     * Get an Array of the distinct values for a given facet name.
     * @param facetName {String} the facet name
     * @returns {Array} distinct values for that facet
     */
    getFacetValues: function( facetName ) {
        var index = this.facetIndexes.byName[facetName];
        if( !index )
            return [];

        return dojof.keys( index.byValue );
    },

    /**
     * Add a callback to be called when this store is ready (i.e. loaded).
     */
    onReady: function( callback ) {
        if( this.ready ) {
            callback.call( this, this );
        }
        else {
            this.onReadyFuncs.push( callback );
        }
    },

    // dojo.data.api.Read support

    getValue: function( i, attr, defaultValue ) {
        var v = i[attr];
        return typeof v == 'undefined' ? defaultValue : v;
    },
    getValues: function( i, attr ) {
        var a = [ i[attr] ];
        return typeof a[0] == 'undefined' ? [] : a;
    },

    getAttributes: function(item)  {
        return dojof.keys( item );
    },

    hasAttribute: function(item,attr) {
        return item.hasOwnProperty(attr);
    },

    containsValue: function(item, attribute, value) {
        return item[attribute] == value;
    },

    isItem: function(item) {
        return typeof item == 'object' && typeof item[name] == 'string';
    },

    isItemLoaded: function() {
        return true;
    },

    loadItem: function( args ) {
    },

    // used by the dojo.data.util.simpleFetch mixin to implement fetch()
    _fetchItems: function( keywordArgs, findCallback, errorCallback ) {
        if( ! this.ready ) {
            this.onReady( dojo.hitch( this, '_fetchItems', keywordArgs, findCallback, errorCallback ) );
            return;
        }

        var query = dojo.clone( keywordArgs.query || {} );
        var textFilter = query.text;
        delete query.text;

        var results;

        // if we don't actually have any facets specified in the
        // query, the results are just all the items
        if( ! dojo.some( dojof.values(query), function(q){ return q.length > 0;}) ) {
            results = dojof.values( this.identIndex );
        }
        else {
            // start with an initial set of items that have the desired
            // value for the most specific (smallest avg index bucket
            // size) facet that was specified
            results = (function() {
                           var highestRankedFacet,
                               queryValues = [],
                               index;
                           dojo.some( this.facetIndexes.facetRank, function(facetName) {
                                          if( query[facetName] ) {
                                              highestRankedFacet = facetName;
                                              queryValues = query[highestRankedFacet];
                                              index = this.facetIndexes.byName[highestRankedFacet];
                                              return queryValues.length > 0;
                                          }
                                          return false;
                                      },this);

                           delete query[highestRankedFacet];

                           var set = [];
                           dojo.forEach( queryValues, function(val) {
                              var bucket = index.byValue[val];
                              if( bucket )
                                  set.push.apply( set, bucket.items );
                           },this);
                           return set;
                       }).call(this);

            // and filter this starting set for the other facets
            dojo.forEach( dojof.keys(query), function(facetName) {
                              var desired_values = query[facetName] || [];
                              if( ! desired_values.length )
                                  return;
                              results = dojo.filter(results, function(item) {
                                                        var value = this.getValue(item,facetName);
                                                        return dojo.some( desired_values, function(desired) {
                                                                              return desired == value;
                                                                          },this);
                                                    },this);
                          },this);
        }

        // TODO: filter with the text filter, if we have it

        // and finally, hand them to the finding callback
        findCallback(results,keywordArgs);
    },

    getFeatures: function() {
        return {
	    'dojo.data.api.Read': true,
	    'dojo.data.api.Identity': true
	};
    },
    close: function() {},

    getLabel: function(i) {
        return this.getValue(i,'key',undefined);
    },
    getLabelAttributes: function(i) {
        return ['key']; },

    // dojo.data.api.Identity support
    getIdentityAttributes: function() {
        return ['label'];
    },
    getIdentity: function(i) {
        return this.getValue(i, 'label', undefined);
    },
    fetchItemByIdentity: function(id) {
        return this.identIndex[id];
    }
});
dojo.require('dojo.data.util.simpleFetch');
dojo.extend( JBrowse.Model.TrackMetaData, dojo.data.util.simpleFetch );<|endoftext|>


<commit_before>var css = require("css");
var request = require("request");
var rp = require("request-promise");
var Promise = require("es6-promise").Promise;
var _ = require("lodash");
var sprintf = require("sprintf");
var path = require("path");
var fs = require("fs");
var logger = require("./log").get_logger({ module: "fontdump" });

var FontSource = function(font, source, extension, format) {
    this.font = font;
    this.source = source;
    this.extension = extension;
    this.format = format;
};

FontSource.prototype = {
    get_filename: function() {
        return sprintf(
            "%s_%s_%s.%s",
            this.font.family.name, this.font.weight, this.font.style, this.extension
        ).replace(/\s+/, "").toLowerCase();
    },
    get_web_filename: function() {
        var filename = this.get_filename();

        if(this.format !== "svg") {
            return filename;
        }

        var hash_pos = this.source.indexOf("#");
        return filename + (hash_pos !== -1 ? this.source.substr(hash_pos) : "");
    }
};

var Font = function(family, weight, style) {
    this.family = family;
    this.weight = weight;
    this.style = style;
    this.sources = {};
};

Font.prototype = {
    add_source: function(source, extension, format) {
        this.sources[format] = new FontSource(this, source, extension, format);
        return this;
    },
    get_sources: function() {
        return _.values(this.sources);
    },
    has_format: function(format) {
        return this.sources.hasOwnProperty(format);
    },
    get_source: function(format) {
        return this.sources[format]
    }
};

Font.FORMATS = {
    "woff": "woff",
    "truetype": "ttf",
    "embedded-opentype": "eot",
    "svg": "svg"
};

var FontFamily = function(name) {
    this.name = name;
    this.fonts = {};

    this._create_font_id = function(weight, style) {
        return sprintf("%s_%s", weight, style);
    }
};

FontFamily.prototype = {
    get_fonts: function() {
        return _.values(this.fonts);
    },
    with_font: function(weight, style) {
        var id = this._create_font_id(weight, style);

        if(typeof this.fonts[id] === "undefined") {
            this.fonts[id] = new Font(this, weight, style);
        }

        return this.fonts[id];
    }
};

var FontFamilyCollection = function() {
    this.families = {};
};

FontFamilyCollection.prototype = {
    sync_fonts: function(synchronizer) {
        var syncs = this.get_font_sources().map((function(font_source) {
            return synchronizer(font_source.source, font_source.get_filename());
        }).bind(this));

        return Promise.all(syncs);
    },
    get_families: function() {
        return _.values(this.families);
    },
    get_fonts: function() {
        return Array.prototype.concat.apply([], this.get_families().map(function(family) {
            return family.get_fonts();
        }));
    },
    get_font_sources: function() {
        return Array.prototype.concat.apply([], this.get_fonts().map(function(font) {
            return font.get_sources();
        }));
    },
    with_font_family: function(name) {
        if(typeof this.families[name] === "undefined") {
            this.families[name] = new FontFamily(name);
        }

        return this.families[name];
    }
};

var FontLoader = function(endpoint, target_directory) {
    var self = this;

    this.agents = _.clone(FontLoader.AGENTS);
    this.endpoint = endpoint;
    this.target_directory = target_directory;

    this._create_request = function(endpoint, ua) {
        request = rp({
            url: endpoint,
            headers: { "User-Agent": ua }
        }).promise();

        request.then(function() {
            // on success
            logger.debug(sprintf("successfully downloaded '%s' with ua '%s'", endpoint, ua));
        }, function() {
            // on error
            logger.error(sprintf("error while downloading '%s' with ua '%s'", endpoint, ua));
        });

        return request;
    };

    this._load_stylesheets = function() {
        var requests = _.values(this.agents).map(function(ua) {
            return self._create_request(self.endpoint, ua);
        });

        return Promise.all(requests);
    };

    this._parse_stylesheet = function(collection, stylesheet) {
        css.parse(stylesheet).stylesheet.rules.forEach(function (rule) {
            var declarations = _.indexBy(rule.declarations, "property");
            var weight = declarations["font-weight"]["value"];
            var style = declarations["font-style"]["value"];
            var source_matches = declarations["src"]["value"]
                .match(/(url\((http.+)\) format\('(.+)'\)|url\((http.+.eot)\))/);
            var source = source_matches[2] ? source_matches[2] : source_matches[4];
            var format = source_matches[2] ? source_matches[3] : "embedded-opentype";
            var extension = Font.FORMATS[format];
            var family = declarations["font-family"]["value"];
            family = family.indexOf("'") === 0 ? family.slice(1, -1) : family;

            logger.debug(sprintf("adding font %s with weight %s and style %s", family, weight, style));

            collection
                .with_font_family(family)
                .with_font(weight, style)
                .add_source(source, extension, format);
        });
    };

    this._parse_stylesheets = function(stylesheets) {
        logger.info("parsing stylesheets");
        return new Promise((function(resolve, reject) {
            var collection = new FontFamilyCollection();
            stylesheets.forEach(this._parse_stylesheet.bind(this, collection));
            resolve(collection);
        }).bind(this));
    };

    this._write_font = function(target_file, data) {
        return new Promise(function(resolve, reject) {
            fs.writeFile(target_file, data, function(err) {
                if(err) reject(err);
                else resolve(target_file);
            })
        });
    };

    this._sync_font = function(font_source, filename) {
        var target_file = path.join(this.target_directory, filename);

        return this
            ._create_request(font_source)
            .then(this._write_font.bind(this, target_file));
    };

    this._sync_fonts = function(collection) {
        logger.info("syncing fonts");
        return collection
            .sync_fonts(this._sync_font.bind(this))
            .then(function() {
                return new Promise(function(resolve) {
                    resolve(collection)
                });
            }, function() {
                logger.error("could not sync font");
            });
    };
};

FontLoader.prototype = {
    request_all: function() {
        return this._load_stylesheets()
            .then(this._parse_stylesheets.bind(this))
            .then(this._sync_fonts.bind(this))
        ;
    },
    disable_format: function(definition_name) {
        if(this.agents.hasOwnProperty(definition_name)) {
            delete this.agents[definition_name];
        } else {
            throw new Error("no definition with that name or you’ve already disabled it")
        }

        return this;
    }
};

FontLoader.AGENTS = {
    // Chrome
    "woff":  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36",
    // IE 9
    "eot":   "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; Trident/5.0)",
    // Android 2
    "ttf":   "Mozilla/5.0 (Linux; U; Android 2.2; en-us; DROID2 GLOBAL Build/S273) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
    // iOS<4.2
    "svg":   "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10",
    // IE 6-8
    "ie6-8": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)"
};

FontFaceRenderer = function(endpoint) {
    this.endpoint = endpoint;

    this._create_url = function(font_source) {
        return sprintf("url('%s')",
            this.endpoint
                ? this.endpoint + "/" + font_source.get_web_filename()
                : font_source.get_web_filename()
        );
    };

    this._add_source_set = function(declaration_set, font) {
        if(font.has_format("embedded-opentype")) {
            this._add_declaration(
                declaration_set, "src",
                this._create_url(font.get_source("embedded-opentype"))
            );
        }

        this._add_declaration(declaration_set, "src", font.get_sources().map((function(source) {
            return sprintf("%s format('%s')", this._create_url(source), source.format);
        }).bind(this)).join(", "));
    };

    this._add_declaration = function(declaration_set, property, value) {
        declaration_set.push({
            type: "declaration",
            property: property,
            value: value
        });
    };

    this._add_rule = function(ruleset, font) {
        var rule = {
            type: "font-face",
            declarations: []
        };

        this._add_declaration(rule.declarations, "font-family", "'" + font.family.name + "'");
        this._add_declaration(rule.declarations, "font-weight", font.weight);
        this._add_declaration(rule.declarations, "font-style", font.style);
        this._add_source_set(rule.declarations, font);

        logger.debug(sprintf(
            "adding font-face rule for font-family %s with weight %s and style %s",
            font.family.name, font.weight, font.style
        ));

        ruleset.push(rule);
    };

    this._build_ast = function(collection) {
        logger.info("building ast");

        var ast = {
            type: "stylesheet",
            stylesheet: {
                rules: []
            }
        };

        collection.get_fonts().map(this._add_rule.bind(this, ast.stylesheet.rules));

        return ast;
    }
};

FontFaceRenderer.prototype = {
    render: function(collection) {
        logger.info("rendering font-faces");
        var ast = this._build_ast(collection);
        return css.stringify(ast);
    }
};

module.exports = {
    dump: function(config) {
        if(!config.hasOwnProperty("url")) {
            throw new Error("url is mandatory");
        }

        if(!config.hasOwnProperty("target_directory")) {
            throw new Error("target_directory is mandatory");
        }

        if(!config.hasOwnProperty("css_file")) {
            config.css_file = "fonts.css";
        }

        var loader = new FontLoader(config.url, config.target_directory);
        var renderer = new FontFaceRenderer(config.web_directory || "");

        return new Promise(function(resolve, reject) {
            loader.request_all().then(
                function(collection) {
                    var css = renderer.render(collection);
                    fs.writeFileSync(path.join(config.target_directory, config.css_file), css);
                    resolve({
                        css: css,
                        fonts: collection
                    });
                },
                function() {
                    logger.error("could not download fonts");
                    reject();
                }
            );
        });

    }
};<commit_msg>fix request encoding for binary files<commit_after>var css = require("css");
var request = require("request");
var rp = require("request-promise");
var Promise = require("es6-promise").Promise;
var _ = require("lodash");
var sprintf = require("sprintf");
var path = require("path");
var fs = require("fs");
var logger = require("./log").get_logger({ module: "fontdump" });

var FontSource = function(font, source, extension, format) {
    this.font = font;
    this.source = source;
    this.extension = extension;
    this.format = format;
};

FontSource.prototype = {
    get_filename: function() {
        return sprintf(
            "%s_%s_%s.%s",
            this.font.family.name, this.font.weight, this.font.style, this.extension
        ).replace(/\s+/, "").toLowerCase();
    },
    get_web_filename: function() {
        var filename = this.get_filename();

        if(this.format !== "svg") {
            return filename;
        }

        var hash_pos = this.source.indexOf("#");
        return filename + (hash_pos !== -1 ? this.source.substr(hash_pos) : "");
    }
};

var Font = function(family, weight, style) {
    this.family = family;
    this.weight = weight;
    this.style = style;
    this.sources = {};
};

Font.prototype = {
    add_source: function(source, extension, format) {
        this.sources[format] = new FontSource(this, source, extension, format);
        return this;
    },
    get_sources: function() {
        return _.values(this.sources);
    },
    has_format: function(format) {
        return this.sources.hasOwnProperty(format);
    },
    get_source: function(format) {
        return this.sources[format]
    }
};

Font.FORMATS = {
    "woff": "woff",
    "truetype": "ttf",
    "embedded-opentype": "eot",
    "svg": "svg"
};

var FontFamily = function(name) {
    this.name = name;
    this.fonts = {};

    this._create_font_id = function(weight, style) {
        return sprintf("%s_%s", weight, style);
    }
};

FontFamily.prototype = {
    get_fonts: function() {
        return _.values(this.fonts);
    },
    with_font: function(weight, style) {
        var id = this._create_font_id(weight, style);

        if(typeof this.fonts[id] === "undefined") {
            this.fonts[id] = new Font(this, weight, style);
        }

        return this.fonts[id];
    }
};

var FontFamilyCollection = function() {
    this.families = {};
};

FontFamilyCollection.prototype = {
    sync_fonts: function(synchronizer) {
        var syncs = this.get_font_sources().map((function(font_source) {
            return synchronizer(font_source.source, font_source.get_filename());
        }).bind(this));

        return Promise.all(syncs);
    },
    get_families: function() {
        return _.values(this.families);
    },
    get_fonts: function() {
        return Array.prototype.concat.apply([], this.get_families().map(function(family) {
            return family.get_fonts();
        }));
    },
    get_font_sources: function() {
        return Array.prototype.concat.apply([], this.get_fonts().map(function(font) {
            return font.get_sources();
        }));
    },
    with_font_family: function(name) {
        if(typeof this.families[name] === "undefined") {
            this.families[name] = new FontFamily(name);
        }

        return this.families[name];
    }
};

var FontLoader = function(endpoint, target_directory) {
    var self = this;

    this.agents = _.clone(FontLoader.AGENTS);
    this.endpoint = endpoint;
    this.target_directory = target_directory;

    this._create_request = function(endpoint, ua, encoding) {
        request = rp({
            url: endpoint,
            encoding: typeof encoding === "undefined" ? "utf8" : null,
            headers: { "User-Agent": ua || FontLoader.AGENTS.woff }
        }).promise();

        request.then(function() {
            // on success
            logger.debug(sprintf("successfully downloaded '%s' with ua '%s'", endpoint, ua));
        }, function() {
            // on error
            logger.error(sprintf("error while downloading '%s' with ua '%s'", endpoint, ua));
        });

        return request;
    };

    this._load_stylesheets = function() {
        var requests = _.values(this.agents).map(function(ua) {
            return self._create_request(self.endpoint, ua);
        });

        return Promise.all(requests);
    };

    this._parse_stylesheet = function(collection, stylesheet) {
        css.parse(stylesheet).stylesheet.rules.forEach(function (rule) {
            var declarations = _.indexBy(rule.declarations, "property");
            var weight = declarations["font-weight"]["value"];
            var style = declarations["font-style"]["value"];
            var source_matches = declarations["src"]["value"]
                .match(/(url\((http.+)\) format\('(.+)'\)|url\((http.+.eot)\))/);
            var source = source_matches[2] ? source_matches[2] : source_matches[4];
            var format = source_matches[2] ? source_matches[3] : "embedded-opentype";
            var extension = Font.FORMATS[format];
            var family = declarations["font-family"]["value"];
            family = family.indexOf("'") === 0 ? family.slice(1, -1) : family;

            logger.debug(sprintf("adding font %s with weight %s and style %s", family, weight, style));

            collection
                .with_font_family(family)
                .with_font(weight, style)
                .add_source(source, extension, format);
        });
    };

    this._parse_stylesheets = function(stylesheets) {
        logger.info("parsing stylesheets");
        return new Promise((function(resolve, reject) {
            var collection = new FontFamilyCollection();
            stylesheets.forEach(this._parse_stylesheet.bind(this, collection));
            resolve(collection);
        }).bind(this));
    };

    this._write_font = function(target_file, data) {
        return new Promise(function(resolve, reject) {
            fs.writeFile(target_file, data, function(err) {
                if(err) reject(err);
                else resolve(target_file);
            })
        });
    };

    this._sync_font = function(font_source, filename) {
        var target_file = path.join(this.target_directory, filename);

        return this
            ._create_request(font_source, null, null)
            .then(this._write_font.bind(this, target_file));
    };

    this._sync_fonts = function(collection) {
        logger.info("syncing fonts");
        return collection
            .sync_fonts(this._sync_font.bind(this))
            .then(function() {
                return new Promise(function(resolve) {
                    resolve(collection)
                });
            }, function() {
                logger.error("could not sync font");
            });
    };
};

FontLoader.prototype = {
    request_all: function() {
        return this._load_stylesheets()
            .then(this._parse_stylesheets.bind(this))
            .then(this._sync_fonts.bind(this))
        ;
    },
    disable_format: function(definition_name) {
        if(this.agents.hasOwnProperty(definition_name)) {
            delete this.agents[definition_name];
        } else {
            throw new Error("no definition with that name or you’ve already disabled it")
        }

        return this;
    }
};

FontLoader.AGENTS = {
    // Chrome
    "woff":  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36",
    // IE 9
    "eot":   "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; Trident/5.0)",
    // Android 2
    "ttf":   "Mozilla/5.0 (Linux; U; Android 2.2; en-us; DROID2 GLOBAL Build/S273) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
    // iOS<4.2
    "svg":   "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10",
    // IE 6-8
    "ie6-8": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)"
};

FontFaceRenderer = function(endpoint) {
    this.endpoint = endpoint;

    this._create_url = function(font_source) {
        return sprintf("url('%s')",
            this.endpoint
                ? this.endpoint + "/" + font_source.get_web_filename()
                : font_source.get_web_filename()
        );
    };

    this._add_source_set = function(declaration_set, font) {
        if(font.has_format("embedded-opentype")) {
            this._add_declaration(
                declaration_set, "src",
                this._create_url(font.get_source("embedded-opentype"))
            );
        }

        this._add_declaration(declaration_set, "src", font.get_sources().map((function(source) {
            return sprintf("%s format('%s')", this._create_url(source), source.format);
        }).bind(this)).join(", "));
    };

    this._add_declaration = function(declaration_set, property, value) {
        declaration_set.push({
            type: "declaration",
            property: property,
            value: value
        });
    };

    this._add_rule = function(ruleset, font) {
        var rule = {
            type: "font-face",
            declarations: []
        };

        this._add_declaration(rule.declarations, "font-family", "'" + font.family.name + "'");
        this._add_declaration(rule.declarations, "font-weight", font.weight);
        this._add_declaration(rule.declarations, "font-style", font.style);
        this._add_source_set(rule.declarations, font);

        logger.debug(sprintf(
            "adding font-face rule for font-family %s with weight %s and style %s",
            font.family.name, font.weight, font.style
        ));

        ruleset.push(rule);
    };

    this._build_ast = function(collection) {
        logger.info("building ast");

        var ast = {
            type: "stylesheet",
            stylesheet: {
                rules: []
            }
        };

        collection.get_fonts().map(this._add_rule.bind(this, ast.stylesheet.rules));

        return ast;
    }
};

FontFaceRenderer.prototype = {
    render: function(collection) {
        logger.info("rendering font-faces");
        var ast = this._build_ast(collection);
        return css.stringify(ast);
    }
};

module.exports = {
    dump: function(config) {
        if(!config.hasOwnProperty("url")) {
            throw new Error("url is mandatory");
        }

        if(!config.hasOwnProperty("target_directory")) {
            throw new Error("target_directory is mandatory");
        }

        if(!config.hasOwnProperty("css_file")) {
            config.css_file = "fonts.css";
        }

        var loader = new FontLoader(config.url, config.target_directory);
        var renderer = new FontFaceRenderer(config.web_directory || "");

        return new Promise(function(resolve, reject) {
            loader.request_all().then(
                function(collection) {
                    var css = renderer.render(collection);
                    fs.writeFileSync(path.join(config.target_directory, config.css_file), css);
                    resolve({
                        css: css,
                        fonts: collection
                    });
                },
                function() {
                    logger.error("could not download fonts");
                    reject();
                }
            );
        });

    }
};<|endoftext|>