Skip to content

Commit d41cf2d

Browse files
author
Chris Joel
committed
Non-destructive @keyframes rule transformation.
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. This change also fixes the accidental introduction of a vestigal semicolon at the end of some generated styles.
1 parent 92edf4a commit d41cf2d

File tree

6 files changed

+426
-285
lines changed

6 files changed

+426
-285
lines changed

src/lib/css-parse.html

+2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
node.type = this.types.MEDIA_RULE;
7474
} else if (s.match(this._rx.keyframesRule)) {
7575
node.type = this.types.KEYFRAMES_RULE;
76+
node.keyframesName =
77+
node.selector.split(this._rx.multipleSpaces).pop();
7678
}
7779
} else {
7880
if (s.indexOf(this.VAR_START) === 0) {

src/lib/style-properties.html

+45-9
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,13 @@
7878

7979
// returns cssText of properties that consume variables/mixins
8080
collectCssText: function(rule) {
81-
var customCssText = '';
8281
var cssText = rule.parsedCssText;
8382
// NOTE: we support consumption inside mixin assignment
8483
// but not production, so strip out {...}
8584
cssText = cssText.replace(this.rx.BRACKETED, '')
8685
.replace(this.rx.VAR_ASSIGN, '');
8786
var parts = cssText.split(';');
88-
for (var i=0, p; i<parts.length; i++) {
89-
p = parts[i];
90-
customCssText += p + ';\n';
91-
}
92-
return customCssText;
87+
return parts.join(';\n');
9388
},
9489

9590
collectPropertiesInCssText: function(cssText, props) {
@@ -179,6 +174,20 @@
179174
rule.cssText = output;
180175
},
181176

177+
// Apply keyframe transformations to the cssText of a given rule. The
178+
// keyframeTransforms object is a map of keyframe names to transformer
179+
// functions which take in cssText and spit out transformed cssText.
180+
applyKeyframeTransforms: function(rule, keyframeTransforms) {
181+
var output = rule.cssText;
182+
if (this.rx.ANIMATION_MATCH.test(output)) {
183+
for (var keyframe in keyframeTransforms) {
184+
var transform = keyframeTransforms[keyframe];
185+
output = transform(output);
186+
}
187+
}
188+
rule.cssText = output;
189+
},
190+
182191
// Test if the rules in these styles matches the given `element` and if so,
183192
// collect any custom properties into `props`.
184193
propertyDataFromStyles: function(styles, element) {
@@ -254,15 +263,41 @@
254263
hostSelector;
255264
var hostRx = new RegExp(this.rx.HOST_PREFIX + rxHostSelector +
256265
this.rx.HOST_SUFFIX);
266+
var keyframeTransforms = {};
267+
257268
return styleTransformer.elementStyles(element, function(rule) {
258269
self.applyProperties(rule, properties);
259-
if (rule.cssText && !nativeShadow) {
260-
self._scopeSelector(rule, hostRx, hostSelector,
261-
element._scopeCssViaAttr, scopeSelector);
270+
self.applyKeyframeTransforms(rule, keyframeTransforms);
271+
if (!nativeShadow) {
272+
// If the rule is a keyframe selector, we put a transformation in
273+
// the keyframeTransforms map so that it can be applied to other
274+
// rules in the future. We use an anonymous function for the
275+
// transformer so that we can avoid creating a new RegExp for every
276+
// call to `applyKeyframeTransforms`.
277+
if (Polymer.StyleUtil.isKeyframesSelector(rule)) {
278+
var keyframesNameRx = new RegExp(rule.parent.keyframesName, 'g');
279+
self._scopeKeyframes(rule.parent, scopeSelector);
280+
keyframeTransforms[rule.parent.keyframesName] = function(cssText) {
281+
return cssText.replace(
282+
keyframesNameRx, rule.parent.transformedKeyframesName);
283+
};
284+
} else if (rule.cssText) {
285+
self._scopeSelector(rule, hostRx, hostSelector,
286+
element._scopeCssViaAttr, scopeSelector);
287+
}
262288
}
263289
});
264290
},
265291

292+
// Transforms `@keyframes` names to be unique for the current host.
293+
// Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0
294+
_scopeKeyframes: function(rule, scopeId) {
295+
rule.transformedKeyframesName = rule.keyframesName + '-' + scopeId;
296+
rule.transformedSelector = rule.transformedSelector || rule.selector;
297+
rule.selector = rule.transformedSelector.replace(
298+
rule.keyframesName, rule.transformedKeyframesName);
299+
},
300+
266301
// Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes):
267302
// non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo
268303
// host selector: x-foo.wide -> x-foo.x-foo-42.wide
@@ -357,6 +392,7 @@
357392
// var(--a, fallback-literal(with-one-nested-parentheses))
358393
VAR_MATCH: /(^|\W+)var\([\s]*([^,)]*)[\s]*,?[\s]*((?:[^,)]*)|(?:[^;]*\([^;)]*\)))[\s]*?\)/gi,
359394
VAR_CAPTURE: /\([\s]*(--[^,\s)]*)(?:,[\s]*(--[^,\s)]*))?(?:\)|,)/gi,
395+
ANIMATION_MATCH: /(animation\s*:)|(animation-name\s*:)/,
360396
IS_VAR: /^--/,
361397
BRACKETED: /\{[^}]*\}/g,
362398
HOST_PREFIX: '(?:^|[^.#[:])',

src/lib/style-transformer.html

+6-2
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,12 @@
148148
// transforms a css rule to a scoped rule.
149149
_transformRule: function(rule, transformer, scope, hostScope) {
150150
var p$ = rule.selector.split(COMPLEX_SELECTOR_SEP);
151-
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
152-
p$[i] = transformer.call(this, p, scope, hostScope);
151+
// we want to skip transformation of rules that appear in keyframes,
152+
// because they are keyframe selectors, not element selectors.
153+
if (!Polymer.StyleUtil.isKeyframesSelector(rule)) {
154+
for (var i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
155+
p$[i] = transformer.call(this, p, scope, hostScope);
156+
}
153157
}
154158
// NOTE: save transformedSelector for subsequent matching of elements
155159
// against selectors (e.g. when calculating style properties)

src/lib/style-util.html

+9-2
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,22 @@
4444
return style.__cssRules;
4545
},
4646

47+
// Tests if a rule is a keyframes selector, which looks almost exactly
48+
// like a normal selector but is not (it has nothing to do with scoping
49+
// for example).
50+
isKeyframesSelector: function(rule) {
51+
return rule.parent &&
52+
rule.parent.type === Polymer.StyleUtil.ruleTypes.KEYFRAMES_RULE;
53+
},
54+
4755
forEachStyleRule: function(node, callback) {
4856
if (!node) {
4957
return;
5058
}
5159
var skipRules = false;
5260
if (node.type === this.ruleTypes.STYLE_RULE) {
5361
callback(node);
54-
} else if (node.type === this.ruleTypes.KEYFRAMES_RULE ||
55-
node.type === this.ruleTypes.MIXIN_RULE) {
62+
} else if (node.type === this.ruleTypes.MIXIN_RULE) {
5663
skipRules = true;
5764
}
5865
var r$ = node.rules;

test/runner.html

+2-4
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,8 @@
5656
'unit/styling-remote.html',
5757
'unit/styling-cross-scope-var.html',
5858
'unit/styling-cross-scope-apply.html',
59-
// TODO(sorvell): disable until this Chrome bug is addressed:
60-
// https://code.google.com/p/chromium/issues/detail?id=525280
61-
// 'unit/styling-cross-scope-var.html?dom=shadow',
62-
// 'unit/styling-cross-scope-apply.html?dom=shadow',
59+
'unit/styling-cross-scope-var.html?dom=shadow',
60+
'unit/styling-cross-scope-apply.html?dom=shadow',
6361
'unit/styling-cross-scope-unknown-host.html',
6462
'unit/custom-style.html',
6563
'unit/custom-style-late.html',

0 commit comments

Comments
 (0)