/** @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 */ 'use strict'; import {removeCustomPropAssignment, StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars import {nativeShadow} from './style-settings.js'; import StyleTransformer from './style-transformer.js'; import * as StyleUtil from './style-util.js'; import * as RX from './common-regex.js'; import StyleInfo from './style-info.js'; // TODO: dedupe with shady /** * @param {string} selector * @return {boolean} * @this {Element} */ const matchesSelector = function(selector) { const method = this.matches || this.matchesSelector || this.mozMatchesSelector || this.msMatchesSelector || this.oMatchesSelector || this.webkitMatchesSelector; return method && method.call(this, selector); }; const IS_IE = navigator.userAgent.match('Trident'); const XSCOPE_NAME = 'x-scope'; class StyleProperties { get XSCOPE_NAME() { return XSCOPE_NAME; } /** * decorates styles with rule info and returns an array of used style property names * * @param {StyleNode} rules * @return {Array} */ decorateStyles(rules) { let self = this, props = {}, keyframes = [], ruleIndex = 0; StyleUtil.forEachRule(rules, function(rule) { self.decorateRule(rule); // mark in-order position of ast rule in styles block, used for cache key rule.index = ruleIndex++; self.collectPropertiesInCssText(rule.propertyInfo.cssText, props); }, function onKeyframesRule(rule) { keyframes.push(rule); }); // Cache all found keyframes rules for later reference: rules._keyframes = keyframes; // return this list of property names *consumes* in these styles. let names = []; for (let i in props) { names.push(i); } return names; } // decorate a single rule with property info decorateRule(rule) { if (rule.propertyInfo) { return rule.propertyInfo; } let info = {}, properties = {}; let hasProperties = this.collectProperties(rule, properties); if (hasProperties) { info.properties = properties; // TODO(sorvell): workaround parser seeing mixins as additional rules rule['rules'] = null; } info.cssText = this.collectCssText(rule); rule.propertyInfo = info; return info; } // collects the custom properties from a rule's cssText collectProperties(rule, properties) { let info = rule.propertyInfo; if (info) { if (info.properties) { Object.assign(properties, info.properties); return true; } } else { let m, rx = RX.VAR_ASSIGN; let cssText = rule['parsedCssText']; let value; let any; while ((m = rx.exec(cssText))) { // note: group 2 is var, 3 is mixin value = (m[2] || m[3]).trim(); // value of 'inherit' or 'unset' is equivalent to not setting the property here if (value !== 'inherit' || value !== 'unset') { properties[m[1].trim()] = value; } any = true; } return any; } } // returns cssText of properties that consume variables/mixins collectCssText(rule) { return this.collectConsumingCssText(rule['parsedCssText']); } // NOTE: we support consumption inside mixin assignment // but not production, so strip out {...} collectConsumingCssText(cssText) { return cssText.replace(RX.BRACKETED, '') .replace(RX.VAR_ASSIGN, ''); } collectPropertiesInCssText(cssText, props) { let m; while ((m = RX.VAR_CONSUMED.exec(cssText))) { let name = m[1]; // This regex catches all variable names, and following non-whitespace char // If next char is not ':', then variable is a consumer if (m[2] !== ':') { props[name] = true; } } } // turns custom properties into realized values. reify(props) { // big perf optimization here: reify only *own* properties // since this object has __proto__ of the element's scope properties let names = Object.getOwnPropertyNames(props); for (let i=0, n; i < names.length; i++) { n = names[i]; props[n] = this.valueForProperty(props[n], props); } } // given a property value, returns the reified value // a property value may be: // (1) a literal value like: red or 5px; // (2) a variable value like: var(--a), var(--a, red), or var(--a, --b) or // var(--a, var(--b)); // (3) a literal mixin value like { properties }. Each of these properties // can have values that are: (a) literal, (b) variables, (c) @apply mixins. valueForProperty(property, props) { // case (1) default // case (3) defines a mixin and we have to reify the internals if (property) { if (property.indexOf(';') >=0) { property = this.valueForProperties(property, props); } else { // case (2) variable let self = this; let fn = function(prefix, value, fallback, suffix) { if (!value) { return prefix + suffix; } let propertyValue = self.valueForProperty(props[value], props); // if value is "initial", then the variable should be treated as unset if (!propertyValue || propertyValue === 'initial') { // fallback may be --a or var(--a) or literal propertyValue = self.valueForProperty(props[fallback] || fallback, props) || fallback; } else if (propertyValue === 'apply-shim-inherit') { // CSS build will replace `inherit` with `apply-shim-inherit` // for use with native css variables. // Since we have full control, we can use `inherit` directly. propertyValue = 'inherit'; } return prefix + (propertyValue || '') + suffix; }; property = StyleUtil.processVariableAndFallback(property, fn); } } return property && property.trim() || ''; } // note: we do not yet support mixin within mixin valueForProperties(property, props) { let parts = property.split(';'); for (let i=0, p, m; i { // TODO(sorvell): we could trim the set of rules at declaration // time to only include ones that have properties if (!rule.propertyInfo) { this.decorateRule(rule); } // match element against transformedSelector: selector may contain // unwanted uniquification and parsedSelector does not directly match // for :host selectors. let selectorToMatch = rule.transformedSelector || rule['parsedSelector']; if (element && rule.propertyInfo.properties && selectorToMatch) { if (matchesSelector.call(element, selectorToMatch)) { this.collectProperties(rule, props); // produce numeric key for these matches for lookup addToBitMask(rule.index, o); } } }, null, true); return {properties: props, key: o}; } /** * @param {Element} scope * @param {StyleNode} rule * @param {string} cssBuild * @param {function(Object)} callback */ whenHostOrRootRule(scope, rule, cssBuild, callback) { if (!rule.propertyInfo) { this.decorateRule(rule); } if (!rule.propertyInfo.properties) { return; } let {is, typeExtension} = StyleUtil.getIsExtends(scope); let hostScope = is ? StyleTransformer._calcHostScope(is, typeExtension) : 'html'; let parsedSelector = rule['parsedSelector']; let isRoot = (parsedSelector === ':host > *' || parsedSelector === 'html'); let isHost = parsedSelector.indexOf(':host') === 0 && !isRoot; // build info is either in scope (when scope is an element) or in the style // when scope is the default scope; note: this allows default scope to have // mixed mode built and unbuilt styles. if (cssBuild === 'shady') { // :root -> x-foo > *.x-foo for elements and html for custom-style isRoot = parsedSelector === (hostScope + ' > *.' + hostScope) || parsedSelector.indexOf('html') !== -1; // :host -> x-foo for elements, but sub-rules have .x-foo in them isHost = !isRoot && parsedSelector.indexOf(hostScope) === 0; } if (!isRoot && !isHost) { return; } let selectorToMatch = hostScope; if (isHost) { // need to transform :host because `:host` does not work with `matches` if (!rule.transformedSelector) { // transform :host into a matchable selector rule.transformedSelector = StyleTransformer._transformRuleCss( rule, StyleTransformer._transformComplexSelector, StyleTransformer._calcElementScope(is), hostScope ); } selectorToMatch = rule.transformedSelector || hostScope; } callback({ selector: selectorToMatch, isHost: isHost, isRoot: isRoot }); } /** * @param {Element} scope * @param {StyleNode} rules * @param {string} cssBuild * @return {Object} */ hostAndRootPropertiesForScope(scope, rules, cssBuild) { let hostProps = {}, rootProps = {}; // note: active rules excludes non-matching @media rules StyleUtil.forEachRule(rules, (rule) => { // if scope is StyleDefaults, use _element for matchesSelector this.whenHostOrRootRule(scope, rule, cssBuild, (info) => { let element = scope._element || scope; if (matchesSelector.call(element, info.selector)) { if (info.isHost) { this.collectProperties(rule, hostProps); } else { this.collectProperties(rule, rootProps); } } }); }, null, true); return {rootProps: rootProps, hostProps: hostProps}; } /** * @param {Element} element * @param {Object} properties * @param {string} scopeSelector */ transformStyles(element, properties, scopeSelector) { let self = this; let {is, typeExtension} = StyleUtil.getIsExtends(element); let hostSelector = StyleTransformer ._calcHostScope(is, typeExtension); let rxHostSelector = element.extends ? '\\' + hostSelector.slice(0, -1) + '\\]' : hostSelector; let hostRx = new RegExp(RX.HOST_PREFIX + rxHostSelector + RX.HOST_SUFFIX); let {styleRules: rules, cssBuild} = StyleInfo.get(element); let keyframeTransforms = this._elementKeyframeTransforms(element, rules, scopeSelector); return StyleTransformer.elementStyles(element, rules, function(rule) { self.applyProperties(rule, properties); if (!nativeShadow && !StyleUtil.isKeyframesSelector(rule) && rule['cssText']) { // NOTE: keyframe transforms only scope munge animation names, so it // is not necessary to apply them in ShadowDOM. self.applyKeyframeTransforms(rule, keyframeTransforms); self._scopeSelector(rule, hostRx, hostSelector, scopeSelector); } }, cssBuild); } /** * @param {Element} element * @param {StyleNode} rules * @param {string} scopeSelector * @return {Object} */ _elementKeyframeTransforms(element, rules, scopeSelector) { let keyframesRules = rules._keyframes; let keyframeTransforms = {}; if (!nativeShadow && keyframesRules) { // For non-ShadowDOM, we transform all known keyframes rules in // advance for the current scope. This allows us to catch keyframes // rules that appear anywhere in the stylesheet: for (let i = 0, keyframesRule = keyframesRules[i]; i < keyframesRules.length; keyframesRule = keyframesRules[++i]) { this._scopeKeyframes(keyframesRule, scopeSelector); keyframeTransforms[keyframesRule['keyframesName']] = this._keyframesRuleTransformer(keyframesRule); } } return keyframeTransforms; } // Generate a factory for transforming a chunk of CSS text to handle a // particular scoped keyframes rule. /** * @param {StyleNode} keyframesRule * @return {function(string):string} */ _keyframesRuleTransformer(keyframesRule) { return function(cssText) { return cssText.replace( keyframesRule.keyframesNameRx, keyframesRule.transformedKeyframesName); }; } /** * Transforms `@keyframes` names to be unique for the current host. * Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0 * * @param {StyleNode} rule * @param {string} scopeId */ _scopeKeyframes(rule, scopeId) { // Animation names are of the form [\w-], so ensure that the name regex does not partially apply // to similarly named keyframe names by checking for a word boundary at the beginning and // a non-word boundary or `-` at the end. rule.keyframesNameRx = new RegExp(`\\b${rule['keyframesName']}(?!\\B|-)`, 'g'); rule.transformedKeyframesName = rule['keyframesName'] + '-' + scopeId; rule.transformedSelector = rule.transformedSelector || rule['selector']; rule['selector'] = rule.transformedSelector.replace( rule['keyframesName'], rule.transformedKeyframesName); } // Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes): // non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo // host selector: x-foo.wide -> .x-foo-42.wide // note: we use only the scope class (.x-foo-42) and not the hostSelector // (x-foo) to scope :host rules; this helps make property host rules // have low specificity. They are overrideable by class selectors but, // unfortunately, not by type selectors (e.g. overriding via // `.special` is ok, but not by `x-foo`). /** * @param {StyleNode} rule * @param {RegExp} hostRx * @param {string} hostSelector * @param {string} scopeId */ _scopeSelector(rule, hostRx, hostSelector, scopeId) { rule.transformedSelector = rule.transformedSelector || rule['selector']; let selector = rule.transformedSelector; let scope = '.' + scopeId; let parts = StyleUtil.splitSelectorList(selector); for (let i=0, l=parts.length, p; (i -1) { // @media rules may be stale in IE 10 and 11 // refresh the text content of the style to revalidate them. style.textContent = cssText; } StyleUtil.applyStyle(style, null, styleInfo.placeholder); } } // ensure this style is our custom style and increment its use count. if (style) { style['_useCount'] = style['_useCount'] || 0; // increment use count if we changed styles if (styleInfo.customStyle != style) { style['_useCount']++; } styleInfo.customStyle = style; } return style; } /** * @param {Element} style * @param {Object} properties */ applyCustomStyle(style, properties) { let rules = StyleUtil.rulesForStyle(/** @type {HTMLStyleElement} */(style)); let self = this; style.textContent = StyleUtil.toCssText(rules, function(/** StyleNode */rule) { let css = rule['cssText'] = rule['parsedCssText']; if (rule.propertyInfo && rule.propertyInfo.cssText) { // remove property assignments // so next function isn't confused // NOTE: we have 3 categories of css: // (1) normal properties, // (2) custom property assignments (--foo: red;), // (3) custom property usage: border: var(--foo); @apply(--foo); // In elements, 1 and 3 are separated for efficiency; here they // are not and this makes this case unique. css = removeCustomPropAssignment(/** @type {string} */(css)); // replace with reified properties, scenario is same as mixin rule['cssText'] = self.valueForProperties(css, properties); } }); } } /** * @param {number} n * @param {Array} bits */ function addToBitMask(n, bits) { let o = parseInt(n / 32, 10); let v = 1 << (n % 32); bits[o] = (bits[o] || 0) | v; } export default new StyleProperties();