318 lines
8.7 KiB
JavaScript
318 lines
8.7 KiB
JavaScript
/* 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;
|
|
}
|