<commit_before>// require
const { promisify:prm } = require('util')
const { join, dirname } = require('path')
const fs = require('fs')
const readFile = prm(fs.readFile),
	writeFile = prm(fs.writeFile)
const { spawn } = require('child_process')
const readline = require('readline')
const express = require('express')
//var fs = require('fs')
//var sh = require('shelljs')
//const glob = require('glob')

// global Msa object
global.Msa = global.MySimpleApp = {}
Msa.dirname = __dirname
//Msa.compsDir = Msa.dirname+'/bower_components'
//Msa.compsUrl = "/bower_components"
Msa.express = express
Msa.bodyParser = require("body-parser")
//Msa.jsonBodyMdw = Msa.bodyMdw.json()
Msa.paramsFiles = []

// html expr
require('./htmlExpr')

// params
require('./params')

// main //////////////////////

const help =
`Script to install or start a MySimpleApp server

Usage: node ${process.argv[1]} [ACTION] [MODULE] [OPTIONS]

  ACTION: "install" or "start". If not provided, both actions will be executed.

  MODULE (install only): module to install. If not provided, all modules are installed.

  OPTIONS:
    -p/--params: MSA parameters (JSON format)
    -pf/--params-file: Path to file containing MSA parameters (JSON format, default: "msa_params.json")
    -y/--yes (install only): Automatically reply with default value to all questions
    -f/--force (install only): Force re-intall, already installed modules`

const main = async function(next){
	try {

		// get input args
		const argv = process.argv
		var action, mod, params=[], paramsFiles=[], yes=false, force=false
		for(let i=2; i<argv.length; ++i){
			arg = argv[i]
			if(arg==="-h" || arg==="--help") { console.log(help); return next() }
			else if(arg==="-p" || arg==="--params") params.push(argv[++i])
			else if(arg==="-pf" || arg==="--params-file") paramsFiles.push(argv[++i])
			else if(arg==="-y" || arg==="--yes") yes = true
			else if(arg==="-f" || arg==="--force") force = true
			else if(!action && arg[0]!=='-') action = arg
			else if(action && !mod && arg[0]!=='-') mod = arg
			else { console.error(`Unknown option ${arg}`); process.exit(1) }
		}

		// fill Msa.paramsFiles
		const defParamFile = join(__dirname, "msa_params.json")
		if(await fileExists(defParamFile))
			Msa.paramsFiles.push(defParamFile)
		for(let f of paramsFiles)
			Msa.paramsFiles.push(f)

		// fill Msa.params
		for(let f of Msa.paramsFiles){
			try {
				const p = await readFile(f)
				deepMerge(Msa.params, JSON.parse(p))
			} catch(err) {
				console.warn(`Could not read & parse params file "${f}"`)
				console.log(err)
			}
		}
		for(let p of params)
			deepMerge(Msa.params, JSON.parse(p))

		// action
		if(!action || action == "install")
			await installMsa({ mod, yes, force })
		if(!action || action == "start")
			await startMsa()

	} catch(err) { next(err) }
	next()
}

// install //////////////////////////////////

const installMsa = function({ mod=null, yes=false, force=false, itf=null } = {}){
	return new Promise(async (ok, ko) => {
		try {
			const mods = mod ? [mod] : Object.values(Msa.params.msa_modules || {})
			if(!itf) itf = new Msa.InstallInterface({ yes:yes, force:force })
			for(let m of mods){
				await itf.install(m)
			}
		} catch(err) { return ko(err) }
		ok()
	})
}

// interface

Msa.InstallInterface = class {
	constructor({ yes=false, force=false } = {}){
		this.yes = yes
		this.force = force
		this.installedMods = []
	}
}
var InstallInterfacePt = Msa.InstallInterface.prototype

InstallInterfacePt.log = function(...args){
	console.log(...args)
}

InstallInterfacePt.exec = function(cmd, args, kwargs){
	return new Promise((ok, ko) => {
		try {
			const spawn_kwargs = Object.assign({ stdio: 'inherit', cwd: Msa.dirname }, kwargs)
			const proc = spawn(cmd, args, spawn_kwargs)
			proc.on('close', code => {
				if (code !== 0) ko(code)
				else ok()
			})
		} catch(err) { return ko(err) }
	})
}

InstallInterfacePt.npm = function(...args){
	return this.exec('npm', args)
}

InstallInterfacePt.question = function(question){
	return new Promise(async (ok, ko) => {
		try {
			if(isArr(question)) {
				var res = []
				for(var q of question)
					res.push( await this.question(q) )
				ok(res)
			} else {
				if(typeof question === "string") question = { question }
				var questionStr = question.question
				if(question.defVal !== undefined) questionStr += ` (default value: ${question.defVal} )`
				if(question.choices !== undefined) questionStr += ` (possible values: ${question.choices.join(' / ')} )`
				const rl = readline.createInterface({
					input: process.stdin,
					output: process.stdout
				})
				rl.question(questionStr +' ', answer => {
					rl.close()
					if(answer==="" && question.defVal!==undefined) answer = question.defVal
					ok(answer)
				})
			}
		} catch(err) { return ko(err) }
	})
}

InstallInterfacePt.questionParam = function(param){
	return new Promise(async (ok, ko) => {
		var res
		try {
			// select & format params to be questionned
			var params = isArr(param) ? param : [param]
			params = params.map(p => (typeof p === "string") ? { key:p } : p)
			if(!this.force) params = params.filter(p => Msa.getParam(p.key) === undefined)
			// format questions
			let questions = []
			for(let p of params) {
				const paramKey = p.key, paramDef = Msa.paramDefs[paramKey]
				const question = p.question || `Choose a value for this parameter "${paramKey}"`
				const { choices, defVal } = Object.assign(p, paramDef)
				questions.push({ question, choices, defVal })
			}
			// ask questions
			res = await this.question(questions)
			// update params
			for(let i=0, len=params.length; i<len; ++i)
				Msa.setParam(params[i].key, res[i])
		} catch(err) { return ko(err) }
		ok(res)
	})
}

InstallInterfacePt.install = function(mod, kwargs){
	return new Promise(async (ok, ko) => {
		try {
			// check that this module has not already been installed by this itf (to avoid infinite loop)
			if(this.installedMods.indexOf(mod) > -1) return ok()
			this.installedMods.push(mod)
			// npm install
			const dir = ( kwargs && kwargs.dir ) || Msa.dirname
			var modPath = tryResolve(mod, { paths:[dir] })
			if(this.force || !modPath) {
				this.log(`### install "${mod}"`)
				await this.exec('npm', ['install', mod], { cwd:dir })
				modPath = require.resolve(mod)
			}
			// msa_install
			const msaInstallPath = tryResolve( join(dirname(modPath), "msa_install") )
			if(msaInstallPath) {
				const msaInstallPrm = prm(require(msaInstallPath))
				await msaInstallPrm(this)
			}
		} catch(err) { return ko(err) }
		ok()
	})
}

InstallInterfacePt.installMsaMod = function(modKey, mod, kwargs){
	const pMod = Msa.getParam(`msa_modules.${modKey}`)
	if(pMod) mod = pMod
	return this.install(mod, kwargs)
}

// start //////////////////////////////////

const startMsa = function() {
	return new Promise(async (ok, ko) => {
		try {
			// create modules router
//			Msa.preRouter = express()
			Msa.modulesRouter = express.Router()
			var msa_modules = Msa.params.msa_modules || {}
			// start msa modules
			for(let modRoute in msa_modules)
				await Msa.start(msa_modules[modRoute])
			// create msa router
			var msaMod = new Msa.Module("msa")
			initMsaModule(msaMod, __dirname)
			// require msa modules
			for(let modRoute in msa_modules){
				let modDir = join(Msa.dirname, "node_modules", msa_modules[modRoute]),
					mod = require(modDir)
				initMsaModule(mod, modDir)
			}
			// use main moduke
			Msa.app = Msa.mainMod.app
			// start server
			startServer()
		} catch(err) { return ko(err) }
		ok()
	})
}

const startedMods = {}
Msa.start = function(mod){
	return new Promise(async (ok, ko) => {
		try {
			// start only once
			var res = startedMods[mod]
			if(res !== undefined) return ok(res)
			res = startedMods[mod] = null
			// msa_install
			const modPath = require.resolve(mod, { paths:[Msa.dirname] })
			const msaStartPath = tryResolve( join(dirname(modPath), "msa_start") )
			if(msaStartPath) {
				const msaStartPrm = prm(require(msaStartPath))
				res = startedMods[mod] = await msaStartPrm()
			}
			ok(res)
		} catch(err){ ko(err) }
	})
}

var startServer = function() {
  var paramsServer = Msa.params.server,
      isHttps = paramsServer.https.activated
  // create server
  if(isHttps) {
    var https = require('https')
    var cred = {
      key: fs.readFileSync(paramsServer.https.key),
      cert: fs.readFileSync(paramsServer.https.cert)
    }
    var server = https.createServer(cred, Msa.app)
  } else {
    var http = require('http')
    var server = http.createServer(Msa.app)
  }
  // determine port
  var port = paramsServer.port
  if(port=="dev") port = isHttps ? 8443 : 8080
  if(port=="prd") port = isHttps ? 81 : 80
  // start server
  server.listen(port)
  // log
  var prot = isHttps ? "https" : "http"
  console.log("Server ready: "+prot+"://localhost:"+port+"/")
}

// modules

Msa.modules = {}

Msa.Module = class {
	constructor(key) {
		this.key = key
		this.app = Msa.subApp()
	}
}

Msa.subApp = function() {
	const oSubApp = express()
//	oSubApp.getAsPartial = subApp_getAsPartial
	oSubApp.subApp = subApp_subApp
	return oSubApp
}

Msa.require = function(key){
  const modDir = join(Msa.dirname, "node_modules", Msa.params.msa_modules[key])
  return require(modDir)
}
/*
var requireMsaModules = function() {
  var msa_modules = Msa.params.msa_modules || {}
  for(let route in msa_modules){
    let modDir = join(Msa.dirname, "node_modules", msa_modules[route]),
        mod = require(modDir)
    initMsaModule(mod, modDir)
  }
}
*/
var initMsaModule = function(mod, modDir) {
  // static files
  mod.dirname = modDir
  var staticDir = modDir+"/static"
  fs.stat(staticDir, (err, stats) => {
    if(!err && stats && stats.isDirectory())
      mod.app.use(Msa.express.static(staticDir))
  })
  // register module
  var key = mod.key
  if(key==='') Msa.mainMod = mod
  else {
    Msa.modules[key] = mod
    Msa.modulesRouter.use('/'+key, mod.app)
  }
}

const subApp_subApp = function(route) {
	var oSubApp = Msa.subApp()
	this.use(route, oSubApp)
	return oSubApp
}
/*
// partial

const Partial = Msa.Partial = class {
	constructor(htmlExpr) {
		formatHtml(htmlExpr, this)
	}
}

// shortcut function to server HTML content as partial
const subApp_getAsPartial = function(route, htmlExpr) {
//  var partial = Msa.formatHtml(htmlExpr)
	const partial = new Partial(htmlExpr)
	return this.get(route, function(req, res, next) {
//		res.partial = partial
		next(partial)
	})
}
*/

// utils

const { isArray: isArr } = Array

const { formatHtml } = Msa

Msa.joinUrl = function(...args){
	return args.join('/').replace(/\/+/g,'/')
}

const deepMerge = function(obj1, obj2) {
	for(let k in obj2) {
		if(typeof obj1[k] === "object" && typeof obj2[k] === "object")
			deepMerge(obj1[k], obj2[k])
		else obj1[k] = obj2[k]
	}
}

const fileExists = function(path) {
	return new Promise((ok, ko) => {
		try {
			fs.access(path, err => {
				ok(err ? false : true)
			})
		} catch(err) { return ko(err) }
	})
}

const tryResolve = function(name, kwargs) {
	var res
	try {
		res = require.resolve(name, kwargs)
	} catch(e) {}
	return res
}

// run main
main(err => {
	if(err) {
		console.error(err)
		process.exit(1)
	}
})
<commit_msg>Add checkStaticDir option<commit_after>// require
const { promisify:prm } = require('util')
const { join, dirname } = require('path')
const fs = require('fs')
const readFile = prm(fs.readFile),
	writeFile = prm(fs.writeFile)
const { spawn } = require('child_process')
const readline = require('readline')
const express = require('express')
//var fs = require('fs')
//var sh = require('shelljs')
//const glob = require('glob')

// global Msa object
global.Msa = global.MySimpleApp = {}
Msa.dirname = __dirname
//Msa.compsDir = Msa.dirname+'/bower_components'
//Msa.compsUrl = "/bower_components"
Msa.express = express
Msa.bodyParser = require("body-parser")
//Msa.jsonBodyMdw = Msa.bodyMdw.json()
Msa.paramsFiles = []

// html expr
require('./htmlExpr')

// params
require('./params')

// main //////////////////////

const help =
`Script to install or start a MySimpleApp server

Usage: node ${process.argv[1]} [ACTION] [MODULE] [OPTIONS]

  ACTION: "install" or "start". If not provided, both actions will be executed.

  MODULE (install only): module to install. If not provided, all modules are installed.

  OPTIONS:
    -p/--params: MSA parameters (JSON format)
    -pf/--params-file: Path to file containing MSA parameters (JSON format, default: "msa_params.json")
    -y/--yes (install only): Automatically reply with default value to all questions
    -f/--force (install only): Force re-intall, already installed modules`

const main = async function(next){
	try {

		// get input args
		const argv = process.argv
		var action, mod, params=[], paramsFiles=[], yes=false, force=false
		for(let i=2; i<argv.length; ++i){
			arg = argv[i]
			if(arg==="-h" || arg==="--help") { console.log(help); return next() }
			else if(arg==="-p" || arg==="--params") params.push(argv[++i])
			else if(arg==="-pf" || arg==="--params-file") paramsFiles.push(argv[++i])
			else if(arg==="-y" || arg==="--yes") yes = true
			else if(arg==="-f" || arg==="--force") force = true
			else if(!action && arg[0]!=='-') action = arg
			else if(action && !mod && arg[0]!=='-') mod = arg
			else { console.error(`Unknown option ${arg}`); process.exit(1) }
		}

		// fill Msa.paramsFiles
		const defParamFile = join(__dirname, "msa_params.json")
		if(await fileExists(defParamFile))
			Msa.paramsFiles.push(defParamFile)
		for(let f of paramsFiles)
			Msa.paramsFiles.push(f)

		// fill Msa.params
		for(let f of Msa.paramsFiles){
			try {
				const p = await readFile(f)
				deepMerge(Msa.params, JSON.parse(p))
			} catch(err) {
				console.warn(`Could not read & parse params file "${f}"`)
				console.log(err)
			}
		}
		for(let p of params)
			deepMerge(Msa.params, JSON.parse(p))

		// action
		if(!action || action == "install")
			await installMsa({ mod, yes, force })
		if(!action || action == "start")
			await startMsa()

	} catch(err) { next(err) }
	next()
}

// install //////////////////////////////////

const installMsa = function({ mod=null, yes=false, force=false, itf=null } = {}){
	return new Promise(async (ok, ko) => {
		try {
			const mods = mod ? [mod] : Object.values(Msa.params.msa_modules || {})
			if(!itf) itf = new Msa.InstallInterface({ yes:yes, force:force })
			for(let m of mods){
				await itf.install(m)
			}
		} catch(err) { return ko(err) }
		ok()
	})
}

// interface

Msa.InstallInterface = class {
	constructor({ yes=false, force=false } = {}){
		this.yes = yes
		this.force = force
		this.installedMods = []
	}
}
var InstallInterfacePt = Msa.InstallInterface.prototype

InstallInterfacePt.log = function(...args){
	console.log(...args)
}

InstallInterfacePt.exec = function(cmd, args, kwargs){
	return new Promise((ok, ko) => {
		try {
			const spawn_kwargs = Object.assign({ stdio: 'inherit', cwd: Msa.dirname }, kwargs)
			const proc = spawn(cmd, args, spawn_kwargs)
			proc.on('close', code => {
				if (code !== 0) ko(code)
				else ok()
			})
		} catch(err) { return ko(err) }
	})
}

InstallInterfacePt.npm = function(...args){
	return this.exec('npm', args)
}

InstallInterfacePt.question = function(question){
	return new Promise(async (ok, ko) => {
		try {
			if(isArr(question)) {
				var res = []
				for(var q of question)
					res.push( await this.question(q) )
				ok(res)
			} else {
				if(typeof question === "string") question = { question }
				var questionStr = question.question
				if(question.defVal !== undefined) questionStr += ` (default value: ${question.defVal} )`
				if(question.choices !== undefined) questionStr += ` (possible values: ${question.choices.join(' / ')} )`
				const rl = readline.createInterface({
					input: process.stdin,
					output: process.stdout
				})
				rl.question(questionStr +' ', answer => {
					rl.close()
					if(answer==="" && question.defVal!==undefined) answer = question.defVal
					ok(answer)
				})
			}
		} catch(err) { return ko(err) }
	})
}

InstallInterfacePt.questionParam = function(param){
	return new Promise(async (ok, ko) => {
		var res
		try {
			// select & format params to be questionned
			var params = isArr(param) ? param : [param]
			params = params.map(p => (typeof p === "string") ? { key:p } : p)
			if(!this.force) params = params.filter(p => Msa.getParam(p.key) === undefined)
			// format questions
			let questions = []
			for(let p of params) {
				const paramKey = p.key, paramDef = Msa.paramDefs[paramKey]
				const question = p.question || `Choose a value for this parameter "${paramKey}"`
				const { choices, defVal } = Object.assign(p, paramDef)
				questions.push({ question, choices, defVal })
			}
			// ask questions
			res = await this.question(questions)
			// update params
			for(let i=0, len=params.length; i<len; ++i)
				Msa.setParam(params[i].key, res[i])
		} catch(err) { return ko(err) }
		ok(res)
	})
}

InstallInterfacePt.install = function(mod, kwargs){
	return new Promise(async (ok, ko) => {
		try {
			// check that this module has not already been installed by this itf (to avoid infinite loop)
			if(this.installedMods.indexOf(mod) > -1) return ok()
			this.installedMods.push(mod)
			// npm install
			const dir = ( kwargs && kwargs.dir ) || Msa.dirname
			var modPath = tryResolve(mod, { paths:[dir] })
			if(this.force || !modPath) {
				this.log(`### install "${mod}"`)
				await this.exec('npm', ['install', mod], { cwd:dir })
				modPath = require.resolve(mod)
			}
			// msa_install
			const msaInstallPath = tryResolve( join(dirname(modPath), "msa_install") )
			if(msaInstallPath) {
				const msaInstallPrm = prm(require(msaInstallPath))
				await msaInstallPrm(this)
			}
		} catch(err) { return ko(err) }
		ok()
	})
}

InstallInterfacePt.installMsaMod = function(modKey, mod, kwargs){
	const pMod = Msa.getParam(`msa_modules.${modKey}`)
	if(pMod) mod = pMod
	return this.install(mod, kwargs)
}

// start //////////////////////////////////

const startMsa = function() {
	return new Promise(async (ok, ko) => {
		try {
			// create modules router
//			Msa.preRouter = express()
			Msa.modulesRouter = express.Router()
			var msa_modules = Msa.params.msa_modules || {}
			// start msa modules
			for(let modRoute in msa_modules)
				await Msa.start(msa_modules[modRoute])
			// create msa router
			var msaMod = new Msa.Module("msa")
			initMsaModule(msaMod, __dirname)
			// require msa modules
			for(let modRoute in msa_modules){
				let modDir = join(Msa.dirname, "node_modules", msa_modules[modRoute]),
					mod = require(modDir)
				initMsaModule(mod, modDir)
			}
			// use main moduke
			Msa.app = Msa.mainMod.app
			// start server
			startServer()
		} catch(err) { return ko(err) }
		ok()
	})
}

const startedMods = {}
Msa.start = function(mod){
	return new Promise(async (ok, ko) => {
		try {
			// start only once
			var res = startedMods[mod]
			if(res !== undefined) return ok(res)
			res = startedMods[mod] = null
			// msa_install
			const modPath = require.resolve(mod, { paths:[Msa.dirname] })
			const msaStartPath = tryResolve( join(dirname(modPath), "msa_start") )
			if(msaStartPath) {
				const msaStartPrm = prm(require(msaStartPath))
				res = startedMods[mod] = await msaStartPrm()
			}
			ok(res)
		} catch(err){ ko(err) }
	})
}

var startServer = function() {
  var paramsServer = Msa.params.server,
      isHttps = paramsServer.https.activated
  // create server
  if(isHttps) {
    var https = require('https')
    var cred = {
      key: fs.readFileSync(paramsServer.https.key),
      cert: fs.readFileSync(paramsServer.https.cert)
    }
    var server = https.createServer(cred, Msa.app)
  } else {
    var http = require('http')
    var server = http.createServer(Msa.app)
  }
  // determine port
  var port = paramsServer.port
  if(port=="dev") port = isHttps ? 8443 : 8080
  if(port=="prd") port = isHttps ? 81 : 80
  // start server
  server.listen(port)
  // log
  var prot = isHttps ? "https" : "http"
  console.log("Server ready: "+prot+"://localhost:"+port+"/")
}

// modules

Msa.modules = {}

Msa.Module = class {
	constructor(key) {
		this.key = key
		this.app = Msa.subApp()
	}
}

Msa.subApp = function() {
	const oSubApp = express()
//	oSubApp.getAsPartial = subApp_getAsPartial
	oSubApp.subApp = subApp_subApp
	return oSubApp
}

Msa.require = function(key){
  const modDir = join(Msa.dirname, "node_modules", Msa.params.msa_modules[key])
  return require(modDir)
}
/*
var requireMsaModules = function() {
  var msa_modules = Msa.params.msa_modules || {}
  for(let route in msa_modules){
    let modDir = join(Msa.dirname, "node_modules", msa_modules[route]),
        mod = require(modDir)
    initMsaModule(mod, modDir)
  }
}
*/
var initMsaModule = function(mod, modDir) {
  // static files
  if(mod.checkStaticDir !== false){
    mod.dirname = modDir
    const staticDir = modDir+"/static"
    fs.stat(staticDir, (err, stats) => {
      if(!err && stats && stats.isDirectory())
        mod.app.use(Msa.express.static(staticDir))
    })
  }
  // register module
  var key = mod.key
  if(key==='') Msa.mainMod = mod
  else {
    Msa.modules[key] = mod
    Msa.modulesRouter.use('/'+key, mod.app)
  }
}

const subApp_subApp = function(route) {
	var oSubApp = Msa.subApp()
	this.use(route, oSubApp)
	return oSubApp
}
/*
// partial

const Partial = Msa.Partial = class {
	constructor(htmlExpr) {
		formatHtml(htmlExpr, this)
	}
}

// shortcut function to server HTML content as partial
const subApp_getAsPartial = function(route, htmlExpr) {
//  var partial = Msa.formatHtml(htmlExpr)
	const partial = new Partial(htmlExpr)
	return this.get(route, function(req, res, next) {
//		res.partial = partial
		next(partial)
	})
}
*/

// utils

const { isArray: isArr } = Array

const { formatHtml } = Msa

Msa.joinUrl = function(...args){
	return args.join('/').replace(/\/+/g,'/')
}

const deepMerge = function(obj1, obj2) {
	for(let k in obj2) {
		if(typeof obj1[k] === "object" && typeof obj2[k] === "object")
			deepMerge(obj1[k], obj2[k])
		else obj1[k] = obj2[k]
	}
}

const fileExists = function(path) {
	return new Promise((ok, ko) => {
		try {
			fs.access(path, err => {
				ok(err ? false : true)
			})
		} catch(err) { return ko(err) }
	})
}

const tryResolve = function(name, kwargs) {
	var res
	try {
		res = require.resolve(name, kwargs)
	} catch(e) {}
	return res
}

// run main
main(err => {
	if(err) {
		console.error(err)
		process.exit(1)
	}
})
<|endoftext|>


<commit_before>function StayDown(opts) {
    opts = opts || {};
    this.target = opts.target;
    this.interval = opts.interval;
    this.max = opts.max || 0;
    this.callback = opts.callback;
    this.userScroll = true;
    this.spinner = opts.spinner;
    this.spin_img = new Image();
    if (this.spinner) {
        this.spin_img.src = this.spinner;
    }
    var staydown = this;
    this.intend_down = true;

    this.emit('lock');

    var wheelevent = "wheel";
    if (document.onmousewheel !== undefined) {
        wheelevent = 'mousewheel';
    }

    window.addEventListener('resize', function (event) {
        staydown.emit('windowresize');
        staydown.checkdown();
    });

    this.target.addEventListener('scroll', function (event) {
        if (staydown.userScroll) {
            if (staydown.intend_down && !staydown.isdown()) {
                staydown.intend_down = false;
                staydown.emit('release');
            } else if (!staydown.indend_down && staydown.isdown()) {
                staydown.intend_down = true;
                staydown.emit('lock');
            }
        }
        staydown.userScroll = true;
    });

    if (window.MutationObserver) {
        //private function for getting images recursively from dom

        //mutation observer for whenever the overflow element changes
        this.mo = new MutationObserver(function (mutations) {
            var mut, idx, nidx, imgs, img, iidx, ilen, parent, spin;
            staydown.userScroll = false;
            //something changed, check scroll
            staydown.checkdown();
            //check to see if image was added, and add onload check
            for (idx = 0; idx < mutations.length; idx++) {
                mut = mutations[idx];
                for (nidx = 0; nidx < mut.addedNodes.length; nidx++) {
                    imgs = mut.addedNodes[nidx].getElementsByTagName('img');
                    for (iidx = 0, ilen = imgs.length; iidx < ilen; iidx++) {
                        img = imgs[iidx];
                        if (!img.complete) {
                            parent = img.parentNode;
                            if (staydown.spinner) {
                                spin = staydown.spin_img.cloneNode();
                                parent.replaceChild(spin, img);
                            }
                            var onImageLoad = function (event) {
                                if (spin) {
                                    //image loads later, and isn't a mutation
                                    parent.replaceChild(img, spin);
                                }
                                staydown.emit('imageload');
                                staydown.checkdown();
                                event.target.removeEventListener('load', onImageLoad);
                            };
                            img.addEventListener('load', onImageLoad);
                        }
                    }
                }
            }
        });
        this.mo.observe(this.target, {attributes: true, childList: true, characterData: true, subtree: true});
    } else {
        var checkdown = function () {
            staydown.checkdown();
            window.setTimeout(checkdown, staydown.interval);
        };
        checkdown();
    }

}

(function () {

    this.isdown = function () {
        return (this.target.scrollTop + this.target.clientHeight == this.target.scrollHeight);
    };

    this.append = function (newel) {
        this.emit('append');
        this.target.appendChild(newel);
        if (this.intend_down) {
            this.target.scrollTop = this.target.scrollHeight;
            this.emit('scrolldown');
        }
        while (this.max !== 0 && this.target.children.length > this.max) {
            this.target.removeChild(this.target.children[0]);
            this.emit('removechild');
        }
    };

    this.emit = function (type, msg) {
        if (typeof this.callback === 'function') {
            this.callback(type, msg);
        }
    };

    this.checkdown = function () {
        if (this.intend_down &&
            this.target.scrollTop + this.target.clientHeight != this.target.scrollHeight) {
            this.target.scrollTop = this.target.scrollHeight;
            this.userScroll = false;
            this.emit('scrolldown');
        }
    };

}).call(StayDown.prototype);

if (!module) {
    window.StayDown = StayDown;
} else {
    module.exports = StayDown;
}
<commit_msg>Fix issue when modify non-element nodes (like text)<commit_after>function StayDown(opts) {
    opts = opts || {};
    this.target = opts.target;
    this.interval = opts.interval;
    this.max = opts.max || 0;
    this.callback = opts.callback;
    this.userScroll = true;
    this.spinner = opts.spinner;
    this.spin_img = new Image();
    if (this.spinner) {
        this.spin_img.src = this.spinner;
    }
    var staydown = this;
    this.intend_down = true;

    this.emit('lock');

    var wheelevent = "wheel";
    if (document.onmousewheel !== undefined) {
        wheelevent = 'mousewheel';
    }

    window.addEventListener('resize', function (event) {
        staydown.emit('windowresize');
        staydown.checkdown();
    });

    this.target.addEventListener('scroll', function (event) {
        if (staydown.userScroll) {
            if (staydown.intend_down && !staydown.isdown()) {
                staydown.intend_down = false;
                staydown.emit('release');
            } else if (!staydown.indend_down && staydown.isdown()) {
                staydown.intend_down = true;
                staydown.emit('lock');
            }
        }
        staydown.userScroll = true;
    });

    if (window.MutationObserver) {
        //private function for getting images recursively from dom

        //mutation observer for whenever the overflow element changes
        this.mo = new MutationObserver(function (mutations) {
            var mut, idx, nidx, imgs, img, iidx, ilen, parent, spin;
            staydown.userScroll = false;
            //something changed, check scroll
            staydown.checkdown();
            //check to see if image was added, and add onload check
            for (idx = 0; idx < mutations.length; idx++) {
                mut = mutations[idx];
                for (nidx = 0; nidx < mut.addedNodes.length; nidx++) {
                    // Check if we appended a node type that isn't
                    // an element that we can search for images inside.
                    if (!mut.addedNodes[nidx].getElementsByTagName) {
                        continue;
                    }

                    imgs = mut.addedNodes[nidx].getElementsByTagName('img');
                    for (iidx = 0, ilen = imgs.length; iidx < ilen; iidx++) {
                        img = imgs[iidx];
                        if (!img.complete) {
                            parent = img.parentNode;
                            if (staydown.spinner) {
                                spin = staydown.spin_img.cloneNode();
                                parent.replaceChild(spin, img);
                            }
                            var onImageLoad = function (event) {
                                if (spin) {
                                    //image loads later, and isn't a mutation
                                    parent.replaceChild(img, spin);
                                }
                                staydown.emit('imageload');
                                staydown.checkdown();
                                event.target.removeEventListener('load', onImageLoad);
                            };
                            img.addEventListener('load', onImageLoad);
                        }
                    }
                }
            }
        });
        this.mo.observe(this.target, {attributes: true, childList: true, characterData: true, subtree: true});
    } else {
        var checkdown = function () {
            staydown.checkdown();
            window.setTimeout(checkdown, staydown.interval);
        };
        checkdown();
    }

}

(function () {

    this.isdown = function () {
        return (this.target.scrollTop + this.target.clientHeight == this.target.scrollHeight);
    };

    this.append = function (newel) {
        this.emit('append');
        this.target.appendChild(newel);
        if (this.intend_down) {
            this.target.scrollTop = this.target.scrollHeight;
            this.emit('scrolldown');
        }
        while (this.max !== 0 && this.target.children.length > this.max) {
            this.target.removeChild(this.target.children[0]);
            this.emit('removechild');
        }
    };

    this.emit = function (type, msg) {
        if (typeof this.callback === 'function') {
            this.callback(type, msg);
        }
    };

    this.checkdown = function () {
        if (this.intend_down &&
            this.target.scrollTop + this.target.clientHeight != this.target.scrollHeight) {
            this.target.scrollTop = this.target.scrollHeight;
            this.userScroll = false;
            this.emit('scrolldown');
        }
    };

}).call(StayDown.prototype);

if (!module) {
    window.StayDown = StayDown;
} else {
    module.exports = StayDown;
}
<|endoftext|>


<commit_before>var XRegexp = require('xregexp').XRegExp;

(function (exportTo) {
    "use strict";

    // Nodes which should be considered implicitly self-closing
    // Taken from http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#void-elements
    var voidElements = [
        "area", "base", "br", "col", "command", "embed", "hr", "img", "input",
        "keygen", "link", "meta", "param", "source", "track", "wbr"
    ];

    var downsize = function (text, inputOptions, offset) {
        var stack = [],
            pointer = 0,
            tagName = "",
            parseState = 0,
            trackedState = {},
            tagBuffer = "",
            truncatedText = "";

        var COUNT_CHARACTERS = -1,
            COUNT_WORDS = -2;

        var options = inputOptions && typeof inputOptions === "object" ? inputOptions : {},
            wordChars = options.wordChars instanceof RegExp ?
                options.wordChars : new XRegexp("[\\p{L}0-9\\-\\']", "i");
        options.countingType = !isNaN(Number(options.words)) ? COUNT_WORDS : COUNT_CHARACTERS;
        options.keepContext = !!options.contextualTags;
        options.contextualTags = options.keepContext &&
            Array.isArray(options.contextualTags) ?
                options.contextualTags : [];
        options.limit = (options.countingType === COUNT_WORDS) ? Number(options.words) :
            Number(options.characters) + 1;
        options.limit = isNaN(options.limit) ? Infinity : options.limit;

        function isAtLimit() {
            var stackIndex = 0;

            // Return true when we've hit our limit
            if (trackedState.unitCount < options.limit) {
                return false;
            }

            // If we've got no special context to retain, do an early return.
            if (!options.keepContext) {
                return true;
            }

            for (; stackIndex < stack.length; stackIndex++) {
                if (~options.contextualTags.indexOf(getTagName(stack[stackIndex]))) {
                    return false;
                }
            }

            // There are no contextual tags left, we can stop.
            return true;
        }

        function count(chr) {

            if (!("unitCount" in trackedState)) {
                trackedState.unitCount = 0;
            }

            // Tick-tock state storage for counting words
            // If it doesn't exist, initialise it with value of current char
            if (!("countState" in trackedState)) {
                trackedState.countState = !!wordChars.test(chr + "");
            }

            switch (options.countingType) {
                case COUNT_WORDS:
                    if (!!wordChars.test(chr + "") !== trackedState.countState) {

                        trackedState.countState = !!wordChars.test(chr + "");

                        // Only count the words on the "tock", or we'd be counting
                        // them twice.
                        if (!trackedState.countState) {
                            trackedState.unitCount++;
                        }
                    }
                    break;

                case COUNT_CHARACTERS:
                    // We pass in empty values to count word boundries
                    // defined by tags.
                    // This isn't relevant to character truncation.
                    if (chr !== "") {
                        trackedState.unitCount++;
                    }
                    break;
            }
        }

        // Define our parse states
        var PARSER_UNINITIALISED = 0,
            PARSER_TAG_COMMENCED = 1,
            PARSER_TAG_STRING = -1,
            PARSER_TAG_STRING_SINGLE = -2,
            PARSER_COMMENT = -3;

        for (; pointer < text.length; pointer++) {

            if (parseState !== PARSER_UNINITIALISED) {
                tagBuffer += text[pointer];
            }

            switch (text[pointer]) {

                case "<":
                    // Ooh look — we're starting a new tag.
                    // (Provided we're in uninitialised state and the next
                    // character is a word character, explamation mark or slash)
                    if (parseState === PARSER_UNINITIALISED &&
                        text[pointer + 1].match(/[a-z0-9\-\_\/\!]/)) {

                        parseState = PARSER_TAG_COMMENCED;
                        tagBuffer += text[pointer];
                    }

                    break;

                case "!":
                    if (parseState === PARSER_TAG_COMMENCED &&
                        text[pointer - 1] === "<") {

                        parseState = PARSER_COMMENT;
                    }

                    break;

                case "-":
                    if (parseState === PARSER_COMMENT)
                        parseState = PARSER_COMMENT;

                    break;

                case "\"":
                    if (parseState === PARSER_TAG_STRING) {
                        parseState = PARSER_TAG_COMMENCED;

                    } else if (parseState === PARSER_TAG_STRING_SINGLE) {
                        // if double quote is found in a single quote string,
                        // ignore it and let the string finish
                        break;

                    } else if (parseState !== PARSER_UNINITIALISED) {
                        parseState = PARSER_TAG_STRING;
                    }

                    break;

                case "'":
                    if (parseState === PARSER_TAG_STRING_SINGLE) {
                        parseState = PARSER_TAG_COMMENCED;

                    } else if (parseState === PARSER_TAG_STRING) {
                        // if single quote is found in a double quote string,
                        // ignore it and let the string finish
                        break;

                    } else if (parseState !== PARSER_UNINITIALISED) {
                        parseState = PARSER_TAG_STRING_SINGLE;
                    }

                    break;

                case ">":

                    if (parseState === PARSER_TAG_COMMENCED) {

                        parseState = PARSER_UNINITIALISED;
                        truncatedText += tagBuffer;
                        tagName = getTagName(tagBuffer);

                        // Closing tag. (Do we need to be more lenient/)
                        if (tagBuffer.match(/<\s*\//)) {

                            // We don't attempt to walk up the stack to close
                            // tags. If the text to be truncated contains
                            // malformed nesting, we just close what we're
                            // permitted to and clean up at the end.
                            if (getTagName(stack[stack.length-1]) === tagName) {
                                stack.pop();
                            }

                            // Nope, it's an opening tag.
                        } else {

                            // Don't push self closing or void elements on to
                            // the stack, since they have no effect on nesting.

                            if (voidElements.indexOf(tagName) < 0 &&
                                !tagBuffer.match(/\/\s*>$/)) {

                                stack.push(tagBuffer);
                            }
                        }

                        tagBuffer = "";

                        // Closed tags are word boundries. Count!
                        // Because we've reset our parser state we need
                        // to manually short circuit the logic that comes next.
                        count("");
                        if (!isAtLimit()) {
                            continue;
                        }
                    }

                    if (parseState === PARSER_COMMENT &&
                        text.substring(pointer - 2, pointer) === "--") {

                        parseState = PARSER_UNINITIALISED;
                        truncatedText += tagBuffer;
                        tagBuffer = "";

                        // Closed tags are word boundries. Count!
                        count("");
                        if (!isAtLimit()) {
                            continue;
                        }
                    }

                    break;
            }

            // We're not inside a tag, comment, attribute, or string.
            // This is just text.
            if (parseState === PARSER_UNINITIALISED) {

                // Have we had enough of a good thing?
                count(text[pointer]);
                if (isAtLimit()) {
                    break;
                }

                // Nope, we still thirst for more.
                truncatedText += text[pointer];
            }
        }

        if (options.append && (stack.length || tagBuffer.length)) {
            truncatedText = truncatedText.trim() + options.append;
        }

        // Append anything still left in the buffer
        truncatedText += tagBuffer;

        // Balance anything still left on the stack
        while (stack.length) {
            truncatedText += closeTag(stack.pop());
        }

        return truncatedText;
    };

    function closeTag(openingTag) {
        // Grab the tag name, including namespace, if there is one.
        var tagName = getTagName(openingTag);

        // We didn't get a tag name, so return nothing. Better than
        // a bad prediction, or a junk tag.
        if (!tagName) {
            return "";
        }

        return "</" + tagName + ">";
    }

    function getTagName(tag) {
        var tagName = (tag || "").match(/<\/*([a-z0-9\:\-\_]+)/i);
        return tagName ? tagName[1] : null;
    }

    // Export to node
    if (typeof module !== 'undefined' && module.exports) {
        return module.exports = downsize;
    }

    // Nope, export to the browser instead.
    exportTo.downsize = downsize;
}(this));
<commit_msg>single setup for trackedState, comment changes<commit_after>var XRegexp = require('xregexp').XRegExp;

(function (exportTo) {
    "use strict";

    // Nodes which should be considered implicitly self-closing
    // Taken from http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#void-elements
    var voidElements = [
        "area", "base", "br", "col", "command", "embed", "hr", "img", "input",
        "keygen", "link", "meta", "param", "source", "track", "wbr"
    ];

    var downsize = function (text, inputOptions, offset) {
        var stack = [],
            pointer = 0,
            tagName = "",
            parseState = 0,
            trackedState = {unitCount: 0, countState: false},
            tagBuffer = "",
            truncatedText = "";

        var COUNT_CHARACTERS = -1,
            COUNT_WORDS = -2;

        var options = inputOptions && typeof inputOptions === "object" ? inputOptions : {},
            wordChars = options.wordChars instanceof RegExp ?
                options.wordChars : new XRegexp("[\\p{L}0-9\\-\\']", "i");
        options.countingType = !isNaN(Number(options.words)) ? COUNT_WORDS : COUNT_CHARACTERS;
        options.keepContext = !!options.contextualTags;
        options.contextualTags = options.keepContext &&
            Array.isArray(options.contextualTags) ?
                options.contextualTags : [];
        options.limit = (options.countingType === COUNT_WORDS) ? Number(options.words) :
            Number(options.characters) + 1;
        options.limit = isNaN(options.limit) ? Infinity : options.limit;

        function isAtLimit() {
            var stackIndex = 0;

            // Return true when we've hit our limit
            if (trackedState.unitCount < options.limit) {
                return false;
            }

            // If we've got no special context to retain, do an early return.
            if (!options.keepContext) {
                return true;
            }

            for (; stackIndex < stack.length; stackIndex++) {
                if (~options.contextualTags.indexOf(getTagName(stack[stackIndex]))) {
                    return false;
                }
            }

            // There are no contextual tags left, we can stop.
            return true;
        }

        function count(chr) {
            switch (options.countingType) {
                case COUNT_WORDS:
                    if (!!wordChars.test(chr + "") !== trackedState.countState) {

                        trackedState.countState = !!wordChars.test(chr + "");

                        // Only count the words on the "tock", or we'd be counting
                        // them twice.
                        if (!trackedState.countState) {
                            trackedState.unitCount++;
                        }
                    }
                    break;

                case COUNT_CHARACTERS:
                    // We pass in empty values to count word boundries
                    // defined by tags.
                    // This isn't relevant to character truncation.
                    if (chr !== "") {
                        trackedState.unitCount++;
                    }
                    break;
            }
        }

        // Define our parse states
        var PARSER_UNINITIALISED = 0,
            PARSER_TAG_COMMENCED = 1,
            PARSER_TAG_STRING = -1,
            PARSER_TAG_STRING_SINGLE = -2,
            PARSER_COMMENT = -3;

        for (; pointer < text.length; pointer++) {

            if (parseState !== PARSER_UNINITIALISED) {
                tagBuffer += text[pointer];
            }

            switch (text[pointer]) {

                case "<":
                    // Ooh look — we're starting a new tag.
                    // (Provided we're in uninitialised state and the next
                    // character is a word character, explamation mark or slash)
                    if (parseState === PARSER_UNINITIALISED &&
                        text[pointer + 1].match(/[a-z0-9\-\_\/\!]/)) {

                        parseState = PARSER_TAG_COMMENCED;
                        tagBuffer += text[pointer];
                    }

                    break;

                case "!":
                    if (parseState === PARSER_TAG_COMMENCED &&
                        text[pointer - 1] === "<") {

                        parseState = PARSER_COMMENT;
                    }

                    break;

                case "-":
                    if (parseState === PARSER_COMMENT)
                        parseState = PARSER_COMMENT;

                    break;

                case "\"":
                    if (parseState === PARSER_TAG_STRING) {
                        parseState = PARSER_TAG_COMMENCED;

                    } else if (parseState === PARSER_TAG_STRING_SINGLE) {
                        // if double quote is found in a single quote string,
                        // ignore it and let the string finish
                        break;

                    } else if (parseState !== PARSER_UNINITIALISED) {
                        parseState = PARSER_TAG_STRING;
                    }

                    break;

                case "'":
                    if (parseState === PARSER_TAG_STRING_SINGLE) {
                        parseState = PARSER_TAG_COMMENCED;

                    } else if (parseState === PARSER_TAG_STRING) {
                        // if single quote is found in a double quote string,
                        // ignore it and let the string finish
                        break;

                    } else if (parseState !== PARSER_UNINITIALISED) {
                        parseState = PARSER_TAG_STRING_SINGLE;
                    }

                    break;

                case ">":

                    if (parseState === PARSER_TAG_COMMENCED) {

                        parseState = PARSER_UNINITIALISED;
                        truncatedText += tagBuffer;
                        tagName = getTagName(tagBuffer);

                        // Closing tag. (Do we need to be more lenient/)
                        if (tagBuffer.match(/<\s*\//)) {

                            // We don't attempt to walk up the stack to close
                            // tags. If the text to be truncated contains
                            // malformed nesting, we just close what we're
                            // permitted to and clean up at the end.
                            if (getTagName(stack[stack.length-1]) === tagName) {
                                stack.pop();
                            }

                        } else {
                            // Nope, it's an opening tag.

                            // Don't push self closing or void elements on to
                            // the stack, since they have no effect on nesting.

                            if (voidElements.indexOf(tagName) < 0 &&
                                !tagBuffer.match(/\/\s*>$/)) {
                                stack.push(tagBuffer);
                            }
                        }

                        tagBuffer = "";

                        // Closed tags are word boundries. Count!
                        count("");
                        if (!isAtLimit()) {
                            continue;
                        }

                    } else if (parseState === PARSER_COMMENT) {
                        if (text.substring(pointer - 2, pointer) === "--") {
                            parseState = PARSER_UNINITIALISED;
                            truncatedText += tagBuffer;
                            tagBuffer = "";

                            // Closed tags are word boundries. Count!
                            count("");
                            if (!isAtLimit()) {
                                continue;
                            }
                        }
                    }

                    break; // break switch
            }

            // We're not inside a tag, comment, attribute, or string.
            // This is just text.
            if (parseState === PARSER_UNINITIALISED) {

                // Have we had enough of a good thing?
                count(text[pointer]);
                if (isAtLimit()) {
                    break;
                }

                // Nope, we still thirst for more.
                truncatedText += text[pointer];
            }

        } // end of main parsing for loop

        if (options.append && (stack.length || tagBuffer.length)) {
            truncatedText = truncatedText.trim() + options.append;
        }

        // Append anything still left in the buffer
        truncatedText += tagBuffer;

        // Balance anything still left on the stack
        while (stack.length) {
            truncatedText += closeTag(stack.pop());
        }

        return truncatedText;
    };

    function closeTag(openingTag) {
        // Grab the tag name, including namespace, if there is one.
        var tagName = getTagName(openingTag);

        // We didn't get a tag name, so return nothing. Better than
        // a bad prediction, or a junk tag.
        if (!tagName) {
            return "";
        }

        return "</" + tagName + ">";
    }

    function getTagName(tag) {
        var tagName = (tag || "").match(/<\/*([a-z0-9\:\-\_]+)/i);
        return tagName ? tagName[1] : null;
    }

    // Export to node
    if (typeof module !== 'undefined' && module.exports) {
        return module.exports = downsize;
    }

    // Nope, export to the browser instead.
    exportTo.downsize = downsize;
}(this));
<|endoftext|>


<commit_before>/**
 * Created by maya on 2015/7/19.
 */
var mqtt = require('mqtt');

var timeSyncTopic = "timesync";
var mqttClientInstance = mqtt.connect("mqtt://localhost:1883", {clientId: timeSyncTopic});
mqttClientInstance.on('connect', function () {
    console.log("connect mqtt server success");
});
mqttClientInstance.on('error',function(error) {
    console.log("mqtt connect failed: ", error);
});
mqttClientInstance.subscribe(timeSyncTopic);

mqttClientInstance.on('message', function(messageTopic, data) {
    var msg = JSON.parse(data);
    if(msg.timesync === "Request"){
        mqttClientInstance.publish(msg.clientId, JSON.stringify({timesync: "Response", serverTime: Date.now()}));
    }
});
/* when got exit signal, then exit
 mqttClientInstance.end();
 mqttClientInstance = null;
 console.log("mqtt client has been destroyed");
 */<commit_msg>add log<commit_after>/**
 * Created by maya on 2015/7/19.
 */
var mqtt = require('mqtt');

var timeSyncTopic = "timesync";
var mqttClientInstance = mqtt.connect("mqtt://localhost:1883", {clientId: timeSyncTopic});
mqttClientInstance.on('connect', function () {
    console.log("connect mqtt server success");
});
mqttClientInstance.on('error',function(error) {
    console.log("mqtt connect failed: ", error);
});
mqttClientInstance.subscribe(timeSyncTopic);

mqttClientInstance.on('message', function(messageTopic, data) {
    var msg = JSON.parse(data);
    if(msg.timesync === "Request"){
        var now = Date.now();
        mqttClientInstance.publish(msg.clientId, JSON.stringify({timesync: "Response", serverTime: now}));
        console.log("response with time: "+now);
    }
});
/* when got exit signal, then exit
 mqttClientInstance.end();
 mqttClientInstance = null;
 console.log("mqtt client has been destroyed");
 */<|endoftext|>


<commit_before>var http = require('http');
var express = require('express');
var twilio = require('twilio');

var app = express();

http.createServer(app).listen(1337, function () {
  console.log("Express server listening on port 1337");
});

app.post('/sms', function(req, res) {
  var twiml = new twilio.TwimlResponse();
  twiml.message('Baby carrots are just cut up normal carrots.');
  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(twiml.toString());
});
<commit_msg>changed port, test calls lyfthandler<commit_after>var lh = require('LyftHandler.js');
var th = require("TwilioHandler.js");
var http = require('http');
var express = require('express');
var twilio = require('twilio');

var app = express();

var port = process.env.port || 3001

http.createServer(app).listen(port, function () {
  console.log("Express server listening on port "+port);
});

app.get("/", function(req, res){
  res.sendfile("index.html");
});

app.get("/lyft", function(req, res){
  lh.getUser("14017145717", "1776");
});

app.post('/sms', function(req, res) {
  var twiml = new twilio.TwimlResponse();
  twiml.message('Baby carrots are just cut up normal carrots.');
  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(twiml.toString());
});
<|endoftext|>


<commit_before>var _ = require('underscore');
var kisslog = require('kisslog');
var JSONPointer = require('jsonpointer');
var syncMixin = require('ampersand-optimistic-sync');
var patcherMixin = require('ampersand-model-patch-mixin');
var jiff = require('jiff');

var internals = {};

var mixin = module.exports = function (_super, protoProps) {
    var baseProto = protoProps || {};
    var config = baseProto._optimisticUpdate || {};

    protoProps = _.omit(baseProto, '_optimisticUpdate');

    var log = kisslog(config);
    var debug = _.partial(log, kisslog.debug);

    var myProto = _.extend({
        _optimisticUpdate: config,
        _invalidHandler: function (model, version, serverData) {
            debug('invalidHandler called:', version, serverData);
            if (_.isObject(serverData)) {
                return model._conflictDetector(version, serverData);
            }
            var xhr = model.sync('read', model, {
                success: function (data) {
                    var serverData = model.parse(data);
                    model._conflictDetector(model._version, serverData);
                }
            });
        },
        _getDiff: function (lhs, rhs) {
            var payload = [];
            var skipChange = [];
            var ops = jiff.diff(lhs, rhs, {
                hash: function (obj) {
                    if (obj && obj.id) return obj.id;
                    return JSON.stringify(obj);
                },
                makeContext: function (index, array) {
                    return {index: index, source: array};
                }
            });
            _.each(ops, function (op, idx) {
                // If we've already processed this op, skip it.
                if (skipChange.indexOf(idx) !== -1) return;
                var test, nextOp;
                if (op.op !== 'test') return payload.push(op);
                nextOp = ops[idx+1];
                if (op.path === nextOp.path) {
                    // Push related op to skip list
                    skipChange.push(idx+1);
                    nextOp.test = op;
                    payload.push(nextOp);
                }
            });
            return payload;
        },
        _sortCollections: function (current) {
            if (config.collectionSort) {
                debug('sorting current collections');
                _.each(this[this._patcherConfig.collectionProperty], function (x, name) {
                    var sorter = config.collectionSort[name] || config.collectionSort.default;
                    var currentList = current[name];
                    debug('sorter: %s, currentList: %o', sorter, currentList);
                    if (sorter && currentList) {
                        current[name] = _.sortBy(currentList, sorter);
                    }
                });
            }
            return current;
        },
        _getLocalOps: function (original, current) {
            if (this._ops && this._ops.length) return this._getOps();
            original = original || this._getOriginal();
            current = current || this.toJSON();
            this._sortCollections(current);
            var omit = [];
            var ops = [];
            if (config.customCompare) {
                _.each(config.customCompare, function (isEqual, name) {
                    var result = isEqual.call(this, original[name], current[name]);
                    if (!result) return;
                    if (_.isArray(result)) {
                        ops = ops.concat(result);
                    }
                    omit.push(name);
                });
            }
            original = _.omit(original, omit);
            current = _.omit(current, omit);
            return ops.concat(this._getDiff(original, current));
        },
        _getByPath: function (path, obj) {
            if (path.charAt(path.length - 1) === '-' && !obj) return null;
            obj = obj || this._getOriginal();
            try {
                return JSONPointer.get(obj, path);
            } catch (e) {
                log(kisslog.warn, 'JSONPointer failed:', e, path, obj);
            }
        },
        _conflictDetector: function (version, serverData) {
            if (this._version !== version) this._version = version;
            var config = this._optimisticUpdate;
            debug('Preparing conflict data%s', config.autoResolve ? ' and resolving non-conflicting changes.' : '.');
            serverData = this.parse(serverData);
            var autoResolve = !!config.autoResolve;
            var serverWins = config.autoResolve === 'server';
            var original = this._getOriginal();
            var changed = this._getDiff(original, serverData);
            var client = this.toJSON();
            var conflicts = [];
            var removeClient = [];
            var removeServer = [];
            var unsaved = this._getLocalOps(original, client);
            _.each(changed, function (op, idx) {
                debug('%d: op:', idx, op);

                var collision = _.findWhere(unsaved, {op: op.op, path: op.path});
                if (collision) {
                    log('found a collision between %j and %j', op, collision);

                    var bothRemoved = (op.op === 'remove' && collision.op === 'remove');
                    var sameChange = (op.op === 'replace' && op.value === collision.value);
                    var bothAppend = (op.op === 'add');
                    // If client and server both removed something or changed it to the same value,
                    // drop both operations --> no conflict
                    if (bothRemoved || sameChange) {
                        removeServer.push(op);
                        return removeClient.push(collision);
                    }
                    if (bothAppend && config.autoResolve) {
                        return;
                    }
                } else {
                    debug('no conflict');
                    if (config.autoResolve) return;
                }
                // When config.autoResolve is set to 'server', we'll apply the server's version locally.
                if (config.autoResolve === 'server') {
                    removeClient.push(collision);
                    op.clientDiscarded = true;
                    op.client = collision.value;
                    log(kisslog.info, 'overwriting client change %o with %o', collision, op);
                    return;
                } else {
                    removeServer.push(op);
                }

                // If we've made it this far, there is a valid conflict
                var payload = {
                    client: collision,
                    server: op,
                    original: op.op === 'add' ? null : this._getByPath(op.path)
                };
                debug('adding conflict:', payload);
                return conflicts.push(payload);
            }, this);

            if (removeClient.length) {
                debug('cleaning up _ops');
                unsaved = _.difference(unsaved, removeClient);
                if (this._ops) this._ops = unsaved;
            }
            unsaved = _.map(unsaved, function (op) {
                var original = (op.op !== 'add') ? this._getByPath(op.path) : null;
                return _.extend({original: original}, op);
            }, this);
            if (removeServer.length) {
                debug('cleaning up server changes: %o', removeServer);
                changed = _.difference(changed, removeServer);
            }
            if (config.autoResolve && changed.length) {
                debug('auto-resolving: %o', changed);
                this._applyDiff(changed);
                if (!conflicts.length) {
                    this._conflict = {
                        resolved: this._prepResolved(changed),
                        serverState: serverData,
                        original: original,
                        unsaved: unsaved
                    };
                    log(kisslog.info, 'emitting sync:conflict-autoResolved event: %o', this._conflict);
                    this.trigger('sync:conflict-autoResolved', this, this._conflict);
                    this._setOriginal(serverData);
                }
            }
            if (conflicts.length) {
                debug('had conflicts');
                // Deal with them
                this._conflict = {
                    conflicts: conflicts,
                    serverState: serverData,
                    resolved: config.autoResolve ? this._prepResolved(changed) : [],
                    unsaved: unsaved
                };
                log(kisslog.info, 'emitting sync:conflict event: %o', this._conflict);
                return this.trigger('sync:conflict', this, this._conflict);
            }
        },
        _prepResolved: function (changed) {
            debug('prepping resolved ops');
            return _.map(changed, function (operation) {
                return {
                    server: _.omit(operation, 'clientDiscarded', 'client'),
                    original: this._getByPath(operation.path),
                    clientDiscarded: operation.clientDiscarded,
                    client: operation.client
                };
            }, this);
        },
        _applyDiff: function (diff) {
            log(kisslog.info, 'applying diff:', diff);
            var config = this._optimisticUpdate;
            var models = this[config.modelProperty];
            var collections = this[config.collectionProperty];
            _.each(diff, function (op) {
                if (_.indexOf(['add', 'remove', 'replace'], op.op) === -1) {
                    return log(kisslog.warn, 'INVALID: Unsupported operation: %s', op.op);
                }
                var pathParts = op.path.slice(1).split('/');
                var root = pathParts.shift();
                var original, child;
                if (this._isCollection(root)) {
                    if (op.op === 'add') {
                        log('adding %o to %s', op.value, root);
                        return this[root].add(op.value);
                    }
                    var index = pathParts.shift();
                    original = this._getOriginal()[root][index];
                    if (original) {
                        child = this[root].get(original);
                    } else {
                        child = this._resolveCollectionOp(this[root], op, index);
                        if (!child) return log(kisslog.error, 'NOT FOUND: Could not find collection member matching %o in %o', op, this[root].toJSON());
                    }
                    if (child && op.op === 'remove') {
                        if (!pathParts.length) {
                            debug('removing %j from %s', child, root);
                            return this[root].remove(child);
                        }
                        return child.unset(pathParts.shift());
                    }
                } else if (this._isModel(root)) {
                    child = this[root];
                } else if (!pathParts.length) {
                    if (op.op === 'add' || op.op === 'replace') {
                        this.set(root, op.value);
                    } else if (op.op === 'remove') {
                        this.unset(root);
                    }
                }
                if (child && op.op === 'replace') {
                    // We're replacing a single prop on the child
                    if (pathParts.length) {
                        debug('setting %o property %s to %s', child, pathParts[0], op.value);
                        child.set(pathParts.shift(), op.value);
                    // We're replacing all the attributes of the child
                    } else if (_.isObject(op.value)) {
                        debug('setting %o attributes to %o', child, op.value);
                        child.set(op.value);
                    }
                }
                if (child && op.op === 'remove') {
                    debug('removing %o', child);
                    delete this[root];
                }
            }, this);
        },
        _resolveCollectionOp: function (collection, op, index) {
            var attempt;
            if (op.op === 'remove') {
                log('finding by index');
                attempt = collection.at(index);
                if (op.test) {
                    var key;
                    var same = true;
                    var test = op.test.value;
                    for (key in test) {
                        same = attempt.get(key) === test[key];
                        if (!same) break;
                    }
                    if (same) return attempt;
                    log('find by index failed');
                    attempt = collection.findWhere(test);
                    if (attempt) return attempt;
                }
            }
            if (op.context) {
                log('finding by context');
                attempt = collection.findWhere(op.context.source[op.context.index]);
                if (attempt) return attempt;
            }
            return null;
        },
        _isCollection: function (name) {
            var collections = this[this._patcherConfig.collectionProperty];
            return collections && name in collections;
        },
        _isModel: function (name) {
            var models = this[this._patcherConfig.modelProperty];
            return models && name in models;
        },
        // payload is the second argument to sync:conflict and
        // sync:conflict-autoResolved handlers
        reverseUnsaved: function (payload) {
            payload = payload || this._conflict;
            var reverse = this._getLocalOps(this._sortCollections(this.toJSON()), payload.serverState);
            log('reverse: %j', _.map(reverse, function (op) {
                return _.pick(op, 'op', 'path', 'value');
            }));
            return this._applyDiff(reverse);
        }
    });

    var patchProto =  patcherMixin(_super, {
        _patcherConfig: config.patcher || {}
    });

    myProto._getOriginal = function () {
        return _.omit(patchProto._getOriginal.call(this), config.ignoreProps);
    };

    var oldSave = baseProto.save || _super.prototype.save;
    myProto.save = function (key, val, options) {
        if (this._optimisticUpdate.JSONPatch === false) return oldSave.call(this, key, val, options);
        patchProto.save.call(this, key, val, options);
    };

    var syncProto = syncMixin(_super, _.defaults({
        invalidHandler: myProto._invalidHandler
    }, config.optimistic || {}));

    return _super.extend(_.extend(baseProto, syncProto, patchProto, myProto));
};
<commit_msg>misc logging and better reverse diffing<commit_after>var _ = require('underscore');
var kisslog = require('kisslog');
var JSONPointer = require('jsonpointer');
var syncMixin = require('ampersand-optimistic-sync');
var patcherMixin = require('ampersand-model-patch-mixin');
var jiff = require('jiff');

var internals = {};

var mixin = module.exports = function (_super, protoProps) {
    var baseProto = protoProps || {};
    var config = baseProto._optimisticUpdate || {};

    protoProps = _.omit(baseProto, '_optimisticUpdate');

    var log = kisslog(config);
    var debug = _.partial(log, kisslog.debug);

    var myProto = _.extend({
        _optimisticUpdate: config,
        _invalidHandler: function (model, version, serverData) {
            debug('invalidHandler called:', version, serverData);
            if (_.isObject(serverData)) {
                return model._conflictDetector(version, serverData);
            }
            var xhr = model.sync('read', model, {
                success: function (data) {
                    var serverData = model.parse(data);
                    model._conflictDetector(model._version, serverData);
                }
            });
        },
        _getDiff: function (lhs, rhs) {
            var payload = [];
            var skipChange = [];
            var ops = jiff.diff(lhs, rhs, {
                hash: function (obj) {
                    if (obj && obj.id) return obj.id;
                    return JSON.stringify(obj);
                },
                makeContext: function (index, array) {
                    return {index: index, source: array};
                }
            });
            _.each(ops, function (op, idx) {
                // If we've already processed this op, skip it.
                if (skipChange.indexOf(idx) !== -1) return;
                var test, nextOp;
                if (op.op !== 'test') return payload.push(op);
                nextOp = ops[idx+1];
                if (op.path === nextOp.path) {
                    debug('adding test %o to op %o', op, nextOp);
                    // Push related op to skip list
                    skipChange.push(idx+1);
                    nextOp.test = op;
                    payload.push(nextOp);
                }
            });
            return payload;
        },
        _sortCollections: function (current) {
            if (config.collectionSort) {
                debug('sorting current collections');
                _.each(this[this._patcherConfig.collectionProperty], function (x, name) {
                    var sorter = config.collectionSort[name] || config.collectionSort.default;
                    var currentList = current[name];
                    debug('sorter: %s, currentList: %o', sorter, currentList);
                    if (sorter && currentList) {
                        current[name] = _.sortBy(currentList, sorter);
                    }
                });
            }
            return current;
        },
        _getLocalOps: function (original, current) {
            if (this._ops && this._ops.length) return this._getOps();
            original = original || this._getOriginal();
            current = current || this.toJSON();
            this._sortCollections(current);
            var omit = [];
            var ops = [];
            if (config.customCompare) {
                _.each(config.customCompare, function (isEqual, name) {
                    var result = isEqual.call(this, original[name], current[name]);
                    if (!result) return;
                    if (_.isArray(result)) {
                        ops = ops.concat(result);
                    }
                    omit.push(name);
                });
            }
            original = _.omit(original, omit);
            current = _.omit(current, omit);
            return ops.concat(this._getDiff(original, current));
        },
        _getByPath: function (path, obj) {
            if (path.charAt(path.length - 1) === '-' && !obj) return null;
            obj = obj || this._getOriginal();
            try {
                return JSONPointer.get(obj, path);
            } catch (e) {
                log(kisslog.warn, 'JSONPointer failed:', e, path, obj);
            }
        },
        _conflictDetector: function (version, serverData) {
            if (this._version !== version) this._version = version;
            var config = this._optimisticUpdate;
            debug('Preparing conflict data%s', config.autoResolve ? ' and resolving non-conflicting changes.' : '.');
            serverData = this.parse(serverData);
            var autoResolve = !!config.autoResolve;
            var serverWins = config.autoResolve === 'server';
            var original = this._getOriginal();
            var changed = this._getDiff(original, serverData);
            var client = this.toJSON();
            var conflicts = [];
            var removeClient = [];
            var removeServer = [];
            var unsaved = this._getLocalOps(original, client);
            _.each(changed, function (op, idx) {
                debug('%d: op:', idx, op);

                var collision = _.findWhere(unsaved, {op: op.op, path: op.path});
                if (collision) {
                    log('found a collision between %j and %j', op, collision);

                    var bothRemoved = (op.op === 'remove' && collision.op === 'remove');
                    var sameChange = (op.op === 'replace' && op.value === collision.value);
                    var bothAppend = (op.op === 'add');
                    // If client and server both removed something or changed it to the same value,
                    // drop both operations --> no conflict
                    if (bothRemoved || sameChange) {
                        removeServer.push(op);
                        return removeClient.push(collision);
                    }
                    if (bothAppend && config.autoResolve) {
                        return;
                    }
                } else {
                    debug('no conflict');
                    if (config.autoResolve) return;
                }
                // When config.autoResolve is set to 'server', we'll apply the server's version locally.
                if (config.autoResolve === 'server') {
                    removeClient.push(collision);
                    op.clientDiscarded = true;
                    op.client = collision.value;
                    log(kisslog.info, 'overwriting client change %o with %o', collision, op);
                    return;
                } else {
                    removeServer.push(op);
                }

                // If we've made it this far, there is a valid conflict
                var payload = {
                    client: collision,
                    server: op,
                    original: op.op === 'add' ? null : this._getByPath(op.path)
                };
                debug('adding conflict:', payload);
                return conflicts.push(payload);
            }, this);

            if (removeClient.length) {
                debug('cleaning up _ops');
                unsaved = _.difference(unsaved, removeClient);
                if (this._ops) this._ops = unsaved;
            }
            unsaved = _.map(unsaved, function (op) {
                var original = (op.op !== 'add') ? this._getByPath(op.path) : null;
                return _.extend({original: original}, op);
            }, this);
            if (removeServer.length) {
                debug('cleaning up server changes: %o', removeServer);
                changed = _.difference(changed, removeServer);
            }
            if (config.autoResolve && changed.length) {
                debug('auto-resolving: %o', changed);
                this._applyDiff(changed);
                if (!conflicts.length) {
                    this._conflict = {
                        resolved: this._prepResolved(changed),
                        serverState: serverData,
                        original: original,
                        unsaved: unsaved
                    };
                    log(kisslog.info, 'emitting sync:conflict-autoResolved event: %o', this._conflict);
                    this.trigger('sync:conflict-autoResolved', this, this._conflict);
                    this._setOriginal(serverData);
                }
            }
            if (conflicts.length) {
                debug('had conflicts');
                // Deal with them
                this._conflict = {
                    conflicts: conflicts,
                    serverState: serverData,
                    resolved: config.autoResolve ? this._prepResolved(changed) : [],
                    unsaved: unsaved
                };
                log(kisslog.info, 'emitting sync:conflict event: %o', this._conflict);
                return this.trigger('sync:conflict', this, this._conflict);
            }
        },
        _prepResolved: function (changed) {
            debug('prepping resolved ops');
            return _.map(changed, function (operation) {
                return {
                    server: _.omit(operation, 'clientDiscarded', 'client'),
                    original: this._getByPath(operation.path),
                    clientDiscarded: operation.clientDiscarded,
                    client: operation.client
                };
            }, this);
        },
        _applyDiff: function (diff) {
            log(kisslog.info, 'applying diff:', diff);
            var config = this._optimisticUpdate;
            var models = this[config.modelProperty];
            var collections = this[config.collectionProperty];
            _.each(diff, function (op) {
                if (_.indexOf(['add', 'remove', 'replace'], op.op) === -1) {
                    return log(kisslog.warn, 'INVALID: Unsupported operation: %s', op.op);
                }
                var pathParts = op.path.slice(1).split('/');
                var root = pathParts.shift();
                var original, child;
                if (this._isCollection(root)) {
                    if (op.op === 'add') {
                        log('adding %o to %s', op.value, root);
                        return this[root].add(op.value);
                    }
                    var index = pathParts.shift();
                    original = this._getOriginal()[root][index];
                    if (original) {
                        child = this[root].get(original);
                    } else {
                        child = this._resolveCollectionOp(this[root], op, index);
                        if (!child) return log(kisslog.error, 'NOT FOUND: Could not find collection member matching %o in %o', op, this[root].toJSON());
                    }
                    if (child && op.op === 'remove') {
                        if (!pathParts.length) {
                            debug('removing %j from %s', child, root);
                            return this[root].remove(child);
                        }
                        return child.unset(pathParts.shift());
                    }
                } else if (this._isModel(root)) {
                    child = this[root];
                } else if (!pathParts.length) {
                    if (op.op === 'add' || op.op === 'replace') {
                        this.set(root, op.value);
                    } else if (op.op === 'remove') {
                        this.unset(root);
                    }
                }
                if (child && op.op === 'replace') {
                    // We're replacing a single prop on the child
                    if (pathParts.length) {
                        debug('setting %o property %s to %s', child, pathParts[0], op.value);
                        child.set(pathParts.shift(), op.value);
                    // We're replacing all the attributes of the child
                    } else if (_.isObject(op.value)) {
                        debug('setting %o attributes to %o', child, op.value);
                        child.set(op.value);
                    }
                }
                if (child && op.op === 'remove') {
                    debug('removing %o', child);
                    delete this[root];
                }
            }, this);
        },
        _resolveCollectionOp: function (collection, op, index) {
            var attempt;
            if (op.op === 'remove') {
                log('finding by index');
                attempt = collection.at(index);
                if (op.test) {
                    var key;
                    var same = true;
                    var test = op.test.value;
                    for (key in test) {
                        same = attempt.get(key) === test[key];
                        if (!same) break;
                    }
                    if (same) return attempt;
                    log('find by index failed');
                    attempt = collection.findWhere(test);
                    if (attempt) return attempt;
                }
            }
            if (op.context) {
                log('finding by context');
                attempt = collection.findWhere(op.context.source[op.context.index]);
                if (attempt) return attempt;
            }
            return null;
        },
        _isCollection: function (name) {
            var collections = this[this._patcherConfig.collectionProperty];
            return collections && name in collections;
        },
        _isModel: function (name) {
            var models = this[this._patcherConfig.modelProperty];
            return models && name in models;
        },
        // payload is the second argument to sync:conflict and
        // sync:conflict-autoResolved handlers
        reverseUnsaved: function (payload) {
            payload = payload || this._conflict;
            if (this._ops) this._resetOps();
            var local = this._sortCollections(this.toJSON());
            debug('reversing from\n%o\n\nto %o', local, payload.serverState);
            var reverse = this._getLocalOps(local, payload.serverState);
            debug('reverse: %j', _.map(reverse, function (op) {
                return _.pick(op, 'op', 'path', 'value');
            }));
            return this._applyDiff(reverse);
        }
    });

    var patchProto =  patcherMixin(_super, {
        _patcherConfig: config.patcher || {}
    });

    myProto._getOriginal = function () {
        return _.omit(patchProto._getOriginal.call(this), config.ignoreProps);
    };

    var oldSave = baseProto.save || _super.prototype.save;
    myProto.save = function (key, val, options) {
        if (this._optimisticUpdate.JSONPatch === false) return oldSave.call(this, key, val, options);
        patchProto.save.call(this, key, val, options);
    };

    var syncProto = syncMixin(_super, _.defaults({
        invalidHandler: myProto._invalidHandler
    }, config.optimistic || {}));

    return _super.extend(_.extend(baseProto, syncProto, patchProto, myProto));
};
<|endoftext|>


<commit_before>'use strict'
const flatten = require('flatten')
const slice = Array.prototype.slice;

module.exports = magicHook

function magicHook(fn) {
  if (typeof fn !== 'function') {
    throw new Error('fn should be a function')
  }
  if (typeof fn.pre !== 'undefined') {
    throw new Error('The passed function is already hooked')
  }

  let pres = []

  function hookedFunc() {
    /*jshint validthis:true */
    let current = -1
    let result

    function next() {
      current++
      if (pres.length <= current) {
        result = fn.apply(this, arguments)
        return
      }
      let args = slice.call(arguments)
      pres[current].apply(this, [next.bind(this)].concat(args))
    }
    next.apply(this, arguments)
    return result
  }

  hookedFunc.pre = function() {
    if (arguments.length === 0) {
      throw new Error('No pre hooks passed')
    }
    let newPres = flatten(slice.call(arguments))
    newPres.forEach(pre => {
      if (typeof pre !== 'function') {
        throw new Error('Pre hook should be a function')
      }
      pres.push(pre)
    })
  }
  hookedFunc.removePre = function(fnToRemove) {
    if (arguments.length === 0) {
      // Remove all pre callbacks for hook `name`
      pres = []
      return
    }
    pres = pres.filter(function(currFn) {
      return currFn !== fnToRemove
    })
  }

  return hookedFunc
}
<commit_msg>empty lines added to the code to make it more readable<commit_after>'use strict'
const flatten = require('flatten')
const slice = Array.prototype.slice;

module.exports = magicHook

function magicHook(fn) {
  if (typeof fn !== 'function') {
    throw new Error('fn should be a function')
  }

  if (typeof fn.pre !== 'undefined') {
    throw new Error('The passed function is already hooked')
  }

  let pres = []

  function hookedFunc() {
    /*jshint validthis:true */
    let current = -1

    function next() {
      current++

      if (pres.length <= current) {
        return fn.apply(this, arguments)
      }

      let args = slice.call(arguments)
      return pres[current].apply(this, [next.bind(this)].concat(args))
    }

    return next.apply(this, arguments)
  }

  hookedFunc.pre = function() {
    if (!arguments.length) {
      throw new Error('No pre hooks passed')
    }

    let newPres = flatten(slice.call(arguments))

    newPres.forEach(pre => {
      if (typeof pre !== 'function') {
        throw new Error('Pre hook should be a function')
      }

      pres.push(pre)
    })
  }

  hookedFunc.removePre = function(fnToRemove) {
    if (!arguments.length) {
      pres = []
      return
    }

    pres = pres.filter(currFn => currFn !== fnToRemove)
  }

  return hookedFunc
}
<|endoftext|>


<commit_before>'use strict';

/**
 * Sets the version in the config.xml of the cordova project.
 *
 * @author Sam Verschueren      <sam.verschueren@gmail.com>
 * @since  20 May 2015
 */

// module dependencies
var path = require('path'),
    through = require('through2'),
    gutil = require('gulp-util'),
    Config = require('cordova-config');

// export the module
module.exports = function(version) {

    var project;

    return through.obj(function(file, enc, cb) {
        project = file;

        // Pipe the file to the next step
        this.push(file);

        cb();
    }, function(cb) {
        try {
            // Load the config.xml file
            var config = new Config(path.join(project.path, 'config.xml'));

			// Sets the version
            config.setVersion(version);

            // Write the config file
            config.write(function() {
				// Call the callback
				cb();
			});
        }
        catch(err) {
			// Oh no, something happened!
            cb(new gutil.PluginError('gulp-cordova-version', err.message));
        }
    });
};
<commit_msg>Added parameter to set the android and ios version codes<commit_after>'use strict';

/**
 * Sets the version in the config.xml of the cordova project.
 *
 * @author Sam Verschueren      <sam.verschueren@gmail.com>
 * @since  20 May 2015
 */

// module dependencies
var path = require('path'),
    through = require('through2'),
    gutil = require('gulp-util'),
    Config = require('cordova-config');

// export the module
module.exports = function(version, versionCodes) {

    var project;

    // Make it a default object if it is not provided
    versionCodes = versionCodes || {};

    return through.obj(function(file, enc, cb) {
        project = file;

        // Pipe the file to the next step
        this.push(file);

        cb();
    }, function(cb) {
        try {
            // Load the config.xml file
            var config = new Config(path.join(project.path, 'config.xml'));

			// Sets the version
            config.setVersion(version);

            if(versionCodes.androidVersionCode) {
                // Set the android version code if provided
                config.setAndroidVersionCode(versionCodes.androidVersionCode);
            }

            if(versionCodes.iosBundleVersion) {
                // Set the android version code if provided
                config.setIOSBundleVersion(versionCodes.iosBundleVersion);
            }

            // Write the config file
            config.write(function() {
				// Call the callback
				cb();
			});
        }
        catch(err) {
			// Oh no, something happened!
            cb(new gutil.PluginError('gulp-cordova-version', err.message));
        }
    });
};
<|endoftext|>