Auto backup before evolver run - 2026年 03月 06日 星期五 19:00:05 CST
This commit is contained in:
+635
@@ -0,0 +1,635 @@
|
||||
/* 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 Property from "./property.js";
|
||||
import Timezone from "./timezone.js";
|
||||
import ICALParse from "./parse.js";
|
||||
import stringify from "./stringify.js";
|
||||
import design from "./design.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Duration from "./duration.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import UtcOffset from "./utc_offset.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Binary from "./binary.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Period from "./period.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Recur from "./recur.js";
|
||||
// needed for typescript type resolution
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Time from "./time.js";
|
||||
|
||||
/**
|
||||
* This lets typescript resolve our custom types in the
|
||||
* generated d.ts files (jsdoc typedefs are converted to typescript types).
|
||||
* Ignore prevents the typedefs from being documented more than once.
|
||||
* @ignore
|
||||
* @typedef {import("./types.js").designSet} designSet
|
||||
* Imports the 'designSet' type from the "types.js" module
|
||||
* @typedef {import("./types.js").Geo} Geo
|
||||
* Imports the 'Geo' type from the "types.js" module
|
||||
*/
|
||||
|
||||
const NAME_INDEX = 0;
|
||||
const PROPERTY_INDEX = 1;
|
||||
const COMPONENT_INDEX = 2;
|
||||
|
||||
const PROPERTY_NAME_INDEX = 0;
|
||||
const PROPERTY_VALUE_INDEX = 3;
|
||||
|
||||
/**
|
||||
* Wraps a jCal component, adding convenience methods to add, remove and update subcomponents and
|
||||
* properties.
|
||||
*
|
||||
* @memberof ICAL
|
||||
*/
|
||||
class Component {
|
||||
/**
|
||||
* Create an {@link ICAL.Component} by parsing the passed iCalendar string.
|
||||
*
|
||||
* @param {String} str The iCalendar string to parse
|
||||
*/
|
||||
static fromString(str) {
|
||||
return new Component(ICALParse.component(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Component instance.
|
||||
*
|
||||
* @param {Array|String} jCal Raw jCal component data OR name of new
|
||||
* component
|
||||
* @param {Component=} parent Parent component to associate
|
||||
*/
|
||||
constructor(jCal, parent) {
|
||||
if (typeof(jCal) === 'string') {
|
||||
// jCal spec (name, properties, components)
|
||||
jCal = [jCal, [], []];
|
||||
}
|
||||
|
||||
// mostly for legacy reasons.
|
||||
this.jCal = jCal;
|
||||
|
||||
this.parent = parent || null;
|
||||
|
||||
if (!this.parent && this.name === 'vcalendar') {
|
||||
this._timezoneCache = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrated properties are inserted into the _properties array at the same
|
||||
* position as in the jCal array, so it is possible that the array contains
|
||||
* undefined values for unhydrdated properties. To avoid iterating the
|
||||
* array when checking if all properties have been hydrated, we save the
|
||||
* count here.
|
||||
*
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
_hydratedPropertyCount = 0;
|
||||
|
||||
/**
|
||||
* The same count as for _hydratedPropertyCount, but for subcomponents
|
||||
*
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
_hydratedComponentCount = 0;
|
||||
|
||||
/**
|
||||
* A cache of hydrated time zone objects which may be used by consumers, keyed
|
||||
* by time zone ID.
|
||||
*
|
||||
* @type {Map}
|
||||
* @private
|
||||
*/
|
||||
_timezoneCache = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_components = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_properties = null;
|
||||
|
||||
/**
|
||||
* The name of this component
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
get name() {
|
||||
return this.jCal[NAME_INDEX];
|
||||
}
|
||||
|
||||
/**
|
||||
* The design set for this component, e.g. icalendar vs vcard
|
||||
*
|
||||
* @type {designSet}
|
||||
* @private
|
||||
*/
|
||||
get _designSet() {
|
||||
let parentDesign = this.parent && this.parent._designSet;
|
||||
if (!parentDesign && this.name == "vcard") {
|
||||
// We can't decide on vcard3 vs vcard4 just based on the component name, the version number is
|
||||
// in the version property. We also can't use hydrated properties here because it would lead
|
||||
// to recursion, but the spec says that the version property needs to be the very first one.
|
||||
let versionProp = this.jCal[PROPERTY_INDEX]?.[0];
|
||||
|
||||
if (versionProp && versionProp[PROPERTY_NAME_INDEX] == "version" && versionProp[PROPERTY_VALUE_INDEX] == "3.0") {
|
||||
return design.getDesignSet("vcard3");
|
||||
}
|
||||
}
|
||||
|
||||
return parentDesign || design.getDesignSet(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_hydrateComponent(index) {
|
||||
if (!this._components) {
|
||||
this._components = [];
|
||||
this._hydratedComponentCount = 0;
|
||||
}
|
||||
|
||||
if (this._components[index]) {
|
||||
return this._components[index];
|
||||
}
|
||||
|
||||
let comp = new Component(
|
||||
this.jCal[COMPONENT_INDEX][index],
|
||||
this
|
||||
);
|
||||
|
||||
this._hydratedComponentCount++;
|
||||
return (this._components[index] = comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_hydrateProperty(index) {
|
||||
if (!this._properties) {
|
||||
this._properties = [];
|
||||
this._hydratedPropertyCount = 0;
|
||||
}
|
||||
|
||||
if (this._properties[index]) {
|
||||
return this._properties[index];
|
||||
}
|
||||
|
||||
let prop = new Property(
|
||||
this.jCal[PROPERTY_INDEX][index],
|
||||
this
|
||||
);
|
||||
|
||||
this._hydratedPropertyCount++;
|
||||
return (this._properties[index] = prop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds first sub component, optionally filtered by name.
|
||||
*
|
||||
* @param {String=} name Optional name to filter by
|
||||
* @return {?Component} The found subcomponent
|
||||
*/
|
||||
getFirstSubcomponent(name) {
|
||||
if (name) {
|
||||
let i = 0;
|
||||
let comps = this.jCal[COMPONENT_INDEX];
|
||||
let len = comps.length;
|
||||
|
||||
for (; i < len; i++) {
|
||||
if (comps[i][NAME_INDEX] === name) {
|
||||
let result = this._hydrateComponent(i);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.jCal[COMPONENT_INDEX].length) {
|
||||
return this._hydrateComponent(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ensure we return a value (strict mode)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all sub components, optionally filtering by name.
|
||||
*
|
||||
* @param {String=} name Optional name to filter by
|
||||
* @return {Component[]} The found sub components
|
||||
*/
|
||||
getAllSubcomponents(name) {
|
||||
let jCalLen = this.jCal[COMPONENT_INDEX].length;
|
||||
let i = 0;
|
||||
|
||||
if (name) {
|
||||
let comps = this.jCal[COMPONENT_INDEX];
|
||||
let result = [];
|
||||
|
||||
for (; i < jCalLen; i++) {
|
||||
if (name === comps[i][NAME_INDEX]) {
|
||||
result.push(
|
||||
this._hydrateComponent(i)
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
if (!this._components ||
|
||||
(this._hydratedComponentCount !== jCalLen)) {
|
||||
for (; i < jCalLen; i++) {
|
||||
this._hydrateComponent(i);
|
||||
}
|
||||
}
|
||||
|
||||
return this._components || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a named property exists.
|
||||
*
|
||||
* @param {String} name The property name
|
||||
* @return {Boolean} True, when property is found
|
||||
*/
|
||||
hasProperty(name) {
|
||||
let props = this.jCal[PROPERTY_INDEX];
|
||||
let len = props.length;
|
||||
|
||||
let i = 0;
|
||||
for (; i < len; i++) {
|
||||
// 0 is property name
|
||||
if (props[i][NAME_INDEX] === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first property, optionally with the given name.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {?Property} The found property
|
||||
*/
|
||||
getFirstProperty(name) {
|
||||
if (name) {
|
||||
let i = 0;
|
||||
let props = this.jCal[PROPERTY_INDEX];
|
||||
let len = props.length;
|
||||
|
||||
for (; i < len; i++) {
|
||||
if (props[i][NAME_INDEX] === name) {
|
||||
let result = this._hydrateProperty(i);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.jCal[PROPERTY_INDEX].length) {
|
||||
return this._hydrateProperty(0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first property's value, if available.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {Binary | Duration | Period |
|
||||
* Recur | Time | UtcOffset | Geo | string | null} The found property value.
|
||||
*/
|
||||
getFirstPropertyValue(name) {
|
||||
let prop = this.getFirstProperty(name);
|
||||
if (prop) {
|
||||
return prop.getFirstValue();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all properties in the component, optionally filtered by name.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {Property[]} List of properties
|
||||
*/
|
||||
getAllProperties(name) {
|
||||
let jCalLen = this.jCal[PROPERTY_INDEX].length;
|
||||
let i = 0;
|
||||
|
||||
if (name) {
|
||||
let props = this.jCal[PROPERTY_INDEX];
|
||||
let result = [];
|
||||
|
||||
for (; i < jCalLen; i++) {
|
||||
if (name === props[i][NAME_INDEX]) {
|
||||
result.push(
|
||||
this._hydrateProperty(i)
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
if (!this._properties ||
|
||||
(this._hydratedPropertyCount !== jCalLen)) {
|
||||
for (; i < jCalLen; i++) {
|
||||
this._hydrateProperty(i);
|
||||
}
|
||||
}
|
||||
|
||||
return this._properties || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeObjectByIndex(jCalIndex, cache, index) {
|
||||
cache = cache || [];
|
||||
// remove cached version
|
||||
if (cache[index]) {
|
||||
let obj = cache[index];
|
||||
if ("parent" in obj) {
|
||||
obj.parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
cache.splice(index, 1);
|
||||
|
||||
// remove it from the jCal
|
||||
this.jCal[jCalIndex].splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeObject(jCalIndex, cache, nameOrObject) {
|
||||
let i = 0;
|
||||
let objects = this.jCal[jCalIndex];
|
||||
let len = objects.length;
|
||||
let cached = this[cache];
|
||||
|
||||
if (typeof(nameOrObject) === 'string') {
|
||||
for (; i < len; i++) {
|
||||
if (objects[i][NAME_INDEX] === nameOrObject) {
|
||||
this._removeObjectByIndex(jCalIndex, cached, i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (cached) {
|
||||
for (; i < len; i++) {
|
||||
if (cached[i] && cached[i] === nameOrObject) {
|
||||
this._removeObjectByIndex(jCalIndex, cached, i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeAllObjects(jCalIndex, cache, name) {
|
||||
let cached = this[cache];
|
||||
|
||||
// Unfortunately we have to run through all children to reset their
|
||||
// parent property.
|
||||
let objects = this.jCal[jCalIndex];
|
||||
let i = objects.length - 1;
|
||||
|
||||
// descending search required because splice
|
||||
// is used and will effect the indices.
|
||||
for (; i >= 0; i--) {
|
||||
if (!name || objects[i][NAME_INDEX] === name) {
|
||||
this._removeObjectByIndex(jCalIndex, cached, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single sub component.
|
||||
*
|
||||
* @param {Component} component The component to add
|
||||
* @return {Component} The passed in component
|
||||
*/
|
||||
addSubcomponent(component) {
|
||||
if (!this._components) {
|
||||
this._components = [];
|
||||
this._hydratedComponentCount = 0;
|
||||
}
|
||||
|
||||
if (component.parent) {
|
||||
component.parent.removeSubcomponent(component);
|
||||
}
|
||||
|
||||
let idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
|
||||
this._components[idx - 1] = component;
|
||||
this._hydratedComponentCount++;
|
||||
component.parent = this;
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single component by name or the instance of a specific
|
||||
* component.
|
||||
*
|
||||
* @param {Component|String} nameOrComp Name of component, or component
|
||||
* @return {Boolean} True when comp is removed
|
||||
*/
|
||||
removeSubcomponent(nameOrComp) {
|
||||
let removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp);
|
||||
if (removed) {
|
||||
this._hydratedComponentCount--;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all components or (if given) all components by a particular
|
||||
* name.
|
||||
*
|
||||
* @param {String=} name Lowercase component name
|
||||
*/
|
||||
removeAllSubcomponents(name) {
|
||||
let removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name);
|
||||
this._hydratedComponentCount = 0;
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link ICAL.Property} to the component.
|
||||
*
|
||||
* @param {Property} property The property to add
|
||||
* @return {Property} The passed in property
|
||||
*/
|
||||
addProperty(property) {
|
||||
if (!(property instanceof Property)) {
|
||||
throw new TypeError('must be instance of ICAL.Property');
|
||||
}
|
||||
|
||||
if (!this._properties) {
|
||||
this._properties = [];
|
||||
this._hydratedPropertyCount = 0;
|
||||
}
|
||||
|
||||
if (property.parent) {
|
||||
property.parent.removeProperty(property);
|
||||
}
|
||||
|
||||
let idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
|
||||
this._properties[idx - 1] = property;
|
||||
this._hydratedPropertyCount++;
|
||||
property.parent = this;
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to add a property with a value to the component.
|
||||
*
|
||||
* @param {String} name Property name to add
|
||||
* @param {String|Number|Object} value Property value
|
||||
* @return {Property} The created property
|
||||
*/
|
||||
addPropertyWithValue(name, value) {
|
||||
let prop = new Property(name);
|
||||
prop.setValue(value);
|
||||
|
||||
this.addProperty(prop);
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will update or create a property of the given name
|
||||
* and sets its value. If multiple properties with the given name exist,
|
||||
* only the first is updated.
|
||||
*
|
||||
* @param {String} name Property name to update
|
||||
* @param {String|Number|Object} value Property value
|
||||
* @return {Property} The created property
|
||||
*/
|
||||
updatePropertyWithValue(name, value) {
|
||||
let prop = this.getFirstProperty(name);
|
||||
|
||||
if (prop) {
|
||||
prop.setValue(value);
|
||||
} else {
|
||||
prop = this.addPropertyWithValue(name, value);
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single property by name or the instance of the specific
|
||||
* property.
|
||||
*
|
||||
* @param {String|Property} nameOrProp Property name or instance to remove
|
||||
* @return {Boolean} True, when deleted
|
||||
*/
|
||||
removeProperty(nameOrProp) {
|
||||
let removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp);
|
||||
if (removed) {
|
||||
this._hydratedPropertyCount--;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all properties associated with this component, optionally
|
||||
* filtered by name.
|
||||
*
|
||||
* @param {String=} name Lowercase property name
|
||||
* @return {Boolean} True, when deleted
|
||||
*/
|
||||
removeAllProperties(name) {
|
||||
let removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name);
|
||||
this._hydratedPropertyCount = 0;
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Object representation of this component. The returned object
|
||||
* is a live jCal object and should be cloned if modified.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return this.jCal;
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of this component.
|
||||
* @return {String}
|
||||
*/
|
||||
toString() {
|
||||
return stringify.component(
|
||||
this.jCal, this._designSet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a time zone definition from the component tree, if any is present.
|
||||
* If the tree contains no time zone definitions or the TZID cannot be
|
||||
* matched, returns null.
|
||||
*
|
||||
* @param {String} tzid The ID of the time zone to retrieve
|
||||
* @return {Timezone} The time zone corresponding to the ID, or null
|
||||
*/
|
||||
getTimeZoneByID(tzid) {
|
||||
// VTIMEZONE components can only appear as a child of the VCALENDAR
|
||||
// component; walk the tree if we're not the root.
|
||||
if (this.parent) {
|
||||
return this.parent.getTimeZoneByID(tzid);
|
||||
}
|
||||
|
||||
// If there is no time zone cache, we are probably parsing an incomplete
|
||||
// file and will have no time zone definitions.
|
||||
if (!this._timezoneCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._timezoneCache.has(tzid)) {
|
||||
return this._timezoneCache.get(tzid);
|
||||
}
|
||||
|
||||
// If the time zone is not already cached, hydrate it from the
|
||||
// subcomponents.
|
||||
const zones = this.getAllSubcomponents('vtimezone');
|
||||
for (const zone of zones) {
|
||||
if (zone.getFirstProperty('tzid').getFirstValue() === tzid) {
|
||||
const hydratedZone = new Timezone({
|
||||
component: zone,
|
||||
tzid: tzid,
|
||||
});
|
||||
|
||||
this._timezoneCache.set(tzid, hydratedZone);
|
||||
|
||||
return hydratedZone;
|
||||
}
|
||||
}
|
||||
|
||||
// Per the standard, we should always have a time zone defined in a file
|
||||
// for any referenced TZID, but don't blow up if the file is invalid.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export default Component;
|
||||
Reference in New Issue
Block a user