mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
420 lines
14 KiB
HTML
420 lines
14 KiB
HTML
<!--
|
|
@license
|
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
Code distributed by Google as part of the polymer project is also
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
-->
|
|
<link rel="import" href="../utils/boot.html">
|
|
<link rel="import" href="../utils/path.html">
|
|
<link rel="import" href="../utils/mixin.html">
|
|
<link rel="import" href="../mixins/property-effects.html">
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
/**
|
|
* The open and corresponding closing brackets for surrounding bindings.
|
|
* @enum {string}
|
|
*/
|
|
const BINDINGS = {
|
|
'{': '}',
|
|
'[': ']'
|
|
};
|
|
|
|
/**
|
|
* All states that the parser can be in. The states represent the state-machine as a whole.
|
|
* @enum {number}
|
|
*/
|
|
const STATE = {
|
|
INITIAL: 1,
|
|
FIRSTOPENINGBINDING: 2,
|
|
FIRSTCHARACTERBINDING: 3,
|
|
BINDING: 4,
|
|
FIRSTCOLON: 5,
|
|
COLONNOTIFYEVENT: 6,
|
|
COLONNOTIFYEVENTFIRSTCLOSINGBINDING: 7,
|
|
FIRSTCLOSINGBINDING: 8,
|
|
STRING: 9,
|
|
METHOD: 10,
|
|
STRINGARG: 11,
|
|
NUMBERARG: 12,
|
|
VARIABLEARG: 13,
|
|
METHODCLOSED: 14,
|
|
METHODCLOSEDBINDING: 15
|
|
};
|
|
|
|
function pushLiteral(text, i, parts, startChar) {
|
|
const literal = text.substring(startChar || 0, i);
|
|
if (literal) {
|
|
parts.push({
|
|
literal
|
|
});
|
|
}
|
|
}
|
|
|
|
function storeMethod(bindingData, templateInfo) {
|
|
const methodName = bindingData.signature.methodName;
|
|
const dynamicFns = templateInfo.dynamicFns;
|
|
if (dynamicFns && dynamicFns[methodName] || bindingData.signature.static) {
|
|
bindingData.dependencies.push(methodName);
|
|
bindingData.signature.dynamicFn = true;
|
|
}
|
|
}
|
|
|
|
function storeVariableBinding(parts, bindingData, prop, i) {
|
|
bindingData.source = prop;
|
|
bindingData.dependencies.push(prop);
|
|
bindingData.startChar = i + 1;
|
|
parts.push(bindingData);
|
|
}
|
|
|
|
function storeMethodVariable(bindingData, text, i) {
|
|
const name = text.substring(bindingData.startChar, i).trim();
|
|
if (name) {
|
|
if (name === 'true' || name === 'false') {
|
|
bindingData.signature.args.push({
|
|
name,
|
|
value: name == 'true',
|
|
literal: true
|
|
});
|
|
} else {
|
|
const arg = {
|
|
name
|
|
};
|
|
arg.structured = Polymer.Path.isPath(name);
|
|
if (arg.structured) {
|
|
arg.wildcard = (name.slice(-2) == '.*');
|
|
if (arg.wildcard) {
|
|
arg.name = name.slice(0, -2);
|
|
}
|
|
}
|
|
bindingData.signature.args.push(arg);
|
|
bindingData.dependencies.push(name);
|
|
bindingData.signature.static = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function storeMethodNumber(bindingData, text, i) {
|
|
const value = text.substring(bindingData.startChar, i).trim();
|
|
bindingData.signature.args.push({
|
|
name: value,
|
|
value: Number(value),
|
|
literal: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mixin that parses binding expressions and generates corresponding metadata.
|
|
* The implementation is different than in `property-effects`, as it uses a
|
|
* state machine instead of a regex. As such, this implementation is able to
|
|
* handle more cases, with the potential performance hit.
|
|
*
|
|
* @mixinFunction
|
|
* @appliesMixin Polymer.PropertyEffects
|
|
* @polymer
|
|
* @memberof Polymer
|
|
* @summary Mixin that parses binding expressions and generates corresponding metadata.
|
|
*/
|
|
const StrictBindingParser = Polymer.dedupingMixin((base) => {
|
|
|
|
/**
|
|
* @constructor
|
|
* @extends {base}
|
|
* @implements {Polymer_PropertyEffects}
|
|
*/
|
|
const elementBase = Polymer.PropertyEffects(base);
|
|
|
|
/**
|
|
* @polymer
|
|
* @mixinClass
|
|
* @implements {Polymer_PropertyEffects}
|
|
*/
|
|
return class extends elementBase {
|
|
|
|
/**
|
|
* Called to parse text in a template (either attribute values or
|
|
* textContent) into binding metadata.
|
|
*
|
|
* Any overrides of this method should return an array of binding part
|
|
* metadata representing one or more bindings found in the provided text
|
|
* and any "literal" text in between. Any non-literal parts will be passed
|
|
* to `_evaluateBinding` when any dependencies change. The only required
|
|
* fields of each "part" in the returned array are as follows:
|
|
*
|
|
* - `dependencies` - Array containing trigger metadata for each property
|
|
* that should trigger the binding to update
|
|
* - `literal` - String containing text if the part represents a literal;
|
|
* in this case no `dependencies` are needed
|
|
*
|
|
* Additional metadata for use by `_evaluateBinding` may be provided in
|
|
* each part object as needed.
|
|
*
|
|
* The default implementation handles the following types of bindings
|
|
* (one or more may be intermixed with literal strings):
|
|
* - Property binding: `[[prop]]`
|
|
* - Path binding: `[[object.prop]]`
|
|
* - Negated property or path bindings: `[[!prop]]` or `[[!object.prop]]`
|
|
* - Two-way property or path bindings (supports negation):
|
|
* `{{prop}}`, `{{object.prop}}`, `{{!prop}}` or `{{!object.prop}}`
|
|
* - Inline computed method (supports negation):
|
|
* `[[compute(a, 'literal', b)]]`, `[[!compute(a, 'literal', b)]]`
|
|
*
|
|
* @param {string} text Text to parse from attribute or textContent
|
|
* @param {Object} templateInfo Current template metadata
|
|
* @return {Array<!BindingPart>} Array of binding part metadata
|
|
* @protected
|
|
*/
|
|
static _parseBindings(text, templateInfo) {
|
|
const parts = [];
|
|
let bindingData = {};
|
|
let escaped = false;
|
|
/** @type {string} */
|
|
let quote;
|
|
/** @type {number} */
|
|
let state = STATE.INITIAL;
|
|
let i,l;
|
|
|
|
for (i=0,l=text.length; i<l; i++) {
|
|
const char = text.charAt(i);
|
|
switch (state) {
|
|
case STATE.INITIAL: {
|
|
if ((char === '{' || char === '[')) {
|
|
bindingData = {
|
|
mode: char,
|
|
dependencies: [],
|
|
startChar: bindingData.startChar
|
|
};
|
|
state = STATE.FIRSTOPENINGBINDING;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.FIRSTOPENINGBINDING: {
|
|
if (char === bindingData.mode) {
|
|
pushLiteral(text, i - 1, parts, bindingData.startChar);
|
|
bindingData.startChar = i + 1;
|
|
state = STATE.FIRSTCHARACTERBINDING;
|
|
} else {
|
|
bindingData = {};
|
|
state = STATE.INITIAL;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.FIRSTCHARACTERBINDING: {
|
|
if (char !== ' ' && char !== '\t' && char !== '\n') {
|
|
if (char === '!') {
|
|
bindingData.negate = true;
|
|
bindingData.startChar = i + 1;
|
|
}
|
|
state = STATE.BINDING;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.BINDING: {
|
|
switch (char) {
|
|
case BINDINGS[bindingData.mode]: {
|
|
state = STATE.FIRSTCLOSINGBINDING;
|
|
break;
|
|
}
|
|
case '\'':
|
|
case '"': {
|
|
quote = char;
|
|
state = STATE.STRING;
|
|
break;
|
|
}
|
|
case '(': {
|
|
bindingData.signature = {
|
|
methodName: text.substring(bindingData.startChar, i).trim(),
|
|
args: [],
|
|
static: true
|
|
};
|
|
bindingData.startChar = i + 1;
|
|
state = STATE.METHOD;
|
|
break;
|
|
}
|
|
case ':': {
|
|
state = STATE.FIRSTCOLON;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case STATE.FIRSTCOLON: {
|
|
if (char === ':') {
|
|
bindingData.customEvent = true;
|
|
bindingData.startCharAfterColon = i + 1;
|
|
state = STATE.COLONNOTIFYEVENT;
|
|
} else {
|
|
state = STATE.BINDING;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.COLONNOTIFYEVENT: {
|
|
if (char === BINDINGS[bindingData.mode]) {
|
|
state = STATE.COLONNOTIFYEVENTFIRSTCLOSINGBINDING;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.COLONNOTIFYEVENTFIRSTCLOSINGBINDING: {
|
|
if (char === BINDINGS[bindingData.mode]) {
|
|
bindingData.event = text.substring(bindingData.startCharAfterColon, i - 1).trim();
|
|
const prop = text.substring(bindingData.startChar, bindingData.startCharAfterColon - 2).trim();
|
|
storeVariableBinding(parts, bindingData, prop, i);
|
|
state = STATE.INITIAL;
|
|
} else {
|
|
state = STATE.BINDING;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.FIRSTCLOSINGBINDING: {
|
|
if (char === BINDINGS[bindingData.mode]) {
|
|
const prop = text.substring(bindingData.startChar, i - 1).trim();
|
|
storeVariableBinding(parts, bindingData, prop, i);
|
|
state = STATE.INITIAL;
|
|
} else {
|
|
state = STATE.BINDING;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.STRING: {
|
|
if (char === '\\') {
|
|
escaped = true;
|
|
} else if (char === quote && !escaped) {
|
|
state = STATE.BINDING;
|
|
} else {
|
|
escaped = false;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.METHOD: {
|
|
switch (char) {
|
|
case ')': {
|
|
storeMethodVariable(bindingData, text, i);
|
|
storeMethod(bindingData, templateInfo);
|
|
bindingData.startChar = i + 1;
|
|
state = STATE.METHODCLOSED;
|
|
break;
|
|
}
|
|
case ',': {
|
|
storeMethodVariable(bindingData, text, i);
|
|
bindingData.startChar = i + 1;
|
|
break;
|
|
}
|
|
case '\'':
|
|
case '"': {
|
|
quote = char;
|
|
state = STATE.STRINGARG;
|
|
break;
|
|
}
|
|
default: {
|
|
if (char >= '0' && char <= '9' || char === '-') {
|
|
state = STATE.NUMBERARG;
|
|
} else if (char != ' ' && char != '\n') {
|
|
state = STATE.VARIABLEARG;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case STATE.STRINGARG: {
|
|
if (char === '\\') {
|
|
escaped = true;
|
|
} else if (char === quote && !escaped) {
|
|
const value = text.substring(bindingData.startChar, i)
|
|
.replace(/^\s+/, '')
|
|
.substring(1)
|
|
// replace comma entity with comma
|
|
.replace(/,/g, ',')
|
|
// repair extra escape sequences; note only commas strictly need
|
|
// escaping, but we allow any other char to be escaped since its
|
|
// likely users will do this
|
|
.replace(/\\(.)/g, '\$1');
|
|
bindingData.signature.args.push({
|
|
value,
|
|
name: value,
|
|
literal: true
|
|
});
|
|
bindingData.startChar = i + 1;
|
|
state = STATE.METHOD;
|
|
} else {
|
|
escaped = false;
|
|
}
|
|
break;
|
|
}
|
|
case STATE.NUMBERARG: {
|
|
switch (char) {
|
|
case ',': {
|
|
storeMethodNumber(bindingData, text, i);
|
|
bindingData.startChar = i + 1;
|
|
state = STATE.METHOD;
|
|
break;
|
|
}
|
|
case ')': {
|
|
storeMethodNumber(bindingData, text, i);
|
|
storeMethod(bindingData, templateInfo);
|
|
state = STATE.METHODCLOSED;
|
|
break;
|
|
}
|
|
default: {
|
|
if (char < '0' || char > '9') {
|
|
state = STATE.VARIABLEARG;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case STATE.VARIABLEARG: {
|
|
switch (char) {
|
|
case ',': {
|
|
storeMethodVariable(bindingData, text, i);
|
|
bindingData.startChar = i + 1;
|
|
state = STATE.METHOD;
|
|
break;
|
|
}
|
|
case ')': {
|
|
storeMethodVariable(bindingData, text, i);
|
|
storeMethod(bindingData, templateInfo);
|
|
state = STATE.METHODCLOSED;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case STATE.METHODCLOSED: {
|
|
if (char === BINDINGS[bindingData.mode]) {
|
|
state = STATE.METHODCLOSEDBINDING;
|
|
} else if (char !== ' ' && char !== '\t' && char !== '\n') {
|
|
console.warn(`Expected two closing "${BINDINGS[bindingData.mode]}" for binding "${text}"`);
|
|
}
|
|
break;
|
|
}
|
|
case STATE.METHODCLOSEDBINDING: {
|
|
if (char === BINDINGS[bindingData.mode]) {
|
|
bindingData.startChar = i + 1;
|
|
parts.push(bindingData);
|
|
state = STATE.INITIAL;
|
|
} else if (char !== ' ' && char !== '\t' && char !== '\n') {
|
|
console.warn(`Expected one closing "${BINDINGS[bindingData.mode]}" for binding "${text}"`);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parts.length) {
|
|
pushLiteral(text, i, parts, parts[parts.length - 1].startChar);
|
|
return parts;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
});
|
|
|
|
Polymer.StrictBindingParser = StrictBindingParser;
|
|
})();
|
|
</script>
|