text: /* Magic Mirror
   * Node Helper: MMM-iCal-ToDo
   *
   * Adapted by nixnuex https://github.com/nixnuex/
   * based on Magic Mirror ToDo by Michael Teeuw http://michaelteeuw.nl
  
   * MIT Licensed.
   */
  
  Module.register("MMM-iCal-ToDo", {
  	// Define module defaults
  	defaults: {
  		header: "ToDo",
  		maximumEntries: 10, // Total Maximum Entries
  		displaySymbol: true,
  		defaultSymbol: "check", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
  		fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
  		animationSpeed: 2000,
  		todos: [
  			{
  				symbol: "check",
  				url: "http://www.todolabs.com/templates/ical/US-Holidays.ics"
  			}
  		],
  
  		// legacy setting from todo, not cleaned up yet:
  		fade: false,
  		maximumNumberOfDays: 365,
  		showLocation: false,
  		displayRepeatingCountTitle: false,
  		defaultRepeatingCountTitle: "",
  		maxTitleLength: 25,
  		wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
  		maxTitleLines: 3,
  		urgency: 7,
  		timeFormat: "relative",
  		dateFormat: "MMM Do",
  		dateEndFormat: "LT",
  		fullDayEventDateFormat: "MMM Do",
  		showEnd: false,
  		getRelative: 6,
  		fadePoint: 0.25, // Start on 1/4th of the list.
  		hidePrivate: false,
  		hideOngoing: false,
  		colored: false,
  		coloredSymbolOnly: false,
  		tableClass: "small",
  		titleReplace: {
  			"De verjaardag van ": "",
  			"'s birthday": ""
  		},
  		broadcastEvents: true,
  		excludedEvents: [],
  		sliceMultiDayEvents: false,
  		broadcastPastEvents: false,
  		nextDaysRelative: false
  	},
  
  	// Define the header scripts.
  	getHeader: function () {
  		return this.config.header;
  	},
  
  	// Define required scripts.
  	getStyles: function () {
  		return ["MMM-iCal-ToDo.css", "font-awesome.css"];
  	},
  
  	// Define required scripts.
  	getScripts: function () {
  		return ["moment.js"];
  	},
  
  	// Define required translations.
  	getTranslations: function () {
  		// The translations for the default modules are defined in the core translation files.
  		// Therefor we can just return false. Otherwise we should have returned a dictionary.
  		// If you're trying to build your own module including translations, check out the documentation.
  		return false;
  	},
  
  	// Override start method.
  	start: function () {
  		Log.log("Starting module: " + this.name);
  
  		// Set locale.
  		moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
  
  		for (var c in this.config.todos) {
  			var todo = this.config.todos[c];
  			todo.url = todo.url.replace("webcal://", "http://");
  
  			var todoConfig = {
  				maximumEntries: todo.maximumEntries,
  				maximumNumberOfDays: todo.maximumNumberOfDays,
  				broadcastPastEvents: todo.broadcastPastEvents
  			};
  			if (todo.symbolClass === "undefined" || todo.symbolClass === null) {
  				todoConfig.symbolClass = "";
  			}
  			if (todo.titleClass === "undefined" || todo.titleClass === null) {
  				todoConfig.titleClass = "";
  			}
  			if (todo.timeClass === "undefined" || todo.timeClass === null) {
  				todoConfig.timeClass = "";
  			}
  
  			// we check user and password here for backwards compatibility with old configs
  			if (todo.user && todo.pass) {
  				Log.warn("Deprecation warning: Please update your todo authentication configuration.");
  				Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/todo#todo-authentication-options");
  				todo.auth = {
  					user: todo.user,
  					pass: todo.pass
  				};
  			}
  
  			this.addToDo(todo.url, todo.auth, todoConfig);
  
  			// Trigger ADD_TODO every fetchInterval to make sure there is always a todo
  			// fetcher running on the server side.
  			var self = this;
  			setInterval(function () {
  				self.addToDo(todo.url, todo.auth, todoConfig);
  			}, self.config.fetchInterval);
  		}
  
  		this.todoData = {};
  		this.loaded = false;
  	},
  
  	// Override socket notification handler.
  	socketNotificationReceived: function (notification, payload) {
  		if (notification === "TODO_EVENTS") {
  			if (this.hasToDoURL(payload.url)) {
  				this.todoData[payload.url] = payload.events;
  				this.loaded = true;
  
  				if (this.config.broadcastEvents) {
  					this.broadcastEvents();
  				}
  			}
  		} else if (notification === "FETCH_ERROR") {
  			Log.error("ToDo Error. Could not fetch todo: " + payload.url);
  			this.loaded = true;
  		} else if (notification === "INCORRECT_URL") {
  			Log.error("ToDo Error. Incorrect url: " + payload.url);
  		} else {
  			Log.log("ToDo received an unknown socket notification: " + notification);
  		}
  
  		this.updateDom(this.config.animationSpeed);
  	},
  
  	// Override dom generator.
  	getDom: function () {
  		var events = this.createEventList();
  		var wrapper = document.createElement("table");
  		wrapper.className = this.config.tableClass;
  
  		if (events.length === 0) {
  			wrapper.innerHTML = this.loaded ? "---" : this.translate("LOADING");
  			wrapper.className = this.config.tableClass + " dimmed";
  			return wrapper;
  		}
  
  		if (this.config.fade && this.config.fadePoint < 1) {
  			if (this.config.fadePoint < 0) {
  				this.config.fadePoint = 0;
  			}
  			var startFade = events.length * this.config.fadePoint;
  			var fadeSteps = events.length - startFade;
  		}
  
  		var currentFadeStep = 0;
  		var lastSeenDate = "";
  
  		for (var e in events) {
  			var event = events[e];
  			var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
  			if (this.config.timeFormat === "dateheaders") {
  				if (lastSeenDate !== dateAsString) {
  					var dateRow = document.createElement("tr");
  					dateRow.className = "normal";
  					var dateCell = document.createElement("td");
  
  					dateCell.colSpan = "3";
  					dateCell.innerHTML = dateAsString;
  					dateRow.appendChild(dateCell);
  					wrapper.appendChild(dateRow);
  
  					if (e >= startFade) {
  						//fading
  						currentFadeStep = e - startFade;
  						dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
  					}
  
  					lastSeenDate = dateAsString;
  				}
  			}
  
  			var eventWrapper = document.createElement("tr");
  
  			if (this.config.colored && !this.config.coloredSymbolOnly) {
  				eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
  			}
  
  			eventWrapper.className = "normal";
  
  			if (this.config.displaySymbol) {
  				var symbolWrapper = document.createElement("td");
  
  				if (this.config.colored && this.config.coloredSymbolOnly) {
  					symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
  				}
  
  				var symbolClass = this.symbolClassForUrl(event.url);
  				symbolWrapper.className = "symbol align-right " + symbolClass;
  
  				var symbols = this.symbolsForUrl(event.url);
  				if (typeof symbols === "string") {
  					symbols = [symbols];
  				}
  
  				for (var i = 0; i < symbols.length; i++) {
  					var symbol = document.createElement("span");
  					symbol.className = "fa fa-fw fa-" + symbols[i];
  					if (i > 0) {
  						symbol.style.paddingLeft = "5px";
  					}
  					symbolWrapper.appendChild(symbol);
  				}
  				eventWrapper.appendChild(symbolWrapper);
  			} else if (this.config.timeFormat === "dateheaders") {
  				var blankCell = document.createElement("td");
  				blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;";
  				eventWrapper.appendChild(blankCell);
  			}
  
  			var titleWrapper = document.createElement("td"),
  				repeatingCountTitle = "";
  
  			if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
  				repeatingCountTitle = this.countTitleForUrl(event.url);
  
  				if (repeatingCountTitle !== "") {
  					var thisYear = new Date(parseInt(event.startDate)).getFullYear(),
  						yearDiff = thisYear - event.firstYear;
  
  					repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
  				}
  			}
  
  			titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
  
  			var titleClass = this.titleClassForUrl(event.url);
  
  			if (!this.config.colored) {
  				titleWrapper.className = "title bright " + titleClass;
  			} else {
  				titleWrapper.className = "title " + titleClass;
  			}
  
  			if (this.config.timeFormat === "dateheaders") {
  				if (event.fullDayEvent) {
  					titleWrapper.colSpan = "2";
  					titleWrapper.align = "left";
  				} else {
  					var timeClass = this.timeClassForUrl(event.url);
  					var timeWrapper = document.createElement("td");
  					timeWrapper.className = "time light " + timeClass;
  					timeWrapper.align = "left";
  					timeWrapper.style.paddingLeft = "2px";
  					timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
  					eventWrapper.appendChild(timeWrapper);
  					titleWrapper.align = "right";
  				}
  
  				eventWrapper.appendChild(titleWrapper);
  			} else {
  				var timeWrapper = document.createElement("td");
  
  				eventWrapper.appendChild(titleWrapper);
  				//console.log(event.today);
  				var now = new Date();
  				// Define second, minute, hour, and day variables
  				var oneSecond = 1000; // 1,000 milliseconds
  				var oneMinute = oneSecond * 60;
  				var oneHour = oneMinute * 60;
  				var oneDay = oneHour * 24;
  				if (event.fullDayEvent) {
  					//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
  					event.endDate -= oneSecond;
  					if (event.today) {
  						timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
  					} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
  						timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
  					} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
  						if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
  							timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
  						} else {
  							timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
  						}
  					} else {
  						/* Check to see if the user displays absolute or relative dates with their events
  						 * Also check to see if an event is happening within an 'urgency' time frameElement
  						 * For example, if the user set an .urgency of 7 days, those events that fall within that
  						 * time frame will be displayed with 'in xxx' time format or moment.fromNow()
  						 *
  						 * Note: this needs to be put in its own function, as the whole thing repeats again verbatim
  						 */
  						if (this.config.timeFormat === "absolute") {
  							if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
  								// This event falls within the config.urgency period that the user has set
  								timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
  							} else {
  								timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
  							}
  						} else {
  							timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
  						}
  					}
  					if (this.config.showEnd) {
  						timeWrapper.innerHTML += "-";
  						timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat));
  					}
  				} else {
  					if (event.startDate >= new Date()) {
  						if (event.startDate - now < 2 * oneDay) {
  							// This event is within the next 48 hours (2 days)
  							if (event.startDate - now < this.config.getRelative * oneHour) {
  								// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
  								timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
  							} else {
  								if (this.config.timeFormat === "absolute" && !this.config.nextDaysRelative) {
  									timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
  								} else {
  									// Otherwise just say 'Today/Tomorrow at such-n-such time'
  									timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").todo());
  								}
  							}
  						} else {
  							/* Check to see if the user displays absolute or relative dates with their events
  							 * Also check to see if an event is happening within an 'urgency' time frameElement
  							 * For example, if the user set an .urgency of 7 days, those events that fall within that
  							 * time frame will be displayed with 'in xxx' time format or moment.fromNow()
  							 *
  							 * Note: this needs to be put in its own function, as the whole thing repeats again verbatim
  							 */
  							if (this.config.timeFormat === "absolute") {
  								if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
  									// This event falls within the config.urgency period that the user has set
  									timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
  								} else {
  									timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
  								}
  							} else {
  								timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
  							}
  						}
  					} else {
  						timeWrapper.innerHTML = this.capFirst(
  							this.translate("RUNNING", {
  								fallback: this.translate("RUNNING") + " {timeUntilEnd}",
  								timeUntilEnd: moment(event.endDate, "x").fromNow(true)
  							})
  						);
  					}
  					if (this.config.showEnd) {
  						timeWrapper.innerHTML += "-";
  						timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
  					}
  				}
  				//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
  				//console.log(event);
  				var timeClass = this.timeClassForUrl(event.url);
  				timeWrapper.className = "time light " + timeClass;
  				//eventWrapper.appendChild(timeWrapper);
  			}
  
  			wrapper.appendChild(eventWrapper);
  
  			// Create fade effect.
  			if (e >= startFade) {
  				currentFadeStep = e - startFade;
  				eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
  			}
  
  			if (this.config.showLocation) {
  				if (event.location !== false) {
  					var locationRow = document.createElement("tr");
  					locationRow.className = "normal xsmall light";
  
  					if (this.config.displaySymbol) {
  						var symbolCell = document.createElement("td");
  						locationRow.appendChild(symbolCell);
  					}
  
  					var descCell = document.createElement("td");
  					descCell.className = "location";
  					descCell.colSpan = "2";
  					descCell.innerHTML = event.location;
  					locationRow.appendChild(descCell);
  
  					wrapper.appendChild(locationRow);
  
  					if (e >= startFade) {
  						currentFadeStep = e - startFade;
  						locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
  					}
  				}
  			}
  		}
  
  		return wrapper;
  	},
  
  	/**
  	 * This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
  	 * corresponding timeformat to be used in the todo display. If no number is given (or otherwise invalid input)
  	 * it will a localeSpecification object with the system locale time format.
  	 *
  	 * @param {number} timeFormat Specifies either 12 or 24 hour time format
  	 * @returns {moment.LocaleSpecification}
  	 */
  	getLocaleSpecification: function (timeFormat) {
  		switch (timeFormat) {
  			case 12: {
  				return { longDateFormat: { LT: "h:mm A" } };
  				break;
  			}
  			case 24: {
  				return { longDateFormat: { LT: "HH:mm" } };
  				break;
  			}
  			default: {
  				return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
  				break;
  			}
  		}
  	},
  
  	/* hasToDoURL(url)
  	 * Check if this config contains the todo url.
  	 *
  	 * argument url string - Url to look for.
  	 *
  	 * return bool - Has todo url
  	 */
  	hasToDoURL: function (url) {
  		for (var c in this.config.todos) {
  			var todo = this.config.todos[c];
  			if (todo.url === url) {
  				return true;
  			}
  		}
  
  		return false;
  	},
  
  	/* createEventList()
  	 * Creates the sorted list of all events.
  	 *
  	 * return array - Array with events.
  	 */
  	createEventList: function () {
  		var events = [];
  		var today = moment().startOf("day");
  		var now = new Date();
  		var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
  		for (var c in this.todoData) {
  			var todo = this.todoData[c];
  			for (var e in todo) {
  				var event = JSON.parse(JSON.stringify(todo[e])); // clone object
  				if (event.endDate < now) {
  					continue;
  				}
  				if (this.config.hidePrivate) {
  					if (event.class === "PRIVATE") {
  						// do not add the current event, skip it
  						continue;
  					}
  				}
  				if (this.config.hideOngoing) {
  					if (event.startDate < now) {
  						continue;
  					}
  				}
  				if (this.listContainsEvent(events, event)) {
  					continue;
  				}
  				event.url = c;
  				event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
  
  				/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
  				 * otherwise, esp. in dateheaders mode it is not clear how long these events are.
  				 */
  				var maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
  				if (this.config.sliceMultiDayEvents && maxCount > 1) {
  					var splitEvents = [];
  					var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
  					var count = 1;
  					while (event.endDate > midnight) {
  						var thisEvent = JSON.parse(JSON.stringify(event)); // clone object
  						thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
  						thisEvent.endDate = midnight;
  						thisEvent.title += " (" + count + "/" + maxCount + ")";
  						splitEvents.push(thisEvent);
  
  						event.startDate = midnight;
  						count += 1;
  						midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
  					}
  					// Last day
  					event.title += " (" + count + "/" + maxCount + ")";
  					splitEvents.push(event);
  
  					for (event of splitEvents) {
  						if (event.endDate > now && event.endDate <= future) {
  							events.push(event);
  						}
  					}
  				} else {
  					events.push(event);
  				}
  			}
  		}
  
  		events.sort(function (a, b) {
  			return a.startDate - b.startDate;
  		});
  		return events.slice(0, this.config.maximumEntries);
  	},
  
  	listContainsEvent: function (eventList, event) {
  		for (var evt of eventList) {
  			if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
  				return true;
  			}
  		}
  		return false;
  	},
  
  	/* createEventList(url)
  	 * Requests node helper to add todo url.
  	 *
  	 * argument url string - Url to add.
  	 */
  	addToDo: function (url, auth, todoConfig) {
  		this.sendSocketNotification("ADD_TODO", {
  			url: url,
  			excludedEvents: todoConfig.excludedEvents || this.config.excludedEvents,
  			maximumEntries: todoConfig.maximumEntries || this.config.maximumEntries,
  			maximumNumberOfDays: todoConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
  			fetchInterval: this.config.fetchInterval,
  			symbolClass: todoConfig.symbolClass,
  			titleClass: todoConfig.titleClass,
  			timeClass: todoConfig.timeClass,
  			auth: auth,
  			broadcastPastEvents: todoConfig.broadcastPastEvents || this.config.broadcastPastEvents
  		});
  	},
  
  	/**
  	 * symbolsForUrl(url)
  	 * Retrieves the symbols for a specific url.
  	 *
  	 * argument url string - Url to look for.
  	 *
  	 * return string/array - The Symbols
  	 */
  	symbolsForUrl: function (url) {
  		let symbols = this.getToDoPropertyAsArray(url, "symbol", this.config.defaultSymbol);
  
  		return symbols;
  	},
  
  	/**
  	 * Helper method to retrieve the property for a specific todo url.
  	 *
  	 * @param {string} url The todo url
  	 * @param {string} property The property to look for
  	 * @param {string} defaultValue The value if the property is not found
  	 * @returns {*} The property
  	 */
  	getToDoProperty: function (url, property, defaultValue) {
  		for (var c in this.config.todos) {
  			var todo = this.config.todos[c];
  			if (todo.url === url && todo.hasOwnProperty(property)) {
  				return todo[property];
  			}
  		}
  
  		return defaultValue;
  	},
  
  	getToDoPropertyAsArray: function (url, property, defaultValue) {
  		let p = this.getToDoProperty(url, property, defaultValue);
  		if (!(p instanceof Array)) p = [p];
  		return p;
  	},
  
  	hasToDoProperty: function (url, property) {
  		return !!this.getToDoProperty(url, property, undefined);
  	},
  
  	/**
  	 * symbolClassForUrl(url)
  	 * Retrieves the symbolClass for a specific url.
  	 *
  	 * @param url string - Url to look for.
  	 *
  	 * @returns string
  	 */
  	symbolClassForUrl: function (url) {
  		return this.getToDoProperty(url, "symbolClass", "");
  	},
  
  	/**
  	 * titleClassForUrl(url)
  	 * Retrieves the titleClass for a specific url.
  	 *
  	 * @param url string - Url to look for.
  	 *
  	 * @returns string
  	 */
  	titleClassForUrl: function (url) {
  		return this.getToDoProperty(url, "titleClass", "");
  	},
  
  	/**
  	 * timeClassForUrl(url)
  	 * Retrieves the timeClass for a specific url.
  	 *
  	 * @param url string - Url to look for.
  	 *
  	 * @returns string
  	 */
  	timeClassForUrl: function (url) {
  		return this.getToDoProperty(url, "timeClass", "");
  	},
  
  	/* todoNameForUrl(url)
  	 * Retrieves the todo name for a specific url.
  	 *
  	 * argument url string - Url to look for.
  	 *
  	 * return string - The name of the todo
  	 */
  	todoNameForUrl: function (url) {
  		return this.getToDoProperty(url, "name", "");
  	},
  
  	/* colorForUrl(url)
  	 * Retrieves the color for a specific url.
  	 *
  	 * argument url string - Url to look for.
  	 *
  	 * return string - The Color
  	 */
  	colorForUrl: function (url) {
  		return this.getToDoProperty(url, "color", "#fff");
  	},
  
  	/* countTitleForUrl(url)
  	 * Retrieves the name for a specific url.
  	 *
  	 * argument url string - Url to look for.
  	 *
  	 * return string - The Symbol
  	 */
  	countTitleForUrl: function (url) {
  		return this.getToDoProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
  	},
  
  	/* getToDoProperty(url, property, defaultValue)
  	 * Helper method to retrieve the property for a specific url.
  	 *
  	 * argument url string - Url to look for.
  	 * argument property string - Property to look for.
  	 * argument defaultValue string - Value if property is not found.
  	 *
  	 * return string - The Property
  	 */
  	getToDoProperty: function (url, property, defaultValue) {
  		for (var c in this.config.todos) {
  			var todo = this.config.todos[c];
  			if (todo.url === url && todo.hasOwnProperty(property)) {
  				return todo[property];
  			}
  		}
  
  		return defaultValue;
  	},
  
  	/**
  	 * Shortens a string if it's longer than maxLength and add a ellipsis to the end
  	 *
  	 * @param {string} string Text string to shorten
  	 * @param {number} maxLength The max length of the string
  	 * @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
  	 * @param {number} maxTitleLines The max number of vertical lines before cutting event title
  	 * @returns {string} The shortened string
  	 */
  	shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
  		if (typeof string !== "string") {
  			return "";
  		}
  
  		if (wrapEvents === true) {
  			var temp = "";
  			var currentLine = "";
  			var words = string.split(" ");
  			var line = 0;
  
  			for (var i = 0; i < words.length; i++) {
  				var word = words[i];
  				if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
  					// max - 1 to account for a space
  					currentLine += word + " ";
  				} else {
  					line++;
  					if (line > maxTitleLines - 1) {
  						if (i < words.length) {
  							currentLine += "&hellip;";
  						}
  						break;
  					}
  
  					if (currentLine.length > 0) {
  						temp += currentLine + "<br>" + word + " ";
  					} else {
  						temp += word + "<br>";
  					}
  					currentLine = "";
  				}
  			}
  
  			return (temp + currentLine).trim();
  		} else {
  			if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
  				return string.trim().slice(0, maxLength) + "&hellip;";
  			} else {
  				return string.trim();
  			}
  		}
  	},
  
  	/* capFirst(string)
  	 * Capitalize the first letter of a string
  	 * Return capitalized string
  	 */
  	capFirst: function (string) {
  		return string.charAt(0).toUpperCase() + string.slice(1);
  	},
  
  	/* titleTransform(title)
  	 * Transforms the title of an event for usage.
  	 * Replaces parts of the text as defined in config.titleReplace.
  	 * Shortens title based on config.maxTitleLength and config.wrapEvents
  	 *
  	 * argument title string - The title to transform.
  	 *
  	 * return string - The transformed title.
  	 */
  	titleTransform: function (title) {
  		for (var needle in this.config.titleReplace) {
  			var replacement = this.config.titleReplace[needle];
  
  			var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
  			if (regParts) {
  				// the parsed pattern is a regexp.
  				needle = new RegExp(regParts[1], regParts[2]);
  			}
  
  			title = title.replace(needle, replacement);
  		}
  
  		title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines);
  		return title;
  	},
  
  	/* broadcastEvents()
  	 * Broadcasts the events to all other modules for reuse.
  	 * The all events available in one array, sorted on startdate.
  	 */
  	broadcastEvents: function () {
  		var eventList = [];
  		for (var url in this.todoData) {
  			var todo = this.todoData[url];
  			for (var e in todo) {
  				var event = cloneObject(todo[e]);
  				event.symbol = this.symbolsForUrl(url);
  				event.todoName = this.todoNameForUrl(url);
  				event.color = this.colorForUrl(url);
  				delete event.url;
  				eventList.push(event);
  			}
  		}
  
  		eventList.sort(function (a, b) {
  			return a.startDate - b.startDate;
  		});
  
  		this.sendNotification("TODO_EVENTS", eventList);
  	}
  });
  