mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-10 06:58:27 +03:00
183 lines
10 KiB
JavaScript
183 lines
10 KiB
JavaScript
ko.expressionRewriting = (() => {
|
|
var javaScriptReservedWords = ["true", "false", "null", "undefined"],
|
|
|
|
// Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
|
|
// This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
|
|
// This also will not properly handle nested brackets (e.g., obj1[obj2['prop']]; see #911).
|
|
javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i,
|
|
|
|
getWriteableValue = expression => {
|
|
if (javaScriptReservedWords.includes(expression))
|
|
return false;
|
|
var match = expression.match(javaScriptAssignmentTarget);
|
|
return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
|
|
},
|
|
|
|
// The following regular expressions will be used to split an object-literal string into tokens
|
|
|
|
specials = ',"\'`{}()/:[\\]', // These characters have special meaning to the parser and must not appear in the middle of a token, except as part of a string.
|
|
// Create the actual regular expression by or-ing the following regex strings. The order is important.
|
|
bindingToken = RegExp([
|
|
// These match strings, either with double quotes, single quotes, or backticks
|
|
'"(?:\\\\.|[^"])*"',
|
|
"'(?:\\\\.|[^'])*'",
|
|
"`(?:\\\\.|[^`])*`",
|
|
// Match C style comments
|
|
"/\\*(?:[^*]|\\*+[^*/])*\\*+/",
|
|
// Match C++ style comments
|
|
"//.*\n",
|
|
// Match a regular expression (text enclosed by slashes), but will also match sets of divisions
|
|
// as a regular expression (this is handled by the parsing loop below).
|
|
'/(?:\\\\.|[^/])+/w*',
|
|
// Match text (at least two characters) that does not contain any of the above special characters,
|
|
// although some of the special characters are allowed to start it (all but the colon and comma).
|
|
// The text can contain spaces, but leading or trailing spaces are skipped.
|
|
'[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
|
|
// Match any non-space character not matched already. This will match colons and commas, since they're
|
|
// not matched by "everyThingElse", but will also match any other single character that wasn't already
|
|
// matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
|
|
'[^\\s]'
|
|
].join('|'), 'g'),
|
|
|
|
// Match end of previous token to determine whether a slash is a division or regex.
|
|
divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
|
|
keywordRegexLookBehind = {'in':1,'return':1,'typeof':1},
|
|
|
|
parseObjectLiteral = objectLiteralString => {
|
|
// Trim leading and trailing spaces from the string
|
|
var str = ko.utils.stringTrim(objectLiteralString);
|
|
|
|
// Trim braces '{' surrounding the whole object literal
|
|
if (str.charCodeAt(0) === 123) str = str.slice(1, -1);
|
|
|
|
// Add a newline to correctly match a C++ style comment at the end of the string and
|
|
// add a comma so that we don't need a separate code block to deal with the last item
|
|
str += "\n,";
|
|
|
|
// Split into tokens
|
|
var result = [], toks = str.match(bindingToken), key, values = [], depth = 0;
|
|
|
|
if (toks.length > 1) {
|
|
var i = 0, tok;
|
|
while ((tok = toks[i++])) {
|
|
var c = tok.charCodeAt(0);
|
|
// A comma signals the end of a key/value pair if depth is zero
|
|
if (c === 44) { // ","
|
|
if (depth <= 0) {
|
|
result.push((key && values.length) ? {key: key, value: values.join('')} : {'unknown': key || values.join('')});
|
|
key = depth = 0;
|
|
values = [];
|
|
continue;
|
|
}
|
|
// Simply skip the colon that separates the name and value
|
|
} else if (c === 58) { // ":"
|
|
if (!depth && !key && values.length === 1) {
|
|
key = values.pop();
|
|
continue;
|
|
}
|
|
// Comments: skip them
|
|
} else if (c === 47 && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) { // "//" or "/*"
|
|
continue;
|
|
// A set of slashes is initially matched as a regular expression, but could be division
|
|
} else if (c === 47 && i && tok.length > 1) { // "/"
|
|
// Look at the end of the previous token to determine if the slash is actually division
|
|
var match = toks[i-1].match(divisionLookBehind);
|
|
if (match && !keywordRegexLookBehind[match[0]]) {
|
|
// The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
|
|
str = str.slice(str.indexOf(tok) + 1);
|
|
toks = str.match(bindingToken);
|
|
i = -1;
|
|
// Continue with just the slash
|
|
tok = '/';
|
|
}
|
|
// Increment depth for parentheses, braces, and brackets so that interior commas are ignored
|
|
} else if (c === 40 || c === 123 || c === 91) { // '(', '{', '['
|
|
++depth;
|
|
} else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
|
|
--depth;
|
|
// The key will be the first token; if it's a string, trim the quotes
|
|
} else if (!key && !values.length && (c === 34 || c === 39)) { // '"', "'"
|
|
tok = tok.slice(1, -1);
|
|
}
|
|
values.push(tok);
|
|
}
|
|
if (depth > 0) {
|
|
throw Error("Unbalanced parentheses, braces, or brackets");
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// Two-way bindings include a write function that allow the handler to update the value even if it's not an observable.
|
|
twoWayBindings = new Set,
|
|
|
|
preProcessBindings = (bindingsStringOrKeyValueArray, bindingOptions) => {
|
|
|
|
var resultStrings = [],
|
|
propertyAccessorResultStrings = [],
|
|
makeValueAccessors = bindingOptions?.['valueAccessors'],
|
|
bindingParams = bindingOptions?.['bindingParams'],
|
|
keyValueArray = typeof bindingsStringOrKeyValueArray === "string" ?
|
|
parseObjectLiteral(bindingsStringOrKeyValueArray) : bindingsStringOrKeyValueArray,
|
|
|
|
processKeyValue = (key, val) => {
|
|
var writableVal,
|
|
callPreprocessHook = obj =>
|
|
obj?.['preprocess'] ? (val = obj['preprocess'](val, key, processKeyValue)) : true;
|
|
if (!bindingParams) {
|
|
if (!callPreprocessHook(ko.bindingHandlers[key]))
|
|
return;
|
|
|
|
if (twoWayBindings.has(key) && (writableVal = getWriteableValue(val))) {
|
|
// For two-way bindings, provide a write method in case the value
|
|
// isn't a writable observable.
|
|
propertyAccessorResultStrings.push("'" + key + "':function(_z){" + writableVal + "=_z}");
|
|
}
|
|
}
|
|
// Values are wrapped in a function so that each value can be accessed independently
|
|
if (makeValueAccessors) {
|
|
val = 'function(){return ' + val + ' }';
|
|
}
|
|
resultStrings.push("'" + key + "':" + val);
|
|
};
|
|
|
|
keyValueArray.forEach(keyValue =>
|
|
processKeyValue(keyValue.key || keyValue['unknown'], keyValue.value)
|
|
);
|
|
|
|
if (propertyAccessorResultStrings.length)
|
|
processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");
|
|
|
|
return resultStrings.join(",");
|
|
};
|
|
|
|
return {
|
|
bindingRewriteValidators: [],
|
|
|
|
twoWayBindings: twoWayBindings,
|
|
|
|
parseObjectLiteral: parseObjectLiteral,
|
|
|
|
preProcessBindings: preProcessBindings,
|
|
|
|
keyValueArrayContainsKey: (keyValueArray, key) =>
|
|
-1 < keyValueArray.findIndex(v => v['key'] == key),
|
|
|
|
// Internal, private KO utility for updating model properties from within bindings
|
|
// property: If the property being updated is (or might be) an observable, pass it here
|
|
// If it turns out to be a writable observable, it will be written to directly
|
|
// allBindings: An object with a get method to retrieve bindings in the current execution context.
|
|
// This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable
|
|
// key: The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus'
|
|
// value: The value to be written
|
|
// checkIfDifferent: If true, and if the property being written is a writable observable, the value will only be written if
|
|
// it is !== existing value on that writable observable
|
|
writeValueToProperty: (property, allBindings, key, value, checkIfDifferent) => {
|
|
if (!property || !ko.isObservable(property)) {
|
|
allBindings.get('_ko_property_writers')?.[key]?.(value);
|
|
} else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
|
|
property(value);
|
|
}
|
|
}
|
|
};
|
|
})();
|