Skip to content

Commit

Permalink
Non-destructive @keyframes rule transformation.
Browse files Browse the repository at this point in the history
Previously, the transformer did not disambiguate selectors in `@media`
blocks and keyframes in `@keyframes` blocks. Now, the transformer can
safely transform `@keyframes` blocks. Before a selector is transformed,
if the selector has a parent, it is checked. If the checked parent is a
`@keyframes` rule, the selector transformation is skipped.

Element-specific `@keyframes` are suffixed with the scoped element name.
For example, `@keyframes foo` in an element scoped with `x-el-0` will by
transformed to `@keyframes foo-x-el-0`. References to that animation in
the element's local styles will be updated as well.

Added tests for the new keyframes transformation.
  • Loading branch information
Chris Joel committed Feb 4, 2016
1 parent 4a9ef8e commit b9f2482
Show file tree
Hide file tree
Showing 11 changed files with 722 additions and 266 deletions.
2 changes: 2 additions & 0 deletions src/lib/css-parse.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
node.type = this.types.MEDIA_RULE;
} else if (s.match(this._rx.keyframesRule)) {
node.type = this.types.KEYFRAMES_RULE;
node.keyframesName =
node.selector.split(this._rx.multipleSpaces).pop();
}
} else {
if (s.indexOf(this.VAR_START) === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/custom-style.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
e.textContent;
}
if (e.textContent) {
styleUtil.forEachStyleRule(styleUtil.rulesForStyle(e), function(rule) {
styleUtil.forEachRule(styleUtil.rulesForStyle(e), function(rule) {
styleTransformer.documentRule(rule);
});
// Allow all custom-styles defined in this turn to register
Expand Down
6 changes: 3 additions & 3 deletions src/lib/style-extends.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
var styleUtil = Polymer.StyleUtil;

return {

hasExtends: function(cssText) {
return Boolean(cssText.match(this.rx.EXTEND));
},

transform: function(style) {
var rules = styleUtil.rulesForStyle(style);
var self = this;
styleUtil.forEachStyleRule(rules, function(rule) {
styleUtil.forEachRule(rules, function(rule) {
var map = self._mapRule(rule);
if (rule.parent) {
var m;
Expand Down Expand Up @@ -72,7 +72,7 @@
// TODO: this misses `%foo, .bar` as an unetended selector but
// this seems rare and could possibly be unsupported.
source.selector = source.selector.replace(this.rx.STRIP, '');
source.selector = (source.selector && source.selector + ',\n') +
source.selector = (source.selector && source.selector + ',\n') +
target.selector;
if (source.extends) {
source.extends.forEach(function(e) {
Expand Down
99 changes: 96 additions & 3 deletions src/lib/style-properties.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
// decorates styles with rule info and returns an array of used style
// property names
decorateStyles: function(styles) {
var self = this, props = {};
var self = this, props = {}, keyframes = [];
styleUtil.forRulesInStyles(styles, function(rule) {
self.decorateRule(rule);
self.collectPropertiesInCssText(rule.propertyInfo.cssText, props);
}, function onKeyframesRule(rule) {
keyframes.push(rule);
});
// Cache all found keyframes rules for later reference:
styles._keyframes = keyframes;
// return this list of property names *consumes* in these styles.
var names = [];
for (var i in props) {
Expand Down Expand Up @@ -87,7 +91,10 @@
var parts = cssText.split(';');
for (var i=0, p; i<parts.length; i++) {
p = parts[i];
if (p.match(this.rx.MIXIN_MATCH) || p.match(this.rx.VAR_MATCH)) {
if (p.match(this.rx.MIXIN_MATCH) ||
p.match(this.rx.VAR_MATCH) ||
this.rx.ANIMATION_MATCH.test(p) ||
styleUtil.isKeyframesSelector(rule)) {
customCssText += p + ';\n';
}
}
Expand Down Expand Up @@ -181,6 +188,46 @@
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: function(rule, keyframeTransforms) {
var input = rule.cssText;
var output = rule.cssText;
if (rule.hasAnimations == null) {
// Cache whether or not the rule has any animations to begin with:
rule.hasAnimations = this.rx.ANIMATION_MATCH.test(input);
}
// If there are no animations referenced, we can skip transforms:
if (rule.hasAnimations) {
var transform;
// If we haven't transformed this rule before, we iterate over all
// transforms:
if (rule.keyframeNamesToTransform == null) {
rule.keyframeNamesToTransform = [];
for (var 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 (var 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`.
propertyDataFromStyles: function(styles, element) {
Expand Down Expand Up @@ -256,15 +303,60 @@
hostSelector;
var hostRx = new RegExp(this.rx.HOST_PREFIX + rxHostSelector +
this.rx.HOST_SUFFIX);
var keyframeTransforms =
this._elementKeyframeTransforms(element, scopeSelector);
return styleTransformer.elementStyles(element, function(rule) {
self.applyProperties(rule, properties);
if (rule.cssText && !nativeShadow) {
if (!nativeShadow &&
!Polymer.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,
element._scopeCssViaAttr, scopeSelector);
}
});
},

_elementKeyframeTransforms: function(element, scopeSelector) {
var keyframesRules = element._styles._keyframes;
var keyframeTransforms = {};
if (!nativeShadow) {
// 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 (var 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.
_keyframesRuleTransformer: function(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
_scopeKeyframes: function(rule, scopeId) {
rule.keyframesNameRx = new RegExp(rule.keyframesName, '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.x-foo-42.wide
Expand Down Expand Up @@ -359,6 +451,7 @@
// var(--a, fallback-literal(with-one-nested-parentheses))
VAR_MATCH: /(^|\W+)var\([\s]*([^,)]*)[\s]*,?[\s]*((?:[^,)]*)|(?:[^;]*\([^;)]*\)))[\s]*?\)/gi,
VAR_CAPTURE: /\([\s]*(--[^,\s)]*)(?:,[\s]*(--[^,\s)]*))?(?:\)|,)/gi,
ANIMATION_MATCH: /(animation\s*:)|(animation-name\s*:)/,
IS_VAR: /^--/,
BRACKETED: /\{[^}]*\}/g,
HOST_PREFIX: '(?:^|[^.#[:])',
Expand Down
8 changes: 6 additions & 2 deletions src/lib/style-transformer.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@
// transforms a css rule to a scoped rule.
_transformRule: function(rule, transformer, scope, hostScope) {
var p$ = rule.selector.split(COMPLEX_SELECTOR_SEP);
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
p$[i] = transformer.call(this, p, scope, hostScope);
// we want to skip transformation of rules that appear in keyframes,
// because they are keyframe selectors, not element selectors.
if (!styleUtil.isKeyframesSelector(rule)) {
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
p$[i] = transformer.call(this, p, scope, hostScope);
}
}
// NOTE: save transformedSelector for subsequent matching of elements
// against selectors (e.g. when calculating style properties)
Expand Down
29 changes: 21 additions & 8 deletions src/lib/style-util.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
rules = this.parser.parse(rules);
}
if (callback) {
this.forEachStyleRule(rules, callback);
this.forEachRule(rules, callback);
}
return this.parser.stringify(rules, preserveProperties);
},

forRulesInStyles: function(styles, callback) {
forRulesInStyles: function(styles, styleRuleCallback, keyframesRuleCallback) {
if (styles) {
for (var i=0, l=styles.length, s; (i<l) && (s=styles[i]); i++) {
this.forEachStyleRule(this.rulesForStyle(s), callback);
this.forEachRule(
this.rulesForStyle(s),
styleRuleCallback,
keyframesRuleCallback);
}
}
},
Expand All @@ -44,21 +47,31 @@
return style.__cssRules;
},

forEachStyleRule: function(node, callback) {
// Tests if a rule is a keyframes selector, which looks almost exactly
// like a normal selector but is not (it has nothing to do with scoping
// for example).
isKeyframesSelector: function(rule) {
return rule.parent &&
rule.parent.type === this.ruleTypes.KEYFRAMES_RULE;
},

forEachRule: function(node, styleRuleCallback, keyframesRuleCallback) {
if (!node) {
return;
}
var skipRules = false;
if (node.type === this.ruleTypes.STYLE_RULE) {
callback(node);
} else if (node.type === this.ruleTypes.KEYFRAMES_RULE ||
node.type === this.ruleTypes.MIXIN_RULE) {
styleRuleCallback(node);
} else if (keyframesRuleCallback &&
node.type === this.ruleTypes.KEYFRAMES_RULE) {
keyframesRuleCallback(node);
} else if (node.type === this.ruleTypes.MIXIN_RULE) {
skipRules = true;
}
var r$ = node.rules;
if (r$ && !skipRules) {
for (var i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) {
this.forEachStyleRule(r, callback);
this.forEachRule(r, styleRuleCallback, keyframesRuleCallback);
}
}
},
Expand Down
60 changes: 60 additions & 0 deletions test/smoke/keyframes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!doctype html>
<!--
@license
Copyright (c) 2014 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
-->
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="../../polymer.html">

<body>
<style is="custom-style">
:root {
--color: blue;
--anim-color: red;
}

.alternative {
--color: green;
--anim-color: blue;
}
</style>

<dom-module id="test-keyframes">
<template><style>
:host {
display: block;
color: var(--color);
animation: foo 3s;
height: 20px;
}

@keyframes foo {
0% {
background: var(--anim-color);
}

100% {
background: yellow;
}
}
</style><content></content></template>
<script>
Polymer({
is: 'test-keyframes'
});

</script>
</dom-module>

<p>Text should be the color blue. Background should animate from the color red to the color yellow, and then become transparent.</p>
<test-keyframes>red</test-keyframes>

<p>Text should be the color green. Background should animate from the color blue to the color yellow, and then become transparent.</p>
<test-keyframes class="alternative">blue</test-keyframes>

</body>
Loading

0 comments on commit b9f2482

Please sign in to comment.