aboutsummaryrefslogtreecommitdiff
path: root/catapult/third_party/polymer/components/shadycss/src/apply-shim.js
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/third_party/polymer/components/shadycss/src/apply-shim.js')
-rw-r--r--catapult/third_party/polymer/components/shadycss/src/apply-shim.js525
1 files changed, 525 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/shadycss/src/apply-shim.js b/catapult/third_party/polymer/components/shadycss/src/apply-shim.js
new file mode 100644
index 00000000..e4bc9cdf
--- /dev/null
+++ b/catapult/third_party/polymer/components/shadycss/src/apply-shim.js
@@ -0,0 +1,525 @@
+/**
+@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
+*/
+/*
+ * The apply shim simulates the behavior of `@apply` proposed at
+ * https://tabatkins.github.io/specs/css-apply-rule/.
+ * The approach is to convert a property like this:
+ *
+ * --foo: {color: red; background: blue;}
+ *
+ * to this:
+ *
+ * --foo_-_color: red;
+ * --foo_-_background: blue;
+ *
+ * Then where `@apply --foo` is used, that is converted to:
+ *
+ * color: var(--foo_-_color);
+ * background: var(--foo_-_background);
+ *
+ * This approach generally works but there are some issues and limitations.
+ * Consider, for example, that somewhere *between* where `--foo` is set and used,
+ * another element sets it to:
+ *
+ * --foo: { border: 2px solid red; }
+ *
+ * We must now ensure that the color and background from the previous setting
+ * do not apply. This is accomplished by changing the property set to this:
+ *
+ * --foo_-_border: 2px solid red;
+ * --foo_-_color: initial;
+ * --foo_-_background: initial;
+ *
+ * This works but introduces one new issue.
+ * Consider this setup at the point where the `@apply` is used:
+ *
+ * background: orange;
+ * `@apply` --foo;
+ *
+ * In this case the background will be unset (initial) rather than the desired
+ * `orange`. We address this by altering the property set to use a fallback
+ * value like this:
+ *
+ * color: var(--foo_-_color);
+ * background: var(--foo_-_background, orange);
+ * border: var(--foo_-_border);
+ *
+ * Note that the default is retained in the property set and the `background` is
+ * the desired `orange`. This leads us to a limitation.
+ *
+ * Limitation 1:
+
+ * Only properties in the rule where the `@apply`
+ * is used are considered as default values.
+ * If another rule matches the element and sets `background` with
+ * less specificity than the rule in which `@apply` appears,
+ * the `background` will not be set.
+ *
+ * Limitation 2:
+ *
+ * When using Polymer's `updateStyles` api, new properties may not be set for
+ * `@apply` properties.
+
+*/
+
+'use strict';
+
+import {forEachRule, processVariableAndFallback, rulesForStyle, toCssText, gatherStyleText} from './style-util.js';
+import {MIXIN_MATCH, VAR_ASSIGN} from './common-regex.js';
+import {detectMixin} from './common-utils.js';
+import {StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars
+
+const APPLY_NAME_CLEAN = /;\s*/m;
+const INITIAL_INHERIT = /^\s*(initial)|(inherit)\s*$/;
+const IMPORTANT = /\s*!important/;
+
+// separator used between mixin-name and mixin-property-name when producing properties
+// NOTE: plain '-' may cause collisions in user styles
+const MIXIN_VAR_SEP = '_-_';
+
+/**
+ * @typedef {!Object<string, string>}
+ */
+let PropertyEntry; // eslint-disable-line no-unused-vars
+
+/**
+ * @typedef {!Object<string, boolean>}
+ */
+let DependantsEntry; // eslint-disable-line no-unused-vars
+
+/** @typedef {{
+ * properties: PropertyEntry,
+ * dependants: DependantsEntry
+ * }}
+ */
+let MixinMapEntry; // eslint-disable-line no-unused-vars
+
+// map of mixin to property names
+// --foo: {border: 2px} -> {properties: {(--foo, ['border'])}, dependants: {'element-name': proto}}
+class MixinMap {
+ constructor() {
+ /** @type {!Object<string, !MixinMapEntry>} */
+ this._map = {};
+ }
+ /**
+ * @param {string} name
+ * @param {!PropertyEntry} props
+ */
+ set(name, props) {
+ name = name.trim();
+ this._map[name] = {
+ properties: props,
+ dependants: {}
+ }
+ }
+ /**
+ * @param {string} name
+ * @return {MixinMapEntry}
+ */
+ get(name) {
+ name = name.trim();
+ return this._map[name] || null;
+ }
+}
+
+/**
+ * Callback for when an element is marked invalid
+ * @type {?function(string)}
+ */
+let invalidCallback = null;
+
+/** @unrestricted */
+class ApplyShim {
+ constructor() {
+ /** @type {?string} */
+ this._currentElement = null;
+ /** @type {HTMLMetaElement} */
+ this._measureElement = null;
+ this._map = new MixinMap();
+ }
+ /**
+ * return true if `cssText` contains a mixin definition or consumption
+ * @param {string} cssText
+ * @return {boolean}
+ */
+ detectMixin(cssText) {
+ return detectMixin(cssText);
+ }
+
+ /**
+ * Gather styles into one style for easier processing
+ * @param {!HTMLTemplateElement} template
+ * @return {HTMLStyleElement}
+ */
+ gatherStyles(template) {
+ const styleText = gatherStyleText(template.content);
+ if (styleText) {
+ const style = /** @type {!HTMLStyleElement} */(document.createElement('style'));
+ style.textContent = styleText;
+ template.content.insertBefore(style, template.content.firstChild);
+ return style;
+ }
+ return null;
+ }
+ /**
+ * @param {!HTMLTemplateElement} template
+ * @param {string} elementName
+ * @return {StyleNode}
+ */
+ transformTemplate(template, elementName) {
+ if (template._gatheredStyle === undefined) {
+ template._gatheredStyle = this.gatherStyles(template);
+ }
+ /** @type {HTMLStyleElement} */
+ const style = template._gatheredStyle;
+ return style ? this.transformStyle(style, elementName) : null;
+ }
+ /**
+ * @param {!HTMLStyleElement} style
+ * @param {string} elementName
+ * @return {StyleNode}
+ */
+ transformStyle(style, elementName = '') {
+ let ast = rulesForStyle(style);
+ this.transformRules(ast, elementName);
+ style.textContent = toCssText(ast);
+ return ast;
+ }
+ /**
+ * @param {!HTMLStyleElement} style
+ * @return {StyleNode}
+ */
+ transformCustomStyle(style) {
+ let ast = rulesForStyle(style);
+ forEachRule(ast, (rule) => {
+ if (rule['selector'] === ':root') {
+ rule['selector'] = 'html';
+ }
+ this.transformRule(rule);
+ })
+ style.textContent = toCssText(ast);
+ return ast;
+ }
+ /**
+ * @param {StyleNode} rules
+ * @param {string} elementName
+ */
+ transformRules(rules, elementName) {
+ this._currentElement = elementName;
+ forEachRule(rules, (r) => {
+ this.transformRule(r);
+ });
+ this._currentElement = null;
+ }
+ /**
+ * @param {!StyleNode} rule
+ */
+ transformRule(rule) {
+ rule['cssText'] = this.transformCssText(rule['parsedCssText'], rule);
+ // :root was only used for variable assignment in property shim,
+ // but generates invalid selectors with real properties.
+ // replace with `:host > *`, which serves the same effect
+ if (rule['selector'] === ':root') {
+ rule['selector'] = ':host > *';
+ }
+ }
+ /**
+ * @param {string} cssText
+ * @param {!StyleNode} rule
+ * @return {string}
+ */
+ transformCssText(cssText, rule) {
+ // produce variables
+ cssText = cssText.replace(VAR_ASSIGN, (matchText, propertyName, valueProperty, valueMixin) =>
+ this._produceCssProperties(matchText, propertyName, valueProperty, valueMixin, rule));
+ // consume mixins
+ return this._consumeCssProperties(cssText, rule);
+ }
+ /**
+ * @param {string} property
+ * @return {string}
+ */
+ _getInitialValueForProperty(property) {
+ if (!this._measureElement) {
+ this._measureElement = /** @type {HTMLMetaElement} */(document.createElement('meta'));
+ this._measureElement.setAttribute('apply-shim-measure', '');
+ this._measureElement.style.all = 'initial';
+ document.head.appendChild(this._measureElement);
+ }
+ return window.getComputedStyle(this._measureElement).getPropertyValue(property);
+ }
+ /**
+ * Walk over all rules before this rule to find fallbacks for mixins
+ *
+ * @param {!StyleNode} startRule
+ * @return {!Object}
+ */
+ _fallbacksFromPreviousRules(startRule) {
+ // find the "top" rule
+ let topRule = startRule;
+ while (topRule['parent']) {
+ topRule = topRule['parent'];
+ }
+ const fallbacks = {};
+ let seenStartRule = false;
+ forEachRule(topRule, (r) => {
+ // stop when we hit the input rule
+ seenStartRule = seenStartRule || r === startRule;
+ if (seenStartRule) {
+ return;
+ }
+ // NOTE: Only matching selectors are "safe" for this fallback processing
+ // It would be prohibitive to run `matchesSelector()` on each selector,
+ // so we cheat and only check if the same selector string is used, which
+ // guarantees things like specificity matching
+ if (r['selector'] === startRule['selector']) {
+ Object.assign(fallbacks, this._cssTextToMap(r['parsedCssText']));
+ }
+ });
+ return fallbacks;
+ }
+ /**
+ * replace mixin consumption with variable consumption
+ * @param {string} text
+ * @param {!StyleNode=} rule
+ * @return {string}
+ */
+ _consumeCssProperties(text, rule) {
+ /** @type {Array} */
+ let m = null;
+ // loop over text until all mixins with defintions have been applied
+ while((m = MIXIN_MATCH.exec(text))) {
+ let matchText = m[0];
+ let mixinName = m[1];
+ let idx = m.index;
+ // collect properties before apply to be "defaults" if mixin might override them
+ // match includes a "prefix", so find the start and end positions of @apply
+ let applyPos = idx + matchText.indexOf('@apply');
+ let afterApplyPos = idx + matchText.length;
+ // find props defined before this @apply
+ let textBeforeApply = text.slice(0, applyPos);
+ let textAfterApply = text.slice(afterApplyPos);
+ let defaults = rule ? this._fallbacksFromPreviousRules(rule) : {};
+ Object.assign(defaults, this._cssTextToMap(textBeforeApply));
+ let replacement = this._atApplyToCssProperties(mixinName, defaults);
+ // use regex match position to replace mixin, keep linear processing time
+ text = `${textBeforeApply}${replacement}${textAfterApply}`;
+ // move regex search to _after_ replacement
+ MIXIN_MATCH.lastIndex = idx + replacement.length;
+ }
+ return text;
+ }
+ /**
+ * produce variable consumption at the site of mixin consumption
+ * `@apply` --foo; -> for all props (${propname}: var(--foo_-_${propname}, ${fallback[propname]}}))
+ * Example:
+ * border: var(--foo_-_border); padding: var(--foo_-_padding, 2px)
+ *
+ * @param {string} mixinName
+ * @param {Object} fallbacks
+ * @return {string}
+ */
+ _atApplyToCssProperties(mixinName, fallbacks) {
+ mixinName = mixinName.replace(APPLY_NAME_CLEAN, '');
+ let vars = [];
+ let mixinEntry = this._map.get(mixinName);
+ // if we depend on a mixin before it is created
+ // make a sentinel entry in the map to add this element as a dependency for when it is defined.
+ if (!mixinEntry) {
+ this._map.set(mixinName, {});
+ mixinEntry = this._map.get(mixinName);
+ }
+ if (mixinEntry) {
+ if (this._currentElement) {
+ mixinEntry.dependants[this._currentElement] = true;
+ }
+ let p, parts, f;
+ const properties = mixinEntry.properties;
+ for (p in properties) {
+ f = fallbacks && fallbacks[p];
+ parts = [p, ': var(', mixinName, MIXIN_VAR_SEP, p];
+ if (f) {
+ parts.push(',', f.replace(IMPORTANT, ''));
+ }
+ parts.push(')');
+ if (IMPORTANT.test(properties[p])) {
+ parts.push(' !important');
+ }
+ vars.push(parts.join(''));
+ }
+ }
+ return vars.join('; ');
+ }
+
+ /**
+ * @param {string} property
+ * @param {string} value
+ * @return {string}
+ */
+ _replaceInitialOrInherit(property, value) {
+ let match = INITIAL_INHERIT.exec(value);
+ if (match) {
+ if (match[1]) {
+ // initial
+ // replace `initial` with the concrete initial value for this property
+ value = this._getInitialValueForProperty(property);
+ } else {
+ // inherit
+ // with this purposfully illegal value, the variable will be invalid at
+ // compute time (https://www.w3.org/TR/css-variables/#invalid-at-computed-value-time)
+ // and for inheriting values, will behave similarly
+ // we cannot support the same behavior for non inheriting values like 'border'
+ value = 'apply-shim-inherit';
+ }
+ }
+ return value;
+ }
+
+ /**
+ * "parse" a mixin definition into a map of properties and values
+ * cssTextToMap('border: 2px solid black') -> ('border', '2px solid black')
+ * @param {string} text
+ * @param {boolean=} replaceInitialOrInherit
+ * @return {!Object<string, string>}
+ */
+ _cssTextToMap(text, replaceInitialOrInherit = false) {
+ let props = text.split(';');
+ let property, value;
+ let out = {};
+ for (let i = 0, p, sp; i < props.length; i++) {
+ p = props[i];
+ if (p) {
+ sp = p.split(':');
+ // ignore lines that aren't definitions like @media
+ if (sp.length > 1) {
+ property = sp[0].trim();
+ // some properties may have ':' in the value, like data urls
+ value = sp.slice(1).join(':');
+ if (replaceInitialOrInherit) {
+ value = this._replaceInitialOrInherit(property, value);
+ }
+ out[property] = value;
+ }
+ }
+ }
+ return out;
+ }
+
+ /**
+ * @param {MixinMapEntry} mixinEntry
+ */
+ _invalidateMixinEntry(mixinEntry) {
+ if (!invalidCallback) {
+ return;
+ }
+ for (let elementName in mixinEntry.dependants) {
+ if (elementName !== this._currentElement) {
+ invalidCallback(elementName);
+ }
+ }
+ }
+
+ /**
+ * @param {string} matchText
+ * @param {string} propertyName
+ * @param {?string} valueProperty
+ * @param {?string} valueMixin
+ * @param {!StyleNode} rule
+ * @return {string}
+ */
+ _produceCssProperties(matchText, propertyName, valueProperty, valueMixin, rule) {
+ // handle case where property value is a mixin
+ if (valueProperty) {
+ // form: --mixin2: var(--mixin1), where --mixin1 is in the map
+ processVariableAndFallback(valueProperty, (prefix, value) => {
+ if (value && this._map.get(value)) {
+ valueMixin = `@apply ${value};`
+ }
+ });
+ }
+ if (!valueMixin) {
+ return matchText;
+ }
+ let mixinAsProperties = this._consumeCssProperties('' + valueMixin, rule);
+ let prefix = matchText.slice(0, matchText.indexOf('--'));
+ // `initial` and `inherit` as properties in a map should be replaced because
+ // these keywords are eagerly evaluated when the mixin becomes CSS Custom Properties,
+ // and would set the variable value, rather than carry the keyword to the `var()` usage.
+ let mixinValues = this._cssTextToMap(mixinAsProperties, true);
+ let combinedProps = mixinValues;
+ let mixinEntry = this._map.get(propertyName);
+ let oldProps = mixinEntry && mixinEntry.properties;
+ if (oldProps) {
+ // NOTE: since we use mixin, the map of properties is updated here
+ // and this is what we want.
+ combinedProps = Object.assign(Object.create(oldProps), mixinValues);
+ } else {
+ this._map.set(propertyName, combinedProps);
+ }
+ let out = [];
+ let p, v;
+ // set variables defined by current mixin
+ let needToInvalidate = false;
+ for (p in combinedProps) {
+ v = mixinValues[p];
+ // if property not defined by current mixin, set initial
+ if (v === undefined) {
+ v = 'initial';
+ }
+ if (oldProps && !(p in oldProps)) {
+ needToInvalidate = true;
+ }
+ out.push(`${propertyName}${MIXIN_VAR_SEP}${p}: ${v}`);
+ }
+ if (needToInvalidate) {
+ this._invalidateMixinEntry(mixinEntry);
+ }
+ if (mixinEntry) {
+ mixinEntry.properties = combinedProps;
+ }
+ // because the mixinMap is global, the mixin might conflict with
+ // a different scope's simple variable definition:
+ // Example:
+ // some style somewhere:
+ // --mixin1:{ ... }
+ // --mixin2: var(--mixin1);
+ // some other element:
+ // --mixin1: 10px solid red;
+ // --foo: var(--mixin1);
+ // In this case, we leave the original variable definition in place.
+ if (valueProperty) {
+ prefix = `${matchText};${prefix}`;
+ }
+ return `${prefix}${out.join('; ')};`;
+ }
+}
+
+/* exports */
+/* eslint-disable no-self-assign */
+ApplyShim.prototype['detectMixin'] = ApplyShim.prototype.detectMixin;
+ApplyShim.prototype['transformStyle'] = ApplyShim.prototype.transformStyle;
+ApplyShim.prototype['transformCustomStyle'] = ApplyShim.prototype.transformCustomStyle;
+ApplyShim.prototype['transformRules'] = ApplyShim.prototype.transformRules;
+ApplyShim.prototype['transformRule'] = ApplyShim.prototype.transformRule;
+ApplyShim.prototype['transformTemplate'] = ApplyShim.prototype.transformTemplate;
+ApplyShim.prototype['_separator'] = MIXIN_VAR_SEP;
+/* eslint-enable no-self-assign */
+Object.defineProperty(ApplyShim.prototype, 'invalidCallback', {
+ /** @return {?function(string)} */
+ get() {
+ return invalidCallback;
+ },
+ /** @param {?function(string)} cb */
+ set(cb) {
+ invalidCallback = cb;
+ }
+});
+
+export default ApplyShim;