<commit_before>const Promise = require("bluebird")
const _ = require("lodash")

class Tab {

	constructor(nick, tabDriver) {
		this._nick = nick
		this._tabDriver = tabDriver
		this._actionInProgress = false
		// set default values for confirm and prompt JS dialogs
		this._tabDriver._onConfirm = (msg) => {
			return true
		}
		this._tabDriver._onPrompt = (msg) => {
			return ""
		}
	}

	// Read-only members
	get nick() { return this._nick }
	get driver() { return this._tabDriver } // shorter but less descriptive way to get the tab driver
	get tabDriver() { return this._tabDriver }
	get actionInProgress() { return this._actionInProgress }
	get closed() { return this._tabDriver.closed }
	get crashed() { return this._tabDriver.crashed }
	get id() { return this._tabDriver.id }

	set onConfirm(f) {
		if (typeof(f) !== "function") {
			throw new TypeError("onConfirm must receive a function")
		}
		this._tabDriver._onConfirm = f
	}

	set onPrompt(f) {
		if (typeof(f) !== "function") {
			throw new TypeError("onPrompt must receive a function")
		}
		this._tabDriver._onPrompt = f
	}

	_callToTabDriver(action, callback, multiArgs = false) {
		if (this._tabDriver.crashed) {
			throw new Error('this tab has crashed, no other actions can be done with it')
		}
		if (this._tabDriver.closed) {
			throw new Error('this tab has finished its work (close() was called), no other actions can be done with it')
		}
		if (this._actionInProgress) {
			throw new Error('cannot do this while another tab method is already running, each tab can execute only one action at a time')
		}
		const getAugmentedCallback = (callback) => {
			let that = this
			return function() {
				that._actionInProgress = false
				callback.apply(null, arguments)
			}
		}
		this._actionInProgress = true
		if (callback != null) {
			if (typeof callback !== 'function') {
				throw new TypeError('callback parameter must be of type function')
			}
			action(getAugmentedCallback(callback))
		} else {
			return Promise.fromCallback((callback) => { action(getAugmentedCallback(callback)) }, { multiArgs: multiArgs })
		}
	}

	wait(duration, callback = null) {
		if ((typeof duration !== 'number') || (duration <= 0)) {
			throw new TypeError(`wait: duration parameter must be a positive number`)
		}
		return this._callToTabDriver((callback) => {
			setTimeout(() => {
				callback()
			}, duration)
		}, callback)
	}

	close(callback = null) {
		return this._callToTabDriver((callback) => {
			this._tabDriver._close((err) => {
				if (!err) {
					this.nick.unrefTabById(this.id)
				}
				callback(err)
			})
		}, callback)
	}

	open(url, options = {}, callback = null) {
		if (typeof url !== 'string') {
			throw new TypeError('open: url parameter must be of type string')
		}
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('open: options parameter must be of type plain object')
		}
		if (url.indexOf('://') < 0) {
			url = `http://${url}`
		}
		return this._callToTabDriver((callback) => { this._tabDriver._open(url.trim(), options, callback) }, callback, true) // use multiArgs
	}

	isVisible(selectors, operator, callback) { return this._isVisibleOrPresent("_waitUntilVisible", selectors, operator, callback) }
	isPresent(selectors, operator, callback) { return this._isVisibleOrPresent("_waitUntilPresent", selectors, operator, callback) }
	_isVisibleOrPresent(method, selectors, operator = null, callback = null) {
		if (typeof operator === 'function') {
			callback = operator
			operator = null
		}
		const f = (callback) => {
			this._callTabDriverWaitMethod(method, selectors, 1, operator, (err) => {
				callback(null, !Boolean(err))
			})
		}
		if (callback) {
			f(callback)
		} else {
			return Promise.fromCallback((callback) => { f(callback) })
		}
	}

	// short variant (made to look pretty when using `await untilVisible()` for example)
	untilVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilVisible', selectors, duration, operator, callback) }
	whileVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhileVisible', selectors, duration, operator, callback) }
	untilPresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilPresent', selectors, duration, operator, callback) }
	whilePresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhilePresent', selectors, duration, operator, callback) }
	// standard variant
	waitUntilVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilVisible', selectors, duration, operator, callback) }
	waitWhileVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhileVisible', selectors, duration, operator, callback) }
	waitUntilPresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilPresent', selectors, duration, operator, callback) }
	waitWhilePresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhilePresent', selectors, duration, operator, callback) }
	_callTabDriverWaitMethod(method, selectors, duration = null, operator = null, callback = null) {
		if (typeof selectors === 'string') {
			selectors = [selectors]
		} else if (Array.isArray(selectors)) {
			if (selectors.length > 0)
				for (const sel of selectors) {
					if (typeof sel !== 'string') {
						throw new TypeError(`${method}: selectors parameter must be a string or an array of strings (css paths)`)
					}
				}
			else
				throw new TypeError(`${method}: selectors parameter must contain at least one string (css path)`)
		} else {
			throw new TypeError(`${method}: selectors parameter must be a string or an array of strings (css paths)`)
		}
		selectors = selectors.map((s) => {
			s = s.trim()
			if (s.length <= 0) {
				throw new TypeError(`${method}: selectors parameter cannot contain an empty string`)
			}
			return s
		})
		if (typeof duration === 'function') {
			callback = duration
			duration = null
		} else if (operator === 'function') {
			callback = operator
			operator = null
		}
		if (typeof duration === 'string') {
			// allow mix-up of duration and operator parameters
			const d = duration
			duration = operator
			operator = d
		}
		if (duration === null) {
			duration = 5000 // same as the default of CasperJS's waitFor()
		} else if ((typeof duration !== 'number') || (duration <= 0)) {
			throw new TypeError(`${method}: duration parameter must be a positive number`)
		}
		if (operator === null) {
			operator = 'and'
		} else if ((operator !== 'and') && (operator !== 'or')) {
			throw new TypeError(`${method}: operator parameter must be either "and" or "or"`)
		}
		return this._callToTabDriver((callback) => { this._tabDriver[method](selectors, duration, operator, callback) }, callback)
	}

	click(selector, options = {}, callback = null) {
		if (typeof selector !== 'string') {
			throw new TypeError('click: selector parameter must be of type string')
		}
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			// TODO add option for "real click", standardize, update skel
			throw new TypeError('click: options parameter must be of type plain object')
		}
		if (_.has(options, 'mouseEmulation')) {
			if (typeof options.submit !== 'boolean') {
				throw new TypeError('click: mouseEmulation option must be of type boolean')
			}
		} else {
			options.mouseEmulation = false
		}
		// TODO
		//this.waitUntilPresent(selector, () => {
			return this._callToTabDriver((callback) => { this._tabDriver._click(selector, options, callback) }, callback)
		//})
	}

	evaluate(func, arg = null, callback = null) {
		if (_.isPlainObject(func)) {
			// allow mix-up of func and arg parameters
			const f = func
			func = arg
			arg = f
		}
		if (typeof func !== 'function') {
			throw new TypeError('evaluate: func parameter must be of type function')
		}
		if (typeof arg === 'function') {
			callback = arg
			arg = null
		}
		if ((arg != null) && !_.isPlainObject(arg) && !_.isArray(arg)) {
			throw new TypeError('evaluate: arg parameter must be a plain object or an array')
		}
		return this._callToTabDriver((callback) => { this._tabDriver._evaluate(func, arg, callback) }, callback)
	}

	getUrl(callback = null) {
		return this._callToTabDriver((callback) => { this._tabDriver._getUrl(callback) }, callback)
	}

	getContent(callback = null) {
		return this._callToTabDriver((callback) => { this._tabDriver._getContent(callback) }, callback)
	}

	fill(selector, params, options = {}, callback = null) {
		if (typeof selector !== 'string') {
			throw new TypeError('fill: selector parameter must be of type string')
		}
		if (!_.isPlainObject(params)) {
			throw new TypeError('fill: params parameter must be of type plain object')
		}
		// TODO check params in more detail
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('fill: options parameter must be of type plain object')
		}
		if (_.has(options, 'submit')) {
			if (typeof options.submit !== 'boolean') {
				throw new TypeError('submit option must be of type boolean')
			}
		} else {
			options.submit = false
		}
		// TODO
		//this.waitUntilPresent(selector, () => {
			return this._callToTabDriver((callback) => { this._tabDriver._fill(selector, params, options, callback) }, callback)
		//})
	}

	screenshot(filename, options = {}, callback = null) {
		if ((filename != null) && (typeof filename !== 'string')) {
			throw new TypeError('screenshot: filename parameter must be null or of type string')
		}
		// TODO check filename for png, jpg, jpeg, pdf, 'base64:png', 'base64:jpg'...
		// TODO more checks
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('screenshot: options parameter must be of type plain object')
		}
		// TODO check options: clipRect, selector, fullPage...
		return this._callToTabDriver((callback) => { this._tabDriver._screenshot(filename, options, callback) }, callback)
	}

	sendKeys(selector, keys, options = {}, callback = null) {
		if (typeof selector !== 'string') {
			throw new TypeError('sendKeys: selector parameter must be of type string')
		}
		if ((typeof keys !== 'string') && (typeof keys !== 'number')) {
			throw new TypeError('sendKeys: keys parameter must be of type string or number')
		}
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('sendKeys: options parameter must be of type plain object')
		}
		if (_.has(options, 'keepFocus')) {
			if (typeof options.keepFocus !== 'boolean') {
				throw new TypeError('sendKeys: keepFocus option must be of type boolean')
			}
		}
		if (_.has(options, 'reset')) {
			if (typeof options.reset !== 'boolean') {
				throw new TypeError('sendKeys: reset option must be of type boolean')
			}
		}
		if (_.has(options, 'modifiers')) {
			if (typeof options.reset !== 'string') {
				throw new TypeError('sendKeys: modifiers option must be of type string')
			}
		}
		// TODO
		//this.waitUntilPresent(selector, () => {
			return this._callToTabDriver((callback) => { this._tabDriver._sendKeys(selector, keys, options, callback) }, callback)
		//})
	}

	inject(url, callback = null) {
		if (typeof url !== 'string') {
			throw new TypeError('inject: url parameter must be of type string')
		}
		// a path beginning by file:// is considered a URL and not a local file
		if ((url.trim().toLowerCase().indexOf('http://') === 0) || (url.trim().toLowerCase().indexOf('https://') === 0) || (url.trim().toLowerCase().indexOf('file://') === 0)) {
			return this._callToTabDriver((callback) => { this._tabDriver._injectFromUrl(url, callback) }, callback)
		} else {
			return this._callToTabDriver((callback) => { this._tabDriver._injectFromDisk(url, callback) }, callback)
		}
	}

	scrollTo(x, y, callback) {
		return this.scroll(x, y, callback)
	}

	scroll(x, y, callback = null) {
		if (typeof(x) !== "number") {
			throw new TypeError("scrollTo: x parameter must be of type number")
		}
		if (typeof(y) !== "number") {
			throw new TypeError("scrollTo: y parameter must be of type number")
		}
		return this._callToTabDriver((callback) => { this._tabDriver._scroll(x, y, callback) }, callback)
	}

	scrollToBottom(callback = null) {
		return this._callToTabDriver((callback) => { this._tabDriver._scrollToBottom(callback) }, callback)
	}

	// These methods are not documented and should not be used:
	// cookies are global to the browser and have nothing to do with a tab.
	// But they work.
	getAllCookies(callback) { return this.nick.getAllCookies(callback) }
	deleteAllCookies(callback) { return this.nick.deleteAllCookies(callback) }
	deleteCookie(name, domain, callback) { return this.nick.deleteCookie(name, domain, callback) }
	setCookie(cookie, callback) { return this.nick.setCookie(cookie, callback) }

}

module.exports = Tab
<commit_msg>sendKeys: fix mistake in options checker<commit_after>const Promise = require("bluebird")
const _ = require("lodash")

class Tab {

	constructor(nick, tabDriver) {
		this._nick = nick
		this._tabDriver = tabDriver
		this._actionInProgress = false
		// set default values for confirm and prompt JS dialogs
		this._tabDriver._onConfirm = (msg) => {
			return true
		}
		this._tabDriver._onPrompt = (msg) => {
			return ""
		}
	}

	// Read-only members
	get nick() { return this._nick }
	get driver() { return this._tabDriver } // shorter but less descriptive way to get the tab driver
	get tabDriver() { return this._tabDriver }
	get actionInProgress() { return this._actionInProgress }
	get closed() { return this._tabDriver.closed }
	get crashed() { return this._tabDriver.crashed }
	get id() { return this._tabDriver.id }

	set onConfirm(f) {
		if (typeof(f) !== "function") {
			throw new TypeError("onConfirm must receive a function")
		}
		this._tabDriver._onConfirm = f
	}

	set onPrompt(f) {
		if (typeof(f) !== "function") {
			throw new TypeError("onPrompt must receive a function")
		}
		this._tabDriver._onPrompt = f
	}

	_callToTabDriver(action, callback, multiArgs = false) {
		if (this._tabDriver.crashed) {
			throw new Error('this tab has crashed, no other actions can be done with it')
		}
		if (this._tabDriver.closed) {
			throw new Error('this tab has finished its work (close() was called), no other actions can be done with it')
		}
		if (this._actionInProgress) {
			throw new Error('cannot do this while another tab method is already running, each tab can execute only one action at a time')
		}
		const getAugmentedCallback = (callback) => {
			let that = this
			return function() {
				that._actionInProgress = false
				callback.apply(null, arguments)
			}
		}
		this._actionInProgress = true
		if (callback != null) {
			if (typeof callback !== 'function') {
				throw new TypeError('callback parameter must be of type function')
			}
			action(getAugmentedCallback(callback))
		} else {
			return Promise.fromCallback((callback) => { action(getAugmentedCallback(callback)) }, { multiArgs: multiArgs })
		}
	}

	wait(duration, callback = null) {
		if ((typeof duration !== 'number') || (duration <= 0)) {
			throw new TypeError(`wait: duration parameter must be a positive number`)
		}
		return this._callToTabDriver((callback) => {
			setTimeout(() => {
				callback()
			}, duration)
		}, callback)
	}

	close(callback = null) {
		return this._callToTabDriver((callback) => {
			this._tabDriver._close((err) => {
				if (!err) {
					this.nick.unrefTabById(this.id)
				}
				callback(err)
			})
		}, callback)
	}

	open(url, options = {}, callback = null) {
		if (typeof url !== 'string') {
			throw new TypeError('open: url parameter must be of type string')
		}
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('open: options parameter must be of type plain object')
		}
		if (url.indexOf('://') < 0) {
			url = `http://${url}`
		}
		return this._callToTabDriver((callback) => { this._tabDriver._open(url.trim(), options, callback) }, callback, true) // use multiArgs
	}

	isVisible(selectors, operator, callback) { return this._isVisibleOrPresent("_waitUntilVisible", selectors, operator, callback) }
	isPresent(selectors, operator, callback) { return this._isVisibleOrPresent("_waitUntilPresent", selectors, operator, callback) }
	_isVisibleOrPresent(method, selectors, operator = null, callback = null) {
		if (typeof operator === 'function') {
			callback = operator
			operator = null
		}
		const f = (callback) => {
			this._callTabDriverWaitMethod(method, selectors, 1, operator, (err) => {
				callback(null, !Boolean(err))
			})
		}
		if (callback) {
			f(callback)
		} else {
			return Promise.fromCallback((callback) => { f(callback) })
		}
	}

	// short variant (made to look pretty when using `await untilVisible()` for example)
	untilVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilVisible', selectors, duration, operator, callback) }
	whileVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhileVisible', selectors, duration, operator, callback) }
	untilPresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilPresent', selectors, duration, operator, callback) }
	whilePresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhilePresent', selectors, duration, operator, callback) }
	// standard variant
	waitUntilVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilVisible', selectors, duration, operator, callback) }
	waitWhileVisible(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhileVisible', selectors, duration, operator, callback) }
	waitUntilPresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitUntilPresent', selectors, duration, operator, callback) }
	waitWhilePresent(selectors, duration, operator, callback) { return this._callTabDriverWaitMethod('_waitWhilePresent', selectors, duration, operator, callback) }
	_callTabDriverWaitMethod(method, selectors, duration = null, operator = null, callback = null) {
		if (typeof selectors === 'string') {
			selectors = [selectors]
		} else if (Array.isArray(selectors)) {
			if (selectors.length > 0)
				for (const sel of selectors) {
					if (typeof sel !== 'string') {
						throw new TypeError(`${method}: selectors parameter must be a string or an array of strings (css paths)`)
					}
				}
			else
				throw new TypeError(`${method}: selectors parameter must contain at least one string (css path)`)
		} else {
			throw new TypeError(`${method}: selectors parameter must be a string or an array of strings (css paths)`)
		}
		selectors = selectors.map((s) => {
			s = s.trim()
			if (s.length <= 0) {
				throw new TypeError(`${method}: selectors parameter cannot contain an empty string`)
			}
			return s
		})
		if (typeof duration === 'function') {
			callback = duration
			duration = null
		} else if (operator === 'function') {
			callback = operator
			operator = null
		}
		if (typeof duration === 'string') {
			// allow mix-up of duration and operator parameters
			const d = duration
			duration = operator
			operator = d
		}
		if (duration === null) {
			duration = 5000 // same as the default of CasperJS's waitFor()
		} else if ((typeof duration !== 'number') || (duration <= 0)) {
			throw new TypeError(`${method}: duration parameter must be a positive number`)
		}
		if (operator === null) {
			operator = 'and'
		} else if ((operator !== 'and') && (operator !== 'or')) {
			throw new TypeError(`${method}: operator parameter must be either "and" or "or"`)
		}
		return this._callToTabDriver((callback) => { this._tabDriver[method](selectors, duration, operator, callback) }, callback)
	}

	click(selector, options = {}, callback = null) {
		if (typeof selector !== 'string') {
			throw new TypeError('click: selector parameter must be of type string')
		}
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			// TODO add option for "real click", standardize, update skel
			throw new TypeError('click: options parameter must be of type plain object')
		}
		if (_.has(options, 'mouseEmulation')) {
			if (typeof options.submit !== 'boolean') {
				throw new TypeError('click: mouseEmulation option must be of type boolean')
			}
		} else {
			options.mouseEmulation = false
		}
		// TODO
		//this.waitUntilPresent(selector, () => {
			return this._callToTabDriver((callback) => { this._tabDriver._click(selector, options, callback) }, callback)
		//})
	}

	evaluate(func, arg = null, callback = null) {
		if (_.isPlainObject(func)) {
			// allow mix-up of func and arg parameters
			const f = func
			func = arg
			arg = f
		}
		if (typeof func !== 'function') {
			throw new TypeError('evaluate: func parameter must be of type function')
		}
		if (typeof arg === 'function') {
			callback = arg
			arg = null
		}
		if ((arg != null) && !_.isPlainObject(arg) && !_.isArray(arg)) {
			throw new TypeError('evaluate: arg parameter must be a plain object or an array')
		}
		return this._callToTabDriver((callback) => { this._tabDriver._evaluate(func, arg, callback) }, callback)
	}

	getUrl(callback = null) {
		return this._callToTabDriver((callback) => { this._tabDriver._getUrl(callback) }, callback)
	}

	getContent(callback = null) {
		return this._callToTabDriver((callback) => { this._tabDriver._getContent(callback) }, callback)
	}

	fill(selector, params, options = {}, callback = null) {
		if (typeof selector !== 'string') {
			throw new TypeError('fill: selector parameter must be of type string')
		}
		if (!_.isPlainObject(params)) {
			throw new TypeError('fill: params parameter must be of type plain object')
		}
		// TODO check params in more detail
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('fill: options parameter must be of type plain object')
		}
		if (_.has(options, 'submit')) {
			if (typeof options.submit !== 'boolean') {
				throw new TypeError('submit option must be of type boolean')
			}
		} else {
			options.submit = false
		}
		// TODO
		//this.waitUntilPresent(selector, () => {
			return this._callToTabDriver((callback) => { this._tabDriver._fill(selector, params, options, callback) }, callback)
		//})
	}

	screenshot(filename, options = {}, callback = null) {
		if ((filename != null) && (typeof filename !== 'string')) {
			throw new TypeError('screenshot: filename parameter must be null or of type string')
		}
		// TODO check filename for png, jpg, jpeg, pdf, 'base64:png', 'base64:jpg'...
		// TODO more checks
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('screenshot: options parameter must be of type plain object')
		}
		// TODO check options: clipRect, selector, fullPage...
		return this._callToTabDriver((callback) => { this._tabDriver._screenshot(filename, options, callback) }, callback)
	}

	sendKeys(selector, keys, options = {}, callback = null) {
		if (typeof selector !== 'string') {
			throw new TypeError('sendKeys: selector parameter must be of type string')
		}
		if ((typeof keys !== 'string') && (typeof keys !== 'number')) {
			throw new TypeError('sendKeys: keys parameter must be of type string or number')
		}
		if (typeof options === 'function') {
			callback = options
			options = {}
		}
		if (!_.isPlainObject(options)) {
			throw new TypeError('sendKeys: options parameter must be of type plain object')
		}
		if (_.has(options, 'keepFocus')) {
			if (typeof options.keepFocus !== 'boolean') {
				throw new TypeError('sendKeys: keepFocus option must be of type boolean')
			}
		}
		if (_.has(options, 'reset')) {
			if (typeof options.reset !== 'boolean') {
				throw new TypeError('sendKeys: reset option must be of type boolean')
			}
		}
		if (_.has(options, 'modifiers')) {
			if (typeof options.modifiers !== 'string') {
				throw new TypeError('sendKeys: modifiers option must be of type string')
			}
		}
		// TODO
		//this.waitUntilPresent(selector, () => {
			return this._callToTabDriver((callback) => { this._tabDriver._sendKeys(selector, keys, options, callback) }, callback)
		//})
	}

	inject(url, callback = null) {
		if (typeof url !== 'string') {
			throw new TypeError('inject: url parameter must be of type string')
		}
		// a path beginning by file:// is considered a URL and not a local file
		if ((url.trim().toLowerCase().indexOf('http://') === 0) || (url.trim().toLowerCase().indexOf('https://') === 0) || (url.trim().toLowerCase().indexOf('file://') === 0)) {
			return this._callToTabDriver((callback) => { this._tabDriver._injectFromUrl(url, callback) }, callback)
		} else {
			return this._callToTabDriver((callback) => { this._tabDriver._injectFromDisk(url, callback) }, callback)
		}
	}

	scrollTo(x, y, callback) {
		return this.scroll(x, y, callback)
	}

	scroll(x, y, callback = null) {
		if (typeof(x) !== "number") {
			throw new TypeError("scrollTo: x parameter must be of type number")
		}
		if (typeof(y) !== "number") {
			throw new TypeError("scrollTo: y parameter must be of type number")
		}
		return this._callToTabDriver((callback) => { this._tabDriver._scroll(x, y, callback) }, callback)
	}

	scrollToBottom(callback = null) {
		return this._callToTabDriver((callback) => { this._tabDriver._scrollToBottom(callback) }, callback)
	}

	// These methods are not documented and should not be used:
	// cookies are global to the browser and have nothing to do with a tab.
	// But they work.
	getAllCookies(callback) { return this.nick.getAllCookies(callback) }
	deleteAllCookies(callback) { return this.nick.deleteAllCookies(callback) }
	deleteCookie(name, domain, callback) { return this.nick.deleteCookie(name, domain, callback) }
	setCookie(cookie, callback) { return this.nick.setCookie(cookie, callback) }

}

module.exports = Tab
<|endoftext|>


<commit_before>/*!
 * Escaper
 * https://github.com/kobezzza/Escaper
 *
 * Released under the MIT license
 * https://github.com/kobezzza/Escaper/blob/master/LICENSE
 */

var Escaper = {
	VERSION: [1, 4, 19],
	isLocal: false
};

var isNode = false;

try {
	isNode = 'object' === typeof process && Object.prototype.toString.call(process) === '[object process]';

} catch (ignore) {

}

/* istanbul ignore next */
if (isNode) {
	Escaper.isLocal = Boolean(global.EscaperIsLocal || global['EscaperIsLocal']);

	if (!Escaper.isLocal) {
		module.exports =
			exports =
			root = Escaper;
	}

} else {
	root.Escaper = Escaper;
}

var escapeMap = {
	'"': true,
	"'" : true,
	'/': true,
	'`': true
};

var sCommentsMap = {
	'//': true
};

var mCommentsMap = {
	'/*': true,
	'/**': true,
	'/*!': true
};

var keyArr = [],
	finalMap = {};

for (let key in escapeMap) {
	/* istanbul ignore if */
	if (!escapeMap.hasOwnProperty(key)) {
		continue;
	}

	keyArr.push(key);
	finalMap[key] = true;
}

for (let key in sCommentsMap) {
	/* istanbul ignore if */
	if (!sCommentsMap.hasOwnProperty(key)) {
		continue;
	}

	keyArr.push(key);
	finalMap[key] = true;
}

for (let key in mCommentsMap) {
	/* istanbul ignore if */
	if (!mCommentsMap.hasOwnProperty(key)) {
		continue;
	}

	keyArr.push(key);
	finalMap[key] = true;
}

var rgxpFlagsMap = {
	'g': true,
	'm': true,
	'i': true,
	'y': true,
	'u': true
};

var rgxpFlags = [];
for (let key in rgxpFlagsMap) {
	/* istanbul ignore if */
	if (!rgxpFlagsMap.hasOwnProperty(key)) {
		continue;
	}

	rgxpFlags.push(key);
}

var escapeEndMap = {
	'-': true,
	'+': true,
	'*': true,
	'%': true,
	'~': true,
	'>': true,
	'<': true,
	'^': true,
	',': true,
	';': true,
	'=': true,
	'|': true,
	'&': true,
	'!': true,
	'?': true,
	':': true,
	'(': true,
	'{': true,
	'[': true
};

var escapeEndWordMap = {
	'typeof': true,
	'void': true,
	'instaceof': true,
	'delete': true,
	'in': true,
	'new': true
};

var cache = {},
	content = [];

/**
 * @param {!Object} obj
 * @param {!Object} p
 * @param {(boolean|number)} val
 */
function mix(obj, p, val) {
	for (let key in obj) {
		/* istanbul ignore if */
		if (!obj.hasOwnProperty(key)) {
			continue;
		}

		if (key in p === false) {
			p[key] = val;
		}
	}
}

/**
 * Стек содержимого
 * @type {!Array}
 */
Escaper.quotContent = content;

var uSRgxp = /[^\s\/]/,
	wRgxp = /[a-z]/,
	sRgxp = /\s/,
	nRgxp = /\r|\n/;

var symbols,
	snakeskinRgxp;

/**
 * Заметить блоки вида ' ... ', " ... ", ` ... `, / ... /, // ..., /* ... *\/ на
 * __ESCAPER_QUOT__номер_ в указанной строке
 *
 * @param {string} str - исходная строка
 * @param {(Object|boolean)=} [opt_withCommentsOrParams=false] - таблица вырезаемых последовательностей:
 *
 *     (если установить значение параметру -1, то он будет полностью вырезаться,
 *     т.е. без возможности обратной замены, иначе true/false - включить/исключить последовательность)
 *
 *     *) @all - специальная команда для выделения всех последовательностей
 *     *) @comments - специальная команда для выделения всех видов комментариев
 *     *) @literals - специальная команда для выделения литералов строк и регулярных выражений
 *     *) `
 *     *) '
 *     *) "
 *     *) /
 *     *) //
 *     *) /*
 *     *) /**
 *     *) /*!
 *
 *     ИЛИ если логическое значение, то вырезаются литералы с комментариями (true) / литералы (false)
 *
 * @param {Array=} [opt_quotContent=Escaper.quotContent] - стек содержимого
 * @param {?boolean=} [opt_snakeskin] - если true, то при экранировании учитываются конструкции Snakeskin
 * @return {string}
 */
Escaper.replace = function (str, opt_withCommentsOrParams, opt_quotContent, opt_snakeskin) {
	symbols = symbols ||
		Escaper.symbols ||
		Escaper['symbols'] ||
		'a-z';

	snakeskinRgxp = snakeskinRgxp ||
		Escaper.snakeskinRgxp ||
		Escaper['snakeskinRgxp'] ||
		new RegExp(`[!$${symbols}_]`, 'i');

	var isObj = opt_withCommentsOrParams instanceof Object;
	var p = isObj ?
		Object(opt_withCommentsOrParams) : {};

	var withComments = false;
	if (typeof opt_withCommentsOrParams === 'boolean') {
		withComments = Boolean(opt_withCommentsOrParams);
	}

	if ('@comments' in p) {
		mix(mCommentsMap, p, p['@comments']);
		mix(sCommentsMap, p, p['@comments']);
		delete p['@comments'];
	}

	if ('@literals' in p) {
		mix(escapeMap, p, p['@literals']);
		delete p['@literals'];
	}

	if ('@all' in p) {
		mix(finalMap, p, p['@all']);
		delete p['@all'];
	}

	var cacheKey = '';
	for (let i = -1; ++i < keyArr.length;) {
		let el = keyArr[i];

		if (mCommentsMap[el] || sCommentsMap[el]) {
			p[el] = withComments || p[el];

		} else {
			p[el] = p[el] || !isObj;
		}

		cacheKey += `${p[el]},`;
	}

	var initStr = str;
	var stack = opt_quotContent ||
		content;

	if (stack === content && cache[cacheKey] && cache[cacheKey][initStr]) {
		return cache[cacheKey][initStr];
	}

	var begin = false,
		end = true;

	var escape = false,
		comment = false;

	var selectionStart = 0,
		block = false;

	var templateVar = 0,
		filterStart = false;

	var cut,
		label;

	var part = '',
		rPart = '';

	for (let i = -1; ++i < str.length;) {
		let el = str.charAt(i),
			next = str.charAt(i + 1);

		let word = str.substr(i, 2),
			extWord = str.substr(i, 3);

		if (!comment) {
			if (!begin) {
				if (el === '/') {
					if (sCommentsMap[word] || mCommentsMap[word]) {
						if (sCommentsMap[extWord] || mCommentsMap[extWord]) {
							comment = extWord;

						} else {
							comment = word;
						}
					}

					if (comment) {
						selectionStart = i;
						continue;
					}
				}

				if (escapeEndMap[el] || escapeEndWordMap[rPart]) {
					end = true;
					rPart = '';

				} else if (uSRgxp.test(el)) {
					end = false;
				}

				if (wRgxp.test(el)) {
					part += el;

				} else {
					rPart = part;
					part = '';
				}

				let skip = false;
				if (opt_snakeskin) {
					if (el === '|' && snakeskinRgxp.test(next)) {
						filterStart = true;
						end = false;
						skip = true;

					} else if (filterStart && sRgxp.test(el)) {
						filterStart = false;
						end = true;
						skip = true;
					}
				}

				if (!skip) {
					if (escapeEndMap[el]) {
						end = true;

					} else if (uSRgxp.test(el)) {
						end = false;
					}
				}
			}

			// Блоки [] внутри регулярного выражения
			if (begin === '/' && !escape) {
				if (el === '[') {
					block = true;

				} else if (el === ']') {
					block = false;
				}
			}

			if (!begin && templateVar) {
				if (el === '}') {
					templateVar--;

				} else if (el === '{') {
					templateVar++;
				}

				if (!templateVar) {
					el = '`';
				}
			}

			if (begin === '`' && !escape && word === '${') {
				el = '`';
				i++;
				templateVar++;
			}

			if (finalMap[el] && (el !== '/' || end) && !begin) {
				begin = el;
				selectionStart = i;

			} else if (begin && (el === '\\' || escape)) {
				escape = !escape;

			} else if (finalMap[el] && begin === el && !escape && (begin !== '/' || !block)) {
				if (el === '/') {
					for (let j = -1; ++j < rgxpFlags.length;) {
						if (rgxpFlagsMap[str.charAt(i + 1)]) {
							i++;
						}
					}
				}

				begin = false;
				end = false;

				if (p[el]) {
					cut = str.substring(selectionStart, i + 1);

					if (p[el] === -1) {
						label = '';

					} else {
						label = `__ESCAPER_QUOT__${stack.length}_`;
						stack.push(cut);
					}

					str = str.substring(0, selectionStart) + label + str.substring(i + 1);
					i += label.length - cut.length;
				}
			}

		} else if ((nRgxp.test(next) && sCommentsMap[comment]) ||
			(mCommentsMap[el + str.charAt(i - 1)] && i - selectionStart > 2 && mCommentsMap[comment])

		) {
			if (p[comment]) {
				cut = str.substring(selectionStart, i + 1);

				if (p[comment] === -1) {
					label = '';

				} else {
					label = `__ESCAPER_QUOT__${stack.length}_`;
					stack.push(cut);
				}

				str = str.substring(0, selectionStart) + label + str.substring(i + 1);
				i += label.length - cut.length;
			}

			comment = false;
		}
	}

	if (stack === content) {
		cache[cacheKey] = cache[cacheKey] || {};
		cache[cacheKey][initStr] = str;
	}

	return str;
};

var pasteRgxp = /__ESCAPER_QUOT__(\d+)_/g;

/**
 * Заметить __ESCAPER_QUOT__номер_ в указанной строке на реальное содержимое
 *
 * @param {string} str - исходная строка
 * @param {Array=} [opt_quotContent=Escaper.quotContent] - стек содержимого
 * @return {string}
 */
Escaper.paste = function (str, opt_quotContent) {
	var stack = opt_quotContent || content;
	return str.replace(pasteRgxp, (sstr, pos) => stack[pos]);
};
<commit_msg>Изменён тип экспорта<commit_after>/*!
 * Escaper
 * https://github.com/kobezzza/Escaper
 *
 * Released under the MIT license
 * https://github.com/kobezzza/Escaper/blob/master/LICENSE
 */

var Escaper = {
	VERSION: [2, 0, 0]
};

var isNode = false;

try {
	isNode = 'object' === typeof process && Object.prototype.toString.call(process) === '[object process]';

} catch (ignore) {

}

/* istanbul ignore next */
if (isNode) {
	module.exports =
		exports =
			root = Escaper;

} else {
	root.Escaper = Escaper;
}

var escapeMap = {
	'"': true,
	"'" : true,
	'/': true,
	'`': true
};

var sCommentsMap = {
	'//': true
};

var mCommentsMap = {
	'/*': true,
	'/**': true,
	'/*!': true
};

var keyArr = [],
	finalMap = {};

for (let key in escapeMap) {
	/* istanbul ignore if */
	if (!escapeMap.hasOwnProperty(key)) {
		continue;
	}

	keyArr.push(key);
	finalMap[key] = true;
}

for (let key in sCommentsMap) {
	/* istanbul ignore if */
	if (!sCommentsMap.hasOwnProperty(key)) {
		continue;
	}

	keyArr.push(key);
	finalMap[key] = true;
}

for (let key in mCommentsMap) {
	/* istanbul ignore if */
	if (!mCommentsMap.hasOwnProperty(key)) {
		continue;
	}

	keyArr.push(key);
	finalMap[key] = true;
}

var rgxpFlagsMap = {
	'g': true,
	'm': true,
	'i': true,
	'y': true,
	'u': true
};

var rgxpFlags = [];
for (let key in rgxpFlagsMap) {
	/* istanbul ignore if */
	if (!rgxpFlagsMap.hasOwnProperty(key)) {
		continue;
	}

	rgxpFlags.push(key);
}

var escapeEndMap = {
	'-': true,
	'+': true,
	'*': true,
	'%': true,
	'~': true,
	'>': true,
	'<': true,
	'^': true,
	',': true,
	';': true,
	'=': true,
	'|': true,
	'&': true,
	'!': true,
	'?': true,
	':': true,
	'(': true,
	'{': true,
	'[': true
};

var escapeEndWordMap = {
	'typeof': true,
	'void': true,
	'instaceof': true,
	'delete': true,
	'in': true,
	'new': true
};

var cache = {},
	content = [];

/**
 * @param {!Object} obj
 * @param {!Object} p
 * @param {(boolean|number)} val
 */
function mix(obj, p, val) {
	for (let key in obj) {
		/* istanbul ignore if */
		if (!obj.hasOwnProperty(key)) {
			continue;
		}

		if (key in p === false) {
			p[key] = val;
		}
	}
}

/**
 * Стек содержимого
 * @type {!Array}
 */
Escaper.quotContent = content;

var uSRgxp = /[^\s\/]/,
	wRgxp = /[a-z]/,
	sRgxp = /\s/,
	nRgxp = /\r|\n/;

var symbols,
	snakeskinRgxp;

Escaper.snakeskinRgxp = Escaper.snakeskinRgxp || null;
Escaper.symbols = Escaper.symbols || null;

/**
 * Заметить блоки вида ' ... ', " ... ", ` ... `, / ... /, // ..., /* ... *\/ на
 * __ESCAPER_QUOT__номер_ в указанной строке
 *
 * @param {string} str - исходная строка
 * @param {(Object|boolean)=} [opt_withCommentsOrParams=false] - таблица вырезаемых последовательностей:
 *
 *     (если установить значение параметру -1, то он будет полностью вырезаться,
 *     т.е. без возможности обратной замены, иначе true/false - включить/исключить последовательность)
 *
 *     *) @all - специальная команда для выделения всех последовательностей
 *     *) @comments - специальная команда для выделения всех видов комментариев
 *     *) @literals - специальная команда для выделения литералов строк и регулярных выражений
 *     *) `
 *     *) '
 *     *) "
 *     *) /
 *     *) //
 *     *) /*
 *     *) /**
 *     *) /*!
 *
 *     ИЛИ если логическое значение, то вырезаются литералы с комментариями (true) / литералы (false)
 *
 * @param {Array=} [opt_quotContent=Escaper.quotContent] - стек содержимого
 * @param {?boolean=} [opt_snakeskin] - если true, то при экранировании учитываются конструкции Snakeskin
 * @return {string}
 */
Escaper.replace = function (str, opt_withCommentsOrParams, opt_quotContent, opt_snakeskin) {
	symbols = symbols ||
		Escaper.symbols ||
		Escaper['symbols'] ||
		'a-z';

	snakeskinRgxp = snakeskinRgxp ||
		Escaper.snakeskinRgxp ||
		Escaper['snakeskinRgxp'] ||
		new RegExp(`[!$${symbols}_]`, 'i');

	var isObj = opt_withCommentsOrParams instanceof Object;
	var p = isObj ?
		Object(opt_withCommentsOrParams) : {};

	var withComments = false;
	if (typeof opt_withCommentsOrParams === 'boolean') {
		withComments = Boolean(opt_withCommentsOrParams);
	}

	if ('@comments' in p) {
		mix(mCommentsMap, p, p['@comments']);
		mix(sCommentsMap, p, p['@comments']);
		delete p['@comments'];
	}

	if ('@literals' in p) {
		mix(escapeMap, p, p['@literals']);
		delete p['@literals'];
	}

	if ('@all' in p) {
		mix(finalMap, p, p['@all']);
		delete p['@all'];
	}

	var cacheKey = '';
	for (let i = -1; ++i < keyArr.length;) {
		let el = keyArr[i];

		if (mCommentsMap[el] || sCommentsMap[el]) {
			p[el] = withComments || p[el];

		} else {
			p[el] = p[el] || !isObj;
		}

		cacheKey += `${p[el]},`;
	}

	var initStr = str;
	var stack = opt_quotContent ||
		content;

	if (stack === content && cache[cacheKey] && cache[cacheKey][initStr]) {
		return cache[cacheKey][initStr];
	}

	var begin = false,
		end = true;

	var escape = false,
		comment = false;

	var selectionStart = 0,
		block = false;

	var templateVar = 0,
		filterStart = false;

	var cut,
		label;

	var part = '',
		rPart = '';

	for (let i = -1; ++i < str.length;) {
		let el = str.charAt(i),
			next = str.charAt(i + 1);

		let word = str.substr(i, 2),
			extWord = str.substr(i, 3);

		if (!comment) {
			if (!begin) {
				if (el === '/') {
					if (sCommentsMap[word] || mCommentsMap[word]) {
						if (sCommentsMap[extWord] || mCommentsMap[extWord]) {
							comment = extWord;

						} else {
							comment = word;
						}
					}

					if (comment) {
						selectionStart = i;
						continue;
					}
				}

				if (escapeEndMap[el] || escapeEndWordMap[rPart]) {
					end = true;
					rPart = '';

				} else if (uSRgxp.test(el)) {
					end = false;
				}

				if (wRgxp.test(el)) {
					part += el;

				} else {
					rPart = part;
					part = '';
				}

				let skip = false;
				if (opt_snakeskin) {
					if (el === '|' && snakeskinRgxp.test(next)) {
						filterStart = true;
						end = false;
						skip = true;

					} else if (filterStart && sRgxp.test(el)) {
						filterStart = false;
						end = true;
						skip = true;
					}
				}

				if (!skip) {
					if (escapeEndMap[el]) {
						end = true;

					} else if (uSRgxp.test(el)) {
						end = false;
					}
				}
			}

			// Блоки [] внутри регулярного выражения
			if (begin === '/' && !escape) {
				if (el === '[') {
					block = true;

				} else if (el === ']') {
					block = false;
				}
			}

			if (!begin && templateVar) {
				if (el === '}') {
					templateVar--;

				} else if (el === '{') {
					templateVar++;
				}

				if (!templateVar) {
					el = '`';
				}
			}

			if (begin === '`' && !escape && word === '${') {
				el = '`';
				i++;
				templateVar++;
			}

			if (finalMap[el] && (el !== '/' || end) && !begin) {
				begin = el;
				selectionStart = i;

			} else if (begin && (el === '\\' || escape)) {
				escape = !escape;

			} else if (finalMap[el] && begin === el && !escape && (begin !== '/' || !block)) {
				if (el === '/') {
					for (let j = -1; ++j < rgxpFlags.length;) {
						if (rgxpFlagsMap[str.charAt(i + 1)]) {
							i++;
						}
					}
				}

				begin = false;
				end = false;

				if (p[el]) {
					cut = str.substring(selectionStart, i + 1);

					if (p[el] === -1) {
						label = '';

					} else {
						label = `__ESCAPER_QUOT__${stack.length}_`;
						stack.push(cut);
					}

					str = str.substring(0, selectionStart) + label + str.substring(i + 1);
					i += label.length - cut.length;
				}
			}

		} else if ((nRgxp.test(next) && sCommentsMap[comment]) ||
			(mCommentsMap[el + str.charAt(i - 1)] && i - selectionStart > 2 && mCommentsMap[comment])

		) {
			if (p[comment]) {
				cut = str.substring(selectionStart, i + 1);

				if (p[comment] === -1) {
					label = '';

				} else {
					label = `__ESCAPER_QUOT__${stack.length}_`;
					stack.push(cut);
				}

				str = str.substring(0, selectionStart) + label + str.substring(i + 1);
				i += label.length - cut.length;
			}

			comment = false;
		}
	}

	if (stack === content) {
		cache[cacheKey] = cache[cacheKey] || {};
		cache[cacheKey][initStr] = str;
	}

	return str;
};

var pasteRgxp = /__ESCAPER_QUOT__(\d+)_/g;

/**
 * Заметить __ESCAPER_QUOT__номер_ в указанной строке на реальное содержимое
 *
 * @param {string} str - исходная строка
 * @param {Array=} [opt_quotContent=Escaper.quotContent] - стек содержимого
 * @return {string}
 */
Escaper.paste = function (str, opt_quotContent) {
	var stack = opt_quotContent || content;
	return str.replace(pasteRgxp, (sstr, pos) => stack[pos]);
};
<|endoftext|>


<commit_before>'use strict';

module.exports = function (grunt) {
    var esprima = require('esprima');
    var getComment = require('./comments').getComment;

    function extractStrings(fileName) {
        function walkTree(node, fn) {
            fn(node);

            for (var key in node) {
                var obj = node[key];
                if (typeof obj === 'object') {
                    walkTree(obj, fn);
                }
            }
        }

        function tryEval(code) {
            var val;
            try {
                val = eval(code);
            } catch (e) {
                grunt.verbose.warn(e + ' - while statically evaluating code: ' + code);
            }
            return val;
        }

        var code = grunt.file.read(fileName);
        var syntax = esprima.parse(code, {comment: true, loc: true, tokens: true, range: true});
        var items;
        var handleGtCall = function (node, gtName, module) {
            if (node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                (node.callee.name === gtName ||
                    (node.callee.type === 'MemberExpression' &&
                     node.callee.object.name === gtName &&
                     node.callee.property.name === 'gettext')
                ) &&
                node['arguments'] !== null &&
                node['arguments'].length >= 1
            ) {
                var msgId = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                if (!msgId) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt/gt.gettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.verbose.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgid: msgId,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            } else if (
                node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                node.callee.type === 'MemberExpression' &&
                node.callee.object.name === gtName &&
                node.callee.property.name === 'pgettext' &&
                node['arguments'] !== null &&
                node['arguments'].length === 2
            ) {
                var msgId = node['arguments'][1].value || tryEval(code.slice(node['arguments'][1].range[0], node['arguments'][1].range[1]));
                var msgContext = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                if (!msgId || !msgContext) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt.pgettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.log.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgid: msgId,
                    msgctxt: msgContext,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            } else if (
                node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                node.callee.type === 'MemberExpression' &&
                node.callee.object.name === gtName &&
                node.callee.property.name === 'ngettext' &&
                node['arguments'] !== null &&
                node['arguments'].length === 3
            ) {
                var msgId = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                var msgIdPlural = node['arguments'][1].value || tryEval(code.slice(node['arguments'][1].range[0], node['arguments'][1].range[1]));
                if (!msgId || !msgIdPlural || !node['arguments'][2]) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt.ngettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.log.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node  in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgid: msgId,
                    msgid_plural: msgIdPlural,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            } else if (
                node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                node.callee.type === 'MemberExpression' &&
                node.callee.object.name === gtName &&
                node.callee.property.name === 'npgettext' &&
                node['arguments'] !== null &&
                node['arguments'].length === 4
            ) {
                var msgContext = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                var msgId = node['arguments'][1].value || tryEval(code.slice(node['arguments'][1].range[0], node['arguments'][1].range[1]));
                var msgIdPlural = node['arguments'][2].value || tryEval(code.slice(node['arguments'][2].range[0], node['arguments'][2].range[1]));
                if (!msgContext || !msgId || !msgIdPlural || !node['arguments'][3]) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt.npgettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.log.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgctxt: msgContext,
                    msgid: msgId,
                    msgid_plural: msgIdPlural,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            }
        };

        walkTree(syntax, function (node) {
            if (node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                (node.callee.name === 'define' ||
                (node.callee.type === 'MemberExpression' &&
                node.callee.object !== null &&
                node.callee.object.name === 'define' &&
                node.callee.property.name === 'async')) &&
                node['arguments'] !== null &&
                node['arguments'].length > 2)
            {
                node['arguments'][1].elements.map(function (el, index) {
                    if (el.value.substr(0, 8) === 'gettext!') {
                        var param = node['arguments'][2].params[index];
                        if (!param) {
                            grunt.log.warn('Unused require-gettext module call in', fileName);
                            return;
                        }
                        return {
                            name: param.name,
                            module: el.value.substr(8)
                        };
                    }
                }).filter(function (arg) {
                    return arg !== undefined;
                }).forEach(function (obj) {
                    var gtName = obj.name;
                    var module = obj.module;

                    if (node['arguments'][2].type === 'FunctionExpression') {
                        walkTree(node['arguments'][2], function (node) {
                            handleGtCall(node, gtName, module);
                        });
                    }
                });
            }
        });
        return items;
    }

    function mkKey(poItem) {
        var key = poItem.msgctxt ? poItem.msgctxt + '\x00' + poItem.msgid : poItem.msgid;
        if (poItem.msgid_plural) {
            key += '\x01' + poItem.msgid_plural;
        }
        return key;
    }

    function MissingContextException(poItem, collidingPoItem) {
        this.poItem = poItem;
        this.collidingPoItem = collidingPoItem;
        this.toString = function () {
            return [
                'Usage of singular and plural form of "',
                poItem.msgid,
                '" in the same context: ',
                (collidingPoItem.msgctxt || '[no context]'),
                '\n\nReferences:\n',
                poItem.references.join(', '),
                '\n',
                collidingPoItem.references.join(', '),
                '\n\nChange the context of at least one of the strings (pgettext) or use ngettext/npgettext in both cases.'
            ].join('');
        };
    }

    function fromFiles(files, options) {
        var PO = require('pofile');
        var _ = require('lodash');
        options = _.defaults({}, options, {
            location: 'full'
        });
        var catalog = files.map(function (srcFile) {
            return {srcFile: srcFile, items: extractStrings(srcFile)};
        }).reduce(function (acc, result) {
            if (!acc) {
                return;
            }
            var items = result.items;
            if (!items) {
                items = [];
                grunt.log.debug('No strings extracted from file ' + result.srcFile);
            }
            items.forEach(function (item) {
                var key = mkKey(item);
                if (!acc[key]) {
                    acc[key] = new PO.Item();
                }
                var poItem = acc[key];
                poItem.msgid = item.msgid;
                poItem.msgid_plural = item.msgid_plural;
                poItem.msgctxt = item.msgctxt;
                if (options.location !== 'none') {
                    var position = item.fileName;
                    if (options.location !== 'file') position += ':' + item.line;
                    if (poItem.references.indexOf(position) < 0) {
                        poItem.references.push(position);
                    }
                }
                if (poItem.references.indexOf('module:' + item.module) < 0) {
                    poItem.references.push('module:' + item.module);
                }
                if (poItem.msgid_plural) {
                    //FIXME: may be, this should be handled by pofile library
                    poItem.msgstr = ['', ''];
                }
                if (item.comment) {
                    var commentItem = PO.parse('msgid ""\nmsgstr ""\n\n' + item.comment + '\nmsgid "temp"\nmsgstr""\n').items[0];
                    poItem.flags = _.extend(poItem.flags, commentItem.flags);
                    poItem.extractedComments = _.uniq([].concat(poItem.extractedComments, commentItem.extractedComments));
                }
            });
            return acc;
        }, {});
        for (var key in catalog) {
            //key without msgid_plural part
            var index = key.indexOf('\x01');
            var shortKey = index < 0 ? key : key.slice(0, index);
            if (shortKey !== key && catalog[shortKey]) {
                throw new MissingContextException(catalog[key], catalog[shortKey]);
            }
        }
        return catalog;
    }

    return {
        extractStrings: extractStrings,
        fromFiles: fromFiles,
        mkKey: mkKey
    };
};
<commit_msg>allow msgctxt to be empty string<commit_after>'use strict';

module.exports = function (grunt) {
    var esprima = require('esprima');
    var getComment = require('./comments').getComment;

    function extractStrings(fileName) {
        function walkTree(node, fn) {
            fn(node);

            for (var key in node) {
                var obj = node[key];
                if (typeof obj === 'object') {
                    walkTree(obj, fn);
                }
            }
        }

        function tryEval(code) {
            var val;
            try {
                val = eval(code);
            } catch (e) {
                grunt.verbose.warn(e + ' - while statically evaluating code: ' + code);
            }
            return val;
        }

        var code = grunt.file.read(fileName);
        var syntax = esprima.parse(code, {comment: true, loc: true, tokens: true, range: true});
        var items;
        var handleGtCall = function (node, gtName, module) {
            if (node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                (node.callee.name === gtName ||
                    (node.callee.type === 'MemberExpression' &&
                     node.callee.object.name === gtName &&
                     node.callee.property.name === 'gettext')
                ) &&
                node['arguments'] !== null &&
                node['arguments'].length >= 1
            ) {
                var msgId = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                if (!msgId) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt/gt.gettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.verbose.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgid: msgId,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            } else if (
                node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                node.callee.type === 'MemberExpression' &&
                node.callee.object.name === gtName &&
                node.callee.property.name === 'pgettext' &&
                node['arguments'] !== null &&
                node['arguments'].length === 2
            ) {
                var msgId = node['arguments'][1].value || tryEval(code.slice(node['arguments'][1].range[0], node['arguments'][1].range[1]));
                var msgContext = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                if (!msgId || msgContext === undefined) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt.pgettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.log.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgid: msgId,
                    msgctxt: msgContext,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            } else if (
                node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                node.callee.type === 'MemberExpression' &&
                node.callee.object.name === gtName &&
                node.callee.property.name === 'ngettext' &&
                node['arguments'] !== null &&
                node['arguments'].length === 3
            ) {
                var msgId = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                var msgIdPlural = node['arguments'][1].value || tryEval(code.slice(node['arguments'][1].range[0], node['arguments'][1].range[1]));
                if (!msgId || !msgIdPlural || !node['arguments'][2]) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt.ngettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.log.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node  in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgid: msgId,
                    msgid_plural: msgIdPlural,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            } else if (
                node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                node.callee.type === 'MemberExpression' &&
                node.callee.object.name === gtName &&
                node.callee.property.name === 'npgettext' &&
                node['arguments'] !== null &&
                node['arguments'].length === 4
            ) {
                var msgContext = node['arguments'][0].value || tryEval(code.slice(node['arguments'][0].range[0], node['arguments'][0].range[1]));
                var msgId = node['arguments'][1].value || tryEval(code.slice(node['arguments'][1].range[0], node['arguments'][1].range[1]));
                var msgIdPlural = node['arguments'][2].value || tryEval(code.slice(node['arguments'][2].range[0], node['arguments'][2].range[1]));
                if (msgContext === undefined || !msgId || !msgIdPlural || !node['arguments'][3]) {
                    if (getComment(syntax, node).indexOf('#, dynamic') >= 0) {
                        grunt.verbose.warn('Ignoring gt.npgettext call with dynamic flag set.');
                        return;
                    }
                    grunt.verbose.warn(JSON.stringify(node['arguments'], null, 4));
                    grunt.log.error('Override this by adding a "dynamic" flag to the translator comments');
                    grunt.fail.warn('Could not read node in file: ' + fileName + ' use --verbose for more info');
                    return;
                }
                items = items || [];
                items.push({
                    msgctxt: msgContext,
                    msgid: msgId,
                    msgid_plural: msgIdPlural,
                    module: module,
                    fileName: fileName,
                    line: node.loc.start.line,
                    comment: getComment(syntax, node)
                });
            }
        };

        walkTree(syntax, function (node) {
            if (node !== null &&
                node.type === 'CallExpression' &&
                node.callee !== null &&
                (node.callee.name === 'define' ||
                (node.callee.type === 'MemberExpression' &&
                node.callee.object !== null &&
                node.callee.object.name === 'define' &&
                node.callee.property.name === 'async')) &&
                node['arguments'] !== null &&
                node['arguments'].length > 2)
            {
                node['arguments'][1].elements.map(function (el, index) {
                    if (el.value.substr(0, 8) === 'gettext!') {
                        var param = node['arguments'][2].params[index];
                        if (!param) {
                            grunt.log.warn('Unused require-gettext module call in', fileName);
                            return;
                        }
                        return {
                            name: param.name,
                            module: el.value.substr(8)
                        };
                    }
                }).filter(function (arg) {
                    return arg !== undefined;
                }).forEach(function (obj) {
                    var gtName = obj.name;
                    var module = obj.module;

                    if (node['arguments'][2].type === 'FunctionExpression') {
                        walkTree(node['arguments'][2], function (node) {
                            handleGtCall(node, gtName, module);
                        });
                    }
                });
            }
        });
        return items;
    }

    function mkKey(poItem) {
        var key = poItem.msgctxt ? poItem.msgctxt + '\x00' + poItem.msgid : poItem.msgid;
        if (poItem.msgid_plural) {
            key += '\x01' + poItem.msgid_plural;
        }
        return key;
    }

    function MissingContextException(poItem, collidingPoItem) {
        this.poItem = poItem;
        this.collidingPoItem = collidingPoItem;
        this.toString = function () {
            return [
                'Usage of singular and plural form of "',
                poItem.msgid,
                '" in the same context: ',
                (collidingPoItem.msgctxt || '[no context]'),
                '\n\nReferences:\n',
                poItem.references.join(', '),
                '\n',
                collidingPoItem.references.join(', '),
                '\n\nChange the context of at least one of the strings (pgettext) or use ngettext/npgettext in both cases.'
            ].join('');
        };
    }

    function fromFiles(files, options) {
        var PO = require('pofile');
        var _ = require('lodash');
        options = _.defaults({}, options, {
            location: 'full'
        });
        var catalog = files.map(function (srcFile) {
            return {srcFile: srcFile, items: extractStrings(srcFile)};
        }).reduce(function (acc, result) {
            if (!acc) {
                return;
            }
            var items = result.items;
            if (!items) {
                items = [];
                grunt.log.debug('No strings extracted from file ' + result.srcFile);
            }
            items.forEach(function (item) {
                var key = mkKey(item);
                if (!acc[key]) {
                    acc[key] = new PO.Item();
                }
                var poItem = acc[key];
                poItem.msgid = item.msgid;
                poItem.msgid_plural = item.msgid_plural;
                poItem.msgctxt = item.msgctxt;
                if (options.location !== 'none') {
                    var position = item.fileName;
                    if (options.location !== 'file') position += ':' + item.line;
                    if (poItem.references.indexOf(position) < 0) {
                        poItem.references.push(position);
                    }
                }
                if (poItem.references.indexOf('module:' + item.module) < 0) {
                    poItem.references.push('module:' + item.module);
                }
                if (poItem.msgid_plural) {
                    //FIXME: may be, this should be handled by pofile library
                    poItem.msgstr = ['', ''];
                }
                if (item.comment) {
                    var commentItem = PO.parse('msgid ""\nmsgstr ""\n\n' + item.comment + '\nmsgid "temp"\nmsgstr""\n').items[0];
                    poItem.flags = _.extend(poItem.flags, commentItem.flags);
                    poItem.extractedComments = _.uniq([].concat(poItem.extractedComments, commentItem.extractedComments));
                }
            });
            return acc;
        }, {});
        for (var key in catalog) {
            //key without msgid_plural part
            var index = key.indexOf('\x01');
            var shortKey = index < 0 ? key : key.slice(0, index);
            if (shortKey !== key && catalog[shortKey]) {
                throw new MissingContextException(catalog[key], catalog[shortKey]);
            }
        }
        return catalog;
    }

    return {
        extractStrings: extractStrings,
        fromFiles: fromFiles,
        mkKey: mkKey
    };
};
<|endoftext|>


<commit_before>/*!
 * fengine
 * Version: 0.0.1
 * Date: 2016/8/2
 * https://github.com/Nuintun/fengine
 *
 * This is licensed under the MIT License (MIT).
 * For details, see: https://github.com/Nuintun/fengine/blob/master/LICENSE
 */

'use strict';

// lib
var os = require('os');
var fs = require('fs');
var url = require('url');
var path = require('path');
var http = require('http');
var util = require('./util');
var colors = require('colors');
var cluster = require('cluster');
var mime = require('mime-types');
var FileSend = require('file-send');
var pkg = require('../package.json');
var Transform = require('./transform');

// variable declaration
var CWD = process.cwd();
var NUMCPUS = os.cpus().length;
var LOGLEVELS = {
  INFO: 'info',
  WARN: 'warn',
  ERROR: 'error'
};
var FAVICON = path.join(__dirname, '../favicon.ico');
var FAVICONSIZE = fs.lstatSync(FAVICON).size;

/**
 * date format
 * @param date
 * @param format
 * @returns {*}
 */
function dateFormat(date, format){
  if (!date instanceof Date) {
    throw new TypeError('param date must be a date.');
  }

  format = format || 'yyyy-MM-dd hh:mm:ss';

  var map = {
    'M': date.getMonth() + 1, // month
    'd': date.getDate(), // date
    'h': date.getHours(), // hours
    'm': date.getMinutes(), // minutes
    's': date.getSeconds(), // seconds
    'q': Math.floor((date.getMonth() + 3) / 3), // quarter
    'S': date.getMilliseconds() // milliseconds
  };

  format = format.replace(/([yMdhmsqS])+/g, function (all, t){
    var v = map[t];

    if (v !== undefined) {
      if (all.length > 1) {
        v = '0' + v;
        v = v.substr(v.length - 2);
      }

      return v;
    } else if (t === 'y') {
      return (date.getFullYear() + '').substr(4 - all.length);
    }

    return all;
  });

  return format;
}

/**
 * is out bound
 * @param path
 * @param root
 * @returns {boolean}
 */
function isOutBound(path, root){
  if (process.platform === 'win32') {
    path = path.toLowerCase();
    root = root.toLowerCase();
  }

  if (path.length < root.length) {
    return true;
  }

  return path.indexOf(root) !== 0;
}

/**
 * decode uri
 * @param uri
 * @returns {*}
 */
function decodeURI(uri){
  try {
    return decodeURIComponent(uri);
  } catch (err) {
    return -1;
  }
}

/**
 * converts a number to a string with
 * a given amount of leading characters.
 * @param {number} number Number to convert.
 * @param {number} width Amount of leading characters to prepend.
 * @param {string} [padding = '0'] leading character.
 * @throws Error
 * @returns {string}
 */
function pad(number, width, padding){
  // Convert number to string.
  var string = number.toString();

  // Return either the original number as string,
  // or the number with leading padding characters.
  if (!width || string.length >= width) {
    return string;
  }

  var leadingCharacters = new Array(width - string.length + 1).join(padding || '0');

  return leadingCharacters + string;
}

/**
 * Fengine
 * @param options
 * @constructor
 */
function Fengine(options){
  this.options = options;

  this.run();
}

/**
 *
 * @type {{
 *   dir: Fengine.dir,
 *   error: Fengine.error,
 *   log: Fengine.log,
 *   run: Fengine.run
 * }}
 */
Fengine.prototype = {
  dir: function (files, dir){
    var parent = util.normalize(path.join(dir, '../'));
    var up = dir === '/'
      ? ''
      : '      —
up
\n';
    var html = '<!DOCTYPE html>\n'
      + '\n'
      + '  \n'
      + '    <meta name="renderer" content="webkit"/>\n'
      + '    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n'
      + '    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>\n'
      + '    <title>' + dir + '</title>\n'
      + '    \n'
      + '  \n'
      + '  \n'
      + '
\n'
      + '
' + dir + '
\n' + up
      + '
\n';

    var width = (files.length + 1).toString().length;

    files.forEach(function (file, index){
      var href = dir + file;

      html += ''
        + '
\n'
        + '      ' + pad(++index, width) + ':\n'
        + '      ' + file + '
\n'
        + '
\n';
    });

    html += '  \n'
      + '\n';

    return html;
  },
  error: function (response, statusCode, next){
    var html = '<!DOCTYPE html>\n'
      + '\n'
      + '  \n'
      + '    <meta name="renderer" content="webkit"/>\n'
      + '    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n'
      + '    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>\n'
      + '    <title>' + statusCode + '</title>\n'
      + '    \n'
      + '  \n'
      + '  \n'
      + '
' + statusCode + '
\n'
      + '
' + http.STATUS_CODES[statusCode] + '
\n'
      + '  \n'
      + '\n';

    if (util.fn(next)) {
      next(html);
    } else {
      response.statusCode = statusCode;

      response.setHeader('Content-Type', 'text/html; charset=UTF-8');
      response.end(html);
    }
  },
  log: function (data){
    var date = dateFormat(new Date(), 'yyyy-MM-dd hh:mm:ss');
    var message = colors.green.bold('[' + date + '] ');

    switch (data.type) {
      case LOGLEVELS.INFO:
        message += colors.cyan.bold(data.message);
        break;
      case LOGLEVELS.WARN:
        message += colors.yellow.bold(data.message);
        break;
      case LOGLEVELS.ERROR:
        message += colors.red.bold(data.message);
        break;
    }

    // log
    console.log(message);
  },
  send: function (requset, response){
    var context = this;
    var options = context.options;

    // file send
    var send = new FileSend(requset, {
      root: options.root
    }).on('error', function (error, next){
      context.error(response, error.statusCode, next);
    }).on('dir', function (realpath, stats, next){
      // set Content-Type
      send.setHeader('Content-Type', 'text/html; charset=UTF-8');

      // read dir
      fs.readdir(realpath, function (error, files){
        if (error) {
          send.statError(response, error);
        } else {
          // response
          next(context.dir(files, send.url));
        }
      });
    }).pipe(response);

    // send
    return send;
  },
  transform: function (pathname, source, response){
    var context = this;
    var options = context.options;

    // set status code
    response.statusCode = 200;

    // set Content-Type
    response.setHeader('Content-Type', mime.lookup(pathname));

    // transform
    var transform = new Transform(pathname, source, {
      root: options.base,
      data: options.data,
      layout: options.layout,
      delimiter: options.tags
    });

    // data
    transform.on('data', function (data){
      response.write(data);
    });

    // error
    transform.on('error', function (event, file, command){
      var type;
      var message;

      switch (event) {
        case 'circle':
          type = 'warn';
          message = 'Found cyclic command \''
            + command + '\'\n  at file: /'
            + util.normalize(path.relative(options.root, file.src));
          break;
        case 'io':
          type = 'error';

          if (command) {
            message = 'File of command \''
              + command + '\' does not exist\n  at file: /'
              + util.normalize(path.relative(options.root, file.src));
          } else {
            message = 'The default layout file does not exist';
          }
          break;
      }

      // send message
      process.send({
        type: type,
        message: message
      })
    });

    // end
    transform.on('end', function (){
      response.end();
    });

    // transform
    return transform;
  },
  run: function (){
    var context = this;
    var options = context.options;

    // master thread
    if (cluster.isMaster) {
      // worker
      var worker;

      // create thread
      for (var i = 0; i < NUMCPUS; i++) {
        // fork
        worker = cluster.fork();

        // listen event
        worker.on('message', context.log);
      }
    } else {
      // create server
      var server = http.createServer(function (requset, response){
        var parsed = requset.url;

        response.setHeader('Server', 'Fengine/' + pkg.version);
        response.setHeader('X-Powered-By', 'Node/' + process.version);

        if (parsed === -1 || parsed.indexOf('\0') !== -1) {
          return context.error(response, 400);
        }

        parsed = url.parse(decodeURI(requset.url));

        var pathname = parsed.pathname;
        var extname = path.extname(pathname).toLowerCase();

        if (options.watch.indexOf(extname) !== -1) {
          pathname = path.join(options.root, pathname);

          if (isOutBound(pathname, CWD)) {
            return context.error(response, 403);
          }

          if (!isOutBound(pathname, options.base)) {
            fs.stat(pathname, function (error, stats){
              if (error) {
                return context.error(response, 404);
              }

              // is file
              if (stats.isFile()) {
                fs.readFile(pathname, function (error, source){
                  if (error) {
                    return context.error(response, 404);
                  }

                  context.transform(pathname, source, response);
                });
              } else {
                context.send(requset, response);
              }
            });
          } else {
            context.send(requset, response);
          }
        } else {
          if (pathname.toLowerCase() === '/favicon.ico') {
            pathname = path.join(options.root, pathname);

            fs.stat(pathname, function (error){
              if (error) {
                response.statusCode = 200;

                response.setHeader('Content-Type', 'image/x-icon');
                response.setHeader('Content-Length', FAVICONSIZE);

                return fs.createReadStream(FAVICON).pipe(response);
              }

              context.send(requset, response);
            });
          } else {
            context.send(requset, response);
          }
        }
      });

      // start listening
      server.on('listening', function (){
        var server = this.address();

        options.data.server = server.address + ':' + server.port;

        // message
        process.send({
          type: 'info',
          message: 'Server thread ' + cluster.worker.id + ' runing at: ' + server.address + ':' + server.port
        });
      });

      // error
      server.on('error', function (error){
        // message
        process.send({
          type: 'error',
          message: 'Server thread ' + cluster.worker.id + ' failed to start: ' + error.message
        });

        // exit
        process.exit();
      });

      // close
      server.on('close', function (){
        // message
        process.send({
          type: 'info',
          message: 'Server thread ' + cluster.worker.id + ' closed'
        });

        // exit
        process.exit();
      });

      // listen
      server.listen(options.port, options.hostname);

      // return
      return server;
    }
  }
};

// exports
module.exports = Fengine;
<commit_msg>improve code<commit_after>/*!
 * fengine
 * Version: 0.0.1
 * Date: 2016/8/2
 * https://github.com/Nuintun/fengine
 *
 * This is licensed under the MIT License (MIT).
 * For details, see: https://github.com/Nuintun/fengine/blob/master/LICENSE
 */

'use strict';

// lib
var os = require('os');
var fs = require('fs');
var url = require('url');
var path = require('path');
var http = require('http');
var util = require('./util');
var colors = require('colors');
var cluster = require('cluster');
var mime = require('mime-types');
var FileSend = require('file-send');
var pkg = require('../package.json');
var Transform = require('./transform');

// variable declaration
var CWD = process.cwd();
var NUMCPUS = os.cpus().length;
var LOGLEVELS = {
  INFO: 'info',
  WARN: 'warn',
  ERROR: 'error'
};
var FAVICON = path.join(__dirname, '../favicon.ico');
var FAVICONSIZE = fs.lstatSync(FAVICON).size;

/**
 * date format
 * @param date
 * @param format
 * @returns {*}
 */
function dateFormat(date, format){
  if (!date instanceof Date) {
    throw new TypeError('param date must be a date.');
  }

  format = format || 'yyyy-MM-dd hh:mm:ss';

  var map = {
    'M': date.getMonth() + 1, // month
    'd': date.getDate(), // date
    'h': date.getHours(), // hours
    'm': date.getMinutes(), // minutes
    's': date.getSeconds(), // seconds
    'q': Math.floor((date.getMonth() + 3) / 3), // quarter
    'S': date.getMilliseconds() // milliseconds
  };

  format = format.replace(/([yMdhmsqS])+/g, function (all, t){
    var v = map[t];

    if (v !== undefined) {
      if (all.length > 1) {
        v = '0' + v;
        v = v.substr(v.length - 2);
      }

      return v;
    } else if (t === 'y') {
      return (date.getFullYear() + '').substr(4 - all.length);
    }

    return all;
  });

  return format;
}

/**
 * is out bound
 * @param path
 * @param root
 * @returns {boolean}
 */
function isOutBound(path, root){
  if (process.platform === 'win32') {
    path = path.toLowerCase();
    root = root.toLowerCase();
  }

  if (path.length < root.length) {
    return true;
  }

  return path.indexOf(root) !== 0;
}

/**
 * decode uri
 * @param uri
 * @returns {*}
 */
function decodeURI(uri){
  try {
    return decodeURIComponent(uri);
  } catch (err) {
    return -1;
  }
}

/**
 * converts a number to a string with
 * a given amount of leading characters.
 * @param {number} number Number to convert.
 * @param {number} width Amount of leading characters to prepend.
 * @param {string} [padding = '0'] leading character.
 * @throws Error
 * @returns {string}
 */
function pad(number, width, padding){
  // Convert number to string.
  var string = number.toString();

  // Return either the original number as string,
  // or the number with leading padding characters.
  if (!width || string.length >= width) {
    return string;
  }

  var leadingCharacters = new Array(width - string.length + 1).join(padding || '0');

  return leadingCharacters + string;
}

/**
 * Fengine
 * @param options
 * @constructor
 */
function Fengine(options){
  this.options = options;

  this.run();
}

/**
 *
 * @type {{
 *   dir: Fengine.dir,
 *   error: Fengine.error,
 *   log: Fengine.log,
 *   run: Fengine.run
 * }}
 */
Fengine.prototype = {
  dir: function (files, dir){
    var parent = util.normalize(path.join(dir, '../'));
    var upLink = '      —\n'
      + '
up
\n';

    var up = dir === '/' ? '' : upLink;
    var html = '<!DOCTYPE html>\n'
      + '\n'
      + '  \n'
      + '    <meta name="renderer" content="webkit"/>\n'
      + '    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n'
      + '    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>\n'
      + '    <title>' + dir + '</title>\n'
      + '    \n'
      + '  \n'
      + '  \n'
      + '
\n'
      + '
' + dir + '
\n' + up
      + '
\n';

    var width = (files.length + 1).toString().length;

    files.forEach(function (file, index){
      var href = dir + file;

      html += ''
        + '
\n'
        + '      ' + pad(++index, width) + ':\n'
        + '      ' + file + '
\n'
        + '
\n';
    });

    html += '  \n'
      + '\n';

    return html;
  },
  error: function (response, statusCode, next){
    var html = '<!DOCTYPE html>\n'
      + '\n'
      + '  \n'
      + '    <meta name="renderer" content="webkit"/>\n'
      + '    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>\n'
      + '    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>\n'
      + '    <title>' + statusCode + '</title>\n'
      + '    \n'
      + '  \n'
      + '  \n'
      + '
' + statusCode + '
\n'
      + '
' + http.STATUS_CODES[statusCode] + '
\n'
      + '  \n'
      + '\n';

    if (util.fn(next)) {
      next(html);
    } else {
      response.statusCode = statusCode;

      response.setHeader('Content-Type', 'text/html; charset=UTF-8');
      response.end(html);
    }
  },
  log: function (data){
    var date = dateFormat(new Date(), 'yyyy-MM-dd hh:mm:ss');
    var message = colors.green.bold('[' + date + '] ');

    switch (data.type) {
      case LOGLEVELS.INFO:
        message += colors.cyan.bold(data.message);
        break;
      case LOGLEVELS.WARN:
        message += colors.yellow.bold(data.message);
        break;
      case LOGLEVELS.ERROR:
        message += colors.red.bold(data.message);
        break;
    }

    // log
    console.log(message);
  },
  send: function (requset, response){
    var context = this;
    var options = context.options;

    // file send
    var send = new FileSend(requset, {
      root: options.root
    }).on('error', function (error, next){
      context.error(response, error.statusCode, next);
    }).on('dir', function (realpath, stats, next){
      // set Content-Type
      send.setHeader('Content-Type', 'text/html; charset=UTF-8');

      // read dir
      fs.readdir(realpath, function (error, files){
        if (error) {
          send.statError(response, error);
        } else {
          // response
          next(context.dir(files, send.url));
        }
      });
    }).pipe(response);

    // send
    return send;
  },
  transform: function (pathname, source, response){
    var context = this;
    var options = context.options;

    // set status code
    response.statusCode = 200;

    // set Content-Type
    response.setHeader('Content-Type', mime.lookup(pathname));

    // transform
    var transform = new Transform(pathname, source, {
      root: options.base,
      data: options.data,
      layout: options.layout,
      delimiter: options.tags
    });

    // data
    transform.on('data', function (data){
      response.write(data);
    });

    // error
    transform.on('error', function (event, file, command){
      var type;
      var message;

      switch (event) {
        case 'circle':
          type = 'warn';
          message = 'Found cyclic command \''
            + command + '\'\n  at file: /'
            + util.normalize(path.relative(options.root, file.src));
          break;
        case 'io':
          type = 'error';

          if (command) {
            message = 'File of command \''
              + command + '\' does not exist\n  at file: /'
              + util.normalize(path.relative(options.root, file.src));
          } else {
            message = 'The default layout file does not exist';
          }
          break;
      }

      // send message
      process.send({
        type: type,
        message: message
      })
    });

    // end
    transform.on('end', function (){
      response.end();
    });

    // transform
    return transform;
  },
  run: function (){
    var context = this;
    var options = context.options;

    // master thread
    if (cluster.isMaster) {
      // worker
      var worker;

      // create thread
      for (var i = 0; i < NUMCPUS; i++) {
        // fork
        worker = cluster.fork();

        // listen event
        worker.on('message', context.log);
      }
    } else {
      // create server
      var server = http.createServer(function (requset, response){
        var parsed = requset.url;

        response.setHeader('Server', 'Fengine/' + pkg.version);
        response.setHeader('X-Powered-By', 'Node/' + process.version);

        if (parsed === -1 || parsed.indexOf('\0') !== -1) {
          return context.error(response, 400);
        }

        parsed = url.parse(decodeURI(requset.url));

        var pathname = parsed.pathname;
        var extname = path.extname(pathname).toLowerCase();

        if (options.watch.indexOf(extname) !== -1) {
          pathname = path.join(options.root, pathname);

          if (isOutBound(pathname, CWD)) {
            return context.error(response, 403);
          }

          if (!isOutBound(pathname, options.base)) {
            fs.stat(pathname, function (error, stats){
              if (error) {
                return context.error(response, 404);
              }

              // is file
              if (stats.isFile()) {
                fs.readFile(pathname, function (error, source){
                  if (error) {
                    return context.error(response, 404);
                  }

                  context.transform(pathname, source, response);
                });
              } else {
                context.send(requset, response);
              }
            });
          } else {
            context.send(requset, response);
          }
        } else {
          if (pathname.toLowerCase() === '/favicon.ico') {
            pathname = path.join(options.root, pathname);

            fs.stat(pathname, function (error){
              if (error) {
                response.statusCode = 200;

                response.setHeader('Content-Type', 'image/x-icon');
                response.setHeader('Content-Length', FAVICONSIZE);

                return fs.createReadStream(FAVICON).pipe(response);
              }

              context.send(requset, response);
            });
          } else {
            context.send(requset, response);
          }
        }
      });

      // start listening
      server.on('listening', function (){
        var server = this.address();

        options.data.server = server.address + ':' + server.port;

        // message
        process.send({
          type: 'info',
          message: 'Server thread ' + cluster.worker.id + ' runing at: ' + server.address + ':' + server.port
        });
      });

      // error
      server.on('error', function (error){
        // message
        process.send({
          type: 'error',
          message: 'Server thread ' + cluster.worker.id + ' failed to start: ' + error.message
        });

        // exit
        process.exit();
      });

      // close
      server.on('close', function (){
        // message
        process.send({
          type: 'info',
          message: 'Server thread ' + cluster.worker.id + ' closed'
        });

        // exit
        process.exit();
      });

      // listen
      server.listen(options.port, options.hostname);

      // return
      return server;
    }
  }
};

// exports
module.exports = Fengine;
<|endoftext|>