1 ko.expressionRewriting = (function () {
2 var javaScriptReservedWords = ["true", "false", "null", "undefined"];
4 // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
5 // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
6 // This also will not properly handle nested brackets (e.g., obj1[obj2['prop']]; see #911).
7 var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
9 function getWriteableValue(expression) {
10 if (ko.utils.arrayIndexOf(javaScriptReservedWords, expression) >= 0)
12 var match = expression.match(javaScriptAssignmentTarget);
13 return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
16 // The following regular expressions will be used to split an object-literal string into tokens
18 // These two match strings, either with double quotes or single quotes
19 var stringDouble = '"(?:[^"\\\\]|\\\\.)*"',
20 stringSingle = "'(?:[^'\\\\]|\\\\.)*'",
21 // Matches a regular expression (text enclosed by slashes), but will also match sets of divisions
22 // as a regular expression (this is handled by the parsing loop below).
23 stringRegexp = '/(?:[^/\\\\]|\\\\.)*/\w*',
24 // These characters have special meaning to the parser and must not appear in the middle of a
25 // token, except as part of a string.
26 specials = ',"\'{}()/:[\\]',
27 // Match text (at least two characters) that does not contain any of the above special characters,
28 // although some of the special characters are allowed to start it (all but the colon and comma).
29 // The text can contain spaces, but leading or trailing spaces are skipped.
30 everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
31 // Match any non-space character not matched already. This will match colons and commas, since they're
32 // not matched by "everyThingElse", but will also match any other single character that wasn't already
33 // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
34 oneNotSpace = '[^\\s]',
36 // Create the actual regular expression by or-ing the above strings. The order is important.
37 bindingToken = RegExp(stringDouble + '|' + stringSingle + '|' + stringRegexp + '|' + everyThingElse + '|' + oneNotSpace, 'g'),
39 // Match end of previous token to determine whether a slash is a division or regex.
40 divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
41 keywordRegexLookBehind = {'in':1,'return':1,'typeof':1};
43 function parseObjectLiteral(objectLiteralString) {
44 // Trim leading and trailing spaces from the string
45 var str = ko.utils.stringTrim(objectLiteralString);
47 // Trim braces '{' surrounding the whole object literal
48 if (str.charCodeAt(0) === 123) str = str.slice(1, -1);
51 var result = [], toks = str.match(bindingToken), key, values = [], depth = 0;
54 // Append a comma so that we don't need a separate code block to deal with the last item
57 for (var i = 0, tok; tok = toks[i]; ++i) {
58 var c = tok.charCodeAt(0);
59 // A comma signals the end of a key/value pair if depth is zero
60 if (c === 44) { // ","
62 result.push((key && values.length) ? {key: key, value: values.join('')} : {'unknown': key || values.join('')});
67 // Simply skip the colon that separates the name and value
68 } else if (c === 58) { // ":"
69 if (!depth && !key && values.length === 1) {
73 // A set of slashes is initially matched as a regular expression, but could be division
74 } else if (c === 47 && i && tok.length > 1) { // "/"
75 // Look at the end of the previous token to determine if the slash is actually division
76 var match = toks[i-1].match(divisionLookBehind);
77 if (match && !keywordRegexLookBehind[match[0]]) {
78 // The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
79 str = str.substr(str.indexOf(tok) + 1);
80 toks = str.match(bindingToken);
83 // Continue with just the slash
86 // Increment depth for parentheses, braces, and brackets so that interior commas are ignored
87 } else if (c === 40 || c === 123 || c === 91) { // '(', '{', '['
89 } else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
91 // The key will be the first token; if it's a string, trim the quotes
92 } else if (!key && !values.length && (c === 34 || c === 39)) { // '"', "'"
93 tok = tok.slice(1, -1);
101 // Two-way bindings include a write function that allow the handler to update the value even if it's not an observable.
102 var twoWayBindings = {};
104 function preProcessBindings(bindingsStringOrKeyValueArray, bindingOptions) {
105 bindingOptions = bindingOptions || {};
107 function processKeyValue(key, val) {
109 function callPreprocessHook(obj) {
110 return (obj && obj['preprocess']) ? (val = obj['preprocess'](val, key, processKeyValue)) : true;
112 if (!bindingParams) {
113 if (!callPreprocessHook(ko['getBindingHandler'](key)))
116 if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
117 // For two-way bindings, provide a write method in case the value
118 // isn't a writable observable.
119 propertyAccessorResultStrings.push("'" + key + "':function(_z){" + writableVal + "=_z}");
122 // Values are wrapped in a function so that each value can be accessed independently
123 if (makeValueAccessors) {
124 val = 'function(){return ' + val + ' }';
126 resultStrings.push("'" + key + "':" + val);
129 var resultStrings = [],
130 propertyAccessorResultStrings = [],
131 makeValueAccessors = bindingOptions['valueAccessors'],
132 bindingParams = bindingOptions['bindingParams'],
133 keyValueArray = typeof bindingsStringOrKeyValueArray === "string" ?
134 parseObjectLiteral(bindingsStringOrKeyValueArray) : bindingsStringOrKeyValueArray;
136 ko.utils.arrayForEach(keyValueArray, function(keyValue) {
137 processKeyValue(keyValue.key || keyValue['unknown'], keyValue.value);
140 if (propertyAccessorResultStrings.length)
141 processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");
143 return resultStrings.join(",");
147 bindingRewriteValidators: [],
149 twoWayBindings: twoWayBindings,
151 parseObjectLiteral: parseObjectLiteral,
153 preProcessBindings: preProcessBindings,
155 keyValueArrayContainsKey: function(keyValueArray, key) {
156 for (var i = 0; i < keyValueArray.length; i++)
157 if (keyValueArray[i]['key'] == key)
162 // Internal, private KO utility for updating model properties from within bindings
163 // property: If the property being updated is (or might be) an observable, pass it here
164 // If it turns out to be a writable observable, it will be written to directly
165 // allBindings: An object with a get method to retrieve bindings in the current execution context.
166 // This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable
167 // key: The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus'
168 // value: The value to be written
169 // checkIfDifferent: If true, and if the property being written is a writable observable, the value will only be written if
170 // it is !== existing value on that writable observable
171 writeValueToProperty: function(property, allBindings, key, value, checkIfDifferent) {
172 if (!property || !ko.isObservable(property)) {
173 var propWriters = allBindings.get('_ko_property_writers');
174 if (propWriters && propWriters[key])
175 propWriters[key](value);
176 } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
183 ko.exportSymbol('expressionRewriting', ko.expressionRewriting);
184 ko.exportSymbol('expressionRewriting.bindingRewriteValidators', ko.expressionRewriting.bindingRewriteValidators);
185 ko.exportSymbol('expressionRewriting.parseObjectLiteral', ko.expressionRewriting.parseObjectLiteral);
186 ko.exportSymbol('expressionRewriting.preProcessBindings', ko.expressionRewriting.preProcessBindings);
188 // Making bindings explicitly declare themselves as "two way" isn't ideal in the long term (it would be better if
189 // all bindings could use an official 'property writer' API without needing to declare that they might). However,
190 // since this is not, and has never been, a public API (_ko_property_writers was never documented), it's acceptable
191 // as an internal implementation detail in the short term.
192 // For those developers who rely on _ko_property_writers in their custom bindings, we expose _twoWayBindings as an
193 // undocumented feature that makes it relatively easy to upgrade to KO 3.0. However, this is still not an official
194 // public API, and we reserve the right to remove it at any time if we create a real public property writers API.
195 ko.exportSymbol('expressionRewriting._twoWayBindings', ko.expressionRewriting.twoWayBindings);
197 // For backward compatibility, define the following aliases. (Previously, these function names were misleading because
198 // they referred to JSON specifically, even though they actually work with arbitrary JavaScript object literal expressions.)
199 ko.exportSymbol('jsonExpressionRewriting', ko.expressionRewriting);
200 ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings);