UNPKG

@polymer/polymer

Version:

The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to

418 lines (401 loc) 13.7 kB
/** @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 */ import '../utils/boot.js'; import { isPath } from '../utils/path.js'; import { dedupingMixin } from '../utils/mixin.js'; import { PropertyEffects } from './property-effects.js'; /** * 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 = 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 PropertyEffects * @polymer * @summary Mixin that parses binding expressions and generates corresponding metadata. * @template T * @param {function(new:T)} superClass Class to apply mixin to. * @return {function(new:T)} superClass with mixin applied. */ const StrictBindingParser = dedupingMixin((base) => { /** * @constructor * @extends {base} * @implements {Polymer_PropertyEffects} * @private */ const elementBase = 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(/&comma;/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; } }; }); export { StrictBindingParser };