/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch */
import TimezoneService from "./timezone_service.js";
// needed for typescript type resolution
// eslint-disable-next-line no-unused-vars
import Component from "./component.js";
import ICALmodule from "./module.js";
/**
* Helper functions used in various places within ical.js
* @module ICAL.helpers
*/
/**
* Compiles a list of all referenced TZIDs in all subcomponents and
* removes any extra VTIMEZONE subcomponents. In addition, if any TZIDs
* are referenced by a component, but a VTIMEZONE does not exist,
* an attempt will be made to generate a VTIMEZONE using ICAL.TimezoneService.
*
* @param {Component} vcal The top-level VCALENDAR component.
* @return {Component} The ICAL.Component that was passed in.
*/
export function updateTimezones(vcal) {
let allsubs, properties, vtimezones, reqTzid, i;
if (!vcal || vcal.name !== "vcalendar") {
//not a top-level vcalendar component
return vcal;
}
//Store vtimezone subcomponents in an object reference by tzid.
//Store properties from everything else in another array
allsubs = vcal.getAllSubcomponents();
properties = [];
vtimezones = {};
for (i = 0; i < allsubs.length; i++) {
if (allsubs[i].name === "vtimezone") {
let tzid = allsubs[i].getFirstProperty("tzid").getFirstValue();
vtimezones[tzid] = allsubs[i];
} else {
properties = properties.concat(allsubs[i].getAllProperties());
}
}
//create an object with one entry for each required tz
reqTzid = {};
for (i = 0; i < properties.length; i++) {
let tzid = properties[i].getParameter("tzid");
if (tzid) {
reqTzid[tzid] = true;
}
}
//delete any vtimezones that are not on the reqTzid list.
for (let [tzid, comp] of Object.entries(vtimezones)) {
if (!reqTzid[tzid]) {
vcal.removeSubcomponent(comp);
}
}
//create any missing, but registered timezones
for (let tzid of Object.keys(reqTzid)) {
if (!vtimezones[tzid] && TimezoneService.has(tzid)) {
vcal.addSubcomponent(TimezoneService.get(tzid).component);
}
}
return vcal;
}
/**
* Checks if the given type is of the number type and also NaN.
*
* @param {Number} number The number to check
* @return {Boolean} True, if the number is strictly NaN
*/
export function isStrictlyNaN(number) {
return typeof(number) === 'number' && isNaN(number);
}
/**
* Parses a string value that is expected to be an integer, when the valid is
* not an integer throws a decoration error.
*
* @param {String} string Raw string input
* @return {Number} Parsed integer
*/
export function strictParseInt(string) {
let result = parseInt(string, 10);
if (isStrictlyNaN(result)) {
throw new Error(
'Could not extract integer from "' + string + '"'
);
}
return result;
}
/**
* Creates or returns a class instance of a given type with the initialization
* data if the data is not already an instance of the given type.
*
* @example
* var time = new ICAL.Time(...);
* var result = ICAL.helpers.formatClassType(time, ICAL.Time);
*
* (result instanceof ICAL.Time)
* // => true
*
* result = ICAL.helpers.formatClassType({}, ICAL.Time);
* (result isntanceof ICAL.Time)
* // => true
*
*
* @param {Object} data object initialization data
* @param {Object} type object type (like ICAL.Time)
* @return {?} An instance of the found type.
*/
export function formatClassType(data, type) {
if (typeof(data) === 'undefined') {
return undefined;
}
if (data instanceof type) {
return data;
}
return new type(data);
}
/**
* Identical to indexOf but will only match values when they are not preceded
* by a backslash character.
*
* @param {String} buffer String to search
* @param {String} search Value to look for
* @param {Number} pos Start position
* @return {Number} The position, or -1 if not found
*/
export function unescapedIndexOf(buffer, search, pos) {
while ((pos = buffer.indexOf(search, pos)) !== -1) {
if (pos > 0 && buffer[pos - 1] === '\\') {
pos += 1;
} else {
return pos;
}
}
return -1;
}
/**
* Find the index for insertion using binary search.
*
* @param {Array} list The list to search
* @param {?} seekVal The value to insert
* @param {function(?,?)} cmpfunc The comparison func, that can
* compare two seekVals
* @return {Number} The insert position
*/
export function binsearchInsert(list, seekVal, cmpfunc) {
if (!list.length)
return 0;
let low = 0, high = list.length - 1,
mid, cmpval;
while (low <= high) {
mid = low + Math.floor((high - low) / 2);
cmpval = cmpfunc(seekVal, list[mid]);
if (cmpval < 0)
high = mid - 1;
else if (cmpval > 0)
low = mid + 1;
else
break;
}
if (cmpval < 0)
return mid; // insertion is displacing, so use mid outright.
else if (cmpval > 0)
return mid + 1;
else
return mid;
}
/**
* Clone the passed object or primitive. By default a shallow clone will be
* executed.
*
* @param {*} aSrc The thing to clone
* @param {Boolean=} aDeep If true, a deep clone will be performed
* @return {*} The copy of the thing
*/
export function clone(aSrc, aDeep) {
if (!aSrc || typeof aSrc != "object") {
return aSrc;
} else if (aSrc instanceof Date) {
return new Date(aSrc.getTime());
} else if ("clone" in aSrc) {
return aSrc.clone();
} else if (Array.isArray(aSrc)) {
let arr = [];
for (let i = 0; i < aSrc.length; i++) {
arr.push(aDeep ? clone(aSrc[i], true) : aSrc[i]);
}
return arr;
} else {
let obj = {};
for (let [name, value] of Object.entries(aSrc)) {
if (aDeep) {
obj[name] = clone(value, true);
} else {
obj[name] = value;
}
}
return obj;
}
}
/**
* Performs iCalendar line folding. A line ending character is inserted and
* the next line begins with a whitespace.
*
* @example
* SUMMARY:This line will be fold
* ed right in the middle of a word.
*
* @param {String} aLine The line to fold
* @return {String} The folded line
*/
export function foldline(aLine) {
let result = "";
let line = aLine || "", pos = 0, line_length = 0;
//pos counts position in line for the UTF-16 presentation
//line_length counts the bytes for the UTF-8 presentation
while (line.length) {
let cp = line.codePointAt(pos);
if (cp < 128) ++line_length;
else if (cp < 2048) line_length += 2;//needs 2 UTF-8 bytes
else if (cp < 65536) line_length += 3;
else line_length += 4; //cp is less than 1114112
if (line_length < ICALmodule.foldLength + 1)
pos += cp > 65535 ? 2 : 1;
else {
result += ICALmodule.newLineChar + " " + line.slice(0, Math.max(0, pos));
line = line.slice(Math.max(0, pos));
pos = line_length = 0;
}
}
return result.slice(ICALmodule.newLineChar.length + 1);
}
/**
* Pads the given string or number with zeros so it will have at least two
* characters.
*
* @param {String|Number} data The string or number to pad
* @return {String} The number padded as a string
*/
export function pad2(data) {
if (typeof(data) !== 'string') {
// handle fractions.
if (typeof(data) === 'number') {
data = parseInt(data);
}
data = String(data);
}
let len = data.length;
switch (len) {
case 0:
return '00';
case 1:
return '0' + data;
default:
return data;
}
}
/**
* Truncates the given number, correctly handling negative numbers.
*
* @param {Number} number The number to truncate
* @return {Number} The truncated number
*/
export function trunc(number) {
return (number < 0 ? Math.ceil(number) : Math.floor(number));
}
/**
* Poor-man's cross-browser object extension. Doesn't support all the
* features, but enough for our usage. Note that the target's properties are
* not overwritten with the source properties.
*
* @example
* var child = ICAL.helpers.extend(parent, {
* "bar": 123
* });
*
* @param {Object} source The object to extend
* @param {Object} target The object to extend with
* @return {Object} Returns the target.
*/
export function extend(source, target) {
for (let key in source) {
let descr = Object.getOwnPropertyDescriptor(source, key);
if (descr && !Object.getOwnPropertyDescriptor(target, key)) {
Object.defineProperty(target, key, descr);
}
}
return target;
}