blob: ecaa38915e34bb1dea6e67f52c3d52ffb8337bc1 [file] [log] [blame]
/**
@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<string>}
*/
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<parts.length; i++) {
if ((p = parts[i])) {
RX.MIXIN_MATCH.lastIndex = 0;
m = RX.MIXIN_MATCH.exec(p);
if (m) {
p = this.valueForProperty(props[m[1]], props);
} else {
let colon = p.indexOf(':');
if (colon !== -1) {
let pp = p.substring(colon);
pp = pp.trim();
pp = this.valueForProperty(pp, props) || pp;
p = p.substring(0, colon) + pp;
}
}
parts[i] = (p && p.lastIndexOf(';') === p.length - 1) ?
// strip trailing ;
p.slice(0, -1) :
p || '';
}
}
return parts.join(';');
}
applyProperties(rule, props) {
let output = '';
// dynamically added sheets may not be decorated so ensure they are.
if (!rule.propertyInfo) {
this.decorateRule(rule);
}
if (rule.propertyInfo.cssText) {
output = this.valueForProperties(rule.propertyInfo.cssText, props);
}
rule['cssText'] = output;
}
// Apply keyframe transformations to the cssText of a given rule. The
// keyframeTransforms object is a map of keyframe names to transformer
// functions which take in cssText and spit out transformed cssText.
applyKeyframeTransforms(rule, keyframeTransforms) {
let input = rule['cssText'];
let output = rule['cssText'];
if (rule.hasAnimations == null) {
// Cache whether or not the rule has any animations to begin with:
rule.hasAnimations = RX.ANIMATION_MATCH.test(input);
}
// If there are no animations referenced, we can skip transforms:
if (rule.hasAnimations) {
let transform;
// If we haven't transformed this rule before, we iterate over all
// transforms:
if (rule.keyframeNamesToTransform == null) {
rule.keyframeNamesToTransform = [];
for (let keyframe in keyframeTransforms) {
transform = keyframeTransforms[keyframe];
output = transform(input);
// If the transform actually changed the CSS text, we cache the
// transform name for future use:
if (input !== output) {
input = output;
rule.keyframeNamesToTransform.push(keyframe);
}
}
} else {
// If we already have a list of keyframe names that apply to this
// rule, we apply only those keyframe name transforms:
for (let i = 0; i < rule.keyframeNamesToTransform.length; ++i) {
transform = keyframeTransforms[rule.keyframeNamesToTransform[i]];
input = transform(input);
}
output = input;
}
}
rule['cssText'] = output;
}
// Test if the rules in these styles matches the given `element` and if so,
// collect any custom properties into `props`.
/**
* @param {StyleNode} rules
* @param {Element} element
*/
propertyDataFromStyles(rules, element) {
let props = {};
// generates a unique key for these matches
let o = [];
// note: active rules excludes non-matching @media rules
StyleUtil.forEachRule(rules, (rule) => {
// 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<l) && (p=parts[i]); i++) {
parts[i] = p.match(hostRx) ?
p.replace(hostSelector, scope) :
scope + ' ' + p;
}
rule['selector'] = parts.join(',');
}
/**
* @param {Element} element
* @param {string} selector
* @param {string} old
*/
applyElementScopeSelector(element, selector, old) {
let c = element.getAttribute('class') || '';
let v = c;
if (old) {
v = c.replace(
new RegExp('\\s*' + XSCOPE_NAME + '\\s*' + old + '\\s*', 'g'), ' ');
}
v += (v ? ' ' : '') + XSCOPE_NAME + ' ' + selector;
if (c !== v) {
StyleUtil.setElementClassRaw(element, v);
}
}
/**
* @param {HTMLElement} element
* @param {Object} properties
* @param {string} selector
* @param {HTMLStyleElement} style
* @return {HTMLStyleElement}
*/
applyElementStyle(element, properties, selector, style) {
// calculate cssText to apply
let cssText = style ? style.textContent || '' :
this.transformStyles(element, properties, selector);
// if shady and we have a cached style that is not style, decrement
let styleInfo = StyleInfo.get(element);
let s = styleInfo.customStyle;
if (s && !nativeShadow && (s !== style)) {
s['_useCount']--;
if (s['_useCount'] <= 0 && s.parentNode) {
s.parentNode.removeChild(s);
}
}
// apply styling always under native or if we generated style
// or the cached style is not in document(!)
if (nativeShadow) {
// update existing style only under native
if (styleInfo.customStyle) {
styleInfo.customStyle.textContent = cssText;
style = styleInfo.customStyle;
// otherwise, if we have css to apply, do so
} else if (cssText) {
// apply css after the scope style of the element to help with
// style precedence rules.
style = StyleUtil.applyCss(cssText, selector, element.shadowRoot,
styleInfo.placeholder);
}
} else {
// shady and no cache hit
if (!style) {
// apply css after the scope style of the element to help with
// style precedence rules.
if (cssText) {
style = StyleUtil.applyCss(cssText, selector, null,
styleInfo.placeholder);
}
// shady and cache hit but not in document
} else if (!style.parentNode) {
if (IS_IE && cssText.indexOf('@media') > -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<number>} 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();