<commit_before>var DELETE_ALL_SENTINEL = -1

function noop () {}

function malarkey (callback, options) {
  options = options || {}

  var text = ''

  var functionQueue = []
  var functionArguments = []
  var functionIndex = -1

  var isStopped = true
  var stoppedCallback = noop

  var methods = {
    call: function (fn) {
      return enqueue(_call, [fn])
    },
    clear: function () {
      return enqueue(_clear, null)
    },
    delete: function (characterCount, deleteOptions) {
      if (typeof characterCount === 'object') {
        deleteOptions = characterCount
        characterCount = DELETE_ALL_SENTINEL
      }
      var deleteSpeed = deleteOptions && deleteOptions.deleteSpeed
      return enqueue(_delete, [
        characterCount || DELETE_ALL_SENTINEL,
        deleteSpeed || options.deleteSpeed || 50
      ])
    },
    isStopped: function () {
      return isStopped
    },
    pause: function (pauseDuration) {
      return enqueue(setTimeout, [
        pauseDuration != null ? pauseDuration : options.pauseDuration || 2000
      ])
    },
    resume: function () {
      if (isStopped) {
        isStopped = false
        next()
      }
      return methods
    },
    stop: function (fn) {
      isStopped = true
      stoppedCallback = fn || noop
      return methods
    },
    type: function (text, typeOptions) {
      var typeSpeed = typeOptions && typeOptions.typeSpeed
      return enqueue(_type, [text, typeSpeed || options.typeSpeed || 50])
    }
  }

  function next () {
    if (isStopped) {
      stoppedCallback(text)
      stoppedCallback = noop
      return
    }
    functionIndex += 1
    if (functionIndex === functionQueue.length) {
      if (!options.repeat) {
        functionIndex = functionQueue.length - 1
        isStopped = true
        return
      }
      functionIndex = 0
    }
    functionQueue[functionIndex].apply(
      null,
      [next].concat(functionArguments[functionIndex])
    )
  }

  function enqueue (callback, args) {
    functionQueue.push(callback)
    functionArguments.push(args)
    if (isStopped) {
      isStopped = false
      setTimeout(next, 0)
    }
    return methods
  }

  function _type (next, typeText, typeSpeed) {
    var length = typeText.length
    var i = 0
    setTimeout(function typeCharacter () {
      text += typeText[i++]
      callback(text)
      if (i === length) {
        next()
        return
      }
      setTimeout(typeCharacter, typeSpeed)
    }, typeSpeed)
  }

  function _delete (next, characterCount, deleteSpeed) {
    var finalLength =
      characterCount === DELETE_ALL_SENTINEL ? 0 : text.length - characterCount
    setTimeout(function deleteCharacter () {
      text = text.substring(0, text.length - 1)
      callback(text)
      if (text.length === finalLength) {
        next()
        return
      }
      setTimeout(deleteCharacter, deleteSpeed)
    }, deleteSpeed)
  }

  function _clear (next) {
    text = ''
    callback(text)
    next()
  }

  function _call (next, fn) {
    fn(next, text)
  }

  return methods
}

module.exports = malarkey
<commit_msg>Refactor options parsing, rename some methods<commit_after>var DELETE_ALL_SENTINEL = -1

function noop () {}

function malarkey (callback, options) {
  options = options || {}

  var text = ''

  var functionQueue = []
  var functionArguments = []
  var functionIndex = -1

  var isStopped = true
  var stoppedCallback = noop

  var methods = {
    call: function (fn) {
      return enqueue(_call, [fn])
    },
    clear: function () {
      return enqueue(_clear, null)
    },
    delete: function (characterCount, deleteOptions) {
      if (typeof characterCount === 'object') {
        deleteOptions = characterCount
        characterCount = DELETE_ALL_SENTINEL
      }
      var deleteSpeed = deleteOptions && deleteOptions.deleteSpeed
      return enqueue(_delete, [
        characterCount || DELETE_ALL_SENTINEL,
        (deleteOptions ? deleteOptions.speed : options.deleteSpeed) || 50
      ])
    },
    isStopped: function () {
      return isStopped
    },
    pause: function (pauseOptions) {
      return enqueue(setTimeout, [
        (pauseOptions ? pauseOptions.duration : options.pauseDuration) || 2000
      ])
    },
    triggerResume: function () {
      if (isStopped) {
        isStopped = false
        next()
      }
      return methods
    },
    triggerStop: function (fn) {
      isStopped = true
      stoppedCallback = fn || noop
      return methods
    },
    type: function (text, typeOptions) {
      return enqueue(_type, [text, (typeOptions ? typeOptions.speed : options.typeSpeed) || 50])
    }
  }

  function next () {
    if (isStopped) {
      stoppedCallback(text)
      stoppedCallback = noop
      return
    }
    functionIndex += 1
    if (functionIndex === functionQueue.length) {
      if (!options.repeat) {
        functionIndex = functionQueue.length - 1
        isStopped = true
        return
      }
      functionIndex = 0
    }
    functionQueue[functionIndex].apply(
      null,
      [next].concat(functionArguments[functionIndex])
    )
  }

  function enqueue (callback, args) {
    functionQueue.push(callback)
    functionArguments.push(args)
    if (isStopped) {
      isStopped = false
      setTimeout(next, 0)
    }
    return methods
  }

  function _type (next, typeText, typeSpeed) {
    var length = typeText.length
    var i = 0
    setTimeout(function typeCharacter () {
      text += typeText[i++]
      callback(text)
      if (i === length) {
        next()
        return
      }
      setTimeout(typeCharacter, typeSpeed)
    }, typeSpeed)
  }

  function _delete (next, characterCount, deleteSpeed) {
    var finalLength =
      characterCount === DELETE_ALL_SENTINEL ? 0 : text.length - characterCount
    setTimeout(function deleteCharacter () {
      text = text.substring(0, text.length - 1)
      callback(text)
      if (text.length === finalLength) {
        next()
        return
      }
      setTimeout(deleteCharacter, deleteSpeed)
    }, deleteSpeed)
  }

  function _clear (next) {
    text = ''
    callback(text)
    next()
  }

  function _call (next, fn) {
    fn(next, text)
  }

  return methods
}

module.exports = malarkey
<|endoftext|>


<commit_before>// 3p
var request = require('request');
var cors = require('cors');
var express = require('express');
var async = require('async');
var log4js = require('log4js');
var nodeExcel = require('excel-export-impr');
var nspAPI = require('nsp-api');
var fs = require('fs');

// local
var createNodeModule = require('./create-node-module');

var logger = log4js.getLogger();
log4js.replaceConsole();
logger.setLevel('INFO');
var app = express();
var port = process.env.PORT || 6901;

app.get('/retrieve/:module', cors(), function(req, res) {
	//TODO: handle the favicon thing?
	if(req.params.module === 'favicon.ico') {return};

	var registry = 'http://registry.npmjs.org/';
  	var url = registry + req.params.module;

  	if(req.query.version){
		url += '/' + req.query.version.toString().replace(/\^/g,'');
	}

	logger.info('NPM Retrieve Request: '+ url);

	request.get({url:url, json:true}, function(err, resp, body) {

		if(!err && resp.statusCode == 200) {

			// build a new module
			var mod = createNodeModule(body);

			// return the module
			res.jsonp(mod.getData());

		}else{
			res.jsonp(err);
		}

	});
});

// listen for requests
app.get('/resolve/:module', cors(), function(req, res) {

	//TODO: handle the favicon thing?
	if(req.params.module === 'favicon.ico') {return};

	var modules = [];
	var errors = [];
	var security = [];
	var modCount = 0;
	var modTotal = 0;
	var timestamp = Date.now();
	var hostname = 'http://' + req.headers.host;
	var registry = 'http://registry.npmjs.org/';
  	var url = registry + req.params.module;

	if(req.query.version){
		url += '/' + req.query.version.toString().replace(/\^/g,'');
	}else{
		url += '/latest';
	}

  	logger.info('NPM Resolve Request: '+ url);

	async.series([
		// our main workhorse
    	function(callback){

    		logger.info('Resolving dependencies...');

    		var getDependencies = function(module) {
    			var dependencies = module.dependencies;

    			// this is the main loop
    			for(mod in dependencies) {

    				// increment our counter for each module
					++modCount;
					++modTotal;

					// build our new url
					var newUrl = registry + mod;
					var modVersion = dependencies[mod].toString().replace(/\^/g,'');

					// check for version url
					if(modVersion.substring(0, 4) === 'http' || modVersion.substring(0, 3) === 'git') {
						logger.debug('Found version url for: ' + mod.name);
						newUrl = newUrl + '/latest';
					}else{
						newUrl = newUrl + '/' + modVersion;
					}

					logger.debug('NPM Request: ' + newUrl);

					request.get({url:newUrl, json:true}, function(error, resp, body) {

						// decrement counter as the request has resolved, good or bad
						--modCount;

						if(error){
							// for reporting
							errors.push(error);
						}else{

							// build a new module
							var dep = createNodeModule(body);

							// save the module
							modules.push(dep.getData());

							// keep running until the queue is empty
							if(modCount !== 0){
								// loop it
								getDependencies(dep.getData());
							}else{
								// done
								callback(null, modules);
							}
						}

					});
				}

    		};

    		// make the initial request for the module and kickoff the
    		// dependency loop
    		request.get({url:url, json:true}, function(err, resp, body) {

    			// build a new module
				var mod = createNodeModule(body);

				// save the module
				modules.push(mod.getData());

				// no dependencies no problem
				if(mod.getData().dependencies){
					// kick it off
					getDependencies(mod.getData());
				}else{
					// done
					callback(null, modules);
				}

			});

	    },
	    // this is our security bit
	    function(callback) {

	    	// reset counter
	    	modCount = 0;

	    	modules.forEach(function(m) {
	    		++modCount;

	    		// security check
	    		nspAPI.validateModule(m.name, m.version, function(err, results) {

	    			--modCount;

					if(Object.prototype.toString.call(results) === '[object Array]' && results.length !== 0) {
						security.push(results);
						m.secFlag = true;
						m.vulnerabilities = results;
					}

					if(modCount === 0){
						callback(null, security);
					}

				});

			});
	    },
	    // exporting
	    function(callback) {

	    	logger.info('Exporting...');

	    	var conf ={};
			conf.cols = [
				{
					caption:'Name',
					type:'string'
				},
				{
					caption:'Version',
					type:'string'
				},
				{
					caption:'ID',
					type:'string'
				},
				{
					caption:'Description',
					type:'string'
				},
				{
					caption:'Vulnerabilities',
					type:'string'
				},
				{
					caption:'Homepage',
					type:'hyperlink',
				},
				{
					caption:'NPM',
					type:'hyperlink'
				},
				{
					caption:'Repository',
					type:'hyperlink'
				},
				{
					caption:'Bugs',
					type:'hyperlink'
				},
				{
					caption:'Prefer Global',
					type:'string'
				},
				{
					caption:'Dependencies',
					type:'string'
				},
				{
					caption:'License Detail',
					type:'string'
				}
			];

			conf.rows = [];

			modules.forEach(function(m) {

				// the what ifs
				m.homepage = m.homepage || '';
				m.npm = 'http://npmjs.org/package/' + m.name;
				m.repository = m.repository || {url:''};
				m.bugs = m.bugs || {url:''};
				m.dep = '';

				// lets format these pesky things
				m.vulnerabilities = m.vulnerabilities || [];
				var v = '';
				m.vulnerabilities.forEach(function(hit){
					if(hit.length > 0) {
						hit.forEach(function(subHit){
							//TODO string huh?
							v = v + 'https://nodesecurity.io/advisories/module/' + subHit.url + ' \n';
						});
					}else if(hit.url) {
						v = v + 'https://nodesecurity.io/advisories/module/' + hit.url + ' \n';
					}
				});

				// just for reference
				var temp = [];
				for(dep in m.dependencies) {
						temp.push(dep + '@' + m.dependencies[dep]);
					}
				if(temp.length > 0) {
					m.dep = temp.join(',\n');
				}

				// autobots assemble
				conf.rows.push([
					m.name,
					m.version,
					m._id,
					m.description,
					v,
					{text: m.homepage, href: m.homepage},
					{text: m.npm, href: m.npm},
					{text: m.repository.url, href: m.repository.url},
					{text: m.bugs.url, href: m.bugs.url},
					m.preferGlobal,
					m.dep,
					m.license
				]);
			});

			var excelConf = nodeExcel.execute(conf);
			var filename = modules[0]._id + '.xlsx';

			if (!fs.existsSync('reports')){
			    fs.mkdirSync('reports');
			}
			fs.writeFileSync('reports/' + filename, excelConf, 'binary');

			logger.info('Exported: ' + filename);

			var downloadUrl = {
				report: hostname + '/report/' + filename
			};

	    	callback(null, downloadUrl);
	    }
	],
	// final callback function
	function(err, results){

		logger.info('Completed');

		// handle the errors, if any
		if(errors.length){
			logger.info('Found Errors: ' + errors.length);
			for(error in errors) {
				logger.error(errors[error]);
			}
		}

		// lets put together some nifty stats
		var resolvedModules = modTotal === 0 ? 1 : modTotal
		logger.info('Resolved Modules: ' + resolvedModules);
		logger.info('Vulnerabilities: ' + security.length);
		var timeDiff = Date.now() - timestamp;
		var totalTime = timeDiff / 1000 + 's';
		logger.info('Total Time: ' +  totalTime);
		logger.info('Timestamp: ' +  timestamp);
		logger.info('########################################');

		// build our response
		var nobleResponse = {
			modules: modules,
			report: results[1].report,
			vulnerabilities: security,
			stats: {
				resolved: resolvedModules,
				securityHits: security.length,
				totalTime: totalTime,
				timestamp: timestamp,
				errors: errors,
				mod: 'be noble.'
			}
		}

		// send back the json request
	    res.jsonp(nobleResponse);
	});

});

// handle requests to download reports
app.get('/report/:name', function (req, res, next) {

	var options = {
		root: __dirname + '/reports/',
		dotfiles: 'deny',
		headers: {
			'x-timestamp': Date.now(),
			'x-sent': true
		}
	};

	var fileName = req.params.name;
		res.sendFile(fileName, options, function (err) {
		if (err) {
			logger.error(err);
			res.status(err.status).end();
		}
		else {
			logger.info('Sent File: ' + fileName);
		}
	});
});

app.listen(port);

logger.info('Started Noble Server on PORT: ' + port);<commit_msg>Fix report property in response<commit_after>// 3p
var request = require('request');
var cors = require('cors');
var express = require('express');
var async = require('async');
var log4js = require('log4js');
var nodeExcel = require('excel-export-impr');
var nspAPI = require('nsp-api');
var fs = require('fs');

// local
var createNodeModule = require('./create-node-module');

var logger = log4js.getLogger();
log4js.replaceConsole();
logger.setLevel('INFO');
var app = express();
var port = process.env.PORT || 6901;

app.get('/retrieve/:module', cors(), function(req, res) {
	//TODO: handle the favicon thing?
	if(req.params.module === 'favicon.ico') {return};

	var registry = 'http://registry.npmjs.org/';
  	var url = registry + req.params.module;

  	if(req.query.version){
		url += '/' + req.query.version.toString().replace(/\^/g,'');
	}

	logger.info('NPM Retrieve Request: '+ url);

	request.get({url:url, json:true}, function(err, resp, body) {

		if(!err && resp.statusCode == 200) {

			// build a new module
			var mod = createNodeModule(body);

			// return the module
			res.jsonp(mod.getData());

		}else{
			res.jsonp(err);
		}

	});
});

// listen for requests
app.get('/resolve/:module', cors(), function(req, res) {

	//TODO: handle the favicon thing?
	if(req.params.module === 'favicon.ico') {return};

	var modules = [];
	var errors = [];
	var security = [];
	var modCount = 0;
	var modTotal = 0;
	var timestamp = Date.now();
	var hostname = 'http://' + req.headers.host;
	var registry = 'http://registry.npmjs.org/';
  	var url = registry + req.params.module;

	if(req.query.version){
		url += '/' + req.query.version.toString().replace(/\^/g,'');
	}else{
		url += '/latest';
	}

  	logger.info('NPM Resolve Request: '+ url);

	async.series([
		// our main workhorse
    	function(callback){

    		logger.info('Resolving dependencies...');

    		var getDependencies = function(module) {
    			var dependencies = module.dependencies;

    			// this is the main loop
    			for(mod in dependencies) {

    				// increment our counter for each module
					++modCount;
					++modTotal;

					// build our new url
					var newUrl = registry + mod;
					var modVersion = dependencies[mod].toString().replace(/\^/g,'');

					// check for version url
					if(modVersion.substring(0, 4) === 'http' || modVersion.substring(0, 3) === 'git') {
						logger.debug('Found version url for: ' + mod.name);
						newUrl = newUrl + '/latest';
					}else{
						newUrl = newUrl + '/' + modVersion;
					}

					logger.debug('NPM Request: ' + newUrl);

					request.get({url:newUrl, json:true}, function(error, resp, body) {

						// decrement counter as the request has resolved, good or bad
						--modCount;

						if(error){
							// for reporting
							errors.push(error);
						}else{

							// build a new module
							var dep = createNodeModule(body);

							// save the module
							modules.push(dep.getData());

							// keep running until the queue is empty
							if(modCount !== 0){
								// loop it
								getDependencies(dep.getData());
							}else{
								// done
								callback(null, modules);
							}
						}

					});
				}

    		};

    		// make the initial request for the module and kickoff the
    		// dependency loop
    		request.get({url:url, json:true}, function(err, resp, body) {

    			// build a new module
				var mod = createNodeModule(body);

				// save the module
				modules.push(mod.getData());

				// no dependencies no problem
				if(mod.getData().dependencies){
					// kick it off
					getDependencies(mod.getData());
				}else{
					// done
					callback(null, modules);
				}

			});

	    },
	    // this is our security bit
	    function(callback) {

	    	// reset counter
	    	modCount = 0;

	    	modules.forEach(function(m) {
	    		++modCount;

	    		// security check
	    		nspAPI.validateModule(m.name, m.version, function(err, results) {

	    			--modCount;

					if(Object.prototype.toString.call(results) === '[object Array]' && results.length !== 0) {
						security.push(results);
						m.secFlag = true;
						m.vulnerabilities = results;
					}

					if(modCount === 0){
						callback(null, security);
					}

				});

			});
	    },
	    // exporting
	    function(callback) {

	    	logger.info('Exporting...');

	    	var conf ={};
			conf.cols = [
				{
					caption:'Name',
					type:'string'
				},
				{
					caption:'Version',
					type:'string'
				},
				{
					caption:'ID',
					type:'string'
				},
				{
					caption:'Description',
					type:'string'
				},
				{
					caption:'Vulnerabilities',
					type:'string'
				},
				{
					caption:'Homepage',
					type:'hyperlink',
				},
				{
					caption:'NPM',
					type:'hyperlink'
				},
				{
					caption:'Repository',
					type:'hyperlink'
				},
				{
					caption:'Bugs',
					type:'hyperlink'
				},
				{
					caption:'Prefer Global',
					type:'string'
				},
				{
					caption:'Dependencies',
					type:'string'
				},
				{
					caption:'License Detail',
					type:'string'
				}
			];

			conf.rows = [];

			modules.forEach(function(m) {

				// the what ifs
				m.homepage = m.homepage || '';
				m.npm = 'http://npmjs.org/package/' + m.name;
				m.repository = m.repository || {url:''};
				m.bugs = m.bugs || {url:''};
				m.dep = '';

				// lets format these pesky things
				m.vulnerabilities = m.vulnerabilities || [];
				var v = '';
				m.vulnerabilities.forEach(function(hit){
					if(hit.length > 0) {
						hit.forEach(function(subHit){
							//TODO string huh?
							v = v + 'https://nodesecurity.io/advisories/module/' + subHit.url + ' \n';
						});
					}else if(hit.url) {
						v = v + 'https://nodesecurity.io/advisories/module/' + hit.url + ' \n';
					}
				});

				// just for reference
				var temp = [];
				for(dep in m.dependencies) {
						temp.push(dep + '@' + m.dependencies[dep]);
					}
				if(temp.length > 0) {
					m.dep = temp.join(',\n');
				}

				// autobots assemble
				conf.rows.push([
					m.name,
					m.version,
					m._id,
					m.description,
					v,
					{text: m.homepage, href: m.homepage},
					{text: m.npm, href: m.npm},
					{text: m.repository.url, href: m.repository.url},
					{text: m.bugs.url, href: m.bugs.url},
					m.preferGlobal,
					m.dep,
					m.license
				]);
			});

			var excelConf = nodeExcel.execute(conf);
			var filename = modules[0]._id + '.xlsx';

			if (!fs.existsSync('reports')){
			    fs.mkdirSync('reports');
			}
			fs.writeFileSync('reports/' + filename, excelConf, 'binary');

			logger.info('Exported: ' + filename);

			var downloadUrl = {
				report: hostname + '/report/' + filename
			};

	    	callback(null, downloadUrl);
	    }
	],
	// final callback function
	function(err, results){

		logger.info('Completed');

		// handle the errors, if any
		if(errors.length){
			logger.info('Found Errors: ' + errors.length);
			for(error in errors) {
				logger.error(errors[error]);
			}
		}

		// lets put together some nifty stats
		var resolvedModules = modTotal === 0 ? 1 : modTotal
		logger.info('Resolved Modules: ' + resolvedModules);
		logger.info('Vulnerabilities: ' + security.length);
		var timeDiff = Date.now() - timestamp;
		var totalTime = timeDiff / 1000 + 's';
		logger.info('Total Time: ' +  totalTime);
		logger.info('Timestamp: ' +  timestamp);
		logger.info('########################################');

		// build our response
		var nobleResponse = {
			modules: modules,
			vulnerabilities: security,
			report: results[2].report,
			stats: {
				resolved: resolvedModules,
				securityHits: security.length,
				totalTime: totalTime,
				timestamp: timestamp,
				errors: errors,
				mod: 'be noble.'
			}
		}

		// send back the json request
	    res.jsonp(nobleResponse);
	});

});

// handle requests to download reports
app.get('/report/:name', function (req, res, next) {

	var options = {
		root: __dirname + '/reports/',
		dotfiles: 'deny',
		headers: {
			'x-timestamp': Date.now(),
			'x-sent': true
		}
	};

	var fileName = req.params.name;
		res.sendFile(fileName, options, function (err) {
		if (err) {
			logger.error(err);
			res.status(err.status).end();
		}
		else {
			logger.info('Sent File: ' + fileName);
		}
	});
});

app.listen(port);

logger.info('Started Noble Server on PORT: ' + port);<|endoftext|>


<commit_before>/* eslint-disable*/
//eslint is disabled to preserve babel config formatting that is easier to copy-paste from .babelrc

require('babel-register')({
  //see https://babeljs.io/docs/usage/options/#options for more config options
  babelrc: false,
  "presets": ["es2015-node5", "stage-2"],
  // "plugins": ["add-module-exports"],
})
require('babel-polyfill')
// require('./lib/usac-scraper')
// require('./lib/scrapers/example-scraper');
require('./lib/scrapers/usac-racers');
<commit_msg>making event scraper to be default<commit_after>/* eslint-disable*/
//eslint is disabled to preserve babel config formatting that is easier to copy-paste from .babelrc

require('babel-register')({
  //see https://babeljs.io/docs/usage/options/#options for more config options
  babelrc: false,
  "presets": ["es2015-node5", "stage-2"],
  // "plugins": ["add-module-exports"],
})
require('babel-polyfill')
require('./lib/usac-scraper')
// require('./lib/scrapers/example-scraper');
// require('./lib/scrapers/usac-racers');
<|endoftext|>


<commit_before>require('coffee-script');
module.exports = require('./lib/main');<commit_msg>Add require to coffee-script/register for coffee 1.7 compatibility<commit_after>require('coffee-script/register');
require('coffee-script');
module.exports = require('./lib/main');
<|endoftext|>


<commit_before>var Milight = require("node-milight-promise").MilightController;
var helper = require("node-milight-promise").helper;

"use strict";

var Service, Characteristic;

module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;

  homebridge.registerPlatform("homebridge-milight", "MiLight", MiLightPlatform);
};

function MiLightPlatform(log, config) {
  this.log = log;
  this.config = config;
}

MiLightPlatform.prototype.accessories = function (callback) {
  var foundBulbs = [];
  var bridgeControllers = {};

  if (this.config.bridges.length > 0) {
    for (var bridgeConfig of this.config.bridges) {
      if (bridgeConfig.lights && Object.keys(bridgeConfig.lights).length > 0) {

        // Setting appropriate commands per bridge version. Defaults to v2 as those are the commands that previous versions of the plugin used
        if (!bridgeConfig.version) {
          bridgeConfig.version = "v2";
        }

        for (var lightType in bridgeConfig.lights) {
          if (["fullColor", "rgbw", "rgb", "white", "bridge"].indexOf(lightType) === -1) {
            this.log.error("Invalid bulb type '%s' specified.", lightType);
          } else if (bridgeConfig.version !== "v6" && ["fullColor", "bridge"].indexOf(lightType) > -1) {
            this.log.error("Bulb type '%s' only avaliable with v6 bridge!", lightType);
          } else {
            var zonesLength = bridgeConfig.lights[lightType].length;

            if (zonesLength < 1) {
              this.log.error("No bulbs found in '%s' configuration.", lightType);
              zonesLength = 0;
            } else if (["rgb", "bridge"].indexOf(lightType) > -1  && zonesLength > 1) {
              this.log.warn("Bulb type '%s' only supports a single zone. Only the first defined bulb will be used.", lightType);
              zonesLength = 1;
            } else if (zonesLength > 4) {
              this.log.warn("Only a maximum of 4 zones per bulb type are supported per bridge. Only recognizing the first 4 zones.");
              zonesLength = 4;
            }

            if (zonesLength > 0) {
              // If it hasn't been already, initialize a new controller to be used for all zones defined for this bridge
              if (typeof(bridgeControllers[bridgeConfig.ip_address]) != "object") {
                bridgeControllers[bridgeConfig.ip_address] = new Milight({
                  ip: bridgeConfig.ip_address,
                  port: bridgeConfig.port,
                  delayBetweenCommands: bridgeConfig.delay,
                  commandRepeat: bridgeConfig.repeat,
                  type: bridgeConfig.version
                });

                // Attach the right commands to the bridgeController object
                if (bridgeConfig.version === "v6") {
                  bridgeControllers[bridgeConfig.ip_address].commands = require("node-milight-promise").commandsV6;
                } else if (bridgeConfig.version === "v3") {
                  bridgeControllers[bridgeConfig.ip_address].commands = require("node-milight-promise").commands2;
                } else {
                  bridgeControllers[bridgeConfig.ip_address].commands = require("node-milight-promise").commands;
                }

                // Used to keep track of the last targeted bulb
                bridgeControllers[bridgeConfig.ip_address].lastSent = {bulb: ''};
              }

              // Create bulb accessories for all of the defined zones
              for (var i = 0; i < zonesLength; i++) {
                var bulbConfig = {};
                if (bulbConfig.name = bridgeConfig.lights[lightType][i]) {
                  bulbConfig.type = lightType;
                  bulbConfig.zone = i + 1;
                  var bulb = new MiLightAccessory(bulbConfig, bridgeControllers[bridgeConfig.ip_address], this.log);
                  foundBulbs.push(bulb);
                } else if (bridgeConfig.lights[lightType][i] !== null) {
                  this.log.error("Unable to add light from '%s' array, index %d", lightType, i);
                }
              }
            }
          }
        }
       } else {
        this.log.error("Could not read any lights from bridge %s",bridgeConfig.ip_address);
      }
    }
  } else {
    this.log.error("No bridges defined.")
  }

  if (foundBulbs.length <= 0) {
    this.log.error("No valid bulbs found in any bridge.");
  }

  callback(foundBulbs);
};

function MiLightAccessory(bulbConfig, bridgeController, log) {
  this.log = log;

  // config info
  this.name = bulbConfig.name;
  this.zone = bulbConfig.zone;
  this.type = bulbConfig.type;

  // have to keep track of the last values we set brightness and colour temp to for rgb/white bulbs
  this.brightness = -1;
  this.hue = -1;

  // assign to the bridge
  this.light = bridgeController;

  // set the version from the bridge
  this.version = this.light.type;

  // use the right commands for this bridge
  this.commands = this.light.commands;

  // keep track of the last bulb an 'on' command was sent to
  this.lastSent = this.light.lastSent;

}

MiLightAccessory.prototype.setPowerState = function (powerOn, callback) {
  if (powerOn) {
    if (this.lastSent.bulb === this.type + this.zone) {
      this.log.debug("[" + this.name + "] Ommiting 'on' command as we've sent it to this bulb most recently");
    } else {
      this.log("[" + this.name + "] Setting power state to on");
      this.lastSent.bulb = this.type + this.zone;
      this.light.sendCommands(this.commands[this.type].on(this.zone));
    }
  } else {
    this.log("[" + this.name + "] Setting power state to off");
    this.lastSent.bulb = '';
    this.light.sendCommands(this.commands[this.type].off(this.zone));
  }
  callback(null);
};

MiLightAccessory.prototype.setBrightness = function (level, callback) {
  if (level === 0) {
    // If brightness is set to 0, turn off the bulb
    this.log("[" + this.name + "] Setting brightness to 0 (off)");
    this.lightbulbService.setCharacteristic(Characteristic.On, false);
  } else if (level <= 5 && this.type !== "rgb") {
    // If setting brightness to 5 or lower, instead set night mode for bulbs that support it
    this.log("[" + this.name + "] Setting night mode");

    this.light.sendCommands(this.commands[this.type].off(this.zone));
    // Ensure we're pausing for 100ms between these commands as per the spec
    this.light.pause(100);
    this.light.sendCommands(this.commands[this.type].nightMode(this.zone));

  } else {
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting brightness to %s", level);

    // If bulb supports it, set the absolute brightness specified
    if (["rgb", "white"].indexOf(this.type) === -1) {
      if (this.version === "v6" && this.type !== "bridge") {
        this.light.sendCommands(this.commands[this.type].brightness(this.zone, level));
      } else {
        this.light.sendCommands(this.commands[this.type].brightness(level));
      }
    } else {
      // If this is an rgb or a white bulb, they only support brightness up and down.
      if (this.type === "white" && level === 100) {
        // But the white bulbs do have a "maximum brightness" command
        this.light.sendCommands(this.commands[this.type].maxBright(this.zone));
        this.brightness = 100;
      } else {
        // We're going to send the number of brightness up or down commands required to get to get from
        // the current value that HomeKit knows to the target value

        // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
        if (this.brightness === -1) this.brightness = this.lightbulbService.getCharacteristic(Characteristic.Brightness).value;
        var currentLevel = this.brightness;

        var targetLevel = level - currentLevel;
        var targetDirection = Math.sign(targetLevel);
        targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/10))); // There are 10 steps of brightness

        if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
          this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
        } else {
          this.brightness = level;

          for (; targetLevel !== 0; targetLevel--) {
            if (targetDirection === 1) {
              this.light.sendCommands(this.commands[this.type].brightUp(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness up command");
            } else if (targetDirection === -1) {
              this.light.sendCommands(this.commands[this.type].brightDown(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness down command");
            }
          }
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setHue = function (value, callback, context) {
  // Send on command to ensure we're addressing the right bulb
  this.lightbulbService.setCharacteristic(Characteristic.On, true);

  this.log("[" + this.name + "] Setting hue to %s", value);

  if (["fullColor", "rgbw", "bridge"].indexOf(this.type) > -1 && this.lightbulbService.getCharacteristic(Characteristic.Saturation).value === 0 && this.hue !== -1 && context !== 'internal') {
    this.log("[" + this.name + "] Saturation is 0, making sure bulb is in white mode");
    this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
  } else if (this.type !== "white") {
    this.hue = value;

    if (this.version === "v6" && this.type !== "bridge") {
      this.light.sendCommands(this.commands[this.type].hue(this.zone, helper.hsvToMilightColor([value, 0, 0]), true));
    } else {
      this.light.sendCommands(this.commands[this.type].hue(helper.hsvToMilightColor([value, 0, 0]),true));
    }
  } else {
    // Again, white bulbs don't support setting an absolue colour temp, so we'll do some math to figure out how to get there

    // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
    if (this.hue === -1) this.hue = this.lightbulbService.getCharacteristic(Characteristic.Hue).value;
    var currentLevel = this.hue;

    var targetLevel = value - currentLevel;
    var targetDirection = Math.sign(targetLevel);
    targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/36))); // There are 10 steps of colour temp (360/10)

    if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
      this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
    } else {
      this.hue = value;

      for (; targetLevel !== 0; targetLevel--) {
        if (targetDirection === 1) {
          this.light.sendCommands(this.commands[this.type].cooler(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb cooler command");
        } else if (targetDirection === -1) {
          this.light.sendCommands(this.commands[this.type].warmer(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb warmer command");
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setSaturation = function (value, callback) {
  if (["rgbw", "bridge"].indexOf(this.type) > -1) {
    if (value === 0) {
      // Send on command to ensure we're addressing the right bulb
      this.lightbulbService.setCharacteristic(Characteristic.On, true);

      this.log("[" + this.name + "] Saturation set to 0, setting bulb to white");
      this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
    } else {
      // We can get these commands out-of-order, so set the hue again just to be sure
      this.log.info("[" + this.name + "] Saturation set to %s, but hue is not 0, resetting hue", value);
      this.lightbulbService.getCharacteristic(Characteristic.Hue).setValue(this.lightbulbService.getCharacteristic(Characteristic.Hue).value, null, 'internal');
    }
  } else if (this.type === "fullColor"){
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting saturation to %s", value);

    // The fullColor bulbs look to actually invert the brightness value
    value = value - 100;
    value = Math.abs(value);
    this.log.debug("[" + this.name + "] Actually sending inverse saturation value %s to fullColor bulb", value);

    this.light.sendCommands(this.commands[this.type].saturation(this.zone, value));

  } else {
    this.log.info("[" + this.name + "] Setting saturation to %s (NOTE: No impact on %s %s bulbs)", value, this.type, this.log.prefix);
  }
  callback(null);
};

MiLightAccessory.prototype.identify = function (callback) {
  this.log("[" + this.name + "] Identify requested!");
  callback(null); // success
};

MiLightAccessory.prototype.getServices = function () {
  this.informationService = new Service.AccessoryInformation();

  this.informationService
    .setCharacteristic(Characteristic.Manufacturer, this.log.prefix)
    .setCharacteristic(Characteristic.Model, this.type)
    .setCharacteristic(Characteristic.SerialNumber, "12345");

  this.lightbulbService = new Service.Lightbulb(this.name);

  this.lightbulbService
    .getCharacteristic(Characteristic.On)
    .on("set", this.setPowerState.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Brightness())
    .on("set", this.setBrightness.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Saturation())
    .on("set", this.setSaturation.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Hue())
    .on("set", this.setHue.bind(this));

  return [this.informationService, this.lightbulbService];
};<commit_msg>Make sure fullColor bulbs get set to white when saturation = 0 too<commit_after>var Milight = require("node-milight-promise").MilightController;
var helper = require("node-milight-promise").helper;

"use strict";

var Service, Characteristic;

module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;

  homebridge.registerPlatform("homebridge-milight", "MiLight", MiLightPlatform);
};

function MiLightPlatform(log, config) {
  this.log = log;
  this.config = config;
}

MiLightPlatform.prototype.accessories = function (callback) {
  var foundBulbs = [];
  var bridgeControllers = {};

  if (this.config.bridges.length > 0) {
    for (var bridgeConfig of this.config.bridges) {
      if (bridgeConfig.lights && Object.keys(bridgeConfig.lights).length > 0) {

        // Setting appropriate commands per bridge version. Defaults to v2 as those are the commands that previous versions of the plugin used
        if (!bridgeConfig.version) {
          bridgeConfig.version = "v2";
        }

        for (var lightType in bridgeConfig.lights) {
          if (["fullColor", "rgbw", "rgb", "white", "bridge"].indexOf(lightType) === -1) {
            this.log.error("Invalid bulb type '%s' specified.", lightType);
          } else if (bridgeConfig.version !== "v6" && ["fullColor", "bridge"].indexOf(lightType) > -1) {
            this.log.error("Bulb type '%s' only avaliable with v6 bridge!", lightType);
          } else {
            var zonesLength = bridgeConfig.lights[lightType].length;

            if (zonesLength < 1) {
              this.log.error("No bulbs found in '%s' configuration.", lightType);
              zonesLength = 0;
            } else if (["rgb", "bridge"].indexOf(lightType) > -1  && zonesLength > 1) {
              this.log.warn("Bulb type '%s' only supports a single zone. Only the first defined bulb will be used.", lightType);
              zonesLength = 1;
            } else if (zonesLength > 4) {
              this.log.warn("Only a maximum of 4 zones per bulb type are supported per bridge. Only recognizing the first 4 zones.");
              zonesLength = 4;
            }

            if (zonesLength > 0) {
              // If it hasn't been already, initialize a new controller to be used for all zones defined for this bridge
              if (typeof(bridgeControllers[bridgeConfig.ip_address]) != "object") {
                bridgeControllers[bridgeConfig.ip_address] = new Milight({
                  ip: bridgeConfig.ip_address,
                  port: bridgeConfig.port,
                  delayBetweenCommands: bridgeConfig.delay,
                  commandRepeat: bridgeConfig.repeat,
                  type: bridgeConfig.version
                });

                // Attach the right commands to the bridgeController object
                if (bridgeConfig.version === "v6") {
                  bridgeControllers[bridgeConfig.ip_address].commands = require("node-milight-promise").commandsV6;
                } else if (bridgeConfig.version === "v3") {
                  bridgeControllers[bridgeConfig.ip_address].commands = require("node-milight-promise").commands2;
                } else {
                  bridgeControllers[bridgeConfig.ip_address].commands = require("node-milight-promise").commands;
                }

                // Used to keep track of the last targeted bulb
                bridgeControllers[bridgeConfig.ip_address].lastSent = {bulb: ''};
              }

              // Create bulb accessories for all of the defined zones
              for (var i = 0; i < zonesLength; i++) {
                var bulbConfig = {};
                if (bulbConfig.name = bridgeConfig.lights[lightType][i]) {
                  bulbConfig.type = lightType;
                  bulbConfig.zone = i + 1;
                  var bulb = new MiLightAccessory(bulbConfig, bridgeControllers[bridgeConfig.ip_address], this.log);
                  foundBulbs.push(bulb);
                } else if (bridgeConfig.lights[lightType][i] !== null) {
                  this.log.error("Unable to add light from '%s' array, index %d", lightType, i);
                }
              }
            }
          }
        }
       } else {
        this.log.error("Could not read any lights from bridge %s",bridgeConfig.ip_address);
      }
    }
  } else {
    this.log.error("No bridges defined.")
  }

  if (foundBulbs.length <= 0) {
    this.log.error("No valid bulbs found in any bridge.");
  }

  callback(foundBulbs);
};

function MiLightAccessory(bulbConfig, bridgeController, log) {
  this.log = log;

  // config info
  this.name = bulbConfig.name;
  this.zone = bulbConfig.zone;
  this.type = bulbConfig.type;

  // have to keep track of the last values we set brightness and colour temp to for rgb/white bulbs
  this.brightness = -1;
  this.hue = -1;

  // assign to the bridge
  this.light = bridgeController;

  // set the version from the bridge
  this.version = this.light.type;

  // use the right commands for this bridge
  this.commands = this.light.commands;

  // keep track of the last bulb an 'on' command was sent to
  this.lastSent = this.light.lastSent;

}

MiLightAccessory.prototype.setPowerState = function (powerOn, callback) {
  if (powerOn) {
    if (this.lastSent.bulb === this.type + this.zone) {
      this.log.debug("[" + this.name + "] Ommiting 'on' command as we've sent it to this bulb most recently");
    } else {
      this.log("[" + this.name + "] Setting power state to on");
      this.lastSent.bulb = this.type + this.zone;
      this.light.sendCommands(this.commands[this.type].on(this.zone));
    }
  } else {
    this.log("[" + this.name + "] Setting power state to off");
    this.lastSent.bulb = '';
    this.light.sendCommands(this.commands[this.type].off(this.zone));
  }
  callback(null);
};

MiLightAccessory.prototype.setBrightness = function (level, callback) {
  if (level === 0) {
    // If brightness is set to 0, turn off the bulb
    this.log("[" + this.name + "] Setting brightness to 0 (off)");
    this.lightbulbService.setCharacteristic(Characteristic.On, false);
  } else if (level <= 5 && this.type !== "rgb") {
    // If setting brightness to 5 or lower, instead set night mode for bulbs that support it
    this.log("[" + this.name + "] Setting night mode");

    this.light.sendCommands(this.commands[this.type].off(this.zone));
    // Ensure we're pausing for 100ms between these commands as per the spec
    this.light.pause(100);
    this.light.sendCommands(this.commands[this.type].nightMode(this.zone));

  } else {
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting brightness to %s", level);

    // If bulb supports it, set the absolute brightness specified
    if (["rgb", "white"].indexOf(this.type) === -1) {
      if (this.version === "v6" && this.type !== "bridge") {
        this.light.sendCommands(this.commands[this.type].brightness(this.zone, level));
      } else {
        this.light.sendCommands(this.commands[this.type].brightness(level));
      }
    } else {
      // If this is an rgb or a white bulb, they only support brightness up and down.
      if (this.type === "white" && level === 100) {
        // But the white bulbs do have a "maximum brightness" command
        this.light.sendCommands(this.commands[this.type].maxBright(this.zone));
        this.brightness = 100;
      } else {
        // We're going to send the number of brightness up or down commands required to get to get from
        // the current value that HomeKit knows to the target value

        // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
        if (this.brightness === -1) this.brightness = this.lightbulbService.getCharacteristic(Characteristic.Brightness).value;
        var currentLevel = this.brightness;

        var targetLevel = level - currentLevel;
        var targetDirection = Math.sign(targetLevel);
        targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/10))); // There are 10 steps of brightness

        if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
          this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
        } else {
          this.brightness = level;

          for (; targetLevel !== 0; targetLevel--) {
            if (targetDirection === 1) {
              this.light.sendCommands(this.commands[this.type].brightUp(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness up command");
            } else if (targetDirection === -1) {
              this.light.sendCommands(this.commands[this.type].brightDown(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness down command");
            }
          }
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setHue = function (value, callback, context) {
  // Send on command to ensure we're addressing the right bulb
  this.lightbulbService.setCharacteristic(Characteristic.On, true);

  this.log("[" + this.name + "] Setting hue to %s", value);

  if (["fullColor", "rgbw", "bridge"].indexOf(this.type) > -1 && this.lightbulbService.getCharacteristic(Characteristic.Saturation).value === 0 && this.hue !== -1 && context !== 'internal') {
    this.log("[" + this.name + "] Saturation is 0, making sure bulb is in white mode");
    this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
  } else if (this.type !== "white") {
    this.hue = value;

    if (this.version === "v6" && this.type !== "bridge") {
      this.light.sendCommands(this.commands[this.type].hue(this.zone, helper.hsvToMilightColor([value, 0, 0]), true));
    } else {
      this.light.sendCommands(this.commands[this.type].hue(helper.hsvToMilightColor([value, 0, 0]),true));
    }
  } else {
    // Again, white bulbs don't support setting an absolue colour temp, so we'll do some math to figure out how to get there

    // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
    if (this.hue === -1) this.hue = this.lightbulbService.getCharacteristic(Characteristic.Hue).value;
    var currentLevel = this.hue;

    var targetLevel = value - currentLevel;
    var targetDirection = Math.sign(targetLevel);
    targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/36))); // There are 10 steps of colour temp (360/10)

    if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
      this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
    } else {
      this.hue = value;

      for (; targetLevel !== 0; targetLevel--) {
        if (targetDirection === 1) {
          this.light.sendCommands(this.commands[this.type].cooler(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb cooler command");
        } else if (targetDirection === -1) {
          this.light.sendCommands(this.commands[this.type].warmer(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb warmer command");
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setSaturation = function (value, callback) {
  if (["rgbw", "bridge", "fullColor"].indexOf(this.type) > -1) {
    if (value === 0) {
      // Send on command to ensure we're addressing the right bulb
      this.lightbulbService.setCharacteristic(Characteristic.On, true);

      this.log("[" + this.name + "] Saturation set to 0, setting bulb to white");
      this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
    } else if (this.type === "fullColor"){
      // Send on command to ensure we're addressing the right bulb
      this.lightbulbService.setCharacteristic(Characteristic.On, true);

      this.log("[" + this.name + "] Setting saturation to %s", value);

      // The fullColor bulbs look to actually invert the brightness value
      value = value - 100;
      value = Math.abs(value);
      this.log.debug("[" + this.name + "] Actually sending inverse saturation value %s to fullColor bulb", value);

      this.light.sendCommands(this.commands[this.type].saturation(this.zone, value));
    } else {
      // We can get these commands out-of-order, so set the hue again just to be sure
      this.log.info("[" + this.name + "] Saturation set to %s, but hue is not 0, resetting hue", value);
      this.lightbulbService.getCharacteristic(Characteristic.Hue).setValue(this.lightbulbService.getCharacteristic(Characteristic.Hue).value, null, 'internal');
    }
  } else {
    this.log.info("[" + this.name + "] Setting saturation to %s (NOTE: No impact on %s %s bulbs)", value, this.type, this.log.prefix);
  }
  callback(null);
};

MiLightAccessory.prototype.identify = function (callback) {
  this.log("[" + this.name + "] Identify requested!");
  callback(null); // success
};

MiLightAccessory.prototype.getServices = function () {
  this.informationService = new Service.AccessoryInformation();

  this.informationService
    .setCharacteristic(Characteristic.Manufacturer, this.log.prefix)
    .setCharacteristic(Characteristic.Model, this.type)
    .setCharacteristic(Characteristic.SerialNumber, "12345");

  this.lightbulbService = new Service.Lightbulb(this.name);

  this.lightbulbService
    .getCharacteristic(Characteristic.On)
    .on("set", this.setPowerState.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Brightness())
    .on("set", this.setBrightness.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Saturation())
    .on("set", this.setSaturation.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Hue())
    .on("set", this.setHue.bind(this));

  return [this.informationService, this.lightbulbService];
};<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var util = require('util');
var os = require('os');
var crypto = require('crypto');

/**
 * replace tabs by indent value
 *
 * @param line
 * @param indent
 * @return string
 */
function properIndentation(line, indent) {
    return line.replace(/\t/, indent || '');
}

/**
 * count leading whitespace chars
 *
 * @param line
 * @return integer
 */
function countWhitespace(line) {
    // return a somewhat high value for empty lines
    return line.length ? parseInt(line.match(/^\s*/)[0].length, 10) : 9999;
}

/**
 * get lowest value of leading whitespace in a given block
 *
 * @param previous
 * @param current
 * @returns integer
 */
function getLeadingWhitespace(previous, current) {
    return previous <= current ? previous : current;
}

/**
 * raise offset in lines
 *
 * @param lines
 * @param offset
 * @returns {Array|*|{}}
 */
function raiseIndent(lines, offset) {
    offset = offset || '    ';

    return lines.map((line) => offset + line);
}

/**
 * Formalize any given value as wrap object
 *
 * @param wrap
 * @returns {{before: '', after: ''}}
 */
function formalizeWrap(wrap) {
    const result = { before: '', after: '' };

    if ((typeof wrap === 'string' && wrap.length > 0) || typeof wrap === 'number') {
        result.before = result.after = wrap;
    } else if  (Array.isArray(wrap) && wrap.length > 0) {
        result.before = [].slice.call(wrap, 0, 1)[0];
        result.after = wrap.length > 1 ? [].slice.call(wrap, 1, 2)[0] : result.before;
    } else if (_.isPlainObject(wrap)) {
        let i = 0;

        // crappy method getting the value of the first and second item in object
        for (let el in wrap) {
            if (!wrap.hasOwnProperty(el)) {
                continue;
            }

            if (i < 2) {
                result.before = wrap[el];
            }

            i++;
        }

        // set value of after to the value of before if after is empty
        result.after = result.after.length < 1 ? result.before : result.after;
    }

    return result;
}

/**
 * read options from annotation
 *
 * e.g.:
 * gets:
 * {
     *   extract: 'teaser/content-teaser--small.html',
     *   viewWrap: {before: '
', after: '
'}
     * }
 *
 * @param annotation
 * @param defaults
 * @returns {{}}
 */
function getBlockOptions(annotation, defaults) {
    const optionValues = annotation.split(/\w+\:/)
        .map((item) => item.replace(/|^\s+|\s+$/, ''))
        .filter((item) => !!item.length);
    const optionKeys = annotation.match(/(\w+)\:/gi).map((item) => item.replace(/[^\w]/, ''));

    defaults = defaults || {
            viewWrap: {before: '', after: ''}
        };

    const opts = {};

    optionValues.forEach((v, i) => {
        const k = optionKeys[i];

        if (typeof k !== 'string') {
            return;
        }

        // Treat option value as array if it has a colon
        // @todo: Allow escaped colons to be ignored
        // RegEx lookbehind negate does not work :(
        // Should be /(?<!\\):/
        if (v.indexOf(':') > -1) {
            v = v.split(':');
        }

        opts[k] = v;
    });

    // Process options
    opts.wrap = formalizeWrap(opts.wrap || defaults.viewWrap);

    return opts;
}

/**
 * Format options as string of HTML data parameters
 *
 * @param options
 * @returns {string}
 */
function optionsToDataString(options) {
    if (typeof options !== 'object') {
        return '';
    }

    const prepared = [];
    const processedOptions = Object.assign({}, options);

    // prepare wrap option
    if (processedOptions.hasOwnProperty('wrap')) {
        processedOptions['wrap-before'] = processedOptions.wrap.before;
        processedOptions['wrap-after'] = processedOptions.wrap.after;

        delete(processedOptions.wrap);
    }

    // create data attributes
    for (const el in processedOptions) {
        if (processedOptions.hasOwnProperty(el) === false) {
            continue;
        }

        const value = processedOptions[el];

        // Ignore callbacks
        if (typeof value === 'function') {
            continue;
        }

        // Cleanup: Remove leading and trailing " and ', replace " by ' (e.g. in stringified objects)
        const preparedVal = JSON.stringify(processedOptions[el])
            .replace(/^['"]|['"]$/g, '')
            .replace(/\\?"/g, "'");

        // Build data parameter: data-name="value"
        const param = 'data-' + el + '="' + preparedVal + '"';

        prepared.push(param);
    }

    return prepared.join(' ');
}

/**
 * trim given number of leading characters
 *
 * @param lines
 * @param num Number of chars to be removed
 * @returns Array
 */
function trimLines(lines, num) {
    return lines.map((line) => line.substr(num));
}

/**
 * create sha1 hash from string
 *
 * @param value
 * @returns {*}
 */
function createId(value) {
    return crypto.createHash('sha1').update(value, 'utf8').digest('hex').slice(0, 8);
}

/**
 * get default options, scope as function instead of "public" property
 *
 * @returns {{indent: string, origin: string, resources: {classnames: {root: string, body: string}, meta: Array, scriptsFoot: {files: Array, inline: Array}, scriptsHead: {files: Array, inline: Array}, stylesHead: {files: Array, inline: Array}}, templateWrap: {before: string, after: string}, viewWrap: {before: string, after: string}}}
 */
function getDefaultOptions() {
    const resources = {
        classnames: {
            root: '',
            body: ''
        },
        meta: [],
        scriptsFoot: {
            files: [],
            inline: []
        },
        scriptsHead: {
            files: [],
            inline: []
        },
        stylesHead: {
            files: [],
            inline: []
        }
    };

    return {
        indent: '    ',
        origin: '',
        resources: resources,
        templateWrap: { before: '', after: '' },
        viewWrap: { before: '', after: '' }
    }
}

/**
 * Constructor
 *
 * Use named function to get better backtraces in node
 *
 * @param data
 * @constructor
 */
var InventoryObject = function (data) {
    data                = data || {};

    this.category       = data.hasOwnProperty('category') ? data.category : 'No category';
    this.id             = data.hasOwnProperty('id') ? data.id : '';
    this.group          = data.hasOwnProperty('group') ? data.group : '';
    this.lines          = data.hasOwnProperty('lines') ? data.lines : 0;
    this.name           = data.hasOwnProperty('name') ? data.name : '';
    this.options        = data.hasOwnProperty('options') ? data.options : {};
    this.optionsData    = data.hasOwnProperty('optionsData') ? data.optionsData : '';
    this.origin         = data.hasOwnProperty('origin') ? data.origin : '';
    this.partial        = data.hasOwnProperty('partial') ? data.partial : '';
    this.resources      = data.hasOwnProperty('resources') ? data.resources : Object.assign({}, getDefaultOptions().resources);
    this.template       = data.hasOwnProperty('template') ? data.template : '';
    this.usage          = data.hasOwnProperty('usage') ? data.usage : [];
    this.view           = data.hasOwnProperty('view') ? data.view : '';
    this.viewId         = data.hasOwnProperty('viewId') ? data.viewId : '';
};

/**
 * parse data from extracted part
 *
 * @param src
 * @param opts
 */
InventoryObject.prototype.parseData = function(src, opts) {
    opts = Object.assign({}, getDefaultOptions(), opts);

    // remove ending annotation
    src = src.replace(//gi, '')

    const parts = src.match(/(?:((?:.|\n)*?)/i);
    const blockOpts = getBlockOptions(parts[1], opts);

    // remove extraction annotation and trim
    const content = _.trimEnd(_.trimStart(src.replace(//gi, ''), '\n\r'));

    // continue if name is empty
    if (!blockOpts.hasOwnProperty('extract')) {
        return;
    }

    // label from name property and fallback to extract value
    const name = blockOpts.hasOwnProperty('name') ? blockOpts.name : blockOpts.extract;

    // set category name
    const category = blockOpts.hasOwnProperty('category') ? blockOpts.category : this.category;

    // set group
    const group = blockOpts.hasOwnProperty('group') ? blockOpts.group : this.group;

    // process source code
    let lines = content.split('\n')
        .map((line) => _.trimEnd(line))
        .map((line) => properIndentation(line, opts.indent));
    const leadingWhitespace = lines.map(countWhitespace);
    const crop = leadingWhitespace.reduce(getLeadingWhitespace);
    const viewWrap = blockOpts.wrap;
    const templateWrapOptions = optionsToDataString(blockOpts);

    lines = trimLines(lines, crop);

    let viewLines = util._extend([], lines);
    let templateLines = util._extend([], lines);

    // wrap partial if inline option viewWrap: exists
    if (viewWrap.before.length) {
        viewLines = raiseIndent(viewLines);
        viewLines.unshift('');
        viewLines.unshift(viewWrap.before);
        viewLines.push('');
        viewLines.push(viewWrap.after);
    }

    // add templateWrap
    if (typeof opts.templateWrap === 'object') {
        let before = opts.templateWrap.before || '';
        let after = opts.templateWrap.after || '';

        before = before.replace('{{wrapData}}', templateWrapOptions);
        after = after.replace('{{wrapData}}', templateWrapOptions);

        templateLines.unshift(before);
        templateLines.push(after);
    }

    // set properties
    this.category       = category;
    this.group          = group;
    // remove all whitespace chars before creating the hash
    this.id             = createId(src.replace(/\s+/gi, ''));
    this.lines          = lines;
    this.name           = name;
    this.options        = blockOpts;
    this.optionsData    = templateWrapOptions;
    this.partial        = lines.join(os.EOL);
    this.resources      = opts.resources;
    this.template       = templateLines.join(os.EOL);
    this.view           = viewLines.join(os.EOL);
    this.viewId         = createId(this.view.replace(/\s+/gi, ''));
};

/**
 * set inventory object property if prop is a valid property name (property exists)
 *
 * @param prop
 * @param value
 * @returns {boolean}
 */
InventoryObject.prototype.setProperty = function (prop, value) {
    if (typeof prop !== 'string' || !this.hasOwnProperty(prop)) {
        return false;
    }

    this[prop] = value;

    return true;
};

/**
 * module
 * @param data
 * @returns {*}
 */
module.exports = InventoryObject;
<commit_msg>Update code example<commit_after>'use strict';

var _ = require('lodash');
var util = require('util');
var os = require('os');
var crypto = require('crypto');

/**
 * replace tabs by indent value
 *
 * @param line
 * @param indent
 * @return string
 */
function properIndentation(line, indent) {
    return line.replace(/\t/, indent || '');
}

/**
 * count leading whitespace chars
 *
 * @param line
 * @return integer
 */
function countWhitespace(line) {
    // return a somewhat high value for empty lines
    return line.length ? parseInt(line.match(/^\s*/)[0].length, 10) : 9999;
}

/**
 * get lowest value of leading whitespace in a given block
 *
 * @param previous
 * @param current
 * @returns integer
 */
function getLeadingWhitespace(previous, current) {
    return previous <= current ? previous : current;
}

/**
 * raise offset in lines
 *
 * @param lines
 * @param offset
 * @returns {Array|*|{}}
 */
function raiseIndent(lines, offset) {
    offset = offset || '    ';

    return lines.map((line) => offset + line);
}

/**
 * Formalize any given value as wrap object
 *
 * @param wrap
 * @returns {{before: '', after: ''}}
 */
function formalizeWrap(wrap) {
    const result = { before: '', after: '' };

    if ((typeof wrap === 'string' && wrap.length > 0) || typeof wrap === 'number') {
        result.before = result.after = wrap;
    } else if  (Array.isArray(wrap) && wrap.length > 0) {
        result.before = [].slice.call(wrap, 0, 1)[0];
        result.after = wrap.length > 1 ? [].slice.call(wrap, 1, 2)[0] : result.before;
    } else if (_.isPlainObject(wrap)) {
        let i = 0;

        // crappy method getting the value of the first and second item in object
        for (let el in wrap) {
            if (!wrap.hasOwnProperty(el)) {
                continue;
            }

            if (i < 2) {
                result.before = wrap[el];
            }

            i++;
        }

        // set value of after to the value of before if after is empty
        result.after = result.after.length < 1 ? result.before : result.after;
    }

    return result;
}

/**
 * read options from annotation
 *
 * e.g.:
 * becomes:
 * {
 *   extract: 'content/element.html',
 *   viewWrap: {before: '
', after: '
'}
     * }
 *
 * @param annotation
 * @param defaults
 * @returns {{}}
 */
function getBlockOptions(annotation, defaults) {
    const optionValues = annotation.split(/\w+\:/)
        .map((item) => item.replace(/|^\s+|\s+$/, ''))
        .filter((item) => !!item.length);
    const optionKeys = annotation.match(/(\w+)\:/gi).map((item) => item.replace(/[^\w]/, ''));

    defaults = defaults || {
            viewWrap: {before: '', after: ''}
        };

    const opts = {};

    optionValues.forEach((v, i) => {
        const k = optionKeys[i];

        if (typeof k !== 'string') {
            return;
        }

        // Treat option value as array if it has a colon
        // @todo: Allow escaped colons to be ignored
        // RegEx lookbehind negate does not work :(
        // Should be /(?<!\\):/
        if (v.indexOf(':') > -1) {
            v = v.split(':');
        }

        opts[k] = v;
    });

    // Process options
    opts.wrap = formalizeWrap(opts.wrap || defaults.viewWrap);

    return opts;
}

/**
 * Format options as string of HTML data parameters
 *
 * @param options
 * @returns {string}
 */
function optionsToDataString(options) {
    if (typeof options !== 'object') {
        return '';
    }

    const prepared = [];
    const processedOptions = Object.assign({}, options);

    // prepare wrap option
    if (processedOptions.hasOwnProperty('wrap')) {
        processedOptions['wrap-before'] = processedOptions.wrap.before;
        processedOptions['wrap-after'] = processedOptions.wrap.after;

        delete(processedOptions.wrap);
    }

    // create data attributes
    for (const el in processedOptions) {
        if (processedOptions.hasOwnProperty(el) === false) {
            continue;
        }

        const value = processedOptions[el];

        // Ignore callbacks
        if (typeof value === 'function') {
            continue;
        }

        // Cleanup: Remove leading and trailing " and ', replace " by ' (e.g. in stringified objects)
        const preparedVal = JSON.stringify(processedOptions[el])
            .replace(/^['"]|['"]$/g, '')
            .replace(/\\?"/g, "'");

        // Build data parameter: data-name="value"
        const param = 'data-' + el + '="' + preparedVal + '"';

        prepared.push(param);
    }

    return prepared.join(' ');
}

/**
 * trim given number of leading characters
 *
 * @param lines
 * @param num Number of chars to be removed
 * @returns Array
 */
function trimLines(lines, num) {
    return lines.map((line) => line.substr(num));
}

/**
 * create sha1 hash from string
 *
 * @param value
 * @returns {*}
 */
function createId(value) {
    return crypto.createHash('sha1').update(value, 'utf8').digest('hex').slice(0, 8);
}

/**
 * get default options, scope as function instead of "public" property
 *
 * @returns {{indent: string, origin: string, resources: {classnames: {root: string, body: string}, meta: Array, scriptsFoot: {files: Array, inline: Array}, scriptsHead: {files: Array, inline: Array}, stylesHead: {files: Array, inline: Array}}, templateWrap: {before: string, after: string}, viewWrap: {before: string, after: string}}}
 */
function getDefaultOptions() {
    const resources = {
        classnames: {
            root: '',
            body: ''
        },
        meta: [],
        scriptsFoot: {
            files: [],
            inline: []
        },
        scriptsHead: {
            files: [],
            inline: []
        },
        stylesHead: {
            files: [],
            inline: []
        }
    };

    return {
        indent: '    ',
        origin: '',
        resources: resources,
        templateWrap: { before: '', after: '' },
        viewWrap: { before: '', after: '' }
    }
}

/**
 * Constructor
 *
 * Use named function to get better backtraces in node
 *
 * @param data
 * @constructor
 */
var InventoryObject = function (data) {
    data                = data || {};

    this.category       = data.hasOwnProperty('category') ? data.category : 'No category';
    this.id             = data.hasOwnProperty('id') ? data.id : '';
    this.group          = data.hasOwnProperty('group') ? data.group : '';
    this.lines          = data.hasOwnProperty('lines') ? data.lines : 0;
    this.name           = data.hasOwnProperty('name') ? data.name : '';
    this.options        = data.hasOwnProperty('options') ? data.options : {};
    this.optionsData    = data.hasOwnProperty('optionsData') ? data.optionsData : '';
    this.origin         = data.hasOwnProperty('origin') ? data.origin : '';
    this.partial        = data.hasOwnProperty('partial') ? data.partial : '';
    this.resources      = data.hasOwnProperty('resources') ? data.resources : Object.assign({}, getDefaultOptions().resources);
    this.template       = data.hasOwnProperty('template') ? data.template : '';
    this.usage          = data.hasOwnProperty('usage') ? data.usage : [];
    this.view           = data.hasOwnProperty('view') ? data.view : '';
    this.viewId         = data.hasOwnProperty('viewId') ? data.viewId : '';
};

/**
 * parse data from extracted part
 *
 * @param src
 * @param opts
 */
InventoryObject.prototype.parseData = function(src, opts) {
    opts = Object.assign({}, getDefaultOptions(), opts);

    // remove ending annotation
    src = src.replace(//gi, '')

    const parts = src.match(/(?:((?:.|\n)*?)/i);
    const blockOpts = getBlockOptions(parts[1], opts);

    // remove extraction annotation and trim
    const content = _.trimEnd(_.trimStart(src.replace(//gi, ''), '\n\r'));

    // continue if name is empty
    if (!blockOpts.hasOwnProperty('extract')) {
        return;
    }

    // label from name property and fallback to extract value
    const name = blockOpts.hasOwnProperty('name') ? blockOpts.name : blockOpts.extract;

    // set category name
    const category = blockOpts.hasOwnProperty('category') ? blockOpts.category : this.category;

    // set group
    const group = blockOpts.hasOwnProperty('group') ? blockOpts.group : this.group;

    // process source code
    let lines = content.split('\n')
        .map((line) => _.trimEnd(line))
        .map((line) => properIndentation(line, opts.indent));
    const leadingWhitespace = lines.map(countWhitespace);
    const crop = leadingWhitespace.reduce(getLeadingWhitespace);
    const viewWrap = blockOpts.wrap;
    const templateWrapOptions = optionsToDataString(blockOpts);

    lines = trimLines(lines, crop);

    let viewLines = util._extend([], lines);
    let templateLines = util._extend([], lines);

    // wrap partial if inline option viewWrap: exists
    if (viewWrap.before.length) {
        viewLines = raiseIndent(viewLines);
        viewLines.unshift('');
        viewLines.unshift(viewWrap.before);
        viewLines.push('');
        viewLines.push(viewWrap.after);
    }

    // add templateWrap
    if (typeof opts.templateWrap === 'object') {
        let before = opts.templateWrap.before || '';
        let after = opts.templateWrap.after || '';

        before = before.replace('{{wrapData}}', templateWrapOptions);
        after = after.replace('{{wrapData}}', templateWrapOptions);

        templateLines.unshift(before);
        templateLines.push(after);
    }

    // set properties
    this.category       = category;
    this.group          = group;
    // remove all whitespace chars before creating the hash
    this.id             = createId(src.replace(/\s+/gi, ''));
    this.lines          = lines;
    this.name           = name;
    this.options        = blockOpts;
    this.optionsData    = templateWrapOptions;
    this.partial        = lines.join(os.EOL);
    this.resources      = opts.resources;
    this.template       = templateLines.join(os.EOL);
    this.view           = viewLines.join(os.EOL);
    this.viewId         = createId(this.view.replace(/\s+/gi, ''));
};

/**
 * set inventory object property if prop is a valid property name (property exists)
 *
 * @param prop
 * @param value
 * @returns {boolean}
 */
InventoryObject.prototype.setProperty = function (prop, value) {
    if (typeof prop !== 'string' || !this.hasOwnProperty(prop)) {
        return false;
    }

    this[prop] = value;

    return true;
};

/**
 * module
 * @param data
 * @returns {*}
 */
module.exports = InventoryObject;
<|endoftext|>


<commit_before>var util = require('util'),
	request = require('request'),
	events = require('events'),
	cheerio = require('cheerio'),
	URI = require('uri-js');

var debug;

if (/\bmetainspector\b/.test(process.env.NODE_DEBUG)) {
  debug = function() {
    console.error('METAINSPECTOR %s', util.format.apply(util, arguments));
  };
} else {
  debug = function() {};
}

function withDefaultScheme(url){
	return URI.parse(url).scheme ? url : "http://" + url;
}

var MetaInspector = function(url, options){
	this.url = URI.normalize(withDefaultScheme(url));
	this.options = options || {};

	this.parsedUrl = URI.parse(this.url);
	this.scheme = this.parsedUrl.scheme;
	this.host = this.parsedUrl.host;
	this.rootUrl = this.scheme + "://" + this.host;

	//default to a sane limit, since for meta-inspector usually 5 redirects should do a job
	//more over beyond this there could be an issue with event emitter loop detection with new nodejs version
	//which prevents error event from getting fired
	this.maxRedirects = this.options.maxRedirects || 5;

	//some urls are timing out after one minute, hence need to specify a reasoable default timeout
	this.timeout = this.options.timeout || 20000; //Timeout in ms

	this.strictSSL = this.options.strictSSL === false ? false : true;
};

//MetaInspector.prototype = new events.EventEmitter();
MetaInspector.prototype.__proto__ = events.EventEmitter.prototype;

module.exports = MetaInspector;

MetaInspector.prototype.getTitle = function()
{
	debug("Parsing page title");

	if(this.title === undefined)
	{
		this.title = this.parsedDocument('title').text();
	}

	return this;
}

MetaInspector.prototype.getOgTitle = function()
{
	debug("Parsing page Open Graph title");

	if(this.ogTitle === undefined)
	{
		this.ogTitle = this.parsedDocument("meta[property='og:title']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getLinks = function()
{
	debug("Parsing page links");

	var _this = this;

	if(!this.links)
	{
		this.links = this.parsedDocument('a').map(function(i ,elem){
			return _this.parsedDocument(this).attr('href');
		});
	}

	return this;
}

MetaInspector.prototype.getMetaDescription = function()
{
	debug("Parsing page description based on meta elements");

	if(!this.description)
	{
		this.description = this.parsedDocument("meta[name='description']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getSecondaryDescription = function()
{
	debug("Parsing page secondary description");
	var _this = this;

	if(!this.description)
	{
		var minimumPLength = 120;

		this.parsedDocument("p").each(function(i, elem){
			if(_this.description){
				return;
			}

			var text = _this.parsedDocument(this).text();

			// If we found a paragraph with more than
			if(text.length >= minimumPLength) {
				_this.description = text;
			}
		});
	}

	return this;
}

MetaInspector.prototype.getDescription = function()
{
	debug("Parsing page description based on meta description or secondary description");
	this.getMetaDescription() && this.getSecondaryDescription();

	return this;
}

MetaInspector.prototype.getKeywords = function()
{
	debug("Parsing page keywords from apropriate metatag");

	if(!this.keywords)
	{
		var keywordsString = this.parsedDocument("meta[name='keywords']").attr("content");

		if(keywordsString) {
			this.keywords = keywordsString.split(',');
		} else {
			this.keywords = [];
		}
	}

	return this;
}

MetaInspector.prototype.getAuthor = function()
{
	debug("Parsing page author from apropriate metatag");

	if(!this.author)
	{
		this.author = this.parsedDocument("meta[name='author']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getCharset = function()
{
	debug("Parsing page charset from apropriate metatag");

	if(!this.charset)
	{
		this.charset = this.parsedDocument("meta[charset]").attr("charset");
	}

	return this;
}

MetaInspector.prototype.getImage = function()
{
	debug("Parsing page image based on the Open Graph image");

	if(!this.image)
	{
		this.image = this.parsedDocument("meta[property='og:image']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getImages = function()
{
	debug("Parsing page body images");
	var _this = this;

	if(this.images === undefined)
	{
		this.images = this.parsedDocument('img').map(function(i ,elem){
			var src = _this.parsedDocument(this).attr('src');
			return _this.getAbsolutePath(src);
		});
	}

	return this;
}

MetaInspector.prototype.getFeeds = function()
{
	debug("Parsing page feeds based on rss or atom feeds");

	if(!this.feeds)
	{
		this.feeds = this.parseFeeds("rss") || this.parseFeeds("atom");
	}

	return this;
}

MetaInspector.prototype.parseFeeds = function(format)
{
	var _this = this;
	var feeds = this.parsedDocument("link[type='application/" + format + "+xml']").map(function(i ,elem){
		return _this.parsedDocument(this).attr('href');
	});

	return feeds;
}

MetaInspector.prototype.initAllProperties = function()
{
	// title of the page, as string
	this.getTitle()
			.getAuthor()
			.getCharset()
			.getKeywords()
			.getLinks()
			.getDescription()
			.getImage()
			.getImages()
			.getFeeds()
			.getOgTitle();
}

MetaInspector.prototype.getAbsolutePath = function(href){
	if((/^(http:|https:)?\/\//i).test(href)) { return href; }
	if(!(/^\//).test(href)){ href = '/' + href; }
	return this.rootUrl + href;
};

MetaInspector.prototype.fetch = function(){
	var _this = this;
	var totalChunks = 0;

	var r = request({uri : this.url, gzip: true, maxRedirects: this.maxRedirects, timeout: this.timeout, strictSSL: this.strictSSL}, function(error, response, body){
		if(!error && response.statusCode === 200){
			_this.document = body;
			_this.parsedDocument = cheerio.load(body);
			_this.response = response;

			_this.initAllProperties();

			_this.emit("fetch");
		}
		else{
			_this.emit("error", error);
		}
	});

	if(_this.options.limit){
		_this.__stoppedAtLimit = false;
		r.on('data', function(chunk){
			totalChunks += chunk.length;
			if(totalChunks > _this.options.limit){
				if(!_this.__stoppedAtLimit) {
					_this.emit("limit");
					_this.__stoppedAtLimit = true;
				}
				r.abort();
			}
		});
	}
};
<commit_msg>Added support for og:description<commit_after>var util = require('util'),
	request = require('request'),
	events = require('events'),
	cheerio = require('cheerio'),
	URI = require('uri-js');

var debug;

if (/\bmetainspector\b/.test(process.env.NODE_DEBUG)) {
  debug = function() {
    console.error('METAINSPECTOR %s', util.format.apply(util, arguments));
  };
} else {
  debug = function() {};
}

function withDefaultScheme(url){
	return URI.parse(url).scheme ? url : "http://" + url;
}

var MetaInspector = function(url, options){
	this.url = URI.normalize(withDefaultScheme(url));
	this.options = options || {};

	this.parsedUrl = URI.parse(this.url);
	this.scheme = this.parsedUrl.scheme;
	this.host = this.parsedUrl.host;
	this.rootUrl = this.scheme + "://" + this.host;

	//default to a sane limit, since for meta-inspector usually 5 redirects should do a job
	//more over beyond this there could be an issue with event emitter loop detection with new nodejs version
	//which prevents error event from getting fired
	this.maxRedirects = this.options.maxRedirects || 5;

	//some urls are timing out after one minute, hence need to specify a reasoable default timeout
	this.timeout = this.options.timeout || 20000; //Timeout in ms

	this.strictSSL = this.options.strictSSL === false ? false : true;
};

//MetaInspector.prototype = new events.EventEmitter();
MetaInspector.prototype.__proto__ = events.EventEmitter.prototype;

module.exports = MetaInspector;

MetaInspector.prototype.getTitle = function()
{
	debug("Parsing page title");

	if(this.title === undefined)
	{
		this.title = this.parsedDocument('title').text();
	}

	return this;
}

MetaInspector.prototype.getOgTitle = function()
{
	debug("Parsing page Open Graph title");

	if(this.ogTitle === undefined)
	{
		this.ogTitle = this.parsedDocument("meta[property='og:title']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getOgDescription = function()
{
	debug("Parsing page Open Graph description");

	if(this.ogDescription === undefined)
	{
		this.ogDescription = this.parsedDocument("meta[property='og:description']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getLinks = function()
{
	debug("Parsing page links");

	var _this = this;

	if(!this.links)
	{
		this.links = this.parsedDocument('a').map(function(i ,elem){
			return _this.parsedDocument(this).attr('href');
		});
	}

	return this;
}

MetaInspector.prototype.getMetaDescription = function()
{
	debug("Parsing page description based on meta elements");

	if(!this.description)
	{
		this.description = this.parsedDocument("meta[name='description']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getSecondaryDescription = function()
{
	debug("Parsing page secondary description");
	var _this = this;

	if(!this.description)
	{
		var minimumPLength = 120;

		this.parsedDocument("p").each(function(i, elem){
			if(_this.description){
				return;
			}

			var text = _this.parsedDocument(this).text();

			// If we found a paragraph with more than
			if(text.length >= minimumPLength) {
				_this.description = text;
			}
		});
	}

	return this;
}

MetaInspector.prototype.getDescription = function()
{
	debug("Parsing page description based on meta description or secondary description");
	this.getMetaDescription() && this.getSecondaryDescription();

	return this;
}

MetaInspector.prototype.getKeywords = function()
{
	debug("Parsing page keywords from apropriate metatag");

	if(!this.keywords)
	{
		var keywordsString = this.parsedDocument("meta[name='keywords']").attr("content");

		if(keywordsString) {
			this.keywords = keywordsString.split(',');
		} else {
			this.keywords = [];
		}
	}

	return this;
}

MetaInspector.prototype.getAuthor = function()
{
	debug("Parsing page author from apropriate metatag");

	if(!this.author)
	{
		this.author = this.parsedDocument("meta[name='author']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getCharset = function()
{
	debug("Parsing page charset from apropriate metatag");

	if(!this.charset)
	{
		this.charset = this.parsedDocument("meta[charset]").attr("charset");
	}

	return this;
}

MetaInspector.prototype.getImage = function()
{
	debug("Parsing page image based on the Open Graph image");

	if(!this.image)
	{
		this.image = this.parsedDocument("meta[property='og:image']").attr("content");
	}

	return this;
}

MetaInspector.prototype.getImages = function()
{
	debug("Parsing page body images");
	var _this = this;

	if(this.images === undefined)
	{
		this.images = this.parsedDocument('img').map(function(i ,elem){
			var src = _this.parsedDocument(this).attr('src');
			return _this.getAbsolutePath(src);
		});
	}

	return this;
}

MetaInspector.prototype.getFeeds = function()
{
	debug("Parsing page feeds based on rss or atom feeds");

	if(!this.feeds)
	{
		this.feeds = this.parseFeeds("rss") || this.parseFeeds("atom");
	}

	return this;
}

MetaInspector.prototype.parseFeeds = function(format)
{
	var _this = this;
	var feeds = this.parsedDocument("link[type='application/" + format + "+xml']").map(function(i ,elem){
		return _this.parsedDocument(this).attr('href');
	});

	return feeds;
}

MetaInspector.prototype.initAllProperties = function()
{
	// title of the page, as string
	this.getTitle()
			.getAuthor()
			.getCharset()
			.getKeywords()
			.getLinks()
			.getDescription()
			.getImage()
			.getImages()
			.getFeeds()
			.getOgTitle()
			.getOgDescription();
}

MetaInspector.prototype.getAbsolutePath = function(href){
	if((/^(http:|https:)?\/\//i).test(href)) { return href; }
	if(!(/^\//).test(href)){ href = '/' + href; }
	return this.rootUrl + href;
};

MetaInspector.prototype.fetch = function(){
	var _this = this;
	var totalChunks = 0;

	var r = request({uri : this.url, gzip: true, maxRedirects: this.maxRedirects, timeout: this.timeout, strictSSL: this.strictSSL}, function(error, response, body){
		if(!error && response.statusCode === 200){
			_this.document = body;
			_this.parsedDocument = cheerio.load(body);
			_this.response = response;

			_this.initAllProperties();

			_this.emit("fetch");
		}
		else{
			_this.emit("error", error);
		}
	});

	if(_this.options.limit){
		_this.__stoppedAtLimit = false;
		r.on('data', function(chunk){
			totalChunks += chunk.length;
			if(totalChunks > _this.options.limit){
				if(!_this.__stoppedAtLimit) {
					_this.emit("limit");
					_this.__stoppedAtLimit = true;
				}
				r.abort();
			}
		});
	}
};
<|endoftext|>


<commit_before>var CLIUpdate = module.exports = {};
CLIUpdate.current = 0;
CLIUpdate.history = [];
CLIUpdate.render = function (output, pushHistory) {
    if (pushHistory !== false) {
        CLIUpdate.history.push(output);
        CLIUpdate.current = CLIUpdate.history.length - 1;
    }
    console.log(output);
};

CLIUpdate.back = function () {
    var o = CLIUpdate.history[--CLIUpdate.current];
    //console.log("------------", CLIUpdate.current, CLIUpdate.history.length);
    if (!o) { return ++CLIUpdate.current; }
    CLIUpdate.render(o, true);
};

CLIUpdate.next = function () {
    var o = CLIUpdate.history[++CLIUpdate.current];
    if (!o) { return --CLIUpdate.current; }
    CLIUpdate.render(o, true);
};
<commit_msg>Navigation data<commit_after>var CLIUpdate = module.exports = {};
CLIUpdate.current = 0;
CLIUpdate.history = [];
CLIUpdate.navigation = function () {};
CLIUpdate.render = function (output, pushHistory, data) {
    if (pushHistory !== false) {
        CLIUpdate.history.push({
            content: output
          , data: data
        });
        CLIUpdate.current = CLIUpdate.history.length - 1;
    }
    console.log(output);
};

CLIUpdate.back = function () {
    var o = CLIUpdate.history[--CLIUpdate.current];
    if (!o) { return ++CLIUpdate.current; }
    CLIUpdate.navigation(o.data);
    CLIUpdate.render(o.content, false);
};

CLIUpdate.next = function () {
    var o = CLIUpdate.history[++CLIUpdate.current];
    if (!o) { return --CLIUpdate.current; }
    CLIUpdate.navigation(o.data);
    CLIUpdate.render(o.content, false);
};
<|endoftext|>