<commit_before>/**
 * @fileOverview Wrapper for node-rdkafka Producer Ctor, a mixin.
 */
var Promise = require('bluebird');
var cip = require('cip');
var kafka = require('node-rdkafka');

/**
 * Wrapper for node-rdkafka Produce Ctor, a mixin.
 *
 * @constructor
 */
var Producer = module.exports = cip.extend();

/**
 * The wrapper of the node-rdkafka package Producer Ctor.
 *
 * @param {Object} opts Producer general options.
 * @see https://github.com/edenhill/librdkafka/blob/2213fb29f98a7a73f22da21ef85e0783f6fd67c4/CONFIGURATION.md
 * @return {Promise(kafka.Producer)} A Promise.
 */
Producer.prototype.getProducer = Promise.method(function (opts) {
  if (!opts) {
    opts = {};
  }

  if (!opts['metadata.broker.list']) {
    opts['metadata.broker.list'] = this.kafkaBrokerUrl;
  }
  console.log('KafkaAvro :: Starting producer with options:', opts);
  var producer = new kafka.Producer(opts);

  // hack node-rdkafka
  producer.__kafkaAvro_produce = producer.produce;
  producer.produce = this._produceWrapper.bind(this, producer);

  producer.connect();

  return new Promise(function(resolve) {
    producer.on('ready', resolve);
  })
    .return(producer);
});

/**
 * The node-rdkafka produce method wrapper, will validate and serialize
 * the message against the existing schemas.
 *
 * @param {kafka.Producer} producerInstance node-rdkafka instance.
 * @param {string} topic Topic to produce on.
 * @param {kafka.Producer.Topic} kafkaTopic node-rdkafka Topic instance.
 * @param {number} partition The partition to produce on.
 * @param {Object} value The message.
 * @param {string|number} key The partioning key.
 */
Producer.prototype._produceWrapper = function (producerInstance, topic, kafkaTopic,
  partition, value, key) {

  if (!this.valueSchemas[topic]) {
    // topic not found in schemas, bail early
    console.log('KafkaAvro :: Warning, did not find topic on SR:', topic);
    var bufVal = new Buffer(JSON.stringify(value));
    return producerInstance.__kafkaAvro_produce(topic, partition, bufVal, key);
  }

  var bufValue = this.valueSchemas[topic].toBuffer(value);
  return producerInstance.__kafkaAvro_produce(topic, partition, bufValue, key);
};
<commit_msg>change producer API, use topic.name()<commit_after>/**
 * @fileOverview Wrapper for node-rdkafka Producer Ctor, a mixin.
 */
var Promise = require('bluebird');
var cip = require('cip');
var kafka = require('node-rdkafka');

/**
 * Wrapper for node-rdkafka Produce Ctor, a mixin.
 *
 * @constructor
 */
var Producer = module.exports = cip.extend();

/**
 * The wrapper of the node-rdkafka package Producer Ctor.
 *
 * @param {Object} opts Producer general options.
 * @see https://github.com/edenhill/librdkafka/blob/2213fb29f98a7a73f22da21ef85e0783f6fd67c4/CONFIGURATION.md
 * @return {Promise(kafka.Producer)} A Promise.
 */
Producer.prototype.getProducer = Promise.method(function (opts) {
  if (!opts) {
    opts = {};
  }

  if (!opts['metadata.broker.list']) {
    opts['metadata.broker.list'] = this.kafkaBrokerUrl;
  }
  console.log('KafkaAvro :: Starting producer with options:', opts);
  var producer = new kafka.Producer(opts);

  // hack node-rdkafka
  producer.__kafkaAvro_produce = producer.produce;
  producer.produce = this._produceWrapper.bind(this, producer);

  producer.connect();

  return new Promise(function(resolve) {
    producer.on('ready', resolve);
  })
    .return(producer);
});

/**
 * The node-rdkafka produce method wrapper, will validate and serialize
 * the message against the existing schemas.
 *
 * @param {kafka.Producer} producerInstance node-rdkafka instance.
 * @param {kafka.Producer.Topic} kafkaTopic node-rdkafka Topic instance.
 * @param {number} partition The partition to produce on.
 * @param {Object} value The message.
 * @param {string|number} key The partioning key.
 */
Producer.prototype._produceWrapper = function (producerInstance, kafkaTopic,
  partition, value, key) {

  var topicName = kafkaTopic.name();

  if (!this.valueSchemas[topicName]) {
    // topic not found in schemas, bail early
    console.log('KafkaAvro :: Warning, did not find topic on SR:', topicName);
    var bufVal = new Buffer(JSON.stringify(value));
    return producerInstance.__kafkaAvro_produce(kafkaTopic, partition, bufVal, key);
  }

  var bufValue = this.valueSchemas[topicName].toBuffer(value);

  return producerInstance.__kafkaAvro_produce(kafkaTopic, partition, bufValue, key);
};
<|endoftext|>


<commit_before>/**
 * @fileOverview The main Kansas Metrics module.
 */
// var __ = require('lodash');
var cip = require('cip');
var Promise = require('bluebird');
var appErr = require('nodeon-error');

var setup = require('./setup');
var Fetch = require('./fetch');

/**
 * The Kansas Metrics main module.
 *
 * @constructor
 */
var Metrics = module.exports = cip.extend(function() {
  /** @type {Kansas} The local ref to the Kansas instance */
  this.kansas = setup.kansas;

  // Check if Kansas is set.
  if (!this.kansas) {
    throw new appErr.Error('No Kansas instance was defined, use "setup()"');
  }

  // Check if Kansas is connected
  if (!this.kansas.connected) {
    throw new appErr.Error('Kansas is not connected to Redis');
  }

  /** @type {Redis} The redis client */
  this.client = this.kansas.client;

  /** @type {boolean} Indicates a query operation is taking place */
  this.operates = false;

  /** @type {boolean} Indicates query has complete and data populated */
  this.fetched = false;

  /** @type {string} Get the db keys prefix from Kansas */
  this.prefix = this.kansas._options.prefix;

  // Promisify Redis required methods
  this.smembers = Promise.promisify(this.client.smembers, this.client);

  /** @type {Object} Internal query object */
  this.query = {
    user: [],
    token: [],
    from: null,
    to: null,
  };

  /** @type {Object} Internal options */
  this._options = {
    /** @type {number} How many concurrent operations to hit Redis with */
    concurrency: 10,
  };

  /** @type {Promise.Defer} The instance's master defer */
  this.defer = Promise.defer();

  /** @type {Promise} The instance's master Promise. */
  this.promise = this.defer.promise;

});

Metrics.mixin(Fetch);

/**
 * Filter by owner id.
 *
 * @param {string|Array.<string>} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.user = function(query) {
  if (Array.isArray(query)) {
    this.query.user.concat(query);
  } else {
    this.query.user.push(query);
  }
  return this;
};

/**
 * Filter by token id.
 *
 * @param {string|Array.<string>} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.token = function(query) {
  if (Array.isArray(query)) {
    this.query.token.concat(query);
  } else {
    this.query.token.push(query);
  }

  return this;
};

/**
 * Filter by date, from.
 *
 * @param {string|Date} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.from = function(query) {
  if (query instanceof Date) {
    this.query.from = query;
  } else {
    this.query.from = new Date(query);
  }

  return this;
};

/**
 * Filter by date, to.
 *
 * @param {string|Date} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.to = function(query) {
  if (query instanceof Date) {
    this.query.to = query;
  } else {
    this.query.to = new Date(query);
  }

  return this;
};

<commit_msg>Add more redis promisifications<commit_after>/**
 * @fileOverview The main Kansas Metrics module.
 */
// var __ = require('lodash');
var cip = require('cip');
var Promise = require('bluebird');
var appErr = require('nodeon-error');

var setup = require('./setup');
var Fetch = require('./fetch');

/**
 * The Kansas Metrics main module.
 *
 * @constructor
 */
var Metrics = module.exports = cip.extend(function() {
  /** @type {Kansas} The local ref to the Kansas instance */
  this.kansas = setup.kansas;

  // Check if Kansas is set.
  if (!this.kansas) {
    throw new appErr.Error('No Kansas instance was defined, use "setup()"');
  }

  // Check if Kansas is connected
  if (!this.kansas.connected) {
    throw new appErr.Error('Kansas is not connected to Redis');
  }

  /** @type {Redis} The redis client */
  this.client = this.kansas.client;

  /** @type {boolean} Indicates a query operation is taking place */
  this.operates = false;

  /** @type {boolean} Indicates query has complete and data populated */
  this.fetched = false;

  /** @type {string} Get the db keys prefix from Kansas */
  this.prefix = this.kansas._options.prefix;

  // Promisify Redis required methods
  this.redisSmembers = Promise.promisify(this.client.smembers, this.client);
  this.redisKeys = Promise.promisify(this.client.keys, this.client);
  this.redisGet = Promise.promisify(this.client.get, this.client);

  /** @type {Object} Internal query object */
  this.query = {
    user: [],
    token: [],
    from: null,
    to: null,
  };

  /** @type {Object} Internal options */
  this._options = {
    /** @type {number} How many concurrent operations to hit Redis with */
    concurrency: 10,
  };

  /** @type {Promise.Defer} The instance's master defer */
  this.defer = Promise.defer();

  /** @type {Promise} The instance's master Promise. */
  this.promise = this.defer.promise;

});

Metrics.mixin(Fetch);

/**
 * Filter by owner id.
 *
 * @param {string|Array.<string>} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.user = function(query) {
  if (Array.isArray(query)) {
    this.query.user.concat(query);
  } else {
    this.query.user.push(query);
  }
  return this;
};

/**
 * Filter by token id.
 *
 * @param {string|Array.<string>} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.token = function(query) {
  if (Array.isArray(query)) {
    this.query.token.concat(query);
  } else {
    this.query.token.push(query);
  }

  return this;
};

/**
 * Filter by date, from.
 *
 * @param {string|Date} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.from = function(query) {
  if (query instanceof Date) {
    this.query.from = query;
  } else {
    this.query.from = new Date(query);
  }

  return this;
};

/**
 * Filter by date, to.
 *
 * @param {string|Date} query The query.
 * @return {self} Chainable.
 */
Metrics.prototype.to = function(query) {
  if (query instanceof Date) {
    this.query.to = query;
  } else {
    this.query.to = new Date(query);
  }

  return this;
};

<|endoftext|>


<commit_before>// m3u8downloader.js
// =================

var async = require('async');
var fs = require('fs');
var util = require('util');
var request = require('request');
var EventEmitter = require('events').EventEmitter;

exports.version = "0.2.0";

var endsWith = function(str, suffix) {
  return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

var m3u8downloader = function(masterPlayListURL, destination, callback)
{
    var self = this;
    var res = masterPlayListURL.split("/");
    var site = masterPlayListURL.replace(res[res.length - 1], '');
    request.get(masterPlayListURL, handleResponse);

    var out = {};

    function handleResponse(error, response, body)
    {
        if (error || response.statusCode !== 200)
        {
            return done(error || new Error('Bad status code: ' + response.statusCode));
        }

        self.emit('start');
        var masterResponse = response.req.path.split("/");

        try
        {
            fs.mkdirSync(destination + '/' + masterResponse[1]);
        }
        catch (err)
        {
            return done(err);
        }

        var fileMasterPlayList = destination + response.req.path;
        fs.writeFile(fileMasterPlayList, body, afterWrite);

        function afterWrite(err)
        {
            if (err)
            {
                return done(err);
            }

            self.emit('progress', 'Created Master Playlist ' + fileMasterPlayList);

            out.master = fileMasterPlayList;
            out.media = [];

            var masterPlayListContents = body.split("\n");
            async.eachSeries(masterPlayListContents, handleContent, done);

            function handleContent(part, asyncCb)
            {
                if (!endsWith(part, "m3u8"))
                {
                    return asyncCb();
                }

                var res2 = part.split("/");
                var mediaPlayListFile = '';
                try
                {
                    if (part.indexOf("://") > -1)
                    {
                        fs.mkdirSync(destination + "/" + masterResponse[1] + '/' + res2[res2.length - 2]);
                        mediaPlayListFile = part;
                    }
                    else
                    {
                        fs.mkdirSync(destination + "/" + masterResponse[1] + '/' + res2[0]);
                        mediaPlayListFile = site + part;
                    }
                }
                catch (err)
                {
                    return asyncCb(err);
                }

                request.get(mediaPlayListFile, handleResponse);

                function handleResponse(error, response, body)
                {
                    if (error || response.statusCode !== 200)
                    {
                        return asyncCb(error || new Error('Bad status code: ' + response.statusCode));
                    }

                    var fileMediaPlayList = destination + response.req.path;
                    var mediaResponse = response.req.path.split('/');

                    fs.writeFile(fileMediaPlayList, body, afterWrite);

                    function afterWrite(err)
                    {
                        if (err)
                        {
                            return asyncCb(err);
                        }

                        var media =
                        {
                            index: fileMediaPlayList
                        }

                        self.emit('progress', 'Created Media Playlist ' + fileMediaPlayList);

                        var mediaPlayListContents = body.split("\n");
                        async.eachSeries(mediaPlayListContents, handleSubContent, innerDone);

                        function handleSubContent(subpart, asyncCbInner)
                        {
                            if (!endsWith(subpart, "ts"))
                            {
                                return asyncCbInner();
                            }

                            var mediaSegmentFile = '';
                            if (subpart.indexOf("://") > -1)
                            {
                                mediaSegmentFile = subpart;
                            }
                            else
                            {
                                mediaSegmentFile = site + mediaResponse[2] + '/' + subpart;
                            }

                            request.get(mediaSegmentFile, { encoding: null }, handleResponse);

                            function handleResponse(error, response, body)
                            {
                                if (error || response.statusCode !== 200)
                                {
                                    return asyncCbInner(error || new Error('Bad status code: ' + response.statusCode));
                                }

                                var fileMediaSegment = destination + response.req.path;
                                fs.writeFile(fileMediaSegment, body, afterWrite);

                                function afterWrite(err)
                                {
                                    if (err)
                                    {
                                        asyncCbInner(err);
                                    }
                                    else
                                    {
                                        self.emit('downloaded', fileMediaSegment);

                                        if (media.segments)
                                        {
                                            media.segments.push(fileMediaSegment);
                                        }
                                        else
                                        {
                                            media.segments = [fileMediaSegment];
                                        }
                                        asyncCbInner();
                                    }
                                }
                            }
                        }

                        function innerDone(err)
                        {
                            if (err)
                            {
                                asyncCb(err);
                            }
                            else
                            {
                                out.media.push(media);
                                asyncCb();
                            }
                        }
                    }
                }
            }
        }
    }

    function done(err)
    {
        if (err)
        {
            self.emit('error', err);
            callback(err);
        }
        else
        {
            self.emit('complete', out);
            callback(null, out);
        }
    }
}
util.inherits(m3u8downloader, EventEmitter);
module.exports = m3u8downloader;
<commit_msg>added async and on complete<commit_after>// m3u8downloader.js
// =================

var async = require('async');
var fs = require('fs');
var util = require('util');
var request = require('request');
var EventEmitter = require('events').EventEmitter;

exports.version = "0.2.0";

var endsWith = function(str, suffix)
{
  return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

var m3u8downloader = function(masterPlayListURL, destination, callback)
{
    var self = this;
    var res = masterPlayListURL.split("/");
    var site = masterPlayListURL.replace(res[res.length - 1], '');
    request.get(masterPlayListURL, handleResponse);

    var out = {};

    function handleResponse(error, response, body)
    {
        if (error || response.statusCode !== 200)
        {
            return done(error || new Error('Bad status code: ' + response.statusCode));
        }

        self.emit('start');
        var masterResponse = response.req.path.split("/");

        try
        {
            fs.mkdirSync(destination + '/' + masterResponse[1]);
        }
        catch (err)
        {
            return done(err);
        }

        var fileMasterPlayList = destination + response.req.path;
        fs.writeFile(fileMasterPlayList, body, afterWrite);

        function afterWrite(err)
        {
            if (err)
            {
                return done(err);
            }

            self.emit('progress', 'Created Master Playlist ' + fileMasterPlayList);

            out.master = fileMasterPlayList;
            out.media = [];

            var masterPlayListContents = body.split("\n");
            async.eachSeries(masterPlayListContents, handleContent, done);

            function handleContent(part, asyncCb)
            {
                if (!endsWith(part, "m3u8"))
                {
                    return asyncCb();
                }

                var res2 = part.split("/");
                var mediaPlayListFile = '';
                try
                {
                    if (part.indexOf("://") > -1)
                    {
                        fs.mkdirSync(destination + "/" + masterResponse[1] + '/' + res2[res2.length - 2]);
                        mediaPlayListFile = part;
                    }
                    else
                    {
                        fs.mkdirSync(destination + "/" + masterResponse[1] + '/' + res2[0]);
                        mediaPlayListFile = site + part;
                    }
                }
                catch (err)
                {
                    return asyncCb(err);
                }

                request.get(mediaPlayListFile, handleResponse);

                function handleResponse(error, response, body)
                {
                    if (error || response.statusCode !== 200)
                    {
                        return asyncCb(error || new Error('Bad status code: ' + response.statusCode));
                    }

                    var fileMediaPlayList = destination + response.req.path;
                    var mediaResponse = response.req.path.split('/');

                    fs.writeFile(fileMediaPlayList, body, afterWrite);

                    function afterWrite(err)
                    {
                        if (err)
                        {
                            return asyncCb(err);
                        }

                        var media =
                        {
                            index: fileMediaPlayList
                        }

                        self.emit('progress', 'Created Media Playlist ' + fileMediaPlayList);

                        var mediaPlayListContents = body.split("\n");
                        async.eachSeries(mediaPlayListContents, handleSubContent, innerDone);

                        function handleSubContent(subpart, asyncCbInner)
                        {
                            if (!endsWith(subpart, "ts"))
                            {
                                return asyncCbInner();
                            }

                            var mediaSegmentFile = '';
                            if (subpart.indexOf("://") > -1)
                            {
                                mediaSegmentFile = subpart;
                            }
                            else
                            {
                                mediaSegmentFile = site + mediaResponse[2] + '/' + subpart;
                            }

                            request.get(mediaSegmentFile, { encoding: null }, handleResponse);

                            function handleResponse(error, response, body)
                            {
                                if (error || response.statusCode !== 200)
                                {
                                    return asyncCbInner(error || new Error('Bad status code: ' + response.statusCode));
                                }

                                var fileMediaSegment = destination + response.req.path;
                                fs.writeFile(fileMediaSegment, body, afterWrite);

                                function afterWrite(err)
                                {
                                    if (err)
                                    {
                                        asyncCbInner(err);
                                    }
                                    else
                                    {
                                        self.emit('downloaded', fileMediaSegment);

                                        if (media.segments)
                                        {
                                            media.segments.push(fileMediaSegment);
                                        }
                                        else
                                        {
                                            media.segments = [fileMediaSegment];
                                        }
                                        asyncCbInner();
                                    }
                                }
                            }
                        }

                        function innerDone(err)
                        {
                            if (err)
                            {
                                asyncCb(err);
                            }
                            else
                            {
                                out.media.push(media);
                                asyncCb();
                            }
                        }
                    }
                }
            }
        }
    }

    function done(err)
    {
        if (err)
        {
            self.emit('error', err);
            callback(err);
        }
        else
        {
            self.emit('complete', out);
            callback(null, out);
        }
    }
}
util.inherits(m3u8downloader, EventEmitter);
module.exports = m3u8downloader;
<|endoftext|>


<commit_before>'use strict';

const fs = require('fs');
const path = require('path');
const url = require('url');
const mapnik = require('@carto/mapnik');
const normalizeURI = require('./uri');
const createMetatileCache = require('./metatile-cache');
const createMapPool = require('./map-pool');
const timeoutDecorator = require('./utils/timeout-decorator');
const mime = require('mime');

mapnik.register_default_input_plugins();

function MapnikSource(uri, callback) {
    this.open = false;

    // cache used to skip encoding of solid images
    this.solidCache = {};

    this._uri = normalizeURI(uri);

    // Public API to announce how we're metatiling.
    this.metatile = this._uri.query.metatile;
    this.bufferSize = this._uri.query.bufferSize;

    if (this._uri.protocol && this._uri.protocol !== 'mapnik:') {
        return callback(new Error('Only the mapnik protocol is supported'));
    }

    this._stats = {
        render: 0,          // # of times a render is requested from mapnik
        total: 0,           // # of tiles returned from source
        encoded: 0,         // # of tiles encoded
        solid: 0,           // # of tiles isSolid
        solidPainted: 0     // # of tiles isSolid && painted
    };

    if (this._uri.query.limits && this._uri.query.limits.render > 0) {
        this.getTile = timeoutDecorator(this.getTile.bind(this), this._uri.query.limits.render);
        this.getGrid = timeoutDecorator(this.getGrid.bind(this), this._uri.query.limits.render);
    }

    this._open(callback);
}

MapnikSource.mapnik = mapnik;

// Finds all XML files in the filepath and returns their tilesource URI.
MapnikSource.list = function (filepath, callback) {
    filepath = path.resolve(filepath);
    fs.readdir(filepath, (err, files) => {
        if (err) {
            return callback(err);
        }

        const result = {};

        for (i = 0; i < files.length; i++) {
            const name = files[i].match(/^([\w-]+)\.xml$/);

            if (name) {
                result[name[1]] = 'mapnik://' + path.join(filepath, name[0]);
            }
        }

        return callback(null, result);
    });
};

// Finds an XML file with the given ID in the filepath and returns a
// tilesource URI.
MapnikSource.findID = function (filepath, id, callback) {
    filepath = path.resolve(filepath);
    const file = path.join(filepath, id + '.xml');

    fs.stat(file, (err, stats) => {
        if (err) {
            return callback(err);
        }

        return callback(null, 'mapnik://' + file);
    });
};

MapnikSource.registerProtocols = function (tilelive) {
    tilelive.protocols['mapnik:'] = MapnikSource;
};

MapnikSource.prototype.toJSON = function () {
    return url.format(this._uri);
};

MapnikSource.prototype.close = function (callback) {
    if (this._metatileCache) {
        this._metatileCache.clear();
    }

    if (this._mapPool) {
        return this._mapPool.drain()
            .then(() => {
                this._mapPool.clear();
                this.open = false;
                return callback();
            });
    }
};

// Render handler for a given tile request.
MapnikSource.prototype.getTile = function (z, x, y, callback) {
    z = +z;
    x = +x;
    y = +y;

    try {
        areValidCoords({ z, x, y });
    } catch (err) {
        return callback(err);
    }

    const format = (this._info && this._info.format) || 'png';
    const key = [format, z, x, y].join(',');
    this._metatileCache.get(key, (err, tile, headers, stats) => {
        if (err) {
            return callback(err);
        }

        return callback(null, tile, headers, stats);
    });
};

MapnikSource.prototype.getGrid = function (z, x, y, callback) {
    z = +z;
    x = +x;
    y = +y;

    try {
        areValidCoords({ z, x, y });
    } catch (err) {
        return callback(err);
    }

    if (!this._info) {
        return callback(new Error('Tilesource info is missing, cannot rendering interactivity'));
    }

    if (!this._info.interactivity_fields || !this._info.interactivity_layer) {
        return callback(new Error('Tileset has no interactivity'));
    }

    if (!mapnik.supports.grid) {
        return callback(new Error('Mapnik is missing grid support'));
    }

    const key = ['utf', z, x, y].join(',');
    this._metatileCache.get(key, (err, grid, headers, stats) => {
        if (err) {
            return callback(err);
        }

        delete grid.solid;

        return callback(null, grid, headers, stats);
    });
};

function areValidCoords ({ z, x, y }) {
    areCoordsNumbers({ z, x, y });
    areCoordsInRange({ z, x, y });
}

function areCoordsNumbers ({ z, x, y }) {
    if (isNaN(z) || isNaN(x) || isNaN(y)) {
        throw new Error('Invalid coordinates: '+z+'/'+x+'/'+y);
    }
}

function areCoordsInRange ({ z, x, y }) {
    const max = Math.pow(2, z);

    if (!isFinite(max) || x >= max || x < 0 || y >= max || y < 0) {
        throw new Error('Coordinates out of range: '+z+'/'+x+'/'+y);
    }
}

MapnikSource.prototype.getInfo = function(callback) {
    if (!this._info) {
        return callback(new Error('Info is unavailable'));
    }

    return callback(null, this._info);
};

MapnikSource.prototype._open = function (callback) {
    if (this.open) {
        return callback(null, this);
    }

    this._loadXML((err, xml) => {
        if (err) {
            return callback(err);
        }

        this._metatileCache = createMetatileCache(this, this._uri.query.tileSize, this._uri.query.metatile, this._uri.query.metatileCache);
        this._mapPool = createMapPool(this._uri, xml);

        this._populateInfo((err) => {
            if (err) {
                return callback(err);
            }

            this.open = true;

            return callback(null, this);
        });
    });
};

// Loads the XML file from the specified path. Calls `callback` when the mapfile
// can be expected to be in `mapfile`. If this isn't successful, `callback` gets
// an error as its first argument.
MapnikSource.prototype._loadXML = function (callback) {
    if (this._uri.xml) {
        return callback(null, this._uri.xml);
    }

    fs.readFile(path.resolve(this._uri.pathname), 'utf8', callback);
};

MapnikSource.prototype._populateInfo = function (callback) {
    const id = path.basename(this._uri.pathname, path.extname(this._uri.pathname));

    this._mapPool.acquire()
        .then((resource) => {
            if (!(resource instanceof mapnik.Map)) {
                const err = resource;
                this._mapPool.release(resource);

                throw err;
            }

            const map = resource;
            const info = { id: id, name: id, minzoom: 0, maxzoom: 22, autoscale: true };

            const p = map.parameters;
            for (const key in p) {
                info[key] = p[key];
            }

            if (p.bounds) {
                info.bounds = p.bounds.split(',').map(parseFloat);
            }

            if (p.center) {
                info.center = p.center.split(',').map(parseFloat);
            }

            if (p.minzoom) {
                info.minzoom = parseInt(p.minzoom, 10);
            }

            if (p.maxzoom) {
                info.maxzoom = parseInt(p.maxzoom, 10);
            }

            if (p.interactivity_fields) {
                info.interactivity_fields = p.interactivity_fields.split(',');
            }

            if (!info.bounds || info.bounds.length !== 4) {
                info.bounds = [ -180, -85.05112877980659, 180, 85.05112877980659 ];
            }

            if (!info.center || info.center.length !== 3) {
                info.center = [
                    (info.bounds[2] - info.bounds[0]) / 2 + info.bounds[0],
                    (info.bounds[3] - info.bounds[1]) / 2 + info.bounds[1],
                    2
                ];
            }

            this._info = info;
            this._mapPool.release(map);

            return callback(null);
        })
        .catch((err) => {
            return callback(err);
        });
};

// Render png/jpg/tif image or a utf grid and return an encoded buffer
MapnikSource.prototype._renderMetatile = function (format, z, x, y, metatile, callback) {
    this._mapPool.acquire()
        .then((resource) => {
            if (!(resource instanceof mapnik.Map)) {
                const err = resource;
                this._mapPool.release(resource)

                throw err;
            }

            const map = resource;

            try {
                const options = {
                    tileSize: this._uri.query.tileSize,
                    buffer_size: this._uri.query.bufferSize,
                    format: format,
                    z: z,
                    x: metatile.x,
                    y: metatile.y,
                    metrics: this._uri.query.metrics,
                    variables: this._uri.query.variables || {},
                    scale: this._uri.query.scale
                };

                // Set x, y, z based on the metatile boundary
                options.variables.zoom = options.z;

                // Set default options.
                if (options.format === 'utf') {
                    options.layer = map.parameters.interactivity_layer;
                    options.fields = map.parameters.interactivity_fields.split(',');
                    options.resolution = this._uri.query.resolution;
                }

                const renderStartTime = Date.now();

                this._stats.render++;

                const image = new mapnik[format === 'utf' ? 'Grid' : 'Image'](metatile.width, metatile.height);

                image.metrics_enabled = options.metrics;

                map.resize(metatile.width, metatile.height);
                map.extent = metatile.bbox;

                map.render(image, options, (err, image) => {
                    this._mapPool.release(map);

                    if (err) {
                        return callback(err);
                    }

                    const renderStats = {};
                    renderStats.render = Math.round((Date.now() - renderStartTime) / metatile.tiles.length);

                    return this._sliceMetatile(image, options, metatile, renderStats, callback);
                });
            } catch(err) {
                this._mapPool.release(map);
                return callback(err);
            }
        })
        .catch((err) => {
            return callback(err);
        });
};

MapnikSource.prototype._sliceMetatile = function (source_image, options, meta, stats, callback) {
    if (meta.tiles.length === 0) {
        return callback(null, {});
    }

    // TODO: handle async in a standard way
    const tiles = {};
    const err_num = 0;
    let tile_num = 0;

    meta.tiles.forEach((coords) => {
        const key = [ options.format, coords[0], coords[1], coords[2] ].join(',');
        const encodeStartTime = Date.now();
        const x = (coords[1] - meta.x) * options.tileSize;
        const y = (coords[2] - meta.y) * options.tileSize;

        this._getImage(source_image, options, x, y, (err, image) => {
            tile_num++;
            if (err) {
                if (!err_num) {
                    return callback(err);
                }

                err_num++;

                return;
            }

            tiles[key] = {
                image: image,
                headers: this._getHeaders(options.format),
                stats: Object.assign(stats, { encode: Date.now() - encodeStartTime }, source_image.get_metrics())
            };

            if (tile_num === meta.tiles.length) {
                return callback(null, tiles);
            }
        });
    });
};

MapnikSource.prototype._getHeaders = function (format) {
    const headers = {};

    if (format === 'utf') {
        headers['Content-Type'] = 'application/json';
        return headers;
    }

    // NOTE: formats use mapnik syntax like `png8:m=h` or `jpeg80`
    // so we need custom handling for png/jpeg
    if (format.indexOf('png') !== -1) {
        headers['Content-Type'] = 'image/png';
        return headers;
    }

    if (format.indexOf('jpeg') !== -1 || format.indexOf('jpg') !== -1) {
        headers['Content-Type'] = 'image/jpeg';
        return headers;
    }

    // will default to 'application/octet-stream' if unable to detect
    headers['Content-Type'] = mime.getType(options.format.split(':')[0]);
    return headers;
};

MapnikSource.prototype._getImage = function (image, options, x, y, callback) {
    const view = image.view(x, y, options.tileSize, options.tileSize);

    view.isSolid((err, solid, pixel) => {
        if (err) {
            return callback(err);
        }

        let pixel_key = '';
        if (solid) {
            if (options.format === 'utf') {
                // TODO https://github.com/mapbox/tilelive-mapnik/issues/56
                pixel_key = pixel.toString();
            } else {
                // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators
                const a = (pixel>>>24) & 0xff;
                const r = pixel & 0xff;
                const g = (pixel>>>8) & 0xff;
                const b = (pixel>>>16) & 0xff;
                pixel_key = options.format + r +','+ g + ',' + b + ',' + a;
            }
        }

        // Add stats.
        this._stats.total++;

        if (solid !== false) {
            this._stats.solid++;
        }

        if (solid !== false && image.painted()) {
            this._stats.solidPainted++;
        }

        // If solid and image buffer is cached skip image encoding.
        if (solid && this.solidCache[pixel_key]) {
            return callback(null, this.solidCache[pixel_key]);
        }

        this._stats.encoded++;

        try {
            const params = options.format === 'utf' ? [ options ] : [ options.format, options ];

            view.encode(...params, (err, buffer) => {
                if (err) {
                    return callback(err);
                }

                if (solid !== false) {
                    // @TODO for 'utf' this attaches an extra, bogus 'solid' key to
                    // to the grid as it is not a buffer but an actual JS object.
                    // Fix is to propagate a third parameter through callbacks all
                    // the way back to tilelive source #getGrid.
                    buffer.solid = pixel_key;

                    if (options.format !== 'utf') {
                        this.solidCache[pixel_key] = buffer;
                    }
                }

                return callback(null, buffer);
            });
        } catch (err) {
            return callback(err);
        }
    });
}

module.exports = MapnikSource;
<commit_msg>Rename param<commit_after>'use strict';

const fs = require('fs');
const path = require('path');
const url = require('url');
const mapnik = require('@carto/mapnik');
const normalizeURI = require('./uri');
const createMetatileCache = require('./metatile-cache');
const createMapPool = require('./map-pool');
const timeoutDecorator = require('./utils/timeout-decorator');
const mime = require('mime');

mapnik.register_default_input_plugins();

function MapnikSource(uri, callback) {
    this.open = false;

    // cache used to skip encoding of solid images
    this.solidCache = {};

    this._uri = normalizeURI(uri);

    // Public API to announce how we're metatiling.
    this.metatile = this._uri.query.metatile;
    this.bufferSize = this._uri.query.bufferSize;

    if (this._uri.protocol && this._uri.protocol !== 'mapnik:') {
        return callback(new Error('Only the mapnik protocol is supported'));
    }

    this._stats = {
        render: 0,          // # of times a render is requested from mapnik
        total: 0,           // # of tiles returned from source
        encoded: 0,         // # of tiles encoded
        solid: 0,           // # of tiles isSolid
        solidPainted: 0     // # of tiles isSolid && painted
    };

    if (this._uri.query.limits && this._uri.query.limits.render > 0) {
        this.getTile = timeoutDecorator(this.getTile.bind(this), this._uri.query.limits.render);
        this.getGrid = timeoutDecorator(this.getGrid.bind(this), this._uri.query.limits.render);
    }

    this._open(callback);
}

MapnikSource.mapnik = mapnik;

// Finds all XML files in the filepath and returns their tilesource URI.
MapnikSource.list = function (filepath, callback) {
    filepath = path.resolve(filepath);
    fs.readdir(filepath, (err, files) => {
        if (err) {
            return callback(err);
        }

        const result = {};

        for (i = 0; i < files.length; i++) {
            const name = files[i].match(/^([\w-]+)\.xml$/);

            if (name) {
                result[name[1]] = 'mapnik://' + path.join(filepath, name[0]);
            }
        }

        return callback(null, result);
    });
};

// Finds an XML file with the given ID in the filepath and returns a
// tilesource URI.
MapnikSource.findID = function (filepath, id, callback) {
    filepath = path.resolve(filepath);
    const file = path.join(filepath, id + '.xml');

    fs.stat(file, (err, stats) => {
        if (err) {
            return callback(err);
        }

        return callback(null, 'mapnik://' + file);
    });
};

MapnikSource.registerProtocols = function (tilelive) {
    tilelive.protocols['mapnik:'] = MapnikSource;
};

MapnikSource.prototype.toJSON = function () {
    return url.format(this._uri);
};

MapnikSource.prototype.close = function (callback) {
    if (this._metatileCache) {
        this._metatileCache.clear();
    }

    if (this._mapPool) {
        return this._mapPool.drain()
            .then(() => {
                this._mapPool.clear();
                this.open = false;
                return callback();
            });
    }
};

// Render handler for a given tile request.
MapnikSource.prototype.getTile = function (z, x, y, callback) {
    z = +z;
    x = +x;
    y = +y;

    try {
        areValidCoords({ z, x, y });
    } catch (err) {
        return callback(err);
    }

    const format = (this._info && this._info.format) || 'png';
    const key = [format, z, x, y].join(',');
    this._metatileCache.get(key, (err, tile, headers, stats) => {
        if (err) {
            return callback(err);
        }

        return callback(null, tile, headers, stats);
    });
};

MapnikSource.prototype.getGrid = function (z, x, y, callback) {
    z = +z;
    x = +x;
    y = +y;

    try {
        areValidCoords({ z, x, y });
    } catch (err) {
        return callback(err);
    }

    if (!this._info) {
        return callback(new Error('Tilesource info is missing, cannot rendering interactivity'));
    }

    if (!this._info.interactivity_fields || !this._info.interactivity_layer) {
        return callback(new Error('Tileset has no interactivity'));
    }

    if (!mapnik.supports.grid) {
        return callback(new Error('Mapnik is missing grid support'));
    }

    const key = ['utf', z, x, y].join(',');
    this._metatileCache.get(key, (err, grid, headers, stats) => {
        if (err) {
            return callback(err);
        }

        delete grid.solid;

        return callback(null, grid, headers, stats);
    });
};

function areValidCoords ({ z, x, y }) {
    areCoordsNumbers({ z, x, y });
    areCoordsInRange({ z, x, y });
}

function areCoordsNumbers ({ z, x, y }) {
    if (isNaN(z) || isNaN(x) || isNaN(y)) {
        throw new Error('Invalid coordinates: '+z+'/'+x+'/'+y);
    }
}

function areCoordsInRange ({ z, x, y }) {
    const max = Math.pow(2, z);

    if (!isFinite(max) || x >= max || x < 0 || y >= max || y < 0) {
        throw new Error('Coordinates out of range: '+z+'/'+x+'/'+y);
    }
}

MapnikSource.prototype.getInfo = function(callback) {
    if (!this._info) {
        return callback(new Error('Info is unavailable'));
    }

    return callback(null, this._info);
};

MapnikSource.prototype._open = function (callback) {
    if (this.open) {
        return callback(null, this);
    }

    this._loadXML((err, xml) => {
        if (err) {
            return callback(err);
        }

        this._metatileCache = createMetatileCache(this, this._uri.query.tileSize, this._uri.query.metatile, this._uri.query.metatileCache);
        this._mapPool = createMapPool(this._uri, xml);

        this._populateInfo((err) => {
            if (err) {
                return callback(err);
            }

            this.open = true;

            return callback(null, this);
        });
    });
};

// Loads the XML file from the specified path. Calls `callback` when the mapfile
// can be expected to be in `mapfile`. If this isn't successful, `callback` gets
// an error as its first argument.
MapnikSource.prototype._loadXML = function (callback) {
    if (this._uri.xml) {
        return callback(null, this._uri.xml);
    }

    fs.readFile(path.resolve(this._uri.pathname), 'utf8', callback);
};

MapnikSource.prototype._populateInfo = function (callback) {
    const id = path.basename(this._uri.pathname, path.extname(this._uri.pathname));

    this._mapPool.acquire()
        .then((resource) => {
            if (!(resource instanceof mapnik.Map)) {
                const err = resource;
                this._mapPool.release(resource);

                throw err;
            }

            const map = resource;
            const info = { id: id, name: id, minzoom: 0, maxzoom: 22, autoscale: true };

            const p = map.parameters;
            for (const key in p) {
                info[key] = p[key];
            }

            if (p.bounds) {
                info.bounds = p.bounds.split(',').map(parseFloat);
            }

            if (p.center) {
                info.center = p.center.split(',').map(parseFloat);
            }

            if (p.minzoom) {
                info.minzoom = parseInt(p.minzoom, 10);
            }

            if (p.maxzoom) {
                info.maxzoom = parseInt(p.maxzoom, 10);
            }

            if (p.interactivity_fields) {
                info.interactivity_fields = p.interactivity_fields.split(',');
            }

            if (!info.bounds || info.bounds.length !== 4) {
                info.bounds = [ -180, -85.05112877980659, 180, 85.05112877980659 ];
            }

            if (!info.center || info.center.length !== 3) {
                info.center = [
                    (info.bounds[2] - info.bounds[0]) / 2 + info.bounds[0],
                    (info.bounds[3] - info.bounds[1]) / 2 + info.bounds[1],
                    2
                ];
            }

            this._info = info;
            this._mapPool.release(map);

            return callback(null);
        })
        .catch((err) => {
            return callback(err);
        });
};

// Render png/jpg/tif image or a utf grid and return an encoded buffer
MapnikSource.prototype._renderMetatile = function (format, z, x, y, metatile, callback) {
    this._mapPool.acquire()
        .then((resource) => {
            if (!(resource instanceof mapnik.Map)) {
                const err = resource;
                this._mapPool.release(resource)

                throw err;
            }

            const map = resource;

            try {
                const options = {
                    tileSize: this._uri.query.tileSize,
                    buffer_size: this._uri.query.bufferSize,
                    format: format,
                    z: z,
                    x: metatile.x,
                    y: metatile.y,
                    metrics: this._uri.query.metrics,
                    variables: this._uri.query.variables || {},
                    scale: this._uri.query.scale
                };

                // Set x, y, z based on the metatile boundary
                options.variables.zoom = options.z;

                // Set default options.
                if (options.format === 'utf') {
                    options.layer = map.parameters.interactivity_layer;
                    options.fields = map.parameters.interactivity_fields.split(',');
                    options.resolution = this._uri.query.resolution;
                }

                const renderStartTime = Date.now();

                this._stats.render++;

                const image = new mapnik[format === 'utf' ? 'Grid' : 'Image'](metatile.width, metatile.height);

                image.metrics_enabled = options.metrics;

                map.resize(metatile.width, metatile.height);
                map.extent = metatile.bbox;

                map.render(image, options, (err, image) => {
                    this._mapPool.release(map);

                    if (err) {
                        return callback(err);
                    }

                    const renderStats = {};
                    renderStats.render = Math.round((Date.now() - renderStartTime) / metatile.tiles.length);

                    return this._sliceMetatile(image, options, metatile, renderStats, callback);
                });
            } catch(err) {
                this._mapPool.release(map);
                return callback(err);
            }
        })
        .catch((err) => {
            return callback(err);
        });
};

MapnikSource.prototype._sliceMetatile = function (source_image, options, metatile, stats, callback) {
    if (metatile.tiles.length === 0) {
        return callback(null, {});
    }

    // TODO: handle async in a standard way
    const tiles = {};
    const err_num = 0;
    let tile_num = 0;

    metatile.tiles.forEach((coords) => {
        const key = [ options.format, coords[0], coords[1], coords[2] ].join(',');
        const encodeStartTime = Date.now();
        const x = (coords[1] - metatile.x) * options.tileSize;
        const y = (coords[2] - metatile.y) * options.tileSize;

        this._getImage(source_image, options, x, y, (err, image) => {
            tile_num++;
            if (err) {
                if (!err_num) {
                    return callback(err);
                }

                err_num++;

                return;
            }

            tiles[key] = {
                image: image,
                headers: this._getHeaders(options.format),
                stats: Object.assign(stats, { encode: Date.now() - encodeStartTime }, source_image.get_metrics())
            };

            if (tile_num === metatile.tiles.length) {
                return callback(null, tiles);
            }
        });
    });
};

MapnikSource.prototype._getHeaders = function (format) {
    const headers = {};

    if (format === 'utf') {
        headers['Content-Type'] = 'application/json';
        return headers;
    }

    // NOTE: formats use mapnik syntax like `png8:m=h` or `jpeg80`
    // so we need custom handling for png/jpeg
    if (format.indexOf('png') !== -1) {
        headers['Content-Type'] = 'image/png';
        return headers;
    }

    if (format.indexOf('jpeg') !== -1 || format.indexOf('jpg') !== -1) {
        headers['Content-Type'] = 'image/jpeg';
        return headers;
    }

    // will default to 'application/octet-stream' if unable to detect
    headers['Content-Type'] = mime.getType(options.format.split(':')[0]);
    return headers;
};

MapnikSource.prototype._getImage = function (image, options, x, y, callback) {
    const view = image.view(x, y, options.tileSize, options.tileSize);

    view.isSolid((err, solid, pixel) => {
        if (err) {
            return callback(err);
        }

        let pixel_key = '';
        if (solid) {
            if (options.format === 'utf') {
                // TODO https://github.com/mapbox/tilelive-mapnik/issues/56
                pixel_key = pixel.toString();
            } else {
                // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators
                const a = (pixel>>>24) & 0xff;
                const r = pixel & 0xff;
                const g = (pixel>>>8) & 0xff;
                const b = (pixel>>>16) & 0xff;
                pixel_key = options.format + r +','+ g + ',' + b + ',' + a;
            }
        }

        // Add stats.
        this._stats.total++;

        if (solid !== false) {
            this._stats.solid++;
        }

        if (solid !== false && image.painted()) {
            this._stats.solidPainted++;
        }

        // If solid and image buffer is cached skip image encoding.
        if (solid && this.solidCache[pixel_key]) {
            return callback(null, this.solidCache[pixel_key]);
        }

        this._stats.encoded++;

        try {
            const params = options.format === 'utf' ? [ options ] : [ options.format, options ];

            view.encode(...params, (err, buffer) => {
                if (err) {
                    return callback(err);
                }

                if (solid !== false) {
                    // @TODO for 'utf' this attaches an extra, bogus 'solid' key to
                    // to the grid as it is not a buffer but an actual JS object.
                    // Fix is to propagate a third parameter through callbacks all
                    // the way back to tilelive source #getGrid.
                    buffer.solid = pixel_key;

                    if (options.format !== 'utf') {
                        this.solidCache[pixel_key] = buffer;
                    }
                }

                return callback(null, buffer);
            });
        } catch (err) {
            return callback(err);
        }
    });
}

module.exports = MapnikSource;
<|endoftext|>


<commit_before>{ "symbolizers" :
  {
    "map": {
      "map-bgcolor": {
        "type": "color_transparent",
        "description": "Map Background color"
      }
    },
    "polygon": {
      "fill": {
        "css": "polygon-fill",
        "api": "fill",
        "type": "color",
        "availability": "0.5.1",
        "default": "rgb(128,128,128)",
        "default-meaning":"grey",
        "doc": "Fill color to assign to a polygon"
      },
      "gamma": {
        "css": "polygon-gamma",
        "api": "gamma",
        "type": "float",
        "availability": "0.7.0",
        "default": 1,
        "default-meaning":"fully antialiased",
        "range":"0-1",
        "doc": "Level of antialiasing of polygon edges"
      },
      "opacity": {
        "css": "polygon-opacity",
        "type": "float",
        "default": 1,
        "default-meaning": "opaque"
      },
      "meta-output": {
        "css": "polygon-meta-output",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "polygon-meta-writer",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter specified"
      }
    },
    "line": {
      "color": {
        "css": "line-color",
        "default": "black",
        "type": "color",
        "doc": "The color of a drawn line"
      },
      "width": {
        "css": "line-width",
        "default": 1,
        "type": "float",
        "doc": "The width of a line"
      },
      "opacity": {
        "css": "line-opacity",
        "default": 1
        "type": "float",
        "doc": "The opacity of a line"
      },
      "join": {
        "css": "line-join",
        "default": "miter",
        "type": ["miter", "round", "bevel"],
        "doc": "The behavior of lines when joining"
      },
      "cap": {
        "css": "line-cap",
        "default": "butt",
        "type": ["butt", "round", "square"],
        "doc": "The display of line endings."
      },
      "dasharray": {
        "css": "line-dasharray",
        "type": "numbers",
        "doc": "A pair of length values [a,b], where (a) is the dash length and (b) is the gap length respectively. More than two values are supported as well (e.g. to start the line not with a stroke, but with a gap).",
        "default": "none"
      },
      "meta-output": {
        "css": "line-meta-output",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "line-meta-writer",
        "type": "string",
        "default": ""
      }
    },
    "marker": {
      "line-color": {
        "css": "marker-line-color",
        "type": "color"
      },
      "line-width": {
        "css": "marker-line-width",
        "type": "float"
      },
      "line-opacity": {
        "css": "marker-line-opacity",
        "default": 1,
        "type": "float"
      },
      "placement": {
        "css": "marker-placement",
        "type": ["point", "line"]
      },
      "type": {
        "css": "marker-type",
        "type": ["arrow", "ellipse"]
      },
      "width": {
        "css": "marker-width",
        "type": "int"
      },
      "height": {
        "css": "marker-height",
        "type": "int"
      },
      "fill": {
        "css": "marker-fill",
        "type": "color"
      },
      "fill-opacity": {
        "css": "marker-opacity",
        "default": 1,
        "type": "float"
      },
      "file": {
        "css": "marker-file",
        "type": "uri"
      },
      "allow-overlap": {
        "css": "marker-allow-overlap",
        "type": "boolean"
      },
      "spacing": {
        "css": "marker-spacing",
        "docs": "Space between repeated labels",
        "type": "float"
      },
      "max-error": {
        "css": "marker-max-error",
        "type": "float"
      },
      "transform": {
        "css": "marker-transform",
        "type": "string"
      },
      "meta-output": {
        "css": "marker-meta-output",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "marker-meta-writer",
        "type": "string",
        "default": "none"
      }
    },
    "shield": {
      "name": {
        "css": "shield-name",
        "type":  "none"
      },
      "face-name": {
        "css": "shield-name",
        "type":  "string"
      },
      "size": {
        "css": "shield-size",
        "type":  "int"
      },
      "fill": {
        "css": "shield-fill",
        "type":  "color"
      },
      "min-distance": {
        "css": "shield-min-distance",
        "type":  "int",
        "default": 0,
        "doc": "Minimum distance to the next shield symbol, not necessarily the same shield."
      },
      "spacing": {
        "css": "shield-spacing",
        "type":  "int",
        "default": 0,
        "doc": "The spacing between repeated occurrences of the same shield"
      },
      "character-spacing": {
        "css": "shield-spacing",
        "type":  "int"
        "default": 0,
        "doc": "Horizontal spacing between characters (in pixels). Currently works for point placement only, not line placement."
      },
      "line-spacing": {
        "css": "shield-line-spacing",
        "doc": "Vertical spacing between lines of multiline labels (in pixels)",
        "type":  "int"
      },
      "file": {
        "css": "shield-file",
        "type":  "uri"
      },
      "width": {
        "css": "shield-width",
        "type":  "int",
        "default": "The image's width"
      },
      "height": {
        "css": "shield-height",
        "type":  "int",
        "default": "The image's height"
      },
      "type": {
        "css": "shield-type",
        "type":  "None",
        "default": "The type of the image file: e.g. png"
      },
      "meta-output": {
        "css": "shield-meta-output",
        "type":  "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "shield-meta-writer",
        "type":  "string",
        "default": ""
      }
    },
    "line-pattern": {
      "file": {
        "css": "line-pattern-file",
        "type":  "uri"
      },
      "width": {
        "css": "line-pattern-width",
        "type":  "int"
      },
      "height": {
        "css": "line-pattern-height",
        "type":  "int"
      },
      "type": {
        "css": "line-pattern-type",
        "type":  "none"
      },
      "meta-output": {
        "css": "line-pattern-meta-output",
        "type":  "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "line-pattern-meta-writer",
        "type":  "string",
        "default": ""
      }
    },
    "polygon-pattern": {
      "file": {
        "css": "polygon-pattern-file",
        "type": "uri"
      },
      "width": {
        "css": "polygon-pattern-width",
        "type": "int"
      },
      "height": {
        "css": "polygon-pattern-height",
        "type": "int"
      },
      "type": {
        "css": "polygon-pattern-type",
        "type": "none"
      },
      "meta-output": {
        "css": "polygon-pattern-meta-output",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "polygon-pattern-meta-writer",
        "type": "string",
        "default": ""
      }
    },
    "raster": {
      "opacity": {
        "css": "raster-opacity",
        "default": 1,
        "type": "float"
      },
      "mode": {
        "css": "raster-mode",
        "default": "normal",
        "type":
          ["normal","grain_merge", "grain_merge2",
          "multiply", "multiply2", "divide", "divide2",
          "screen", "hard_light"]
      },
      "scaling": {
        "css": "raster-scaling",
        "type": ["fast", "bilinear", "bilinear8"]
      }
    },
    "point": {
      "file": {
        "css": "point-file",
        "type": "uri"
      },
      "width": {
        "css": "point-width",
        "type": "int"
      },
      "height": {
        "css": "point-height",
        "type": "int"
      },
      "type": {
        "css": "point-type",
        "type": "none"
      },
      "allow-overlap": {
        "css": "point-allow-overlap",
        "type": "boolean"
      },
      "meta-output": {
        "css": "point-meta-output",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "point-meta-writer",
        "type": "string",
        "default": ""
      }
    },
    "text": {
      "face-name": {
        "css": "text-face-name",
        "type": "string"
      },
      "size": {
        "css": "text-size",
        "type": "integer"
      },
      "ratio": {
        "css": "text-ratio",
        "type": "integer"
        "type": "none"
      },
      "wrap-width": {
        "css": "text-wrap-width",
        "doc": "Length of a chunk of text in characters before wrapping text",
        "type": "integer"
      },
      "spacing": {
        "css": "text-spacing",
        "type": "integer"
      },
      "character-spacing": {
        "css": "text-character-spacing",
        "type": "integer"
        "default": 0
      },
      "line-spacing": {
        "css": "text-line-spacing",
        "type": "integer"
      },
      "label-position-tolerance": {
        "css": "text-label-position-tolerance",
        "type": "integer"
      },
      "max-char-angle-delta": {
        "css": "text-max-char-angle-delta",
        "type": "integer"
      },
      "fill": {
        "css": "text-fill",
        "type": "color"
      },
      "halo-fill": {
        "css": "text-halo-fill",
        "docs": "Color of the text halo",
        "type": "color"
      },
      "halo-radius": {
        "css": "text-halo-radius",
        "type": "integer"
      },
      "dx": {
        "css": "text-dx",
        "type": "integer"
      },
      "dy": {
        "css": "text-dy",
        "type": "integer"
      },
      "avoid-edges": {
        "css": "text-avoid-edges",
        "doc": "Tell positioning algorithm to avoid labeling near intersection edges."
        "type": "boolean"
      },
      "min-distance": {
        "css": "text-min-distance",
        "type": "integer"
      },
      "allow-overlap": {
        "css": "text-allow-overlap",
        "type": "boolean"
      },
      "placement": {
        "css": "text-placement",
        "type": ["point", "line"]
      },
      "transform": {
        "css": "text-transform",
        "type": ["point", "line"]
        "type": ["uppercase", "lowercase"]
      },
      "meta-output": {
        "css": "text-meta-output",
        "type": "string",
        "default": "",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "text-meta-writer",
        "type": "string",
        "default": ""
      }
    },
    "inline": {
      "color": {
        "css": "inline-color",
        "default": "black",
        "type": "color"
      },
      "width": {
        "css": "inline-width",
        "type": "float"
      },
      "opacity": {
        "css": "inline-opacity",
        "default": 1,
        "type": "float"
      },
      "join": {
        "css": "inline-join",
        "default": "miter",
        "type": ["miter", "round", "bevel"]
      },
      "cap": {
        "css": "inline-cap",
        "default": "butt",
        "type": ["butt", "round", "square"]
      },
      "dasharray": {
        "css": "inline-dasharray",
        "type": "numbers"
      },
      "meta-output": {
        "css": "inline-meta-output",
        "type": "string",
        "default": "none",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "inline-meta-writer",
        "type": "string",
        "default": "none"
      }
    },
    "outline": {
      "color": {
        "css": "outline-color",
        "default": "black",
        "type": "color"
      },
      "width": {
        "css": "outline-width",
        "default": 1,
        "type": "float"
      },
      "opacity": {
        "css": "outline-opacity",
        "default": 1,
        "type": "float"
      },
      "join": {
        "css": "outline-join",
        "default": "miter",
        "type": ["miter", "round", "bevel"]
      },
      "cap": {
        "css": "outline-cap",
        "default": "butt",
        "type": ["butt", "round", "square"]
      },
      "dasharray": {
        "css": "outline-dasharray",
        "type": "numbers"
      },
      "meta-output": {
        "css": "outline-meta-output",
        "type": "string",
        "default": "none",
        "default-meaning": "No MetaWriter Output"
      },
      "meta-writer": {
        "css": "outline-meta-writer",
        "type": "string",
        "default": "none"
      }
    }
  }
};
<commit_msg>Removing stale reference file<commit_after><|endoftext|>


<commit_before>const getLastMessage = require('../modules/getlastmessage')

module.exports = (self) => {
  return (req, res) => {
    return new Promise((resolve, reject) => {
      const data = req.body.object

      switch (req.body.type) {
        case 'confirmation':
          res.end(self.settings.confirmation)
          break

        case 'message_new':
          data.body = getLastMessage(data).body

          if (self.settings.sub) {
            self.members.push(data)
          } else {
            self.handler(data)
          }

          break

        default:
          if (typeof self.action[req.body.type] === 'function') {
            return self.action[req.body.type](data)
          }
      }

      setInterval(() => {
        self.method.follow().then(data => {
          if (data.followers.length) {
            data.followers.forEach(ctx => self.handler(ctx.data))
          }

          if (data.users.length) {
            data.users.forEach(ctx => self.reply(ctx.data.user_id, self.settings.sub))
          }
        })

        self.method.execute().then(body => {
          console.log(JSON.stringify(body, null, 2))
        }).catch(console.log)
      }, 350)

      resolve(req.body)
      res.end('ok')
    })
  }
}
<commit_msg>fix bugs<commit_after>const getLastMessage = require('../modules/getlastmessage')

module.exports = (self) => {
  return (req, res) => {
    return new Promise((resolve, reject) => {
      const data = req.body.object

      switch (req.body.type) {
        case 'confirmation':
          res.end(self.settings.confirmation)
          break

        case 'message_new':
          data.body = getLastMessage(data).body

          if (self.settings.sub) {
            self.members.push(data)
          } else {
            self.handler(data)
          }

          break

        default:
          if (typeof self.action[req.body.type] === 'function') {
            return self.action[req.body.type](data)
          }
      }

      setInterval(() => {
        self.method.follow().then(data => {
          if (data.followers.length) {
            data.followers.forEach(ctx => self.handler(ctx))
          }

          if (data.viewers.length) {
            data.viewers.forEach(ctx => self.reply(ctx.user_id, self.settings.sub))
          }
        })

        self.method.execute().then(body => {
          console.log(JSON.stringify(body, null, 2))
        }).catch(console.log)
      }, 350)

      resolve(req.body)
      res.end('ok')
    })
  }
}
<|endoftext|>


<commit_before>/** internal
 *  class Context
 *
 *  `Context` provides helper methods to all [[Template]] processors.
 *  They are typically accessed by EJS templates. You can mix in custom
 *  helpers by injecting them into [[Environment#ContextClass]]. Do not
 *  mix them into `Context` directly.
 *
 *      environment.registerHelper('asset_url', function () {
 *        // ...
 *      });
 *
 *      // or in batch-mode
 *      environment.registerHelper({
 *        asset_url: function () {
 *          // ...
 *        },
 *        // ...
 *      });
 *
 *      <%= asset_url("foo.png") %>
 *
 *  The `Context` also collects dependencies declared by assets.
 *  See [[DirectiveProcessor]] for an example of this.
 **/

'use strict';

// stdlib
var fs        = require('fs');
var path      = require('path');
var inherits  = require('util').inherits;

// 3rd-party
var _ = require('underscore');
var async = require('async');

// internal
var getter = require('./common').getter;
var prop   = require('./common').prop;

////////////////////////////////////////////////////////////////////////////////

/**
 *  new Context(environment, logicalPath, pathname)
 *  - environment (Environment)
 *  - logicalPath (String)
 *  - pathname (String)
 **/
var Context = module.exports = function Context(environment, logicalPath, pathname) {
  prop(this, 'environment',           environment);
  prop(this, 'pathname',              pathname);

  prop(this, '__logicalPath__',       logicalPath);

  prop(this, '__requiredPaths__',     []);
  prop(this, '__stubbedAssets__',     []);
  prop(this, '__dependencyPaths__',   []);
  prop(this, '__dependencyAssets__',  [pathname]);
};

// Hash of helpers available as locals in the renderers.
// For each helper, a wrpper will be generated to preserve `this` context.
//
// See Context.registerHelper()
prop(Context.prototype, '__helpers__', {
  // note we'll need to backpatch `func` later
  asset_data_uri:   {name: 'asset_data_uri', func: Context.prototype.assetDataUri},
  asset_path:       {name: 'asset_path', func: Context.prototype.assetPath}
});

/**
 *  Context#assetPath(pathname, options) -> String
 *
 *  Helper that returns path to asset and that acts like a "base" method for
 *  other `*Path` helpers.
 *
 *  Exposed to the engines as `asset_path` helper.
 *
 *  By default this method is not implemented, and you must provide it's
 *  implementation that will fit your needs, e.g.:
 *
 *      environment.ContextClass.prototype.assetPath = function (pathname, options) {
 *        var asset = this.environment.findAsset(pathname, options);
 *        return '/assets/' + asset.digestPath;
 *      };
 *
 *  Once implemented, you can use `asset_path` helper, e.g.:
 *
 *      #logo { background: url(<%= asset_path('logo.png') %>) }
 **/
Context.prototype.assetPath = function assetPath() {
  throw new Error(
    "Custom asset_path helper is not implemented\n\n" +
    "Extend your environment context with a custom method.\n\n" +
    "  environment.ContextClass.prototype.assetPath = function (pathname, options) {\n" +
    "    // ... your code here ... \n" +
    "  };"
  );
};
Context.prototype.__helpers__['asset_path'].func = Context.prototype.assetPath;

/**
 *  Context#imagePath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for image assets.
 *  Exposed to the engines as `image_path` helper.
 **/

/**
 *  Context#videoPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for video assets.
 *  Exposed to the engines as `video_path` helper.
 **/

/**
 *  Context#audioPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for audio assets.
 *  Exposed to the engines as `audio_path` helper.
 **/

/**
 *  Context#fontPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for font assets.
 *  Exposed to the engines as `font_path` helper.
 **/

/**
 *  Context#javascriptPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for javascript assets.
 *  Exposed to the engines as `javascript_path` helper.
 **/

/**
 *  Context#stylesheetPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for stylesheet assets.
 *  Exposed to the engines as `stylesheet_path` helper.
 **/

['image', 'video', 'audio', 'font', 'javascript', 'stylesheet'].forEach(function (assetType) {
  var func = function (pathname) {
    return this.assetPath(pathname, {type: assetType});
  };
  //Context.registerHelper(assetType + '_path', func);
  var helper = {
    name: assetType + '_path',
    func: func
  };
  Context.prototype.__helpers__[assetType + '_path'] = helper;
  Context.prototype[assetType + 'Path'] = func;
});

/**
 *  Context#rootPath -> String
 *
 *  Returns the environment path that contains the file.
 *
 *  If `app/javascripts` and `app/stylesheets` are in your path, and
 *  current file is `app/javascripts/foo/bar.js`, `root_path` would
 *  return `app/javascripts`.
 **/
getter(Context.prototype, 'rootPath', function () {
  var pathname = this.pathname;
  return _.find(this.environment.paths, function (path) {
    return pathname.substr(0, path.length) === path;
  }).substr(this.environment.root.length + 1);
});

/**
 * Context#logicalPath -> String
 *
 *  Returns logical path without any file extensions.
 *
 *      'app/javascripts/application.js'
 *      # => 'app/javascripts/application'
 **/
getter(Context.prototype, 'logicalPath', function () {
  var len = this.__logicalPath__.length, ext = path.extname(this.__logicalPath__);
  return this.__logicalPath__.substr(0, len - ext.length);
});

/**
 *  Context#contentType -> String
 *
 *  Returns content type of file
 *
 *      'application/javascript'
 *      'text/css'
 **/
getter(Context.prototype, 'contentType', function () {
  return this.environment.contentTypeOf(this.pathname);
});

/**
 *  Context#context -> Object
 *
 *  Returns the context itself
 **/
getter(Context.prototype, 'context', function () {
  return this;
});

/**
 *  Context#resolve(pathname[, options = {}[, fn]]) -> String
 *  - pathname (String)
 *  - options (Object)
 *  - fn (Function)
 *
 *  Given a logical path, `resolve` will find and return the fully
 *  expanded path. Relative paths will also be resolved. An optional
 *  `contentType` restriction can be supplied to restrict the
 *  search.
 *
 *      context.resolve("foo.js")
 *      # => "/path/to/app/javascripts/foo.js"
 *
 *      context.resolve("./bar.js")
 *      # => "/path/to/app/javascripts/bar.js"
 *
 *      context.resolve("foo", {contentType: 'application/javascript'})
 *      # => "/path/to/app/javascripts/foo.js"
 *
 *  You may also provide an iterator function `fn`, that wil be passed to
 *  environments [[Base#resolve]] when needed.
 **/
Context.prototype.resolve = function (pathname, options, fn) {
  var self = this, attributes, content_type, resolved;

  options = options || {};

  if ('/' === pathname[0]) {
    if (fs.existsSync(pathname)) {
      return pathname;
    }

    throw new Error("Couldn't find file '" + pathname + "'");
  }

  content_type = options.contentType;

  if (content_type) {
    if ('~self~' === content_type) {
      content_type = self.contentType;
    }

    attributes = this.environment.attributesFor(pathname);
    if (attributes.formatExtension && content_type !== attributes.contentType) {
      throw new Error(pathname + " is '" + attributes.contentType + "', " +
                      "not '" + content_type + "'");
    }

    resolved = this.resolve(pathname, {}, function (candidate) {
      if (self.contentType === self.environment.contentTypeOf(candidate)) {
        return candidate;
      }
    });

    if (!resolved) {
      throw new Error("Could not find file '" + pathname + "'.");
    }

    return resolved;
  }

  return this.environment.resolve(pathname, _.extend({
    basePath: path.dirname(this.pathname)
  }, options), fn);
};

/**
 *  Context#dependOn(pathname) -> Void
 *
 *  Allows you to state a dependency on a file without including it.
 *
 *  This is used for caching purposes. Any changes made to
 *  the dependency file with invalidate the cache of the
 *  source file.
 **/
Context.prototype.dependOn = function (pathname) {
  this.__dependencyPaths__.push(this.resolve(pathname));
};

/**
 *  Context#dependOnAsset(pathname) -> Void
 *
 *  Allows you to state an asset dependency without including it.
 *
 *  This is used for caching purposes. Any changes that would
 *  invalidate the dependency asset will invalidate the source
 *  file. Unlike [[Context#dependOn]], this will include recursively
 *  the target asset's dependencies.
 **/
Context.prototype.dependOnAsset = function (pathname) {
  this.__dependencyAssets__.push(this.resolve(pathname));
};

/**
 *  Context#requireAsset(pathname) -> Void
 *
 *  `requireAsset` declares `path` as a dependency of the file. The
 *  dependency will be inserted before the file and will only be
 *  included once.
 *
 *  If EJS processing is enabled, you can use it to dynamically
 *  require assets.
 *
 *      <%= requireAsset("#{framework}.js") %>
 **/
Context.prototype.requireAsset = function (pathname) {
  pathname = this.resolve(pathname, {contentType: '~self~'});
  this.dependOnAsset(pathname);
  this.__requiredPaths__.push(pathname);
};

/**
 *  Context#stubAsset(pathname) -> Void
 *
 *  `stubAsset` blacklists `pathname` from being included in the bundle.
 *  `pathname` must be an asset which may or may not already be included
 *  in the bundle.
 **/
Context.prototype.stubAsset = function (pathname) {
  this.__stubbedAssets__.push(this.resolve(pathname, {contentType: '~self~'}));
};

/**
 *  Context#isAssetRequirable(pathname) -> Boolean
 *
 *  Tests if target path is able to be safely required into the
 *  current concatenation.
 **/
Context.prototype.isAssetRequirable = function (pathname) {
  var content_type, stat;

  pathname      = this.resolve(pathname);
  content_type  = this.environment.contentTypeOf(pathname);
  stat          = this.environment.stat(pathname);

  return stat && stat.isFile() &&
         (!this.contentType || this.contentType === content_type);
};

/**
 *  Context#assetDataUri(pathname) -> String
 *
 *  Returns a Base64-encoded `data:` URI with the contents of the
 *  asset at the specified path, and marks that path as a dependency
 *  of the current file.
 *
 *  Use `assetDataUri` from EJS with CSS or JavaScript assets:
 *
 *      #logo { background: url(<%= asset_data_uri('logo.png') %>) }
 *
 *      $('<img>').attr('src', '<%= asset_data_uri('avatar.jpg') %>')
 **/
Context.prototype.assetDataUri = function (pathname) {
  var asset = this.environment.findAsset(pathname),
      buff  = asset.buffer || new Buffer(asset.toString());

  this.dependOn(pathname);
  return "data:" + asset.contentType + ";base64," + buff.toString('base64');
};
Context.prototype.__helpers__['asset_data_uri'].func = Context.prototype.assetDataUri;

// returns new Error with anotated message telling where error happened.
// original error can be accessed via `originalError` property.
function beautifyError(err, pathname) {
  var b = new Error(String(err.message || err) + '\n\n(in ' + pathname + ')');

  // provide reference to original error
  b.originalError = err;

  return b;
}

/**
 *  Context#evaluate(pathname, options = {}, callback) -> Void
 *  - pathname (String)
 *  - options (Object)
 *  - callback (Function)
 *
 *  Reads `pathname` and runs processors on the file.
 **/
Context.prototype.evaluate = function (pathname, options, callback) {
  var self = this, attributes, processors, result, locals = {};

  options     = options || {};
  pathname    = this.resolve(pathname);
  attributes  = this.environment.attributesFor(pathname);
  processors  = options.processors || attributes.processors;
  result      = !!options.data ? options.data
              : fs.readFileSync(pathname, 'utf8');

  _.each(this.__helpers__, function (helper, name) {
    var opts = helper.opts || {};
    if (opts.type == 'stylus') {
      // for stylus helpers, the context is the one passed by stylus
      locals[name] = helper.func;  // no wrapper here: stylus helpers need the stylus context
    } else {
      // for other helpers, the context is this Context itself
      locals[name] = function () {
        return helper.func.apply(self, arguments);
      };
    }
  });

  async.forEachSeries(processors, function (ProcessorKlass, next) {
    var template = new ProcessorKlass(pathname, function () {
      return result;
    });

    template.evaluate(self, locals, function (err, data) {
      if (err) {
        next(beautifyError(err, self.pathname));
        return;
      }

      result = data;

      // some processors are sync, some async, some are mixed, so
      // to avoid call-stack overflow, we schedule each next step
      // on nextTick, so the call stack is kept reasonable.
      //
      // See: https://github.com/nodeca/mincer/issues/31

      process.nextTick(next);
    });
  }, function (err) {
    callback(err, result);
  });
};

// internal helper to define registerHelper() method on destination class
function define_helpers_registrator(Klass) {
  Klass.registerHelper = function (name, func, opts) {
    // Scenario: registerHelper('foo', foo_helper);
    // Scenario: registerHelper('foo', foo_helper, foo_opts);
    var helper = {
      name: name,
      func: func,
      opts: opts || {}
    };
    if (_.isString(name)) {
      Klass.prototype[name]             = func;
      Klass.prototype.__helpers__[name] = helper;
      return;
    }

    // Scenario: registerHelper({ foo: foo_helper, ... });
    _.each(name, function (func, name) {
      Klass.registerHelper(name, func);
    });
  };
}

/**
 *  Context.registerHelper(name, func) -> Void
 *  Context.registerHelper(helpers) -> Void
 *  - name (String)
 *  - func (Function)
 *  - helpers (Object)
 *
 *  Register a helper that will be available in the engines that supports
 *  local helpers (e.g. EJS or Stylus). You should avoid registering helpers
 *  directly on `Context` class in favour of [[Environment#ContextClass]]
 *  (see [[Environment#registerHelper]] as well).
 *
 *  ##### Example
 *
 *      Context.registerHelper('foo', foo_helper);
 *      Context.registerHelper('bar', bar_helper);
 *
 *      // equals to
 *
 *      Context.registerHelper({
 *        foo: foo_helper,
 *        bar: bar_helper
 *      });
 **/
define_helpers_registrator(Context);

/** internal
 *  Context#subclass -> Function
 *
 *  Returns new subclass of [[Context]].
 **/
getter(Context, 'subclass', function () {
  var Klass = function () { Context.apply(this, arguments); };

  // add class inheritance
  inherits(Klass, Context);

  // clone some own-prototype properties
  prop(Klass.prototype, '__helpers__', _.clone(Context.prototype.__helpers__));

  // Provide helpers registrator
  define_helpers_registrator(Klass);

  return Klass;
});
<commit_msg>Small Context code/docs cleanup<commit_after>/** internal
 *  class Context
 *
 *  `Context` provides helper methods to all [[Template]] processors.
 *  They are typically accessed by EJS templates. You can mix in custom
 *  helpers by injecting them into [[Environment#ContextClass]]. Do not
 *  mix them into `Context` directly.
 *
 *      environment.registerHelper('asset_url', function () {
 *        // ...
 *      });
 *
 *      // or in batch-mode
 *      environment.registerHelper({
 *        asset_url: function () {
 *          // ...
 *        },
 *        // ...
 *      });
 *
 *      <%= asset_url("foo.png") %>
 *
 *  The `Context` also collects dependencies declared by assets.
 *  See [[DirectiveProcessor]] for an example of this.
 **/

'use strict';

// stdlib
var fs        = require('fs');
var path      = require('path');
var inherits  = require('util').inherits;

// 3rd-party
var _ = require('underscore');
var async = require('async');

// internal
var getter = require('./common').getter;
var prop   = require('./common').prop;

////////////////////////////////////////////////////////////////////////////////

/**
 *  new Context(environment, logicalPath, pathname)
 *  - environment (Environment)
 *  - logicalPath (String)
 *  - pathname (String)
 **/
var Context = module.exports = function Context(environment, logicalPath, pathname) {
  prop(this, 'environment',           environment);
  prop(this, 'pathname',              pathname);

  prop(this, '__logicalPath__',       logicalPath);

  prop(this, '__requiredPaths__',     []);
  prop(this, '__stubbedAssets__',     []);
  prop(this, '__dependencyPaths__',   []);
  prop(this, '__dependencyAssets__',  [pathname]);
};

// Hash of helpers available as locals in the renderers.
// For each helper, a wrpper will be generated to preserve `this` context.
//
// See Context.registerHelper()
prop(Context.prototype, '__helpers__', {
  // note we'll need to backpatch `func` later
  asset_data_uri:   {name: 'asset_data_uri', func: Context.prototype.assetDataUri},
  asset_path:       {name: 'asset_path', func: Context.prototype.assetPath}
});

/**
 *  Context#assetPath(pathname, options) -> String
 *
 *  Helper that returns path to asset and that acts like a "base" method for
 *  other `*Path` helpers.
 *
 *  Exposed to the engines as `asset_path` helper.
 *
 *  By default this method is not implemented, and you must provide it's
 *  implementation that will fit your needs, e.g.:
 *
 *      environment.ContextClass.prototype.assetPath = function (pathname, options) {
 *        var asset = this.environment.findAsset(pathname, options);
 *        return '/assets/' + asset.digestPath;
 *      };
 *
 *  Once implemented, you can use `asset_path` helper, e.g.:
 *
 *      #logo { background: url(<%= asset_path('logo.png') %>) }
 **/
Context.prototype.assetPath = function assetPath() {
  throw new Error(
    "Custom asset_path helper is not implemented\n\n" +
    "Extend your environment context with a custom method.\n\n" +
    "  environment.ContextClass.prototype.assetPath = function (pathname, options) {\n" +
    "    // ... your code here ... \n" +
    "  };"
  );
};
Context.prototype.__helpers__['asset_path'].func = Context.prototype.assetPath;

/**
 *  Context#imagePath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for image assets.
 *  Exposed to the engines as `image_path` helper.
 **/

/**
 *  Context#videoPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for video assets.
 *  Exposed to the engines as `video_path` helper.
 **/

/**
 *  Context#audioPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for audio assets.
 *  Exposed to the engines as `audio_path` helper.
 **/

/**
 *  Context#fontPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for font assets.
 *  Exposed to the engines as `font_path` helper.
 **/

/**
 *  Context#javascriptPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for javascript assets.
 *  Exposed to the engines as `javascript_path` helper.
 **/

/**
 *  Context#stylesheetPath(pathname) -> String
 *
 *  Wrapper over [[Context#assetPath]] for stylesheet assets.
 *  Exposed to the engines as `stylesheet_path` helper.
 **/

['image', 'video', 'audio', 'font', 'javascript', 'stylesheet'].forEach(function (assetType) {
  var func = function (pathname) {
    return this.assetPath(pathname, {type: assetType});
  };
  //Context.registerHelper(assetType + '_path', func);
  var helper = {
    name: assetType + '_path',
    func: func
  };
  Context.prototype.__helpers__[assetType + '_path'] = helper;
  Context.prototype[assetType + 'Path'] = func;
});

/**
 *  Context#rootPath -> String
 *
 *  Returns the environment path that contains the file.
 *
 *  If `app/javascripts` and `app/stylesheets` are in your path, and
 *  current file is `app/javascripts/foo/bar.js`, `root_path` would
 *  return `app/javascripts`.
 **/
getter(Context.prototype, 'rootPath', function () {
  var pathname = this.pathname;
  return _.find(this.environment.paths, function (path) {
    return pathname.substr(0, path.length) === path;
  }).substr(this.environment.root.length + 1);
});

/**
 * Context#logicalPath -> String
 *
 *  Returns logical path without any file extensions.
 *
 *      'app/javascripts/application.js'
 *      # => 'app/javascripts/application'
 **/
getter(Context.prototype, 'logicalPath', function () {
  var len = this.__logicalPath__.length, ext = path.extname(this.__logicalPath__);
  return this.__logicalPath__.substr(0, len - ext.length);
});

/**
 *  Context#contentType -> String
 *
 *  Returns content type of file
 *
 *      'application/javascript'
 *      'text/css'
 **/
getter(Context.prototype, 'contentType', function () {
  return this.environment.contentTypeOf(this.pathname);
});

/**
 *  Context#context -> Context
 *
 *  Returns the context itself.
 **/
getter(Context.prototype, 'context', function () {
  return this;
});

/**
 *  Context#resolve(pathname[, options = {}[, fn]]) -> String
 *  - pathname (String)
 *  - options (Object)
 *  - fn (Function)
 *
 *  Given a logical path, `resolve` will find and return the fully
 *  expanded path. Relative paths will also be resolved. An optional
 *  `contentType` restriction can be supplied to restrict the
 *  search.
 *
 *      context.resolve("foo.js")
 *      # => "/path/to/app/javascripts/foo.js"
 *
 *      context.resolve("./bar.js")
 *      # => "/path/to/app/javascripts/bar.js"
 *
 *      context.resolve("foo", {contentType: 'application/javascript'})
 *      # => "/path/to/app/javascripts/foo.js"
 *
 *  You may also provide an iterator function `fn`, that wil be passed to
 *  environments [[Base#resolve]] when needed.
 **/
Context.prototype.resolve = function (pathname, options, fn) {
  var self = this, attributes, content_type, resolved;

  options = options || {};

  if ('/' === pathname[0]) {
    if (fs.existsSync(pathname)) {
      return pathname;
    }

    throw new Error("Couldn't find file '" + pathname + "'");
  }

  content_type = options.contentType;

  if (content_type) {
    if ('~self~' === content_type) {
      content_type = self.contentType;
    }

    attributes = this.environment.attributesFor(pathname);
    if (attributes.formatExtension && content_type !== attributes.contentType) {
      throw new Error(pathname + " is '" + attributes.contentType + "', " +
                      "not '" + content_type + "'");
    }

    resolved = this.resolve(pathname, {}, function (candidate) {
      if (self.contentType === self.environment.contentTypeOf(candidate)) {
        return candidate;
      }
    });

    if (!resolved) {
      throw new Error("Could not find file '" + pathname + "'.");
    }

    return resolved;
  }

  return this.environment.resolve(pathname, _.extend({
    basePath: path.dirname(this.pathname)
  }, options), fn);
};

/**
 *  Context#dependOn(pathname) -> Void
 *
 *  Allows you to state a dependency on a file without including it.
 *
 *  This is used for caching purposes. Any changes made to
 *  the dependency file with invalidate the cache of the
 *  source file.
 **/
Context.prototype.dependOn = function (pathname) {
  this.__dependencyPaths__.push(this.resolve(pathname));
};

/**
 *  Context#dependOnAsset(pathname) -> Void
 *
 *  Allows you to state an asset dependency without including it.
 *
 *  This is used for caching purposes. Any changes that would
 *  invalidate the dependency asset will invalidate the source
 *  file. Unlike [[Context#dependOn]], this will include recursively
 *  the target asset's dependencies.
 **/
Context.prototype.dependOnAsset = function (pathname) {
  this.__dependencyAssets__.push(this.resolve(pathname));
};

/**
 *  Context#requireAsset(pathname) -> Void
 *
 *  `requireAsset` declares `path` as a dependency of the file. The
 *  dependency will be inserted before the file and will only be
 *  included once.
 *
 *  If EJS processing is enabled, you can use it to dynamically
 *  require assets.
 *
 *      <%= requireAsset("#{framework}.js") %>
 **/
Context.prototype.requireAsset = function (pathname) {
  pathname = this.resolve(pathname, {contentType: '~self~'});
  this.dependOnAsset(pathname);
  this.__requiredPaths__.push(pathname);
};

/**
 *  Context#stubAsset(pathname) -> Void
 *
 *  `stubAsset` blacklists `pathname` from being included in the bundle.
 *  `pathname` must be an asset which may or may not already be included
 *  in the bundle.
 **/
Context.prototype.stubAsset = function (pathname) {
  this.__stubbedAssets__.push(this.resolve(pathname, {contentType: '~self~'}));
};

/**
 *  Context#isAssetRequirable(pathname) -> Boolean
 *
 *  Tests if target path is able to be safely required into the
 *  current concatenation.
 **/
Context.prototype.isAssetRequirable = function (pathname) {
  var content_type, stat;

  pathname      = this.resolve(pathname);
  content_type  = this.environment.contentTypeOf(pathname);
  stat          = this.environment.stat(pathname);

  return stat && stat.isFile() &&
         (!this.contentType || this.contentType === content_type);
};

/**
 *  Context#assetDataUri(pathname) -> String
 *
 *  Returns a Base64-encoded `data:` URI with the contents of the
 *  asset at the specified path, and marks that path as a dependency
 *  of the current file.
 *
 *  Use `assetDataUri` from EJS with CSS or JavaScript assets:
 *
 *      #logo { background: url(<%= asset_data_uri('logo.png') %>) }
 *
 *      $('<img>').attr('src', '<%= asset_data_uri('avatar.jpg') %>')
 **/
Context.prototype.assetDataUri = function (pathname) {
  var asset = this.environment.findAsset(pathname),
      buff  = asset.buffer || new Buffer(asset.toString());

  this.dependOn(pathname);
  return "data:" + asset.contentType + ";base64," + buff.toString('base64');
};
Context.prototype.__helpers__['asset_data_uri'].func = Context.prototype.assetDataUri;

// returns new Error with anotated message telling where error happened.
// original error can be accessed via `originalError` property.
function beautifyError(err, pathname) {
  var b = new Error(String(err.message || err) + '\n\n(in ' + pathname + ')');

  // provide reference to original error
  b.originalError = err;

  return b;
}

/**
 *  Context#evaluate(pathname, options = {}, callback) -> Void
 *  - pathname (String)
 *  - options (Object)
 *  - callback (Function)
 *
 *  Reads `pathname` and runs processors on the file.
 **/
Context.prototype.evaluate = function (pathname, options, callback) {
  var self = this, attributes, processors, result, locals = {};

  options     = options || {};
  pathname    = this.resolve(pathname);
  attributes  = this.environment.attributesFor(pathname);
  processors  = options.processors || attributes.processors;
  result      = !!options.data ? options.data
              : fs.readFileSync(pathname, 'utf8');

  _.each(this.__helpers__, function (helper, name) {
    var opts = helper.opts || {};
    if (opts.type == 'stylus') {
      // for stylus helpers, the context is the one passed by stylus
      locals[name] = helper.func;  // no wrapper here: stylus helpers need the stylus context
    } else {
      // for other helpers, the context is this Context itself
      locals[name] = function () {
        return helper.func.apply(self, arguments);
      };
    }
  });

  async.forEachSeries(processors, function (ProcessorKlass, next) {
    var template = new ProcessorKlass(pathname, function () {
      return result;
    });

    template.evaluate(self, locals, function (err, data) {
      if (err) {
        next(beautifyError(err, self.pathname));
        return;
      }

      result = data;

      // some processors are sync, some async, some are mixed, so
      // to avoid call-stack overflow, we schedule each next step
      // on nextTick, so the call stack is kept reasonable.
      //
      // See: https://github.com/nodeca/mincer/issues/31

      process.nextTick(next);
    });
  }, function (err) {
    callback(err, result);
  });
};

// internal helper to define registerHelper() method on destination class
function define_helpers_registrator(Klass) {
  Klass.registerHelper = function (name, func, opts) {
    // Scenario: registerHelper('foo', foo_helper[, foo_opts]);
    if (_.isString(name)) {
      Klass.prototype[name]             = func;
      Klass.prototype.__helpers__[name] = {
        name: name,
        func: func,
        opts: opts || {}
      };
      return;
    }

    // Scenario: registerHelper({ foo: foo_helper, ... });
    _.each(name, function (func, name) {
      Klass.registerHelper(name, func);
    });
  };
}

/**
 *  Context.registerHelper(name, func) -> Void
 *  Context.registerHelper(helpers) -> Void
 *  - name (String)
 *  - func (Function)
 *  - helpers (Object)
 *
 *  Register a helper that will be available in the engines that supports
 *  local helpers (e.g. EJS or Stylus). You should avoid registering helpers
 *  directly on `Context` class in favour of [[Environment#ContextClass]]
 *  (see [[Environment#registerHelper]] as well).
 *
 *  ##### Example
 *
 *      Context.registerHelper('foo', foo_helper);
 *      Context.registerHelper('bar', bar_helper);
 *
 *      // equals to
 *
 *      Context.registerHelper({
 *        foo: foo_helper,
 *        bar: bar_helper
 *      });
 **/
define_helpers_registrator(Context);

/** internal
 *  Context#subclass -> Function
 *
 *  Returns new subclass of [[Context]].
 **/
getter(Context, 'subclass', function () {
  var Klass = function () { Context.apply(this, arguments); };

  // add class inheritance
  inherits(Klass, Context);

  // clone some own-prototype properties
  prop(Klass.prototype, '__helpers__', _.clone(Context.prototype.__helpers__));

  // Provide helpers registrator
  define_helpers_registrator(Klass);

  return Klass;
});
<|endoftext|>