Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(20)

Unified Diff: sky/framework/sky-element/polymer-expressions.sky

Issue 698653002: Add initial SkyElement & city-list example (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: moar cleanup Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: sky/framework/sky-element/polymer-expressions.sky
diff --git a/sky/framework/sky-element/polymer-expressions.sky b/sky/framework/sky-element/polymer-expressions.sky
new file mode 100644
index 0000000000000000000000000000000000000000..eee7a3ec119197ba249e038fef06a55bde960d27
--- /dev/null
+++ b/sky/framework/sky-element/polymer-expressions.sky
@@ -0,0 +1,622 @@
+<!--
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+-->
+<link rel="import" href="third_party/esprima/esprima.sky" as="esprima" />
+
+<script>
+function prepareBinding(expressionText, name, node, filterRegistry) {
+ var expression;
+ try {
+ expression = getExpression(expressionText);
+ if (expression.scopeIdent &&
+ (node.nodeType !== Node.ELEMENT_NODE ||
+ node.tagName !== 'TEMPLATE' ||
+ (name !== 'bind' && name !== 'repeat'))) {
+ throw Error('as and in can only be used within <template bind/repeat>');
+ }
+ } catch (ex) {
+ console.error('Invalid expression syntax: ' + expressionText, ex);
+ return;
+ }
+
+ return function(model, node, oneTime) {
+ var binding = expression.getBinding(model, filterRegistry, oneTime);
+ if (expression.scopeIdent && binding) {
+ node.polymerExpressionScopeIdent_ = expression.scopeIdent;
+ if (expression.indexIdent)
+ node.polymerExpressionIndexIdent_ = expression.indexIdent;
+ }
+
+ return binding;
+ }
+}
+
+// TODO(rafaelw): Implement simple LRU.
+var expressionParseCache = Object.create(null);
+
+function getExpression(expressionText) {
+ var expression = expressionParseCache[expressionText];
+ if (!expression) {
+ var delegate = new ASTDelegate();
+ esprima.parse(expressionText, delegate);
+ expression = new Expression(delegate);
+ expressionParseCache[expressionText] = expression;
+ }
+ return expression;
+}
+
+function Literal(value) {
+ this.value = value;
+ this.valueFn_ = undefined;
+}
+
+Literal.prototype = {
+ valueFn: function() {
+ if (!this.valueFn_) {
+ var value = this.value;
+ this.valueFn_ = function() {
+ return value;
+ }
+ }
+
+ return this.valueFn_;
+ }
+}
+
+function IdentPath(name) {
+ this.name = name;
+ this.path = Path.get(name);
+}
+
+IdentPath.prototype = {
+ valueFn: function() {
+ if (!this.valueFn_) {
+ var name = this.name;
+ var path = this.path;
+ this.valueFn_ = function(model, observer) {
+ if (observer)
+ observer.addPath(model, path);
+
+ return path.getValueFrom(model);
+ }
+ }
+
+ return this.valueFn_;
+ },
+
+ setValue: function(model, newValue) {
+ if (this.path.length == 1);
+ model = findScope(model, this.path[0]);
+
+ return this.path.setValueFrom(model, newValue);
+ }
+};
+
+function MemberExpression(object, property, accessor) {
+ this.computed = accessor == '[';
+
+ this.dynamicDeps = typeof object == 'function' ||
+ object.dynamicDeps ||
+ (this.computed && !(property instanceof Literal));
+
+ this.simplePath =
+ !this.dynamicDeps &&
+ (property instanceof IdentPath || property instanceof Literal) &&
+ (object instanceof MemberExpression || object instanceof IdentPath);
+
+ this.object = this.simplePath ? object : getFn(object);
+ this.property = !this.computed || this.simplePath ?
+ property : getFn(property);
+}
+
+MemberExpression.prototype = {
+ get fullPath() {
+ if (!this.fullPath_) {
+
+ var parts = this.object instanceof MemberExpression ?
+ this.object.fullPath.slice() : [this.object.name];
+ parts.push(this.property instanceof IdentPath ?
+ this.property.name : this.property.value);
+ this.fullPath_ = Path.get(parts);
+ }
+
+ return this.fullPath_;
+ },
+
+ valueFn: function() {
+ if (!this.valueFn_) {
+ var object = this.object;
+
+ if (this.simplePath) {
+ var path = this.fullPath;
+
+ this.valueFn_ = function(model, observer) {
+ if (observer)
+ observer.addPath(model, path);
+
+ return path.getValueFrom(model);
+ };
+ } else if (!this.computed) {
+ var path = Path.get(this.property.name);
+
+ this.valueFn_ = function(model, observer, filterRegistry) {
+ var context = object(model, observer, filterRegistry);
+
+ if (observer)
+ observer.addPath(context, path);
+
+ return path.getValueFrom(context);
+ }
+ } else {
+ // Computed property.
+ var property = this.property;
+
+ this.valueFn_ = function(model, observer, filterRegistry) {
+ var context = object(model, observer, filterRegistry);
+ var propName = property(model, observer, filterRegistry);
+ if (observer)
+ observer.addPath(context, [propName]);
+
+ return context ? context[propName] : undefined;
+ };
+ }
+ }
+ return this.valueFn_;
+ },
+
+ setValue: function(model, newValue) {
+ if (this.simplePath) {
+ this.fullPath.setValueFrom(model, newValue);
+ return newValue;
+ }
+
+ var object = this.object(model);
+ var propName = this.property instanceof IdentPath ? this.property.name :
+ this.property(model);
+ return object[propName] = newValue;
+ }
+};
+
+function Filter(name, args) {
+ this.name = name;
+ this.args = [];
+ for (var i = 0; i < args.length; i++) {
+ this.args[i] = getFn(args[i]);
+ }
+}
+
+Filter.prototype = {
+ transform: function(model, observer, filterRegistry, toModelDirection,
+ initialArgs) {
+ var fn = filterRegistry[this.name];
+ var context = model;
+ if (fn) {
+ context = undefined;
+ } else {
+ fn = context[this.name];
+ if (!fn) {
+ console.error('Cannot find function or filter: ' + this.name);
+ return;
+ }
+ }
+
+ // If toModelDirection is falsey, then the "normal" (dom-bound) direction
+ // is used. Otherwise, it looks for a 'toModel' property function on the
+ // object.
+ if (toModelDirection) {
+ fn = fn.toModel;
+ } else if (typeof fn.toDOM == 'function') {
+ fn = fn.toDOM;
+ }
+
+ if (typeof fn != 'function') {
+ console.error('Cannot find function or filter: ' + this.name);
+ return;
+ }
+
+ var args = initialArgs || [];
+ for (var i = 0; i < this.args.length; i++) {
+ args.push(getFn(this.args[i])(model, observer, filterRegistry));
+ }
+
+ return fn.apply(context, args);
+ }
+};
+
+function notImplemented() { throw Error('Not Implemented'); }
+
+var unaryOperators = {
+ '+': function(v) { return +v; },
+ '-': function(v) { return -v; },
+ '!': function(v) { return !v; }
+};
+
+var binaryOperators = {
+ '+': function(l, r) { return l+r; },
+ '-': function(l, r) { return l-r; },
+ '*': function(l, r) { return l*r; },
+ '/': function(l, r) { return l/r; },
+ '%': function(l, r) { return l%r; },
+ '<': function(l, r) { return l<r; },
+ '>': function(l, r) { return l>r; },
+ '<=': function(l, r) { return l<=r; },
+ '>=': function(l, r) { return l>=r; },
+ '==': function(l, r) { return l==r; },
+ '!=': function(l, r) { return l!=r; },
+ '===': function(l, r) { return l===r; },
+ '!==': function(l, r) { return l!==r; },
+ '&&': function(l, r) { return l&&r; },
+ '||': function(l, r) { return l||r; },
+};
+
+function getFn(arg) {
+ return typeof arg == 'function' ? arg : arg.valueFn();
+}
+
+function ASTDelegate() {
+ this.expression = null;
+ this.filters = [];
+ this.deps = {};
+ this.currentPath = undefined;
+ this.scopeIdent = undefined;
+ this.indexIdent = undefined;
+ this.dynamicDeps = false;
+}
+
+ASTDelegate.prototype = {
+ createUnaryExpression: function(op, argument) {
+ if (!unaryOperators[op])
+ throw Error('Disallowed operator: ' + op);
+
+ argument = getFn(argument);
+
+ return function(model, observer, filterRegistry) {
+ return unaryOperators[op](argument(model, observer, filterRegistry));
+ };
+ },
+
+ createBinaryExpression: function(op, left, right) {
+ if (!binaryOperators[op])
+ throw Error('Disallowed operator: ' + op);
+
+ left = getFn(left);
+ right = getFn(right);
+
+ switch (op) {
+ case '||':
+ this.dynamicDeps = true;
+ return function(model, observer, filterRegistry) {
+ return left(model, observer, filterRegistry) ||
+ right(model, observer, filterRegistry);
+ };
+ case '&&':
+ this.dynamicDeps = true;
+ return function(model, observer, filterRegistry) {
+ return left(model, observer, filterRegistry) &&
+ right(model, observer, filterRegistry);
+ };
+ }
+
+ return function(model, observer, filterRegistry) {
+ return binaryOperators[op](left(model, observer, filterRegistry),
+ right(model, observer, filterRegistry));
+ };
+ },
+
+ createConditionalExpression: function(test, consequent, alternate) {
+ test = getFn(test);
+ consequent = getFn(consequent);
+ alternate = getFn(alternate);
+
+ this.dynamicDeps = true;
+
+ return function(model, observer, filterRegistry) {
+ return test(model, observer, filterRegistry) ?
+ consequent(model, observer, filterRegistry) :
+ alternate(model, observer, filterRegistry);
+ }
+ },
+
+ createIdentifier: function(name) {
+ var ident = new IdentPath(name);
+ ident.type = 'Identifier';
+ return ident;
+ },
+
+ createMemberExpression: function(accessor, object, property) {
+ var ex = new MemberExpression(object, property, accessor);
+ if (ex.dynamicDeps)
+ this.dynamicDeps = true;
+ return ex;
+ },
+
+ createCallExpression: function(expression, args) {
+ if (!(expression instanceof IdentPath))
+ throw Error('Only identifier function invocations are allowed');
+
+ var filter = new Filter(expression.name, args);
+
+ return function(model, observer, filterRegistry) {
+ return filter.transform(model, observer, filterRegistry, false);
+ };
+ },
+
+ createLiteral: function(token) {
+ return new Literal(token.value);
+ },
+
+ createArrayExpression: function(elements) {
+ for (var i = 0; i < elements.length; i++)
+ elements[i] = getFn(elements[i]);
+
+ return function(model, observer, filterRegistry) {
+ var arr = []
+ for (var i = 0; i < elements.length; i++)
+ arr.push(elements[i](model, observer, filterRegistry));
+ return arr;
+ }
+ },
+
+ createProperty: function(kind, key, value) {
+ return {
+ key: key instanceof IdentPath ? key.name : key.value,
+ value: value
+ };
+ },
+
+ createObjectExpression: function(properties) {
+ for (var i = 0; i < properties.length; i++)
+ properties[i].value = getFn(properties[i].value);
+
+ return function(model, observer, filterRegistry) {
+ var obj = {};
+ for (var i = 0; i < properties.length; i++)
+ obj[properties[i].key] =
+ properties[i].value(model, observer, filterRegistry);
+ return obj;
+ }
+ },
+
+ createFilter: function(name, args) {
+ this.filters.push(new Filter(name, args));
+ },
+
+ createAsExpression: function(expression, scopeIdent) {
+ this.expression = expression;
+ this.scopeIdent = scopeIdent;
+ },
+
+ createInExpression: function(scopeIdent, indexIdent, expression) {
+ this.expression = expression;
+ this.scopeIdent = scopeIdent;
+ this.indexIdent = indexIdent;
+ },
+
+ createTopLevel: function(expression) {
+ this.expression = expression;
+ },
+
+ createThisExpression: notImplemented
+}
+
+function ConstantObservable(value) {
+ this.value_ = value;
+}
+
+ConstantObservable.prototype = {
+ open: function() { return this.value_; },
+ discardChanges: function() { return this.value_; },
+ deliver: function() {},
+ close: function() {},
+}
+
+function Expression(delegate) {
+ this.scopeIdent = delegate.scopeIdent;
+ this.indexIdent = delegate.indexIdent;
+
+ if (!delegate.expression)
+ throw Error('No expression found.');
+
+ this.expression = delegate.expression;
+ getFn(this.expression); // forces enumeration of path dependencies
+
+ this.filters = delegate.filters;
+ this.dynamicDeps = delegate.dynamicDeps;
+}
+
+Expression.prototype = {
+ getBinding: function(model, filterRegistry, oneTime) {
+ if (oneTime)
+ return this.getValue(model, undefined, filterRegistry);
+
+ var observer = new CompoundObserver();
+ // captures deps.
+ var firstValue = this.getValue(model, observer, filterRegistry);
+ var firstTime = true;
+ var self = this;
+
+ function valueFn() {
+ // deps cannot have changed on first value retrieval.
+ if (firstTime) {
+ firstTime = false;
+ return firstValue;
+ }
+
+ if (self.dynamicDeps)
+ observer.startReset();
+
+ var value = self.getValue(model,
+ self.dynamicDeps ? observer : undefined,
+ filterRegistry);
+ if (self.dynamicDeps)
+ observer.finishReset();
+
+ return value;
+ }
+
+ function setValueFn(newValue) {
+ self.setValue(model, newValue, filterRegistry);
+ return newValue;
+ }
+
+ return new ObserverTransform(observer, valueFn, setValueFn, true);
+ },
+
+ getValue: function(model, observer, filterRegistry) {
+ var value = getFn(this.expression)(model, observer, filterRegistry);
+ for (var i = 0; i < this.filters.length; i++) {
+ value = this.filters[i].transform(model, observer, filterRegistry,
+ false, [value]);
+ }
+
+ return value;
+ },
+
+ setValue: function(model, newValue, filterRegistry) {
+ var count = this.filters ? this.filters.length : 0;
+ while (count-- > 0) {
+ newValue = this.filters[count].transform(model, undefined,
+ filterRegistry, true, [newValue]);
+ }
+
+ if (this.expression.setValue)
+ return this.expression.setValue(model, newValue);
+ }
+}
+
+/**
+ * Converts a style property name to a css property name. For example:
+ * "WebkitUserSelect" to "-webkit-user-select"
+ */
+function convertStylePropertyName(name) {
+ return String(name).replace(/[A-Z]/g, function(c) {
+ return '-' + c.toLowerCase();
+ });
+}
+
+var parentScopeName = '@' + Math.random().toString(36).slice(2);
+
+// Single ident paths must bind directly to the appropriate scope object.
+// I.e. Pushed values in two-bindings need to be assigned to the actual model
+// object.
+function findScope(model, prop) {
+ while (model[parentScopeName] &&
+ !Object.prototype.hasOwnProperty.call(model, prop)) {
+ model = model[parentScopeName];
+ }
+
+ return model;
+}
+
+function isLiteralExpression(pathString) {
+ switch (pathString) {
+ case '':
+ return false;
+
+ case 'false':
+ case 'null':
+ case 'true':
+ return true;
+ }
+
+ if (!isNaN(Number(pathString)))
+ return true;
+
+ return false;
+};
+
+function PolymerExpressions() {}
+
+PolymerExpressions.prototype = {
+ // "built-in" filters
+ styleObject: function(value) {
+ var parts = [];
+ for (var key in value) {
+ parts.push(convertStylePropertyName(key) + ': ' + value[key]);
+ }
+ return parts.join('; ');
+ },
+
+ tokenList: function(value) {
+ var tokens = [];
+ for (var key in value) {
+ if (value[key])
+ tokens.push(key);
+ }
+ return tokens.join(' ');
+ },
+
+ // binding delegate API
+ prepareInstancePositionChanged: function(template) {
+ var indexIdent = template.polymerExpressionIndexIdent_;
+ if (!indexIdent)
+ return;
+
+ return function(templateInstance, index) {
+ templateInstance.model[indexIdent] = index;
+ };
+ },
+
+ prepareBinding: function(pathString, name, node) {
+ var path = Path.get(pathString);
+
+ if (!isLiteralExpression(pathString) && path.valid) {
+ if (path.length == 1) {
+ return function(model, node, oneTime) {
+ if (oneTime)
+ return path.getValueFrom(model);
+
+ var scope = findScope(model, path[0]);
+ return new PathObserver(scope, path);
+ };
+ }
+ return; // bail out early if pathString is simple path.
+ }
+
+ return prepareBinding(pathString, name, node, this);
+ },
+
+ prepareInstanceModel: function(template) {
+ var scopeName = template.polymerExpressionScopeIdent_;
+ if (!scopeName)
+ return;
+
+ var parentScope = template.templateInstance ?
+ template.templateInstance.model :
+ template.model;
+
+ var indexName = template.polymerExpressionIndexIdent_;
+
+ return function(model) {
+ return createScopeObject(parentScope, model, scopeName, indexName);
+ };
+ }
+};
+
+var createScopeObject = ('__proto__' in {}) ?
+ function(parentScope, model, scopeName, indexName) {
+ var scope = {};
+ scope[scopeName] = model;
+ scope[indexName] = undefined;
+ scope[parentScopeName] = parentScope;
+ scope.__proto__ = parentScope;
+ return scope;
+ } :
+ function(parentScope, model, scopeName, indexName) {
+ var scope = Object.create(parentScope);
+ Object.defineProperty(scope, scopeName,
+ { value: model, configurable: true, writable: true });
+ Object.defineProperty(scope, indexName,
+ { value: undefined, configurable: true, writable: true });
+ Object.defineProperty(scope, parentScopeName,
+ { value: parentScope, configurable: true, writable: true });
+ return scope;
+ };
+
+PolymerExpressions.getExpression = getExpression;
+
+module.exports = PolymerExpressions;
+
+</script>

Powered by Google App Engine
This is Rietveld 408576698