Auto backup before evolver run - 2026年 03月 06日 星期五 19:00:05 CST
This commit is contained in:
+557
@@ -0,0 +1,557 @@
|
||||
/* 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 { unescapedIndexOf } 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").parserState} parserState
|
||||
* Imports the 'parserState' type from the "types.js" module
|
||||
* @typedef {import("./types.js").designSet} designSet
|
||||
* Imports the 'designSet' type from the "types.js" module
|
||||
*/
|
||||
|
||||
const CHAR = /[^ \t]/;
|
||||
const VALUE_DELIMITER = ':';
|
||||
const PARAM_DELIMITER = ';';
|
||||
const PARAM_NAME_DELIMITER = '=';
|
||||
const DEFAULT_VALUE_TYPE = 'unknown';
|
||||
const DEFAULT_PARAM_TYPE = 'text';
|
||||
const RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" };
|
||||
|
||||
/**
|
||||
* Parses iCalendar or vCard data into a raw jCal object. Consult
|
||||
* documentation on the {@tutorial layers|layers of parsing} for more
|
||||
* details.
|
||||
*
|
||||
* @function ICAL.parse
|
||||
* @memberof ICAL
|
||||
* @variation function
|
||||
* @todo Fix the API to be more clear on the return type
|
||||
* @param {String} input The string data to parse
|
||||
* @return {Object|Object[]} A single jCal object, or an array thereof
|
||||
*/
|
||||
export default function parse(input) {
|
||||
let state = {};
|
||||
let root = state.component = [];
|
||||
|
||||
state.stack = [root];
|
||||
|
||||
parse._eachLine(input, function(err, line) {
|
||||
parse._handleContentLine(line, state);
|
||||
});
|
||||
|
||||
|
||||
// when there are still items on the stack
|
||||
// throw a fatal error, a component was not closed
|
||||
// correctly in that case.
|
||||
if (state.stack.length > 1) {
|
||||
throw new ParserError(
|
||||
'invalid ical body. component began but did not end'
|
||||
);
|
||||
}
|
||||
|
||||
state = null;
|
||||
|
||||
return (root.length == 1 ? root[0] : root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an iCalendar property value into the jCal for a single property
|
||||
*
|
||||
* @function ICAL.parse.property
|
||||
* @param {String} str
|
||||
* The iCalendar property string to parse
|
||||
* @param {designSet=} designSet
|
||||
* The design data to use for this property
|
||||
* @return {Object}
|
||||
* The jCal Object containing the property
|
||||
*/
|
||||
parse.property = function(str, designSet) {
|
||||
let state = {
|
||||
component: [[], []],
|
||||
designSet: designSet || design.defaultSet
|
||||
};
|
||||
parse._handleContentLine(str, state);
|
||||
return state.component[1][0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience method to parse a component. You can use ICAL.parse() directly
|
||||
* instead.
|
||||
*
|
||||
* @function ICAL.parse.component
|
||||
* @see ICAL.parse(function)
|
||||
* @param {String} str The iCalendar component string to parse
|
||||
* @return {Object} The jCal Object containing the component
|
||||
*/
|
||||
parse.component = function(str) {
|
||||
return parse(str);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An error that occurred during parsing.
|
||||
*
|
||||
* @param {String} message The error message
|
||||
* @memberof ICAL.parse
|
||||
* @extends {Error}
|
||||
*/
|
||||
class ParserError extends Error {
|
||||
name = this.constructor.name;
|
||||
}
|
||||
|
||||
// classes & constants
|
||||
parse.ParserError = ParserError;
|
||||
|
||||
|
||||
/**
|
||||
* Handles a single line of iCalendar/vCard, updating the state.
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._handleContentLine
|
||||
* @param {String} line The content line to process
|
||||
* @param {parserState} state The current state of the line parsing
|
||||
*/
|
||||
parse._handleContentLine = function(line, state) {
|
||||
// break up the parts of the line
|
||||
let valuePos = line.indexOf(VALUE_DELIMITER);
|
||||
let paramPos = line.indexOf(PARAM_DELIMITER);
|
||||
|
||||
let lastParamIndex;
|
||||
let lastValuePos;
|
||||
|
||||
// name of property or begin/end
|
||||
let name;
|
||||
let value;
|
||||
// params is only overridden if paramPos !== -1.
|
||||
// we can't do params = params || {} later on
|
||||
// because it sacrifices ops.
|
||||
let params = {};
|
||||
|
||||
/**
|
||||
* Different property cases
|
||||
*
|
||||
*
|
||||
* 1. RRULE:FREQ=foo
|
||||
* // FREQ= is not a param but the value
|
||||
*
|
||||
* 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
|
||||
* // ROLE= is a param because : has not happened yet
|
||||
*/
|
||||
// when the parameter delimiter is after the
|
||||
// value delimiter then it is not a parameter.
|
||||
|
||||
if ((paramPos !== -1 && valuePos !== -1)) {
|
||||
// when the parameter delimiter is after the
|
||||
// value delimiter then it is not a parameter.
|
||||
if (paramPos > valuePos) {
|
||||
paramPos = -1;
|
||||
}
|
||||
}
|
||||
|
||||
let parsedParams;
|
||||
if (paramPos !== -1) {
|
||||
name = line.slice(0, Math.max(0, paramPos)).toLowerCase();
|
||||
parsedParams = parse._parseParameters(line.slice(Math.max(0, paramPos)), 0, state.designSet);
|
||||
if (parsedParams[2] == -1) {
|
||||
throw new ParserError("Invalid parameters in '" + line + "'");
|
||||
}
|
||||
params = parsedParams[0];
|
||||
// Handle parameter values with multiple entries
|
||||
let parsedParamLength;
|
||||
if (typeof parsedParams[1] === 'string') {
|
||||
parsedParamLength = parsedParams[1].length;
|
||||
} else {
|
||||
parsedParamLength = parsedParams[1].reduce((accumulator, currentValue) => {
|
||||
return accumulator + currentValue.length;
|
||||
}, 0);
|
||||
}
|
||||
lastParamIndex = parsedParamLength + parsedParams[2] + paramPos;
|
||||
if ((lastValuePos =
|
||||
line.slice(Math.max(0, lastParamIndex)).indexOf(VALUE_DELIMITER)) !== -1) {
|
||||
value = line.slice(Math.max(0, lastParamIndex + lastValuePos + 1));
|
||||
} else {
|
||||
throw new ParserError("Missing parameter value in '" + line + "'");
|
||||
}
|
||||
} else if (valuePos !== -1) {
|
||||
// without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
|
||||
name = line.slice(0, Math.max(0, valuePos)).toLowerCase();
|
||||
value = line.slice(Math.max(0, valuePos + 1));
|
||||
|
||||
if (name === 'begin') {
|
||||
let newComponent = [value.toLowerCase(), [], []];
|
||||
if (state.stack.length === 1) {
|
||||
state.component.push(newComponent);
|
||||
} else {
|
||||
state.component[2].push(newComponent);
|
||||
}
|
||||
state.stack.push(state.component);
|
||||
state.component = newComponent;
|
||||
if (!state.designSet) {
|
||||
state.designSet = design.getDesignSet(state.component[0]);
|
||||
}
|
||||
return;
|
||||
} else if (name === 'end') {
|
||||
state.component = state.stack.pop();
|
||||
return;
|
||||
}
|
||||
// If it is not begin/end, then this is a property with an empty value,
|
||||
// which should be considered valid.
|
||||
} else {
|
||||
/**
|
||||
* Invalid line.
|
||||
* The rational to throw an error is we will
|
||||
* never be certain that the rest of the file
|
||||
* is sane and it is unlikely that we can serialize
|
||||
* the result correctly either.
|
||||
*/
|
||||
throw new ParserError(
|
||||
'invalid line (no token ";" or ":") "' + line + '"'
|
||||
);
|
||||
}
|
||||
|
||||
let valueType;
|
||||
let multiValue = false;
|
||||
let structuredValue = false;
|
||||
let propertyDetails;
|
||||
let splitName;
|
||||
let ungroupedName;
|
||||
|
||||
// fetch the ungrouped part of the name
|
||||
if (state.designSet.propertyGroups && name.indexOf('.') !== -1) {
|
||||
splitName = name.split('.');
|
||||
params.group = splitName[0];
|
||||
ungroupedName = splitName[1];
|
||||
} else {
|
||||
ungroupedName = name;
|
||||
}
|
||||
|
||||
if (ungroupedName in state.designSet.property) {
|
||||
propertyDetails = state.designSet.property[ungroupedName];
|
||||
|
||||
if ('multiValue' in propertyDetails) {
|
||||
multiValue = propertyDetails.multiValue;
|
||||
}
|
||||
|
||||
if ('structuredValue' in propertyDetails) {
|
||||
structuredValue = propertyDetails.structuredValue;
|
||||
}
|
||||
|
||||
if (value && 'detectType' in propertyDetails) {
|
||||
valueType = propertyDetails.detectType(value);
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to determine value
|
||||
if (!valueType) {
|
||||
if (!('value' in params)) {
|
||||
if (propertyDetails) {
|
||||
valueType = propertyDetails.defaultType;
|
||||
} else {
|
||||
valueType = DEFAULT_VALUE_TYPE;
|
||||
}
|
||||
} else {
|
||||
// possible to avoid this?
|
||||
valueType = params.value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
delete params.value;
|
||||
|
||||
/**
|
||||
* Note on `var result` juggling:
|
||||
*
|
||||
* I observed that building the array in pieces has adverse
|
||||
* effects on performance, so where possible we inline the creation.
|
||||
* It is a little ugly but resulted in ~2000 additional ops/sec.
|
||||
*/
|
||||
|
||||
let result;
|
||||
if (multiValue && structuredValue) {
|
||||
value = parse._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue);
|
||||
result = [ungroupedName, params, valueType, value];
|
||||
} else if (multiValue) {
|
||||
result = [ungroupedName, params, valueType];
|
||||
parse._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false);
|
||||
} else if (structuredValue) {
|
||||
value = parse._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue);
|
||||
result = [ungroupedName, params, valueType, value];
|
||||
} else {
|
||||
value = parse._parseValue(value, valueType, state.designSet, false);
|
||||
result = [ungroupedName, params, valueType, value];
|
||||
}
|
||||
// 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 (state.component[0] === 'vcard' && state.component[1].length === 0 &&
|
||||
!(name === 'version' && value === '4.0')) {
|
||||
state.designSet = design.getDesignSet("vcard3");
|
||||
}
|
||||
state.component[1].push(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a value from the raw value into the jCard/jCal value.
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._parseValue
|
||||
* @param {String} value Original value
|
||||
* @param {String} type Type of value
|
||||
* @param {Object} designSet The design data to use for this value
|
||||
* @return {Object} varies on type
|
||||
*/
|
||||
parse._parseValue = function(value, type, designSet, structuredValue) {
|
||||
if (type in designSet.value && 'fromICAL' in designSet.value[type]) {
|
||||
return designSet.value[type].fromICAL(value, structuredValue);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse parameters from a string to object.
|
||||
*
|
||||
* @function ICAL.parse._parseParameters
|
||||
* @private
|
||||
* @param {String} line A single unfolded line
|
||||
* @param {Number} start Position to start looking for properties
|
||||
* @param {Object} designSet The design data to use for this property
|
||||
* @return {Array} Array containing key/valye pairs of parsed parameters, the
|
||||
* parsed value, and the position of the last parameter found
|
||||
*/
|
||||
parse._parseParameters = function(line, start, designSet) {
|
||||
let lastParam = start;
|
||||
let pos = 0;
|
||||
let delim = PARAM_NAME_DELIMITER;
|
||||
let result = {};
|
||||
let name, lcname;
|
||||
let value, valuePos = -1;
|
||||
let type, multiValue, mvdelim;
|
||||
|
||||
// find the next '=' sign
|
||||
// use lastParam and pos to find name
|
||||
// check if " is used if so get value from "->"
|
||||
// then increment pos to find next ;
|
||||
|
||||
while ((pos !== false) &&
|
||||
(pos = line.indexOf(delim, pos + 1)) !== -1) {
|
||||
|
||||
name = line.slice(lastParam + 1, pos);
|
||||
if (name.length == 0) {
|
||||
throw new ParserError("Empty parameter name in '" + line + "'");
|
||||
}
|
||||
lcname = name.toLowerCase();
|
||||
mvdelim = false;
|
||||
multiValue = false;
|
||||
|
||||
if (lcname in designSet.param && designSet.param[lcname].valueType) {
|
||||
type = designSet.param[lcname].valueType;
|
||||
} else {
|
||||
type = DEFAULT_PARAM_TYPE;
|
||||
}
|
||||
|
||||
if (lcname in designSet.param) {
|
||||
multiValue = designSet.param[lcname].multiValue;
|
||||
if (designSet.param[lcname].multiValueSeparateDQuote) {
|
||||
mvdelim = parse._rfc6868Escape('"' + multiValue + '"');
|
||||
}
|
||||
}
|
||||
|
||||
let nextChar = line[pos + 1];
|
||||
if (nextChar === '"') {
|
||||
valuePos = pos + 2;
|
||||
pos = line.indexOf('"', valuePos);
|
||||
if (multiValue && pos != -1) {
|
||||
let extendedValue = true;
|
||||
while (extendedValue) {
|
||||
if (line[pos + 1] == multiValue && line[pos + 2] == '"') {
|
||||
pos = line.indexOf('"', pos + 3);
|
||||
} else {
|
||||
extendedValue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos === -1) {
|
||||
throw new ParserError(
|
||||
'invalid line (no matching double quote) "' + line + '"'
|
||||
);
|
||||
}
|
||||
value = line.slice(valuePos, pos);
|
||||
lastParam = line.indexOf(PARAM_DELIMITER, pos);
|
||||
let propValuePos = line.indexOf(VALUE_DELIMITER, pos);
|
||||
// if either no next parameter or delimeter in property value, let's stop here
|
||||
if (lastParam === -1 || (propValuePos !== -1 && lastParam > propValuePos)) {
|
||||
pos = false;
|
||||
}
|
||||
} else {
|
||||
valuePos = pos + 1;
|
||||
|
||||
// move to next ";"
|
||||
let nextPos = line.indexOf(PARAM_DELIMITER, valuePos);
|
||||
let propValuePos = line.indexOf(VALUE_DELIMITER, valuePos);
|
||||
if (propValuePos !== -1 && nextPos > propValuePos) {
|
||||
// this is a delimiter in the property value, let's stop here
|
||||
nextPos = propValuePos;
|
||||
pos = false;
|
||||
} else if (nextPos === -1) {
|
||||
// no ";"
|
||||
if (propValuePos === -1) {
|
||||
nextPos = line.length;
|
||||
} else {
|
||||
nextPos = propValuePos;
|
||||
}
|
||||
pos = false;
|
||||
} else {
|
||||
lastParam = nextPos;
|
||||
pos = nextPos;
|
||||
}
|
||||
|
||||
value = line.slice(valuePos, nextPos);
|
||||
}
|
||||
|
||||
const length_before = value.length;
|
||||
value = parse._rfc6868Escape(value);
|
||||
valuePos += length_before - value.length;
|
||||
if (multiValue) {
|
||||
let delimiter = mvdelim || multiValue;
|
||||
value = parse._parseMultiValue(value, delimiter, type, [], null, designSet);
|
||||
} else {
|
||||
value = parse._parseValue(value, type, designSet);
|
||||
}
|
||||
|
||||
if (multiValue && (lcname in result)) {
|
||||
if (Array.isArray(result[lcname])) {
|
||||
result[lcname].push(value);
|
||||
} else {
|
||||
result[lcname] = [
|
||||
result[lcname],
|
||||
value
|
||||
];
|
||||
}
|
||||
} else {
|
||||
result[lcname] = value;
|
||||
}
|
||||
}
|
||||
return [result, value, valuePos];
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal helper for rfc6868. Exposing this on ICAL.parse so that
|
||||
* hackers can disable the rfc6868 parsing if the really need to.
|
||||
*
|
||||
* @function ICAL.parse._rfc6868Escape
|
||||
* @param {String} val The value to escape
|
||||
* @return {String} The escaped value
|
||||
*/
|
||||
parse._rfc6868Escape = function(val) {
|
||||
return val.replace(/\^['n^]/g, function(x) {
|
||||
return RFC6868_REPLACE_MAP[x];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a multi value string. This function is used either for parsing
|
||||
* actual multi-value property's values, or for handling parameter values. It
|
||||
* can be used for both multi-value properties and structured value properties.
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._parseMultiValue
|
||||
* @param {String} buffer The buffer containing the full value
|
||||
* @param {String} delim The multi-value delimiter
|
||||
* @param {String} type The value type to be parsed
|
||||
* @param {Array.<?>} result The array to append results to, varies on value type
|
||||
* @param {String} innerMulti The inner delimiter to split each value with
|
||||
* @param {designSet} designSet The design data for this value
|
||||
* @return {?|Array.<?>} Either an array of results, or the first result
|
||||
*/
|
||||
parse._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) {
|
||||
let pos = 0;
|
||||
let lastPos = 0;
|
||||
let value;
|
||||
if (delim.length === 0) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// split each piece
|
||||
while ((pos = unescapedIndexOf(buffer, delim, lastPos)) !== -1) {
|
||||
value = buffer.slice(lastPos, pos);
|
||||
if (innerMulti) {
|
||||
value = parse._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
|
||||
} else {
|
||||
value = parse._parseValue(value, type, designSet, structuredValue);
|
||||
}
|
||||
result.push(value);
|
||||
lastPos = pos + delim.length;
|
||||
}
|
||||
|
||||
// on the last piece take the rest of string
|
||||
value = buffer.slice(lastPos);
|
||||
if (innerMulti) {
|
||||
value = parse._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue);
|
||||
} else {
|
||||
value = parse._parseValue(value, type, designSet, structuredValue);
|
||||
}
|
||||
result.push(value);
|
||||
|
||||
return result.length == 1 ? result[0] : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a complete buffer of iCalendar/vCard data line by line, correctly
|
||||
* unfolding content. Each line will be processed with the given callback
|
||||
*
|
||||
* @private
|
||||
* @function ICAL.parse._eachLine
|
||||
* @param {String} buffer The buffer to process
|
||||
* @param {function(?String, String)} callback The callback for each line
|
||||
*/
|
||||
parse._eachLine = function(buffer, callback) {
|
||||
let len = buffer.length;
|
||||
let lastPos = buffer.search(CHAR);
|
||||
let pos = lastPos;
|
||||
let line;
|
||||
let firstChar;
|
||||
|
||||
let newlineOffset;
|
||||
|
||||
do {
|
||||
pos = buffer.indexOf('\n', lastPos) + 1;
|
||||
|
||||
if (pos > 1 && buffer[pos - 2] === '\r') {
|
||||
newlineOffset = 2;
|
||||
} else {
|
||||
newlineOffset = 1;
|
||||
}
|
||||
|
||||
if (pos === 0) {
|
||||
pos = len;
|
||||
newlineOffset = 0;
|
||||
}
|
||||
|
||||
firstChar = buffer[lastPos];
|
||||
|
||||
if (firstChar === ' ' || firstChar === '\t') {
|
||||
// add to line
|
||||
line += buffer.slice(lastPos + 1, pos - newlineOffset);
|
||||
} else {
|
||||
if (line)
|
||||
callback(null, line);
|
||||
// push line
|
||||
line = buffer.slice(lastPos, pos - newlineOffset);
|
||||
}
|
||||
|
||||
lastPos = pos;
|
||||
} while (pos !== len);
|
||||
|
||||
// extra ending line
|
||||
line = line.trim();
|
||||
|
||||
if (line.length)
|
||||
callback(null, line);
|
||||
};
|
||||
Reference in New Issue
Block a user