<commit_before>var mongoose     = require('mongoose');
// mongoose.set('debug', true);
var Schema       = mongoose.Schema;

var ObjectId = mongoose.Schema.Types.ObjectId;
var Mixed =  mongoose.Schema.Types.Mixed;

var User = require("./user_model");
var Organisation = require("./organisation_model");
var Partner = require("./partner_model");
var Source = require("./source_model");
var Balance = require("./balance_model");
var Log = require("./log_model");
var Currency = require("./currency_model");
var Wallet = require("./wallet_model");
var moment = require("moment");
var async = require("async");

var credTypes = ["space", "stuff", "daily"];

var LedgerSchema   = new Schema({
	user_id: { type: ObjectId, index: true, ref: "User", required: true },
	organisation_id: { type: ObjectId, index: true, ref: "Organisation" },
	description: String,
	details: Mixed,
	partner_id: { type: ObjectId, index: true, ref: "Partner" },
	partner_reference: { type: Mixed, unique: true },
	date: { type: Date, default: Date.now, required: true, index: true },
	source_type: String,
	source_id: ObjectId,
	amount: { type: Number, required: true },
	balance: Number,
	reserve: { type: Boolean, default: false },
	reserve_expires: { type: Date, default: Date.now },
	cred_type: { type: String, validate: /space|stuff|creditcard|account|daily/, index: true, required: true },
	currency_id: { type: ObjectId, index: true, ref: "Currency", required: true },
	wallet_id: [{ type: ObjectId, index: true, ref: "Wallet" }],
	wallet_split: [ Mixed ],
	email: String,
	transaction_type: { type: String, validate: /credit|debit|reserve/ },
	is_transfer: { type: Boolean, default: false },
	_owner_id: ObjectId,
	_deleted: { type: Boolean, default: false, index: true },
});

LedgerSchema.set("_perms", {
	admin: "crud",
	owner: "cru",
	user: "cr",
	all: ""
});

var log = (id, level, title, message, data, code) => {
	log = new Log({
		id: id,
		model: "ledger",
		level,
		user_id: self.__user || null,
		title,
		message,
		code,
		data,
	}).save();
};

var logError = (id, title, message, data) => {
	log(id, 1, title, message, data, "ledger-error");
};

var notDeleted = function(item) {
	return item._deleted !== true;
};

var getUsers = function() {
	return new Promise((resolve, reject) => {
		User.find(function(err, users) {
			if (err) {
				console.error(err);
				reject(err);
			} else {
				resolve(users.filter(notDeleted));
			}
		});
	});
};

var getOrganisations = function() {
	return new Promise((resolve, reject) => {
		Organisation.find(function(err, organisations) {
			if (err) {
				console.error(err);
				return reject(err);
			}
			return resolve(organisations.filter(notDeleted));
		});
	});
};

var getUser = _id => {
	return new Promise((resolve, reject) => {
		User.findOne({_id}, (err, user) => {
			if (err)
				return reject(err);
			if (!user)
				return reject(new Error("Cannot find user"));
			if (user.status === "inactive" || user._deleted === true)
				return reject(new Error("User is inactive"));
			if (user._deleted === true)
				return reject(new Error("User is deleted"));
			resolve(user);
		});
	});
};

var getOrganisation = _id => {
	return new Promise((resolve, reject) => {
		Organisation.findOne({_id}, (err, organisation) => {
			if (err)
				return reject(err);
			if (!organisation)
				return reject(new Error("Cannot find organisation"));
			resolve(organisation);
		});
	});
};

var getLedger = _id => {
	return new Promise((resolve, reject) => {
		mongoose.model('Ledger', LedgerSchema).findOne({_id}, (err, ledger) => {
			if (err)
				return reject(err);
			// if (!ledger)
			// 	return reject(new Error("Cannot find ledger"));
			resolve(ledger);
		});
	});
};

LedgerSchema.statics.transfer = function(data) {
	return new Promise((resolve, reject) => {
		if ((data.sender + "" !== data.__user._id + "") && (!data.__user.admin)) {
			console.log("Reject", data.sender, data.__user._id);
			reject("This is not your account and you are not an admin");
		} else {
			var Ledger = require("./ledger_model");
			// sender = data.__user;
			var credit = new Ledger();
			var debit = new Ledger();
			var sender = null;
			var recipient = null;
			getUser(data.sender)
			.then(function(user) {
				sender = user;
				return getUser(data.recipient);
			})
			.then(function(user) {
				recipient = user;
				credit.user_id = data.recipient;
				credit.amount = Math.abs(data.amount);
				credit.cred_type = data.cred_type;
				credit.description = "Transfer from " + sender.name + " <" + sender.email + "> to " + recipient.name + " <" + recipient.email + ">";
				credit.__user = data.__user;
				credit.is_transfer = true;
				debit.user_id = data.sender;
				debit.amount = Math.abs(data.amount) * -1;
				debit.cred_type = data.cred_type;
				debit.description = "Transfer from " + sender.name + " <" + sender.email + "> to " + recipient.name + " <" + recipient.email + ">";
				debit.__user = data.__user;
				debit.is_transfer = true;
				debit.save(function(err, result) {
					if (err) {
						console.error(err);
						reject(err);
					} else {
						credit.save(function(err, result) {
							if (err) {
								console.error(err);
								reject(err);
							} else {
								resolve({ credit: credit, debit: debit });
							}
						});
					}
				});
			})
			.then(null, function(err) {
				console.error(err);
				reject(err);
			});
		}
	});
};

LedgerSchema.statics.confirm_reserve = function(_id) {
	return new Promise((resolve, reject) => {
		var Ledger = require("./ledger_model");
		Ledger.findOne({ _id: _id }, (err, ledger) => {
			if (err)
				reject(err);
			if (!ledger)
				reject("Could not find ledger entry");
			if (!ledger.reserve)
				reject("Ledger is not a reserve");
			ledger.transaction_type = "debit";
			ledger.reserve = false;
			ledger.save((err, result) => {
				if (err) {
					return reject(err);
				}
				resolve(result);
			});
		});
	});
};

LedgerSchema.statics.report = function(params) {
	return new Promise((resolve, reject) => {
		var Ledger = require("./ledger_model");
		params = params || {};
		find = {};
		params.start_date = params.start_date || moment().subtract(1, "month").toISOString();
		params.end_date = params.end_date || moment().toISOString();
		find.date = { "$gte": params.start_date, "$lte": params.end_date };
		if (params.cred_type && (params.cred_type != "*")) {
			find.cred_type = params.cred_type;
		} else {
			params.cred_type = "*";
		}
		if (params.partner_id && (params.partner_id != "*")) {
			find.partner_id = params.partner_id;
		} else {
			params.partner_id = "*";
		}
		Ledger.find(find).sort({ date: 1 }).exec(function(err, entries) {
			if (!entries) {
				reject("No entries found");
				return;
			}
			console.log("Entries found", entries);
			var tot = 0;
			var debs = 0;
			var creds = 0;
			entries.forEach(function(entry) {
				tot += entry.amount;
				if (entry.amount >= 0) {
					creds += entry.amount;
				} else {
					debs += entry.amount;
				}
			});
			var avg = tot / entries.length;
			resolve({ params: params, total: tot, average: avg, count: entries.length, debits: debs, credits: creds });
		});
	});
};

var totalFromWallets = (user_id, currency_id) => {
	return new Promise((resolve, reject) => {
		Wallet.find({ user_id, currency_id }).sort({ priority: 1 }).exec()
		.then(result => {
			var total = result.reduce((sum, wallet) => sum + wallet.balance, 0);
			resolve(total);
		})
		.catch(err => {
			console.error(err);
			reject();
		});
	});
};

LedgerSchema.pre("save", function(next) {
	console.log("Saving Ledger");
	var transaction = this;
	var user = null;
	var organisation = null;
	var totals = null;
	var original = null;
	if (!transaction.user_id) {
		transaction.invalidate("user_id", "could not find user");
		return next(new Error("user_id required"));
	}
	getUser(transaction.user_id)
	.then(result => {
		user = result;
		return getOrganisation(user.organisation_id);
	}, err => {
		transaction.invalidate("user_id", err);
		console.error(err);
		next(new Error(err));
	})
	.then(result => {
		organisation = result;
		transaction.organisation_id = organisation._id;

		// Set Transaction Type
		if (transaction.amount >= 0) {
			transaction.transaction_type = "credit";
		} else {
			if (transaction.reserve) {
				transaction.transaction_type = "reserve";
			} else {
				transaction.transaction_type = "debit";
			}
		}
		// Reserves must be negative
		if ((transaction.amount > 0) && (transaction.reserve)) {
			throw("Reserves must be a negative value");
		}
		// Only admins can assign Credit
		if ((transaction.amount > 0) && (!transaction.sender.admin) && (!transaction.is_transfer)) {
			throw("Only admins can give credit. Amount must be less than zero.");
		}
		// Only admins can delete non-reserve
		if ((transaction._deleted) && (transaction.transaction_type !== "reserve") && (!transaction.sender.admin)) {
			throw("You are not allowed to reverse this transaction");
		}
		// Only admins can assign Credit
		if ((transaction.amount > 0) && (!transaction.sender.admin) && (!transaction.is_transfer)) {
			throw("Only admins can give credit. Amount must be less than zero.");
		}
		// Only admins can delete non-reserve
		if ((transaction._deleted) && (transaction.transaction_type !== "reserve") && (!transaction.sender.admin)) {
			throw("You are not allowed to reverse this transaction");
		}
	}, err => {
		transaction.invalidate("organisation_id", err);
		console.error(err);
		return next(new Error(err));
	})
	.then(result => {
		totals = result;
		if (this._id)
			return getLedger(this._id);
	})
	.then(result => {
		// If this is a reserve conversion, don't check totals
		if (result) {
			if ((result.reserve) && (!transaction.reserve)) {
				return next();
			}
		}
		// Users can only debit from their own accounts
		if ((String(transaction.user_id) !== String(transaction.sender._id)) && (!transaction.sender.admin) && (!transaction.is_transfer)) {
			throw("This is not your account");
		}
		return totalFromWallets(transaction.user_id, transaction.currency_id, transaction.amount);
	})
	.then(result => {
		// Make sure we have credit
		var test = transaction.amount + result;
		if ((transaction.amount < 0) && (test < 0)) {
			throw("Insufficient Credit");
		} else {
			next();
		}
	})
	.catch(err => {
		transaction.invalidate("amount", err);
		console.error(err);
		return next(new Error(err));
	});
});

LedgerSchema.post("save", function(transaction) { //Keep our running total up to date
	if (transaction.amount < 0) {
		Wallet.find({ user_id: transaction.user_id, currency_id: transaction.currency_id }).sort({ priority: 1 }).exec()
		.then(result => {
			var wallets = result;
			var outstanding = Math.abs(transaction.amount);
			var wallet_split = [];
			while ((outstanding > 0) && wallets) {
				var wallet = wallets.shift();
				if (wallet.balance) { // Ignore empty wallets
					if (outstanding <= wallet.balance) { // Enough money in this wallet;
						wallet_split.push({ _id: wallet._id, amount: outstanding, balance: wallet.balance - outstanding });
						outstanding = 0;
					} else { // Not enough money, clear out this wallet and continue
						wallet_split.push({ _id: wallet._id, amount: wallet.balance, balance: 0 });
						outstanding -= wallet.balance;
					}
				}
			}
			var queue = [];
			wallet_split.forEach(wallet => {
				queue.push(cb => {
					Wallet.findByIdAndUpdate(wallet._id, { $set: { balance: wallet.balance } })
					.then(result => {
						cb(null, result);
					})
					.catch(err => {
						console.error(err);
						cb(err);
					});
				});
			});
			queue.push(cb => {
				LedgerModel.findByIdAndUpdate(transaction._id, { $set: { wallet_split } })
				.then(result => {
					cb(null);
				}, err => {
					cb(err);
				});
			});
			async.series(queue, (err, result) => {
				if (err)
					console.error(err);
			});
		})
		.catch(err => {
			console.error(err);
		});
	}
});

LedgerSchema.virtual("__user").set(function(user) {
	this.sender = user;
});

var LedgerModel = mongoose.model('Ledger', LedgerSchema);
module.exports = mongoose.model('Ledger', LedgerSchema);<commit_msg>Survive a missing currency_id<commit_after>var mongoose     = require('mongoose');
// mongoose.set('debug', true);
var Schema       = mongoose.Schema;

var ObjectId = mongoose.Schema.Types.ObjectId;
var Mixed =  mongoose.Schema.Types.Mixed;

var User = require("./user_model");
var Organisation = require("./organisation_model");
var Partner = require("./partner_model");
var Source = require("./source_model");
var Balance = require("./balance_model");
var Log = require("./log_model");
var Currency = require("./currency_model");
var Wallet = require("./wallet_model");
var moment = require("moment");
var async = require("async");

var credTypes = ["space", "stuff", "daily"];

var LedgerSchema   = new Schema({
	user_id: { type: ObjectId, index: true, ref: "User", required: true },
	organisation_id: { type: ObjectId, index: true, ref: "Organisation" },
	description: String,
	details: Mixed,
	partner_id: { type: ObjectId, index: true, ref: "Partner" },
	partner_reference: { type: Mixed, unique: true },
	date: { type: Date, default: Date.now, required: true, index: true },
	source_type: String,
	source_id: ObjectId,
	amount: { type: Number, required: true },
	balance: Number,
	reserve: { type: Boolean, default: false },
	reserve_expires: { type: Date, default: Date.now },
	cred_type: { type: String, validate: /space|stuff|creditcard|account|daily/, index: true, required: true },
	currency_id: { type: ObjectId, index: true, ref: "Currency" },
	wallet_id: [{ type: ObjectId, index: true, ref: "Wallet" }],
	wallet_split: [ Mixed ],
	email: String,
	transaction_type: { type: String, validate: /credit|debit|reserve/ },
	is_transfer: { type: Boolean, default: false },
	_owner_id: ObjectId,
	_deleted: { type: Boolean, default: false, index: true },
});

LedgerSchema.set("_perms", {
	admin: "crud",
	owner: "cru",
	user: "cr",
	all: ""
});

var log = (id, level, title, message, data, code) => {
	log = new Log({
		id: id,
		model: "ledger",
		level,
		user_id: self.__user || null,
		title,
		message,
		code,
		data,
	}).save();
};

var logError = (id, title, message, data) => {
	log(id, 1, title, message, data, "ledger-error");
};

var notDeleted = function(item) {
	return item._deleted !== true;
};

var getUsers = function() {
	return new Promise((resolve, reject) => {
		User.find(function(err, users) {
			if (err) {
				console.error(err);
				reject(err);
			} else {
				resolve(users.filter(notDeleted));
			}
		});
	});
};

var getOrganisations = function() {
	return new Promise((resolve, reject) => {
		Organisation.find(function(err, organisations) {
			if (err) {
				console.error(err);
				return reject(err);
			}
			return resolve(organisations.filter(notDeleted));
		});
	});
};

var getUser = _id => {
	return new Promise((resolve, reject) => {
		User.findOne({_id}, (err, user) => {
			if (err)
				return reject(err);
			if (!user)
				return reject(new Error("Cannot find user"));
			if (user.status === "inactive" || user._deleted === true)
				return reject(new Error("User is inactive"));
			if (user._deleted === true)
				return reject(new Error("User is deleted"));
			resolve(user);
		});
	});
};

var getOrganisation = _id => {
	return new Promise((resolve, reject) => {
		Organisation.findOne({_id}, (err, organisation) => {
			if (err)
				return reject(err);
			if (!organisation)
				return reject(new Error("Cannot find organisation"));
			resolve(organisation);
		});
	});
};

var getLedger = _id => {
	return new Promise((resolve, reject) => {
		mongoose.model('Ledger', LedgerSchema).findOne({_id}, (err, ledger) => {
			if (err)
				return reject(err);
			// if (!ledger)
			// 	return reject(new Error("Cannot find ledger"));
			resolve(ledger);
		});
	});
};

LedgerSchema.statics.transfer = function(data) {
	return new Promise((resolve, reject) => {
		if ((data.sender + "" !== data.__user._id + "") && (!data.__user.admin)) {
			console.log("Reject", data.sender, data.__user._id);
			reject("This is not your account and you are not an admin");
		} else {
			var Ledger = require("./ledger_model");
			// sender = data.__user;
			var credit = new Ledger();
			var debit = new Ledger();
			var sender = null;
			var recipient = null;
			getUser(data.sender)
			.then(function(user) {
				sender = user;
				return getUser(data.recipient);
			})
			.then(function(user) {
				recipient = user;
				credit.user_id = data.recipient;
				credit.amount = Math.abs(data.amount);
				credit.cred_type = data.cred_type;
				credit.description = "Transfer from " + sender.name + " <" + sender.email + "> to " + recipient.name + " <" + recipient.email + ">";
				credit.__user = data.__user;
				credit.is_transfer = true;
				debit.user_id = data.sender;
				debit.amount = Math.abs(data.amount) * -1;
				debit.cred_type = data.cred_type;
				debit.description = "Transfer from " + sender.name + " <" + sender.email + "> to " + recipient.name + " <" + recipient.email + ">";
				debit.__user = data.__user;
				debit.is_transfer = true;
				debit.save(function(err, result) {
					if (err) {
						console.error(err);
						reject(err);
					} else {
						credit.save(function(err, result) {
							if (err) {
								console.error(err);
								reject(err);
							} else {
								resolve({ credit: credit, debit: debit });
							}
						});
					}
				});
			})
			.then(null, function(err) {
				console.error(err);
				reject(err);
			});
		}
	});
};

LedgerSchema.statics.confirm_reserve = function(_id) {
	return new Promise((resolve, reject) => {
		var Ledger = require("./ledger_model");
		Ledger.findOne({ _id: _id }, (err, ledger) => {
			if (err)
				reject(err);
			if (!ledger)
				reject("Could not find ledger entry");
			if (!ledger.reserve)
				reject("Ledger is not a reserve");
			ledger.transaction_type = "debit";
			ledger.reserve = false;
			ledger.save((err, result) => {
				if (err) {
					return reject(err);
				}
				resolve(result);
			});
		});
	});
};

LedgerSchema.statics.report = function(params) {
	return new Promise((resolve, reject) => {
		var Ledger = require("./ledger_model");
		params = params || {};
		find = {};
		params.start_date = params.start_date || moment().subtract(1, "month").toISOString();
		params.end_date = params.end_date || moment().toISOString();
		find.date = { "$gte": params.start_date, "$lte": params.end_date };
		if (params.cred_type && (params.cred_type != "*")) {
			find.cred_type = params.cred_type;
		} else {
			params.cred_type = "*";
		}
		if (params.partner_id && (params.partner_id != "*")) {
			find.partner_id = params.partner_id;
		} else {
			params.partner_id = "*";
		}
		Ledger.find(find).sort({ date: 1 }).exec(function(err, entries) {
			if (!entries) {
				reject("No entries found");
				return;
			}
			console.log("Entries found", entries);
			var tot = 0;
			var debs = 0;
			var creds = 0;
			entries.forEach(function(entry) {
				tot += entry.amount;
				if (entry.amount >= 0) {
					creds += entry.amount;
				} else {
					debs += entry.amount;
				}
			});
			var avg = tot / entries.length;
			resolve({ params: params, total: tot, average: avg, count: entries.length, debits: debs, credits: creds });
		});
	});
};

var totalFromWallets = (user_id, currency_id) => {
	return new Promise((resolve, reject) => {
		Wallet.find({ user_id, currency_id }).sort({ priority: 1 }).exec()
		.then(result => {
			var total = result.reduce((sum, wallet) => sum + wallet.balance, 0);
			resolve(total);
		})
		.catch(err => {
			console.error(err);
			reject();
		});
	});
};

LedgerSchema.pre("save", function(next) {
	console.log("Saving Ledger");
	var transaction = this;
	var user = null;
	var organisation = null;
	var totals = null;
	var original = null;
	var currency = null;
	if (!transaction.user_id) {
		transaction.invalidate("user_id", "could not find user");
		return next(new Error("user_id required"));
	}
	getUser(transaction.user_id)
	.then(result => {
		user = result;
		return getOrganisation(user.organisation_id);
	}, err => {
		transaction.invalidate("user_id", err);
		console.error(err);
		next(new Error(err));
	})
	.then(result => {
		organisation = result;
		if (!transaction.currency_id) {
			return Currency.findOne({ name: transaction.cred_type[0].toUpperCase() + transaction.cred_type.slice(1) });
		} else {
			return Currency.findOne({ _id: transaction.currency_id });
		}
	})
	.then(result => {
		currency = result;
		transaction.currency_id = currency._id;

		transaction.organisation_id = organisation._id;

		// Set Transaction Type
		if (transaction.amount >= 0) {
			transaction.transaction_type = "credit";
		} else {
			if (transaction.reserve) {
				transaction.transaction_type = "reserve";
			} else {
				transaction.transaction_type = "debit";
			}
		}
		// Reserves must be negative
		if ((transaction.amount > 0) && (transaction.reserve)) {
			throw("Reserves must be a negative value");
		}
		// Only admins can assign Credit
		if ((transaction.amount > 0) && (!transaction.sender.admin) && (!transaction.is_transfer)) {
			throw("Only admins can give credit. Amount must be less than zero.");
		}
		// Only admins can delete non-reserve
		if ((transaction._deleted) && (transaction.transaction_type !== "reserve") && (!transaction.sender.admin)) {
			throw("You are not allowed to reverse this transaction");
		}
		// Only admins can assign Credit
		if ((transaction.amount > 0) && (!transaction.sender.admin) && (!transaction.is_transfer)) {
			throw("Only admins can give credit. Amount must be less than zero.");
		}
		// Only admins can delete non-reserve
		if ((transaction._deleted) && (transaction.transaction_type !== "reserve") && (!transaction.sender.admin)) {
			throw("You are not allowed to reverse this transaction");
		}
	}, err => {
		transaction.invalidate("organisation_id", err);
		console.error(err);
		return next(new Error(err));
	})
	.then(result => {
		totals = result;
		if (this._id)
			return getLedger(this._id);
	})
	.then(result => {
		// If this is a reserve conversion, don't check totals
		if (result) {
			if ((result.reserve) && (!transaction.reserve)) {
				return next();
			}
		}
		// Users can only debit from their own accounts
		if ((String(transaction.user_id) !== String(transaction.sender._id)) && (!transaction.sender.admin) && (!transaction.is_transfer)) {
			throw("This is not your account");
		}
		return totalFromWallets(transaction.user_id, transaction.currency_id, transaction.amount);
	})
	.then(result => {
		// Make sure we have credit
		var test = transaction.amount + result;
		if ((transaction.amount < 0) && (test < 0)) {
			throw("Insufficient Credit");
		} else {
			next();
		}
	})
	.catch(err => {
		transaction.invalidate("amount", err);
		console.error(err);
		return next(new Error(err));
	});
});

LedgerSchema.post("save", function(transaction) { //Keep our running total up to date
	// console.log("Transaction", transaction);
	if (transaction.amount < 0) {
		Wallet.find({ user_id: transaction.user_id, currency_id: transaction.currency_id }).sort({ priority: 1 }).exec()
		.then(result => {
			// console.log("Wallets", result);
			var wallets = result;
			var outstanding = Math.abs(transaction.amount);
			var wallet_split = [];
			while ((outstanding > 0) && wallets) {
				var wallet = wallets.shift();
				if (wallet.balance) { // Ignore empty wallets
					if (outstanding <= wallet.balance) { // Enough money in this wallet;
						wallet_split.push({ _id: wallet._id, amount: outstanding, balance: wallet.balance - outstanding });
						outstanding = 0;
					} else { // Not enough money, clear out this wallet and continue
						wallet_split.push({ _id: wallet._id, amount: wallet.balance, balance: 0 });
						outstanding -= wallet.balance;
					}
				}
			}
			var queue = [];
			// console.log(wallet_split);
			wallet_split.forEach(wallet => {
				queue.push(cb => {
					Wallet.findByIdAndUpdate(wallet._id, { $set: { balance: wallet.balance } })
					.then(result => {
						cb(null, result);
					})
					.catch(err => {
						console.error(err);
						cb(err);
					});
				});
			});
			queue.push(cb => {
				LedgerModel.findByIdAndUpdate(transaction._id, { $set: { wallet_split } })
				.then(result => {
					cb(null);
				}, err => {
					cb(err);
				});
			});
			async.series(queue, (err, result) => {
				if (err)
					console.error(err);
			});
		})
		.catch(err => {
			console.error(err);
		});
	}
});

LedgerSchema.virtual("__user").set(function(user) {
	this.sender = user;
});

var LedgerModel = mongoose.model('Ledger', LedgerSchema);
module.exports = mongoose.model('Ledger', LedgerSchema);<|endoftext|>


<commit_before>'use strict';

/* jshint -W067 */
export
  const curry = (fn, aux) => (
    aux = args => args.length < fn.length ? (...newArgs) => aux(args.concat(newArgs))
                                          : fn(...args)
  )([]);
/* jshint +W067 */

export
  const reduce = (op, a, [x, ...y]) => x !== undefined ? reduce(op, op(a, x), y) : a;

export
  const map = (op, y) => reduce((a, x) => [...a, op(x)], [], y);

export
  const filter = (op, y) => reduce((a, x) => {return op(x) ? [...a, x] : a}, [], y);

export
  const reverse = (y) => reduce((a, x)=> [x,...a], [] ,y);

export
  const sequence = (...ops) => (a) => reduce((a, op) => op(a), a, ops);

export
  const compose = (...ops) => sequence(...reverse(ops));

<commit_msg>style(): whitespace removed<commit_after>'use strict';

/* jshint -W067 */
export
  const curry = (fn, aux) => (
    aux = args => args.length < fn.length ? (...newArgs) => aux(args.concat(newArgs))
                                          : fn(...args)
  )([]);
/* jshint +W067 */

export
  const reduce = (op, a, [x, ...y]) => x !== undefined ? reduce(op, op(a, x), y) : a;

export
  const map = (op, y) => reduce((a, x) => [...a, op(x)], [], y);

export
  const filter = (op, y) => reduce((a, x) => {return op(x) ? [...a, x] : a}, [], y);

export
  const reverse = (y) => reduce((a, x)=> [x,...a], [] ,y);

export
  const sequence = (...ops) => (a) => reduce((a, op) => op(a), a, ops);

export
  const compose = (...ops) => sequence(...reverse(ops));

export
  const all = (fn, [x,...y]) => false;
<|endoftext|>


<commit_before>/* global Symbol */
'use strict'

import lxValid from 'lx-valid'
import jshiki from 'jshiki'

function mapData (mapping, data = {}, { out = false } = {}) {
  let mappedData = {}
  for (let targetProperty of Object.getOwnPropertyNames(mapping)) {
    if (typeof mapping[targetProperty] === 'object') {
      if (out) {
        Object.assign(mappedData, mapData(mapping[targetProperty], data[targetProperty], { out }))
      } else {
        mappedData[targetProperty] = mapData(mapping[targetProperty], data, { out })
      }
    } else if (typeof mapping[targetProperty] === 'string') {
      if (out) {
        mappedData[mapping[targetProperty]] = data[targetProperty]
      } else {
        let map = jshiki.parse(mapping[targetProperty], { scope: data })
        mappedData[targetProperty] = map.eval()
      }
    } else {
      throw new Error('mapping value must be an object or a string')
    }
  }
  return mappedData
}

function unrequire (schema) {
  schema = Object.assign({}, schema)
  Object.keys(schema).forEach(prop => {
    schema[prop] = Object.assign({}, schema[prop])
    if (schema[prop].properties) {
      schema[prop].properties = unrequire(schema[prop].properties)
    }
    schema[prop].required = false
  })
  return schema
}

function constructData (schema, data = {}, {
  defaults = true, nullAsUndefined
} = {}) {
  let target = {}
  for (let property of Object.getOwnPropertyNames(schema)) {
    let subData = typeof data[property] !== 'undefined' ? data[property] : {}
    let subSchema = schema[property].properties
    let subProperties = subSchema ? Object.getOwnPropertyNames(subSchema) : []
    if (subProperties.some(property =>
      typeof subData[property] !== 'undefined' ||
      (defaults && typeof subSchema[property].default !== 'undefined')
    )) {
      target[property] = constructData(subSchema, subData, {
        defaults, nullAsUndefined
      })
    } else {
      target[property] = (
        data.hasOwnProperty(property) &&
        (!nullAsUndefined || data[property] !== null)
      ) ? data[property]
        : (defaults ? schema[property].default : undefined)
    }
  }
  return target
}

export class Model {
  constructor (data = {}, { mapping, defaults = true, nullAsUndefined } = {}) {
    let schema = this.constructor.schema
    if (typeof schema !== 'object' || !schema) {
      throw new Error('No schema set on class')
    }
    if (data === null) {
      data = {}
    } else if (typeof data !== 'object' || !data) {
      throw new Error('data must be an object')
    }

    if (mapping) {
      if (typeof mapping === 'string') {
        if (!this.constructor.mappings || !this.constructor.mappings[mapping]) {
          throw new Error(`The mapping ${mapping} is not defined on the class`)
        }
        mapping = this.constructor.mappings[mapping]
      }
      if (typeof mapping !== 'object' && typeof mapping !== 'string') {
        throw new Error('mapping must be specified as an object or a string')
      }
      for (let targetProperty of Object.getOwnPropertyNames(mapping)) {
        if (!schema.hasOwnProperty(targetProperty)) {
          throw new Error(
            `Property ${targetProperty} is not defined on the schema`
          )
        }
      }
      let mappedData = mapData(mapping, data)
      for (let targetProperty of Object.getOwnPropertyNames(schema)) {
        if (!mappedData.hasOwnProperty(targetProperty)) {
          mappedData[targetProperty] = data[targetProperty]
        }
      }
      data = mappedData
    }

    data = constructData(schema, data, { defaults, nullAsUndefined })

    for (let property of Object.getOwnPropertyNames(schema)) {
      if (typeof schema[property] !== 'object' || !schema[property]) {
        throw new Error(`${property} is not a valid property definition`)
      }
      let key = Symbol(property)
      this[key] = data[property]
      Object.defineProperty(this, property, {
        get: schema[property].get
          ? () => this.schema[property].get(this[key])
          : () => this[key],
        set: schema[property].set
          ? val => this[key] = this.schema[property].set(val, this[key])
          : val => this[key] = val,
        enumerable: true,
        configurable: false
      })
    }

    Object.preventExtensions(this)
    Object.seal(this)
  }

  static subset (...properties) {
    if (typeof this.schema !== 'object' || !this.schema) {
      throw new Error('No schema set on class')
    }
    if (properties.length < 1) {
      throw new Error('At least one property must be specified')
    }

    let subsetSchema = {}

    for (let property of properties) {
      if (!this.schema.hasOwnProperty(property)) {
        throw new Error(`Property ${property} is not defined on the schema`)
      }
      subsetSchema[property] = this.schema[property]
    }

    return class Subset extends this {
      static get schema () { return subsetSchema }
    }
  }

  static exclude (...properties) {
    if (typeof this.schema !== 'object' || !this.schema) {
      throw new Error('No schema set on class')
    }
    if (properties.length < 1) {
      throw new Error('At least one property must be specified')
    }

    let subset = Object.getOwnPropertyNames(this.schema)

    for (let property of properties) {
      if (!this.schema.hasOwnProperty(property)) {
        throw new Error(`Property ${property} is not defined on the schema`)
      }
      subset.splice(subset.indexOf(property), 1)
    }

    let subsetSchema = {}

    for (let property of subset) {
      subsetSchema[property] = this.schema[property]
    }

    return class Subset extends this {
      static get schema () { return subsetSchema }
    }
  }

  static validate (data, { definedOnly = false } = {}) {
    let schema = this.schema
    if (typeof schema !== 'object' || !schema) {
      throw new Error('No schema set on class')
    }
    if (definedOnly) {
      schema = unrequire(schema)
    }
    return lxValid.validate(data, { properties: schema })
  }

  static map (instance, mapping) {
    if (typeof mapping === 'string') {
      if (!this.mappings || !this.mappings[mapping]) {
        throw new Error(`The mapping ${mapping} is not defined on the class`)
      }
      mapping = this.mappings[mapping]
    }
    if (typeof mapping !== 'object' && typeof mapping !== 'string') {
      throw new Error('mapping must be specified as an object or a string')
    }
    return mapData(mapping, instance, { out: true })
  }
}
<commit_msg>Add modelName static property<commit_after>/* global Symbol */
'use strict'

import lxValid from 'lx-valid'
import jshiki from 'jshiki'

function mapData (mapping, data = {}, { out = false } = {}) {
  let mappedData = {}
  for (let targetProperty of Object.getOwnPropertyNames(mapping)) {
    if (typeof mapping[targetProperty] === 'object') {
      if (out) {
        Object.assign(mappedData, mapData(mapping[targetProperty], data[targetProperty], { out }))
      } else {
        mappedData[targetProperty] = mapData(mapping[targetProperty], data, { out })
      }
    } else if (typeof mapping[targetProperty] === 'string') {
      if (out) {
        mappedData[mapping[targetProperty]] = data[targetProperty]
      } else {
        let map = jshiki.parse(mapping[targetProperty], { scope: data })
        mappedData[targetProperty] = map.eval()
      }
    } else {
      throw new Error('mapping value must be an object or a string')
    }
  }
  return mappedData
}

function unrequire (schema) {
  schema = Object.assign({}, schema)
  Object.keys(schema).forEach(prop => {
    schema[prop] = Object.assign({}, schema[prop])
    if (schema[prop].properties) {
      schema[prop].properties = unrequire(schema[prop].properties)
    }
    schema[prop].required = false
  })
  return schema
}

function constructData (schema, data = {}, {
  defaults = true, nullAsUndefined
} = {}) {
  let target = {}
  for (let property of Object.getOwnPropertyNames(schema)) {
    let subData = typeof data[property] !== 'undefined' ? data[property] : {}
    let subSchema = schema[property].properties
    let subProperties = subSchema ? Object.getOwnPropertyNames(subSchema) : []
    if (subProperties.some(property =>
      typeof subData[property] !== 'undefined' ||
      (defaults && typeof subSchema[property].default !== 'undefined')
    )) {
      target[property] = constructData(subSchema, subData, {
        defaults, nullAsUndefined
      })
    } else {
      target[property] = (
        data.hasOwnProperty(property) &&
        (!nullAsUndefined || data[property] !== null)
      ) ? data[property]
        : (defaults ? schema[property].default : undefined)
    }
  }
  return target
}

export class Model {
  constructor (data = {}, { mapping, defaults = true, nullAsUndefined } = {}) {
    let schema = this.constructor.schema
    if (typeof schema !== 'object' || !schema) {
      throw new Error('No schema set on class')
    }
    if (data === null) {
      data = {}
    } else if (typeof data !== 'object' || !data) {
      throw new Error('data must be an object')
    }

    if (mapping) {
      if (typeof mapping === 'string') {
        if (!this.constructor.mappings || !this.constructor.mappings[mapping]) {
          throw new Error(`The mapping ${mapping} is not defined on the class`)
        }
        mapping = this.constructor.mappings[mapping]
      }
      if (typeof mapping !== 'object' && typeof mapping !== 'string') {
        throw new Error('mapping must be specified as an object or a string')
      }
      for (let targetProperty of Object.getOwnPropertyNames(mapping)) {
        if (!schema.hasOwnProperty(targetProperty)) {
          throw new Error(
            `Property ${targetProperty} is not defined on the schema`
          )
        }
      }
      let mappedData = mapData(mapping, data)
      for (let targetProperty of Object.getOwnPropertyNames(schema)) {
        if (!mappedData.hasOwnProperty(targetProperty)) {
          mappedData[targetProperty] = data[targetProperty]
        }
      }
      data = mappedData
    }

    data = constructData(schema, data, { defaults, nullAsUndefined })

    for (let property of Object.getOwnPropertyNames(schema)) {
      if (typeof schema[property] !== 'object' || !schema[property]) {
        throw new Error(`${property} is not a valid property definition`)
      }
      let key = Symbol(property)
      this[key] = data[property]
      Object.defineProperty(this, property, {
        get: schema[property].get
          ? () => this.schema[property].get(this[key])
          : () => this[key],
        set: schema[property].set
          ? val => this[key] = this.schema[property].set(val, this[key])
          : val => this[key] = val,
        enumerable: true,
        configurable: false
      })
    }

    Object.preventExtensions(this)
    Object.seal(this)
  }

  static get modelName () {
    return this.name
  }

  static subset (...properties) {
    if (typeof this.schema !== 'object' || !this.schema) {
      throw new Error('No schema set on class')
    }
    if (properties.length < 1) {
      throw new Error('At least one property must be specified')
    }

    let subsetSchema = {}

    for (let property of properties) {
      if (!this.schema.hasOwnProperty(property)) {
        throw new Error(`Property ${property} is not defined on the schema`)
      }
      subsetSchema[property] = this.schema[property]
    }

    return class Subset extends this {
      static get schema () { return subsetSchema }
    }
  }

  static exclude (...properties) {
    if (typeof this.schema !== 'object' || !this.schema) {
      throw new Error('No schema set on class')
    }
    if (properties.length < 1) {
      throw new Error('At least one property must be specified')
    }

    let subset = Object.getOwnPropertyNames(this.schema)

    for (let property of properties) {
      if (!this.schema.hasOwnProperty(property)) {
        throw new Error(`Property ${property} is not defined on the schema`)
      }
      subset.splice(subset.indexOf(property), 1)
    }

    let subsetSchema = {}

    for (let property of subset) {
      subsetSchema[property] = this.schema[property]
    }

    return class Subset extends this {
      static get schema () { return subsetSchema }
    }
  }

  static validate (data, { definedOnly = false } = {}) {
    let schema = this.schema
    if (typeof schema !== 'object' || !schema) {
      throw new Error('No schema set on class')
    }
    if (definedOnly) {
      schema = unrequire(schema)
    }
    return lxValid.validate(data, { properties: schema })
  }

  static map (instance, mapping) {
    if (typeof mapping === 'string') {
      if (!this.mappings || !this.mappings[mapping]) {
        throw new Error(`The mapping ${mapping} is not defined on the class`)
      }
      mapping = this.mappings[mapping]
    }
    if (typeof mapping !== 'object' && typeof mapping !== 'string') {
      throw new Error('mapping must be specified as an object or a string')
    }
    return mapData(mapping, instance, { out: true })
  }
}
<|endoftext|>


<commit_before>'use strict';

var express = require('express'),
    passport = require('passport'),
    RdioStrategy = require('passport-rdio').Strategy;
var User = require('../../models/user');
var q = require('q');

// Passport session setup.
// ßTo support persistent login sessions, Passport needs to be able to
// ßserialize users into and deserialize users out of the session.  Typically,
// ßthis will be as simple as storing the user ID when serializing, and finding
// ßthe user by ID when deserializing.  However, since this example does not
// ßhave a database of user records, the complete Rdio profile is
// ßserialized and deserialized.
passport.serializeUser(function(user, done) {
    done(null, user);
});

passport.deserializeUser(function(obj, done) {
    done(null, obj);
});

// Use the RdioStrategy within Passport.
//   Strategies in passport require a `verify` function, which accept
//   credentials (in this case, a token, tokenSecret, and Rdio profile), and
//   invoke a callback with a user object.
passport.use(new RdioStrategy({
        consumerKey: process.env.RDIO_API_KEY,
        consumerSecret: process.env.RDIO_SHARED_SECRET,
        callbackURL: process.env.URL + '/auth/rdio/callback'
    },
    function(token, tokenSecret, profile, done) {
        console.log('profile: ' + JSON.stringify(profile, null, 4));

        var getRequest = q.resolve().then(function () {
            return User.getByRdioId(profile.id).then(function (user) {
                var updates = {
                    oauth: {
                        token: token,
                        tokenSecret: tokenSecret
                    }
                };
                var updateRequest = q.defer();
                user.update(updates, updateRequest.makeNodeResolver());
                return updateRequest.promise;
            });
        });

        getRequest.then(function () {
            done(null, profile);
        }).fail(function (err) {
            console.log('Error: ' + err);
            done(null, profile);
        });
        // // asynchronous verification, for effect...
        // process.nextTick(function () {

        //   // To keep the example simple, the user's Rdio profile is returned to
        //   // represent the logged-in user.  In a typical application, you would want
        //   // to associate the Rdio account with a user record in your database,
        //   // and return that user instead.
        //     return done(null, profile);
        // });
    }
));

var app = express();

// configure Express
app.configure(function() {
    app.set('views', __dirname + '/views');
    app.set('view engine', 'ejs');
    // Initialize Passport!  Also use passport.session() middleware, to support
    // persistent login sessions (recommended).
    app.use(passport.initialize());
    app.use(passport.session());
    app.use(app.router);
});

app.get('/auth/rdio',
  passport.authenticate('rdio')
);

app.get('/auth/rdio/fail',
  function(req, res) {
    res.redirect('authfail');
}
);

app.get('/auth/rdio/callback',
  passport.authenticate('rdio', {
    successRedirect: '/',
    failureRedirect: '/auth/rdio/fail'
})
);

app.get('/logout', function(req, res){
    req.logout();
    res.redirect('/');
});

module.exports = app;<commit_msg>indentation<commit_after>'use strict';

var express = require('express'),
    passport = require('passport'),
    RdioStrategy = require('passport-rdio').Strategy;
var User = require('../../models/user');
var q = require('q');

// Passport session setup.
// ßTo support persistent login sessions, Passport needs to be able to
// ßserialize users into and deserialize users out of the session.  Typically,
// ßthis will be as simple as storing the user ID when serializing, and finding
// ßthe user by ID when deserializing.  However, since this example does not
// ßhave a database of user records, the complete Rdio profile is
// ßserialized and deserialized.
passport.serializeUser(function(user, done) {
    done(null, user);
});

passport.deserializeUser(function(obj, done) {
    done(null, obj);
});

// Use the RdioStrategy within Passport.
//   Strategies in passport require a `verify` function, which accept
//   credentials (in this case, a token, tokenSecret, and Rdio profile), and
//   invoke a callback with a user object.
passport.use(new RdioStrategy({
    consumerKey: process.env.RDIO_API_KEY,
    consumerSecret: process.env.RDIO_SHARED_SECRET,
    callbackURL: process.env.URL + '/auth/rdio/callback'
}, function(token, tokenSecret, profile, done) {
    console.log('profile: ' + JSON.stringify(profile, null, 4));

    q.resolve().then(function () {
        return User.getByRdioId(profile.id);
    }).then(function (user) {
        var updates = {
            oauth: {
                token: token,
                tokenSecret: tokenSecret
            }
        };
        var updateRequest = q.defer();
        user.update(updates, updateRequest.makeNodeResolver());
        return updateRequest.promise;
    }).then(function () {
        done(null, profile);
    }).fail(function (err) {
        console.log('Error: ' + err);
        done(null, profile);
    });
    // // asynchronous verification, for effect...
    // process.nextTick(function () {

    //   // To keep the example simple, the user's Rdio profile is returned to
    //   // represent the logged-in user.  In a typical application, you would want
    //   // to associate the Rdio account with a user record in your database,
    //   // and return that user instead.
    //     return done(null, profile);
    // });
}));

var app = express();

// configure Express
app.configure(function() {
    app.set('views', __dirname + '/views');
    app.set('view engine', 'ejs');
    // Initialize Passport!  Also use passport.session() middleware, to support
    // persistent login sessions (recommended).
    app.use(passport.initialize());
    app.use(passport.session());
    app.use(app.router);
});

app.get('/auth/rdio',
  passport.authenticate('rdio')
);

app.get('/auth/rdio/fail',
    function(req, res) {
        res.redirect('authfail');
    }
);

app.get('/auth/rdio/callback',
  passport.authenticate('rdio', {
    successRedirect: '/',
    failureRedirect: '/auth/rdio/fail'
})
);

app.get('/logout', function(req, res){
    req.logout();
    res.redirect('/');
});

module.exports = app;<|endoftext|>


<commit_before>import fs from 'fs';
import path from 'path';
import dot from 'dot';
import { encoding } from '../constants';

dot.templateSettings.strip = false;

function getRelativePaths(fromDir, files) {
  files.sort();
  return files.map((file) => {
    let relativePath = path.relative(fromDir, file);

    if (relativePath.substr(0, 2) !== '..' || relativePath.substr(0, 2) !== './') {
      relativePath = `./${relativePath}`;
    }

    return {
      relative: relativePath,
      full: file,
    };
  });
}

function ensureFileDirectoryExists(filePath) {
  const directory = path.dirname(filePath);

  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory);
  }
}

export const templateContents = `
// template for doT (https://github.com/olado/doT)

function loadStories() {

  {{~it.files :value:index}}require('{{=value.relative}}');
  {{~}}
}

module.exports = {
  loadStories,
};
`;

export function writeFile(files, outputPath) {
  const template = dot.template(templateContents);
  const relativePaths = getRelativePaths(path.dirname(outputPath), files);

  const output = template({ files: relativePaths });

  ensureFileDirectoryExists(outputPath);

  fs.writeFileSync(outputPath, output, { encoding });
}
<commit_msg>fix(writer): Fix path generation for windows<commit_after>import fs from 'fs';
import path from 'path';
import dot from 'dot';
import { encoding } from '../constants';

dot.templateSettings.strip = false;

function getRelativePaths(fromDir, files) {
  files.sort();
  return files.map((file) => {
    let relativePath = path.relative(fromDir, file);

    if (relativePath.substr(0, 2) !== '..' || relativePath.substr(0, 2) !== './') {
      relativePath = `./${relativePath}`;
    }
    relativePath  = require('os').platform() === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath

    return {
      relative: relativePath,
      full: file,
    };
  });
}

function ensureFileDirectoryExists(filePath) {
  const directory = path.dirname(filePath);

  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory);
  }
}

export const templateContents = `
// template for doT (https://github.com/olado/doT)

function loadStories() {

  {{~it.files :value:index}}require('{{=value.relative}}');
  {{~}}
}

module.exports = {
  loadStories,
};
`;

export function writeFile(files, outputPath) {
  const template = dot.template(templateContents);
  const relativePaths = getRelativePaths(path.dirname(outputPath), files);

  const output = template({ files: relativePaths });

  ensureFileDirectoryExists(outputPath);

  fs.writeFileSync(outputPath, output, { encoding });
}
<|endoftext|>


<commit_before>/**
 * Main entry point
 *
 * There are no arguments to this function, because it will be controlled by Config arguments
 * and invoked by testAutomatically.js (a generated file).  See the documentation on how to
 * set up and run Illuminator.
 */
function IlluminatorIlluminate() {
    // initial sanity checks
    assertDesiredSimVersion();

    // send test definitions back to framework
    if (!host().writeToFile(config.buildArtifacts.automatorScenarioJSON,
                            JSON.stringify(automator.toScenarioObject(false), null, "    ")))
    {
        throw new IlluminatorSetupException("Could not save necessary build artifact to " +
                                            config.buildArtifacts.automatorScenarioJSON);
    }
    notifyIlluminatorFramework("Saved scenario definitions to: " + config.buildArtifacts.automatorScenarioJSON);

    // run app-specific callback
    if (!automator._executeCallback("onInit", {entryPoint: config.entryPoint}, false, false)) return;

    // choose entry point and go
    switch (config.entryPoint) {

    case "runTestsByName":
        automator.runNamedScenarios(config.automatorScenarioNames, config.automatorSequenceRandomSeed);
        break;

    case "runTestsByTag":
        if (0 == (config.automatorTagsAny.length + config.automatorTagsAll.length + config.automatorTagsNone.length)) {
            UIALogger.logMessage("No tag sets (any / all / none) were defined, so printing some information about defined scenarios");
            automator.logInfo();
        } else {
            automator.runTaggedScenarios(config.automatorTagsAny,
                                         config.automatorTagsAll,
                                         config.automatorTagsNone,
                                         config.automatorSequenceRandomSeed);
        }
        break;

    case "describe":
        notifyIlluminatorFramework("Successful launch");
        var appMapMarkdownPath    = config.buildArtifacts.appMapMarkdown;
        var automatorMarkdownPath = config.buildArtifacts.automatorMarkdown;
        var automatorJSONPath     = config.buildArtifacts.automatorJSON;
        host().writeToFile(appMapMarkdownPath, appmap.toMarkdown());
        UIALogger.logMessage("Wrote AppMap definitions to " + appMapMarkdownPath);
        host().writeToFile(automatorMarkdownPath, automator.toMarkdown());
        UIALogger.logMessage("Wrote automator definitions to " + automatorMarkdownPath);
        host().writeToFile(automatorJSONPath, JSON.stringify(automator.toScenarioObject(true), null, "    "));
        UIALogger.logMessage("Wrote automator definition data to " + automatorJSONPath);
        break;

    default:
        notifyIlluminatorFramework("Successful launch");
        throw new IlluminatorSetupException("Unknown Illuminator entry point specified: " + config.entryPoint);
    }
}

/**
 * Send a message back to the log analyzers (ruby)
 *
 * @param message string
 */
function notifyIlluminatorFramework(message) {
    UIALogger.logDebug(config.saltinel + " " + message + " " + config.saltinel);
}

function isMatchingVersion(input, prefix, major, minor, rev) {
    var findStr = prefix + major;

    if (undefined !== minor) {
        findStr += "." + minor;
        if (undefined !== rev) {
            findStr += "." + rev;
        }
    }

    return input.indexOf(findStr) > -1;
}

function isSimVersion(major, minor, rev) {
    return isMatchingVersion(target().systemVersion(), "", major, minor, rev);
}

function assertDesiredSimVersion() {
    var ver = target().systemVersion();
    if (("iOS " + ver).indexOf(config.automatorDesiredSimVersion) == -1) {
        throw new IlluminatorSetupException("Simulator version " + ver + " is running, but generated-config.js " +
            "specifies " + config.automatorDesiredSimVersion);
    }
}

function actionCompareScreenshotToMaster(parm) {
    var masterPath   = parm.masterPath;
    var maskPath     = parm.maskPath;
    var captureTitle = parm.captureTitle;
    var delayCapture = parm.delay === undefined ? 0.4 : parm.delay;

    delay(delayCapture); // wait for any animations to settle

    var diff_pngPath = IlluminatorScriptsDirectory + "/diff_png.sh";
    UIATarget.localTarget().captureScreenWithName(captureTitle);

    var screenshotFile  = captureTitle + ".png";
    var screenshotPath  = config.screenshotDir + "/" + screenshotFile;
    var compareFileBase = config.screenshotDir + "/compared_" + captureTitle;

    var output = target().host().performTaskWithPathArgumentsTimeout("/bin/sh",
                                                                     [diff_pngPath,
                                                                      masterPath,
                                                                      screenshotPath,
                                                                      maskPath,
                                                                      compareFileBase],
                                                                     20);

    // turn the output into key/value pairs separated by ":"
    var outputArr = output.stdout.split("\n");
    var outputObj = {};
    for (var i = 0; i < outputArr.length; ++i) {
        var sp = outputArr[i].split(": ", 2)
        if (sp.length == 2) {
            outputObj[sp[0]] = sp[1];
        }
    }

    // sanity check
    if (!outputObj["pixels changed"]) {
        throw new IlluminatorRuntimeVerificationException("actionCompareScreenshotToMaster: diff_png.sh failed to produce 'pixels changed' output");
    }

    // if differences are outside tolerances, throw errors
    var allPixels = parseInt(outputObj["pixels (total)"]);
    var wrongPixels = parseInt(outputObj["pixels changed"]);

    var allowedPixels = parm.allowedPixels === undefined ? 0 : parm.allowedPixels;
    var errmsg = "";
    if (allowedPixels < wrongPixels) {
        errmsg = ["Screenshot differed from", masterPath,
                  "by", wrongPixels, "pixels. ",
                  "Comparison image saved to:", compareFileBase + ".png",
                  " and comparison animation saved to:", compareFileBase + ".gif"].join(" ");

        if (parm.deferFailure === true) {
            automator.deferFailure(errmsg);
        } else {
            throw new IlluminatorRuntimeVerificationException(errmsg);
        }
    }

    if (parm.allowedPercent !== undefined) {
        var wrongPct = 100.0 * wrongPixels / allPixels;
        if (wrongPct > parm.allowedPercent) {
            errmsg = ["Screenshot differed from", masterPath,
                      "by", wrongPct, "%. ",
                      "Comparison image saved to:", compareFileBase + ".png",
                      " and comparison animation saved to:", compareFileBase + ".gif"].join(" ");

            if (parm.deferFailure === true) {
            } else {
                throw new IlluminatorRuntimeVerificationException(errmsg);
            }
        }
    }
}

function actionLogAccessors(parm) {
    if (parm !== undefined && parm.delay !== undefined) {
        delay(parm.delay);
    }
    var visibleOnly = parm !== undefined && parm.visibleOnly === true;
    UIALogger.logDebug(mainWindow().elementReferenceDump("mainWindow", visibleOnly));
}

function actionCaptureElementTree(parm) {
    target().captureImageTree(parm.imageBaseName);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Appmap additions - common capabilities
////////////////////////////////////////////////////////////////////////////////////////////////////
appmap.createOrAugmentApp("Illuminator").withScreen("do")
    .onTarget(config.implementation, function() { return true; })
    // onTarget(config.implementation...) is a HACK.
    // Illuminator doesn't define implementations, so we use what we're given.
    //   (i.e. you shouldn't copy/paste this example into your project code)

    .withAction("delay", "Delay a given amount of time")
    .withParam("seconds", "Number of seconds to delay", true, true)
    .withImplementation(function(parm) {delay(parm.seconds);})

    .withAction("debug", "Print the results of a debug function")
    .withParam("debug_fn", "Function returning a string", true)
    .withImplementation(function(parm) { UIALogger.logMessage(parm.debug_fn()); })

    .withAction("logTree", "Log the UI element tree")
    .withImplementation(function() { UIATarget.localTarget().logElementTree(); })

    .withAction("logAccessors", "Log the list of valid element accessors")
    .withParam("visibleOnly", "Whether to log only the visible elements", false, true)
    .withParam("delay", "Number of seconds to delay before logging", false, true)
    .withImplementation(actionLogAccessors)

    .withAction("captureElementTree", "Take individual screenshots of all screen elements")
    .withParam("imageBaseName", "The base name for the image files", true, true)
    .withImplementation(actionCaptureElementTree)

    .withAction("fail", "Unconditionally fail the current test for debugging purposes")
    .withImplementation(function() { throw new IlluminatorRuntimeVerificationException("purposely-thrown exception to halt the test scenario"); })

    .withAction("verifyScreenshot", "Validate a screenshot against a png template of the expected view")
    .withParam("masterPath", "The path to the file that is considered the 'expected' view", true, true)
    .withParam("maskPath", "The path to the file that masks variable portions of the 'expected' view", true, true)
    .withParam("captureTitle", "The title of the screenshot to capture", true, true)
    .withParam("delay", "The amount of time to delay before taking the screenshot", false, true)
    .withParam("allowedPixels", "The maximum number of pixels that are allowed to differ (default 0)", false, true)
    .withParam("allowedPercent", "The maximum percentage of pixels that are allowed to differ (default 0)", false, true)
    .withParam("deferFailure", "Whether to defer a failure until the end of the test", false, true)
    .withImplementation(actionCompareScreenshotToMaster)

    .withAction("testAsAction", "Execute an entire test as one action")
    .withParam("test", "The function that performs the entire test")
    .withImplementation(function (parm) { parm.test(); });
<commit_msg>[runner] Having no tags defined needs to abort the test but be considered a successful launch<commit_after>/**
 * Main entry point
 *
 * There are no arguments to this function, because it will be controlled by Config arguments
 * and invoked by testAutomatically.js (a generated file).  See the documentation on how to
 * set up and run Illuminator.
 */
function IlluminatorIlluminate() {
    // initial sanity checks
    assertDesiredSimVersion();

    // send test definitions back to framework
    if (!host().writeToFile(config.buildArtifacts.automatorScenarioJSON,
                            JSON.stringify(automator.toScenarioObject(false), null, "    ")))
    {
        throw new IlluminatorSetupException("Could not save necessary build artifact to " +
                                            config.buildArtifacts.automatorScenarioJSON);
    }
    notifyIlluminatorFramework("Saved scenario definitions to: " + config.buildArtifacts.automatorScenarioJSON);

    // run app-specific callback
    if (!automator._executeCallback("onInit", {entryPoint: config.entryPoint}, false, false)) return;

    // choose entry point and go
    switch (config.entryPoint) {

    case "runTestsByName":
        automator.runNamedScenarios(config.automatorScenarioNames, config.automatorSequenceRandomSeed);
        break;

    case "runTestsByTag":
        if (0 == (config.automatorTagsAny.length + config.automatorTagsAll.length + config.automatorTagsNone.length)) {
            UIALogger.logMessage("No tag sets (any / all / none) were defined, so printing some information about defined scenarios");
            automator.logInfo();
            notifyIlluminatorFramework("Successful launch");
        } else {
            automator.runTaggedScenarios(config.automatorTagsAny,
                                         config.automatorTagsAll,
                                         config.automatorTagsNone,
                                         config.automatorSequenceRandomSeed);
        }
        break;

    case "describe":
        notifyIlluminatorFramework("Successful launch");
        var appMapMarkdownPath    = config.buildArtifacts.appMapMarkdown;
        var automatorMarkdownPath = config.buildArtifacts.automatorMarkdown;
        var automatorJSONPath     = config.buildArtifacts.automatorJSON;
        host().writeToFile(appMapMarkdownPath, appmap.toMarkdown());
        UIALogger.logMessage("Wrote AppMap definitions to " + appMapMarkdownPath);
        host().writeToFile(automatorMarkdownPath, automator.toMarkdown());
        UIALogger.logMessage("Wrote automator definitions to " + automatorMarkdownPath);
        host().writeToFile(automatorJSONPath, JSON.stringify(automator.toScenarioObject(true), null, "    "));
        UIALogger.logMessage("Wrote automator definition data to " + automatorJSONPath);
        break;

    default:
        notifyIlluminatorFramework("Successful launch");
        throw new IlluminatorSetupException("Unknown Illuminator entry point specified: " + config.entryPoint);
    }
}

/**
 * Send a message back to the log analyzers (ruby)
 *
 * @param message string
 */
function notifyIlluminatorFramework(message) {
    UIALogger.logDebug(config.saltinel + " " + message + " " + config.saltinel);
}

function isMatchingVersion(input, prefix, major, minor, rev) {
    var findStr = prefix + major;

    if (undefined !== minor) {
        findStr += "." + minor;
        if (undefined !== rev) {
            findStr += "." + rev;
        }
    }

    return input.indexOf(findStr) > -1;
}

function isSimVersion(major, minor, rev) {
    return isMatchingVersion(target().systemVersion(), "", major, minor, rev);
}

function assertDesiredSimVersion() {
    var ver = target().systemVersion();
    if (("iOS " + ver).indexOf(config.automatorDesiredSimVersion) == -1) {
        throw new IlluminatorSetupException("Simulator version " + ver + " is running, but generated-config.js " +
            "specifies " + config.automatorDesiredSimVersion);
    }
}

function actionCompareScreenshotToMaster(parm) {
    var masterPath   = parm.masterPath;
    var maskPath     = parm.maskPath;
    var captureTitle = parm.captureTitle;
    var delayCapture = parm.delay === undefined ? 0.4 : parm.delay;

    delay(delayCapture); // wait for any animations to settle

    var diff_pngPath = IlluminatorScriptsDirectory + "/diff_png.sh";
    UIATarget.localTarget().captureScreenWithName(captureTitle);

    var screenshotFile  = captureTitle + ".png";
    var screenshotPath  = config.screenshotDir + "/" + screenshotFile;
    var compareFileBase = config.screenshotDir + "/compared_" + captureTitle;

    var output = target().host().performTaskWithPathArgumentsTimeout("/bin/sh",
                                                                     [diff_pngPath,
                                                                      masterPath,
                                                                      screenshotPath,
                                                                      maskPath,
                                                                      compareFileBase],
                                                                     20);

    // turn the output into key/value pairs separated by ":"
    var outputArr = output.stdout.split("\n");
    var outputObj = {};
    for (var i = 0; i < outputArr.length; ++i) {
        var sp = outputArr[i].split(": ", 2)
        if (sp.length == 2) {
            outputObj[sp[0]] = sp[1];
        }
    }

    // sanity check
    if (!outputObj["pixels changed"]) {
        throw new IlluminatorRuntimeVerificationException("actionCompareScreenshotToMaster: diff_png.sh failed to produce 'pixels changed' output");
    }

    // if differences are outside tolerances, throw errors
    var allPixels = parseInt(outputObj["pixels (total)"]);
    var wrongPixels = parseInt(outputObj["pixels changed"]);

    var allowedPixels = parm.allowedPixels === undefined ? 0 : parm.allowedPixels;
    var errmsg = "";
    if (allowedPixels < wrongPixels) {
        errmsg = ["Screenshot differed from", masterPath,
                  "by", wrongPixels, "pixels. ",
                  "Comparison image saved to:", compareFileBase + ".png",
                  " and comparison animation saved to:", compareFileBase + ".gif"].join(" ");

        if (parm.deferFailure === true) {
            automator.deferFailure(errmsg);
        } else {
            throw new IlluminatorRuntimeVerificationException(errmsg);
        }
    }

    if (parm.allowedPercent !== undefined) {
        var wrongPct = 100.0 * wrongPixels / allPixels;
        if (wrongPct > parm.allowedPercent) {
            errmsg = ["Screenshot differed from", masterPath,
                      "by", wrongPct, "%. ",
                      "Comparison image saved to:", compareFileBase + ".png",
                      " and comparison animation saved to:", compareFileBase + ".gif"].join(" ");

            if (parm.deferFailure === true) {
            } else {
                throw new IlluminatorRuntimeVerificationException(errmsg);
            }
        }
    }
}

function actionLogAccessors(parm) {
    if (parm !== undefined && parm.delay !== undefined) {
        delay(parm.delay);
    }
    var visibleOnly = parm !== undefined && parm.visibleOnly === true;
    UIALogger.logDebug(mainWindow().elementReferenceDump("mainWindow", visibleOnly));
}

function actionCaptureElementTree(parm) {
    target().captureImageTree(parm.imageBaseName);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Appmap additions - common capabilities
////////////////////////////////////////////////////////////////////////////////////////////////////
appmap.createOrAugmentApp("Illuminator").withScreen("do")
    .onTarget(config.implementation, function() { return true; })
    // onTarget(config.implementation...) is a HACK.
    // Illuminator doesn't define implementations, so we use what we're given.
    //   (i.e. you shouldn't copy/paste this example into your project code)

    .withAction("delay", "Delay a given amount of time")
    .withParam("seconds", "Number of seconds to delay", true, true)
    .withImplementation(function(parm) {delay(parm.seconds);})

    .withAction("debug", "Print the results of a debug function")
    .withParam("debug_fn", "Function returning a string", true)
    .withImplementation(function(parm) { UIALogger.logMessage(parm.debug_fn()); })

    .withAction("logTree", "Log the UI element tree")
    .withImplementation(function() { UIATarget.localTarget().logElementTree(); })

    .withAction("logAccessors", "Log the list of valid element accessors")
    .withParam("visibleOnly", "Whether to log only the visible elements", false, true)
    .withParam("delay", "Number of seconds to delay before logging", false, true)
    .withImplementation(actionLogAccessors)

    .withAction("captureElementTree", "Take individual screenshots of all screen elements")
    .withParam("imageBaseName", "The base name for the image files", true, true)
    .withImplementation(actionCaptureElementTree)

    .withAction("fail", "Unconditionally fail the current test for debugging purposes")
    .withImplementation(function() { throw new IlluminatorRuntimeVerificationException("purposely-thrown exception to halt the test scenario"); })

    .withAction("verifyScreenshot", "Validate a screenshot against a png template of the expected view")
    .withParam("masterPath", "The path to the file that is considered the 'expected' view", true, true)
    .withParam("maskPath", "The path to the file that masks variable portions of the 'expected' view", true, true)
    .withParam("captureTitle", "The title of the screenshot to capture", true, true)
    .withParam("delay", "The amount of time to delay before taking the screenshot", false, true)
    .withParam("allowedPixels", "The maximum number of pixels that are allowed to differ (default 0)", false, true)
    .withParam("allowedPercent", "The maximum percentage of pixels that are allowed to differ (default 0)", false, true)
    .withParam("deferFailure", "Whether to defer a failure until the end of the test", false, true)
    .withImplementation(actionCompareScreenshotToMaster)

    .withAction("testAsAction", "Execute an entire test as one action")
    .withParam("test", "The function that performs the entire test")
    .withImplementation(function (parm) { parm.test(); });
<|endoftext|>


<commit_before>const util = require('./util')
const sanitizeHtml = require('sanitize-html')
const sanitizeOpts = require('./sanitizeOpts')
const toolbars = require('./toolbars')
const MQ = MathQuill.getInterface(2)
const locales = {
    FI: require('./FI'),
    SV: require('./SV')
}
const l = locales[window.locale || 'FI'].editor
const keyCodes = {
    ENTER: 13,
    ESC: 27
}

let $toolbar
const $outerPlaceholder = $(`<div class="math-editor-hidden" data-js="outerPlaceholder">`)
let mathEditor

function moveElementAfter($element, $after) {
    $after.after($element)
}

function hideElementInDOM($element) {
    $outerPlaceholder.append($element)
}

let editor

// TODO: replace with data attributes?
let answerFocus = true
let latexEditorFocus = false
let equationEditorFocus = false
let mathEditorVisible = false
let $editor

$('body').append($outerPlaceholder)

mathEditor = initMathEditor()
$toolbar = toolbars.init(mathEditor, () => answerFocus, l)
hideElementInDOM($toolbar)

function initMathEditor() {
    const $mathEditor = $(`

${l.close}

                <textarea class="math-editor-latex-editor" data-js="latexEditor" placeholder="LaTex"></textarea>

`)

    hideElementInDOM($mathEditor)

    const $latexEditor = $mathEditor.find('[data-js="latexEditor"]')
    const $equationEditor = $mathEditor.find('[data-js="equationEditor"]')
    const mathField = MQ.MathField($equationEditor.get(0), {
        handlers: {
            edit: () => !latexEditorFocus && $latexEditor.val(mathField.latex()),
            enter: field => {
                // TODO: do not close editor / o not create a new equation if there is no text?
                mathEditor.closeMathEditor(true)
                setTimeout(() => insertNewEquation('

'), 2)
            }
        }
    })

    $equationEditor
        .on('focus mousedown', e => equationEditorFocus = true)
        .on('focus blur', '.mq-textarea textarea', e => {
            equationEditorFocus = e.type !== 'blur'
            onFocusChanged()
        })

    function onLatexUpdate() {
        setTimeout(() => mathField.latex($latexEditor.val()), 1)
    }

    $latexEditor
        .keyup(onLatexUpdate)
        .on('focus blur', e => {
            latexEditorFocus = e.type !== 'blur'
            onFocusChanged()
        })

    $mathEditor.find('.close').mousedown(e => {
        e.preventDefault()
        closeMathEditor(true)
    })

    let focusChanged = null

    function onFocusChanged() {
        clearTimeout(focusChanged)
        focusChanged = setTimeout(() => {
            if (!latexEditorFocus && !equationEditorFocus) closeMathEditor()
            if (!answerFocus && !mathEditorVisible && !latexEditorFocus && !equationEditorFocus) closeEditor()
        }, 0)

    }

    function insertNewEquation(optionalMarkup) {
        window.document.execCommand('insertHTML', false, (optionalMarkup ? optionalMarkup : '') + '');
        const $addedEquationImage = $('[data-js="new"]')
        $addedEquationImage
            .removeAttr('data-js')

        moveElementAfter($mathEditor, $addedEquationImage)

        mathField.latex('')
        mathEditorVisible = true
        $toolbar.find('[data-js="newEquation"]').hide()
        $toolbar.find('[data-js="mathToolbar"]').show()
        setTimeout(() => mathField.focus(), 0)
    }

    function insertMath(symbol, alternativeSymbol, useWrite) {
        if (latexEditorFocus) {
            util.insertToTextAreaAtCursor($latexEditor.get(0), alternativeSymbol || symbol)
            onLatexUpdate()
        } else if (equationEditorFocus) {
            if (useWrite) {
                mathField.write(symbol)
            } else {
                mathField.typedText(symbol)
            }

            if (symbol.startsWith('\\')) mathField.keystroke('Tab')
            setTimeout(() => mathField.focus(), 0)
        }
    }

    function closeMathEditor(setFocusAfterClose = false) {
        // TODO: remove event bindings
        const $currentEditor = $mathEditor.closest('[data-js="answer"]')
        const $img = $mathEditor.prev()
        if ($latexEditor.val().trim() === '') {
            $img.remove()
        } else {
            $img.show()
                .prop('src', '/math.svg?latex=' + encodeURIComponent($latexEditor.val()))
                .prop('alt', $latexEditor.val())
        }

        $toolbar.find('[data-js="newEquation"]').show()
        $toolbar.find('[data-js="mathToolbar"]').hide()
        hideElementInDOM($mathEditor)
        mathEditorVisible = false
        latexEditorFocus = false
        equationEditorFocus = false
        if (setFocusAfterClose) $currentEditor.focus()
    }

    function openMathEditor($img) {
        if (mathEditorVisible) closeMathEditor()
        $img.hide()
        moveElementAfter($mathEditor, $img)
        const latex = $img.prop('alt')
        $latexEditor.val(latex)
        onLatexUpdate()
        mathEditorVisible = true
        $toolbar.find('[data-js="newEquation"]').hide()
        $toolbar.find('[data-js="mathToolbar"]').show()
        setTimeout(() => mathField.focus(), 0)
    }

    return {
        insertNewEquation,
        insertMath,
        closeMathEditor,
        openMathEditor,
        onFocusChanged
    }
}

function openEditor($element) {
    $editor = $element
    $element.before($toolbar)
    $toolbar.show()
}

function closeEditor() {
    // TODO: remove event bindings
    $toolbar.find('[data-js="mathToolbar"]').hide()
    hideElementInDOM($toolbar)
    mathEditor.closeMathEditor()
    // $editor.off()

    answerFocus = false
    mathEditorVisible = false
    latexEditorFocus = false
}

let blurred

function onEditorFocusChanged(e) {
    answerFocus = e.type === 'focus'

    clearTimeout(blurred)
    blurred = setTimeout(() => {
        if (!answerFocus && !mathEditorVisible && !latexEditorFocus && !equationEditorFocus) closeEditor()
        else if (answerFocus && mathEditorVisible) mathEditor.closeMathEditor()
        else openEditor($(e.target))
    }, 0)
}

function isMathEditorVisible() {
    return mathEditorVisible
}

editor = {
    openEditor,
    closeEditor,
    onEditorFocusChanged,
    isMathEditorVisible,
    openMathEditor: mathEditor.openMathEditor,
    closeMathEditor: mathEditor.closeMathEditor,
    insertNewEquation: mathEditor.insertNewEquation
}

const markAndGetInlineImages = $editor => $editor.find('img[src^="data"]')
    .each((i, el) => el.setAttribute('id', new Date().getTime() + '-' + i))
    .map((i, el) => Object.assign(decodeBase64Image(el.getAttribute('src')), {id: el.getAttribute('id')}))
    .toArray()
    .filter(({type}) => type === 'image/png')

const persistInlineImages = $editor => {
    return Bacon.combineAsArray(
        markAndGetInlineImages($editor)
            .map(data =>
                Bacon.fromPromise($.post({
                    type:'POST',
                    url: `/saveImg?answerId=${$editor.attr('id')}&id=${data.id}`,
                    data: data.data,
                    processData: false,
                    contentType:false
                }))))
        .flatMap(results => {
            results.forEach(({id}) => {
                $editor.find('#' + id).attr('src', `/loadImg?answerId=${$editor.attr('id')}&id=${id}`)
            })
        })
        .onValue(() => $editor.trigger('input'))
}

function decodeBase64Image(dataString) {
    if (!dataString)
        return null
    const matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/)
    if (matches.length !== 3) {
        return null
    }
    return {
        type: matches[1],
        data: new Buffer(matches[2], 'base64')
    }
}

const makeRichText = (element, onValueChanged = () => { }) => {
    const $answer = $(element)
    $answer
        .attr('contenteditable', 'true')
        .attr('data-js', 'answer')
        .addClass('math-editor-answer')
        .on('keydown', e => {
            if (isCtrlKey(e, keyCodes.ENTER) || isKey(e, keyCodes.ESC)) mathEditor.closeMathEditor(true)
        })
        .on('mousedown', 'img[src^="/math.svg"]', e => editor.openMathEditor($(e.target))) // TODO: open editor if clicked on equation in another editor
        .on('keypress', e => {
            if (isCtrlKey(e, 'l') || isCtrlKey(e, 'i')) editor.insertNewEquation()
        })
        .on('focus blur', e => {
            if (editor.isMathEditorVisible() && e.type === 'focus') editor.closeMathEditor()
            editor.onEditorFocusChanged(e)
        })
        .on('input focus', e => onValueChanged(sanitizeContent(e.currentTarget)))
        .on('paste', e => {
            if (e.target.tagName === 'TEXTAREA')
                return
            const clipboardData = e.originalEvent.clipboardData
            const file = clipboardData.items && clipboardData.items[0].getAsFile()
            if (file) {
                e.preventDefault()
                if(file.type !== 'image/png')
                    return
                Bacon.fromPromise($.post({
                    type:'POST',
                    url: `/saveImg?answerId=${$editor.attr('id')}`,
                    data: file,
                    processData: false,
                    contentType:false
                })).onValue(({id}) => {
                    const src = `/loadImg?answerId=${$editor.attr('id')}&id=${id}`
                    const img = `<img src="${src}"/>`
                    window.document.execCommand('insertHTML', false, img)
                })
            } else {
                const clipboardDataAsHtml = clipboardData.getData('text/html')
                if (clipboardDataAsHtml) {
                    e.preventDefault()
                    window.document.execCommand('insertHTML', false, sanitizeHtml(clipboardDataAsHtml, sanitizeOpts));
                    setTimeout(()=> persistInlineImages($editor), 0)

                } else {
                    setTimeout(()=> persistInlineImages($editor), 0)
                }
            }
        })
}

function sanitizeContent(answerElement) {
    $(answerElement).find('[data-js="mathEditor"]').hide()
    const text = $(answerElement)[0].innerText
    $(answerElement).find('[data-js="mathEditor"]').show()

    const html = sanitizeHtml($(answerElement).html(), sanitizeOpts)

    return { answerHTML: html, answerText: text }
}

function isKey(e, key) { return preventIfTrue(e, !e.altKey && !e.shiftKey && !e.ctrlKey  && keyOrKeyCode(e, key))}

function isCtrlKey(e, key) { return preventIfTrue(e, !e.altKey && !e.shiftKey && e.ctrlKey && keyOrKeyCode(e, key))}

function keyOrKeyCode(e, val) { return typeof val === 'string' ? e.key === val : e.keyCode === val }
function preventIfTrue(e, val) {
    if(val) e.preventDefault()
    return val
}
module.exports = {
    makeRichText
}
<commit_msg>Disable spell check in answer field<commit_after>const util = require('./util')
const sanitizeHtml = require('sanitize-html')
const sanitizeOpts = require('./sanitizeOpts')
const toolbars = require('./toolbars')
const MQ = MathQuill.getInterface(2)
const locales = {
    FI: require('./FI'),
    SV: require('./SV')
}
const l = locales[window.locale || 'FI'].editor
const keyCodes = {
    ENTER: 13,
    ESC: 27
}

let $toolbar
const $outerPlaceholder = $(`<div class="math-editor-hidden" data-js="outerPlaceholder">`)
let mathEditor

function moveElementAfter($element, $after) {
    $after.after($element)
}

function hideElementInDOM($element) {
    $outerPlaceholder.append($element)
}

let editor

// TODO: replace with data attributes?
let answerFocus = true
let latexEditorFocus = false
let equationEditorFocus = false
let mathEditorVisible = false
let $editor

$('body').append($outerPlaceholder)

mathEditor = initMathEditor()
$toolbar = toolbars.init(mathEditor, () => answerFocus, l)
hideElementInDOM($toolbar)

function initMathEditor() {
    const $mathEditor = $(`

${l.close}

                <textarea class="math-editor-latex-editor" data-js="latexEditor" placeholder="LaTex"></textarea>

`)

    hideElementInDOM($mathEditor)

    const $latexEditor = $mathEditor.find('[data-js="latexEditor"]')
    const $equationEditor = $mathEditor.find('[data-js="equationEditor"]')
    const mathField = MQ.MathField($equationEditor.get(0), {
        handlers: {
            edit: () => !latexEditorFocus && $latexEditor.val(mathField.latex()),
            enter: field => {
                // TODO: do not close editor / o not create a new equation if there is no text?
                mathEditor.closeMathEditor(true)
                setTimeout(() => insertNewEquation('

'), 2)
            }
        }
    })

    $equationEditor
        .on('focus mousedown', e => equationEditorFocus = true)
        .on('focus blur', '.mq-textarea textarea', e => {
            equationEditorFocus = e.type !== 'blur'
            onFocusChanged()
        })

    function onLatexUpdate() {
        setTimeout(() => mathField.latex($latexEditor.val()), 1)
    }

    $latexEditor
        .keyup(onLatexUpdate)
        .on('focus blur', e => {
            latexEditorFocus = e.type !== 'blur'
            onFocusChanged()
        })

    $mathEditor.find('.close').mousedown(e => {
        e.preventDefault()
        closeMathEditor(true)
    })

    let focusChanged = null

    function onFocusChanged() {
        clearTimeout(focusChanged)
        focusChanged = setTimeout(() => {
            if (!latexEditorFocus && !equationEditorFocus) closeMathEditor()
            if (!answerFocus && !mathEditorVisible && !latexEditorFocus && !equationEditorFocus) closeEditor()
        }, 0)

    }

    function insertNewEquation(optionalMarkup) {
        window.document.execCommand('insertHTML', false, (optionalMarkup ? optionalMarkup : '') + '');
        const $addedEquationImage = $('[data-js="new"]')
        $addedEquationImage
            .removeAttr('data-js')

        moveElementAfter($mathEditor, $addedEquationImage)

        mathField.latex('')
        mathEditorVisible = true
        $toolbar.find('[data-js="newEquation"]').hide()
        $toolbar.find('[data-js="mathToolbar"]').show()
        setTimeout(() => mathField.focus(), 0)
    }

    function insertMath(symbol, alternativeSymbol, useWrite) {
        if (latexEditorFocus) {
            util.insertToTextAreaAtCursor($latexEditor.get(0), alternativeSymbol || symbol)
            onLatexUpdate()
        } else if (equationEditorFocus) {
            if (useWrite) {
                mathField.write(symbol)
            } else {
                mathField.typedText(symbol)
            }

            if (symbol.startsWith('\\')) mathField.keystroke('Tab')
            setTimeout(() => mathField.focus(), 0)
        }
    }

    function closeMathEditor(setFocusAfterClose = false) {
        // TODO: remove event bindings
        const $currentEditor = $mathEditor.closest('[data-js="answer"]')
        const $img = $mathEditor.prev()
        if ($latexEditor.val().trim() === '') {
            $img.remove()
        } else {
            $img.show()
                .prop('src', '/math.svg?latex=' + encodeURIComponent($latexEditor.val()))
                .prop('alt', $latexEditor.val())
        }

        $toolbar.find('[data-js="newEquation"]').show()
        $toolbar.find('[data-js="mathToolbar"]').hide()
        hideElementInDOM($mathEditor)
        mathEditorVisible = false
        latexEditorFocus = false
        equationEditorFocus = false
        if (setFocusAfterClose) $currentEditor.focus()
    }

    function openMathEditor($img) {
        if (mathEditorVisible) closeMathEditor()
        $img.hide()
        moveElementAfter($mathEditor, $img)
        const latex = $img.prop('alt')
        $latexEditor.val(latex)
        onLatexUpdate()
        mathEditorVisible = true
        $toolbar.find('[data-js="newEquation"]').hide()
        $toolbar.find('[data-js="mathToolbar"]').show()
        setTimeout(() => mathField.focus(), 0)
    }

    return {
        insertNewEquation,
        insertMath,
        closeMathEditor,
        openMathEditor,
        onFocusChanged
    }
}

function openEditor($element) {
    $editor = $element
    $element.before($toolbar)
    $toolbar.show()
}

function closeEditor() {
    // TODO: remove event bindings
    $toolbar.find('[data-js="mathToolbar"]').hide()
    hideElementInDOM($toolbar)
    mathEditor.closeMathEditor()
    // $editor.off()

    answerFocus = false
    mathEditorVisible = false
    latexEditorFocus = false
}

let blurred

function onEditorFocusChanged(e) {
    answerFocus = e.type === 'focus'

    clearTimeout(blurred)
    blurred = setTimeout(() => {
        if (!answerFocus && !mathEditorVisible && !latexEditorFocus && !equationEditorFocus) closeEditor()
        else if (answerFocus && mathEditorVisible) mathEditor.closeMathEditor()
        else openEditor($(e.target))
    }, 0)
}

function isMathEditorVisible() {
    return mathEditorVisible
}

editor = {
    openEditor,
    closeEditor,
    onEditorFocusChanged,
    isMathEditorVisible,
    openMathEditor: mathEditor.openMathEditor,
    closeMathEditor: mathEditor.closeMathEditor,
    insertNewEquation: mathEditor.insertNewEquation
}

const markAndGetInlineImages = $editor => $editor.find('img[src^="data"]')
    .each((i, el) => el.setAttribute('id', new Date().getTime() + '-' + i))
    .map((i, el) => Object.assign(decodeBase64Image(el.getAttribute('src')), {id: el.getAttribute('id')}))
    .toArray()
    .filter(({type}) => type === 'image/png')

const persistInlineImages = $editor => {
    return Bacon.combineAsArray(
        markAndGetInlineImages($editor)
            .map(data =>
                Bacon.fromPromise($.post({
                    type:'POST',
                    url: `/saveImg?answerId=${$editor.attr('id')}&id=${data.id}`,
                    data: data.data,
                    processData: false,
                    contentType:false
                }))))
        .flatMap(results => {
            results.forEach(({id}) => {
                $editor.find('#' + id).attr('src', `/loadImg?answerId=${$editor.attr('id')}&id=${id}`)
            })
        })
        .onValue(() => $editor.trigger('input'))
}

function decodeBase64Image(dataString) {
    if (!dataString)
        return null
    const matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/)
    if (matches.length !== 3) {
        return null
    }
    return {
        type: matches[1],
        data: new Buffer(matches[2], 'base64')
    }
}

const makeRichText = (element, onValueChanged = () => { }) => {
    const $answer = $(element)
    $answer
        .attr('contenteditable', 'true')
        .attr('spellcheck', 'false')
        .attr('data-js', 'answer')
        .addClass('math-editor-answer')
        .on('keydown', e => {
            if (isCtrlKey(e, keyCodes.ENTER) || isKey(e, keyCodes.ESC)) mathEditor.closeMathEditor(true)
        })
        .on('mousedown', 'img[src^="/math.svg"]', e => editor.openMathEditor($(e.target))) // TODO: open editor if clicked on equation in another editor
        .on('keypress', e => {
            if (isCtrlKey(e, 'l') || isCtrlKey(e, 'i')) editor.insertNewEquation()
        })
        .on('focus blur', e => {
            if (editor.isMathEditorVisible() && e.type === 'focus') editor.closeMathEditor()
            editor.onEditorFocusChanged(e)
        })
        .on('input focus', e => onValueChanged(sanitizeContent(e.currentTarget)))
        .on('paste', e => {
            if (e.target.tagName === 'TEXTAREA')
                return
            const clipboardData = e.originalEvent.clipboardData
            const file = clipboardData.items && clipboardData.items[0].getAsFile()
            if (file) {
                e.preventDefault()
                if(file.type !== 'image/png')
                    return
                Bacon.fromPromise($.post({
                    type:'POST',
                    url: `/saveImg?answerId=${$editor.attr('id')}`,
                    data: file,
                    processData: false,
                    contentType:false
                })).onValue(({id}) => {
                    const src = `/loadImg?answerId=${$editor.attr('id')}&id=${id}`
                    const img = `<img src="${src}"/>`
                    window.document.execCommand('insertHTML', false, img)
                })
            } else {
                const clipboardDataAsHtml = clipboardData.getData('text/html')
                if (clipboardDataAsHtml) {
                    e.preventDefault()
                    window.document.execCommand('insertHTML', false, sanitizeHtml(clipboardDataAsHtml, sanitizeOpts));
                    setTimeout(()=> persistInlineImages($editor), 0)

                } else {
                    setTimeout(()=> persistInlineImages($editor), 0)
                }
            }
        })
}

function sanitizeContent(answerElement) {
    $(answerElement).find('[data-js="mathEditor"]').hide()
    const text = $(answerElement)[0].innerText
    $(answerElement).find('[data-js="mathEditor"]').show()

    const html = sanitizeHtml($(answerElement).html(), sanitizeOpts)

    return { answerHTML: html, answerText: text }
}

function isKey(e, key) { return preventIfTrue(e, !e.altKey && !e.shiftKey && !e.ctrlKey  && keyOrKeyCode(e, key))}

function isCtrlKey(e, key) { return preventIfTrue(e, !e.altKey && !e.shiftKey && e.ctrlKey && keyOrKeyCode(e, key))}

function keyOrKeyCode(e, val) { return typeof val === 'string' ? e.key === val : e.keyCode === val }
function preventIfTrue(e, val) {
    if(val) e.preventDefault()
    return val
}
module.exports = {
    makeRichText
}
<|endoftext|>