/* 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]; }); };