303 lines
8.8 KiB
JavaScript
303 lines
8.8 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 design from "./design.js";
|
|
import { foldline } from "./helpers.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
|
|
*/
|
|
|
|
const LINE_ENDING = '\r\n';
|
|
const DEFAULT_VALUE_TYPE = 'unknown';
|
|
const RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" };
|
|
|
|
/**
|
|
* Convert a full jCal/jCard array into a iCalendar/vCard string.
|
|
*
|
|
* @function ICAL.stringify
|
|
* @variation function
|
|
* @param {Array} jCal The jCal/jCard document
|
|
* @return {String} The stringified iCalendar/vCard document
|
|
*/
|
|
export default function stringify(jCal) {
|
|
if (typeof jCal[0] == "string") {
|
|
// This is a single component
|
|
jCal = [jCal];
|
|
}
|
|
|
|
let i = 0;
|
|
let len = jCal.length;
|
|
let result = '';
|
|
|
|
for (; i < len; i++) {
|
|
result += stringify.component(jCal[i]) + LINE_ENDING;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Converts an jCal component array into a ICAL string.
|
|
* Recursive will resolve sub-components.
|
|
*
|
|
* Exact component/property order is not saved all
|
|
* properties will come before subcomponents.
|
|
*
|
|
* @function ICAL.stringify.component
|
|
* @param {Array} component
|
|
* jCal/jCard fragment of a component
|
|
* @param {designSet} designSet
|
|
* The design data to use for this component
|
|
* @return {String} The iCalendar/vCard string
|
|
*/
|
|
stringify.component = function(component, designSet) {
|
|
let name = component[0].toUpperCase();
|
|
let result = 'BEGIN:' + name + LINE_ENDING;
|
|
|
|
let props = component[1];
|
|
let propIdx = 0;
|
|
let propLen = props.length;
|
|
|
|
let designSetName = component[0];
|
|
// rfc6350 requires that in vCard 4.0 the first component is the VERSION
|
|
// component with as value 4.0, note that 3.0 does not have this requirement.
|
|
if (designSetName === 'vcard' && component[1].length > 0 &&
|
|
!(component[1][0][0] === "version" && component[1][0][3] === "4.0")) {
|
|
designSetName = "vcard3";
|
|
}
|
|
designSet = designSet || design.getDesignSet(designSetName);
|
|
|
|
for (; propIdx < propLen; propIdx++) {
|
|
result += stringify.property(props[propIdx], designSet) + LINE_ENDING;
|
|
}
|
|
|
|
// Ignore subcomponents if none exist, e.g. in vCard.
|
|
let comps = component[2] || [];
|
|
let compIdx = 0;
|
|
let compLen = comps.length;
|
|
|
|
for (; compIdx < compLen; compIdx++) {
|
|
result += stringify.component(comps[compIdx], designSet) + LINE_ENDING;
|
|
}
|
|
|
|
result += 'END:' + name;
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Converts a single jCal/jCard property to a iCalendar/vCard string.
|
|
*
|
|
* @function ICAL.stringify.property
|
|
* @param {Array} property
|
|
* jCal/jCard property array
|
|
* @param {designSet} designSet
|
|
* The design data to use for this property
|
|
* @param {Boolean} noFold
|
|
* If true, the line is not folded
|
|
* @return {String} The iCalendar/vCard string
|
|
*/
|
|
stringify.property = function(property, designSet, noFold) {
|
|
let name = property[0].toUpperCase();
|
|
let jsName = property[0];
|
|
let params = property[1];
|
|
|
|
if (!designSet) {
|
|
designSet = design.defaultSet;
|
|
}
|
|
|
|
let groupName = params.group;
|
|
let line;
|
|
if (designSet.propertyGroups && groupName) {
|
|
line = groupName.toUpperCase() + "." + name;
|
|
} else {
|
|
line = name;
|
|
}
|
|
|
|
for (let [paramName, value] of Object.entries(params)) {
|
|
if (designSet.propertyGroups && paramName == 'group') {
|
|
continue;
|
|
}
|
|
|
|
let paramDesign = designSet.param[paramName];
|
|
let multiValue = paramDesign && paramDesign.multiValue;
|
|
if (multiValue && Array.isArray(value)) {
|
|
value = value.map(function(val) {
|
|
val = stringify._rfc6868Unescape(val);
|
|
val = stringify.paramPropertyValue(val, paramDesign.multiValueSeparateDQuote);
|
|
return val;
|
|
});
|
|
value = stringify.multiValue(value, multiValue, "unknown", null, designSet);
|
|
} else {
|
|
value = stringify._rfc6868Unescape(value);
|
|
value = stringify.paramPropertyValue(value);
|
|
}
|
|
|
|
line += ';' + paramName.toUpperCase() + '=' + value;
|
|
}
|
|
|
|
if (property.length === 3) {
|
|
// If there are no values, we must assume a blank value
|
|
return line + ':';
|
|
}
|
|
|
|
let valueType = property[2];
|
|
|
|
let propDetails;
|
|
let multiValue = false;
|
|
let structuredValue = false;
|
|
let isDefault = false;
|
|
|
|
if (jsName in designSet.property) {
|
|
propDetails = designSet.property[jsName];
|
|
|
|
if ('multiValue' in propDetails) {
|
|
multiValue = propDetails.multiValue;
|
|
}
|
|
|
|
if (('structuredValue' in propDetails) && Array.isArray(property[3])) {
|
|
structuredValue = propDetails.structuredValue;
|
|
}
|
|
|
|
if ('defaultType' in propDetails) {
|
|
if (valueType === propDetails.defaultType) {
|
|
isDefault = true;
|
|
}
|
|
} else {
|
|
if (valueType === DEFAULT_VALUE_TYPE) {
|
|
isDefault = true;
|
|
}
|
|
}
|
|
} else {
|
|
if (valueType === DEFAULT_VALUE_TYPE) {
|
|
isDefault = true;
|
|
}
|
|
}
|
|
|
|
// push the VALUE property if type is not the default
|
|
// for the current property.
|
|
if (!isDefault) {
|
|
// value will never contain ;/:/, so we don't escape it here.
|
|
line += ';VALUE=' + valueType.toUpperCase();
|
|
}
|
|
|
|
line += ':';
|
|
|
|
if (multiValue && structuredValue) {
|
|
line += stringify.multiValue(
|
|
property[3], structuredValue, valueType, multiValue, designSet, structuredValue
|
|
);
|
|
} else if (multiValue) {
|
|
line += stringify.multiValue(
|
|
property.slice(3), multiValue, valueType, null, designSet, false
|
|
);
|
|
} else if (structuredValue) {
|
|
line += stringify.multiValue(
|
|
property[3], structuredValue, valueType, null, designSet, structuredValue
|
|
);
|
|
} else {
|
|
line += stringify.value(property[3], valueType, designSet, false);
|
|
}
|
|
|
|
return noFold ? line : foldline(line);
|
|
};
|
|
|
|
/**
|
|
* Handles escaping of property values that may contain:
|
|
*
|
|
* COLON (:), SEMICOLON (;), or COMMA (,)
|
|
*
|
|
* If any of the above are present the result is wrapped
|
|
* in double quotes.
|
|
*
|
|
* @function ICAL.stringify.paramPropertyValue
|
|
* @param {String} value Raw property value
|
|
* @param {boolean} force If value should be escaped even when unnecessary
|
|
* @return {String} Given or escaped value when needed
|
|
*/
|
|
stringify.paramPropertyValue = function(value, force) {
|
|
if (!force &&
|
|
(value.indexOf(',') === -1) &&
|
|
(value.indexOf(':') === -1) &&
|
|
(value.indexOf(';') === -1)) {
|
|
|
|
return value;
|
|
}
|
|
|
|
return '"' + value + '"';
|
|
};
|
|
|
|
/**
|
|
* Converts an array of ical values into a single
|
|
* string based on a type and a delimiter value (like ",").
|
|
*
|
|
* @function ICAL.stringify.multiValue
|
|
* @param {Array} values List of values to convert
|
|
* @param {String} delim Used to join the values (",", ";", ":")
|
|
* @param {String} type Lowecase ical value type
|
|
* (like boolean, date-time, etc..)
|
|
* @param {?String} innerMulti If set, each value will again be processed
|
|
* Used for structured values
|
|
* @param {designSet} designSet
|
|
* The design data to use for this property
|
|
*
|
|
* @return {String} iCalendar/vCard string for value
|
|
*/
|
|
stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) {
|
|
let result = '';
|
|
let len = values.length;
|
|
let i = 0;
|
|
|
|
for (; i < len; i++) {
|
|
if (innerMulti && Array.isArray(values[i])) {
|
|
result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue);
|
|
} else {
|
|
result += stringify.value(values[i], type, designSet, structuredValue);
|
|
}
|
|
|
|
if (i !== (len - 1)) {
|
|
result += delim;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Processes a single ical value runs the associated "toICAL" method from the
|
|
* design value type if available to convert the value.
|
|
*
|
|
* @function ICAL.stringify.value
|
|
* @param {String|Number} value A formatted value
|
|
* @param {String} type Lowercase iCalendar/vCard value type
|
|
* (like boolean, date-time, etc..)
|
|
* @return {String} iCalendar/vCard value for single value
|
|
*/
|
|
stringify.value = function(value, type, designSet, structuredValue) {
|
|
if (type in designSet.value && 'toICAL' in designSet.value[type]) {
|
|
return designSet.value[type].toICAL(value, structuredValue);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* Internal helper for rfc6868. Exposing this on ICAL.stringify so that
|
|
* hackers can disable the rfc6868 parsing if the really need to.
|
|
*
|
|
* @param {String} val The value to unescape
|
|
* @return {String} The escaped value
|
|
*/
|
|
stringify._rfc6868Unescape = function(val) {
|
|
return val.replace(/[\n^"]/g, function(x) {
|
|
return RFC6868_REPLACE_MAP[x];
|
|
});
|
|
};
|