fix(compiler): scope css keyframes in emulated view encapsulation (#42608)

Ensure that keyframes rules, defined within components with emulated
view encapsulation, are scoped to avoid collisions with keyframes in
other components.

This is achieved by renaming these keyframes to add a prefix that makes
them unique across the application.

In order to enable the handling of keyframes names defined as strings
the previous strategy of replacing quoted css content with `%QUOTED%`
(introduced in commit 7f689a2) has been removed and in its place now
only specific characters inside quotes are being replaced with
placeholder text (those are `;`, `:` and `,`, more can be added in
the future if the need arises).

Closes #33885

BREAKING CHANGE:

Keyframes names are now prefixed with the component's "scope name".
For example, the following keyframes rule in a component definition,
whose "scope name" is host-my-cmp:

   @keyframes foo { ... }

will become:

   @keyframes host-my-cmp_foo { ... }

Any TypeScript/JavaScript code which relied on the names of keyframes rules
will no longer match.

The recommended solutions in this case are to either:
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
- define keyframes rules programmatically in code.

PR Close #42608
This commit is contained in:
dario-piotrowicz 2021-06-20 12:03:48 +01:00 committed by Dylan Hunn
parent bf98c646f7
commit f03e313f24
3 changed files with 844 additions and 37 deletions

View file

@ -7,7 +7,27 @@
*/
/**
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
* The following set contains all keywords that can be used in the animation css shorthand
* property and is used during the scoping of keyframes to make sure such keywords
* are not modified.
*/
const animationKeywords = new Set([
// global values
'inherit', 'initial', 'revert', 'unset',
// animation-direction
'alternate', 'alternate-reverse', 'normal', 'reverse',
// animation-fill-mode
'backwards', 'both', 'forwards', 'none',
// animation-play-state
'paused', 'running',
// animation-timing-function
'ease', 'ease-in', 'ease-in-out', 'ease-out', 'linear', 'step-start', 'step-end',
// `steps()` function
'end', 'jump-both', 'jump-end', 'jump-none', 'jump-start', 'start'
]);
/**
* The following class is a port of shadowCSS from webcomponents.js to TypeScript.
*
* Please make sure to keep to edits in sync with the source file.
*
@ -131,7 +151,6 @@
declaration. This is a directive to the styling shim to use the selector
in comments in lieu of the next selector when running under polyfill.
*/
export class ShadowCss {
strictStyling: boolean = true;
@ -157,6 +176,187 @@ export class ShadowCss {
return this._insertPolyfillRulesInCssText(cssText);
}
/**
* Process styles to add scope to keyframes.
*
* Modify both the names of the keyframes defined in the component styles and also the css
* animation rules using them.
*
* Animation rules using keyframes defined elsewhere are not modified to allow for globally
* defined keyframes.
*
* For example, we convert this css:
*
* ```
* .box {
* animation: box-animation 1s forwards;
* }
*
* @keyframes box-animation {
* to {
* background-color: green;
* }
* }
* ```
*
* to this:
*
* ```
* .box {
* animation: scopeName_box-animation 1s forwards;
* }
*
* @keyframes scopeName_box-animation {
* to {
* background-color: green;
* }
* }
* ```
*
* @param cssText the component's css text that needs to be scoped.
* @param scopeSelector the component's scope selector.
*
* @returns the scoped css text.
*/
private _scopeKeyframesRelatedCss(cssText: string, scopeSelector: string): string {
const unscopedKeyframesSet = new Set<string>();
const scopedKeyframesCssText = processRules(
cssText,
rule => this._scopeLocalKeyframeDeclarations(rule, scopeSelector, unscopedKeyframesSet));
return processRules(
scopedKeyframesCssText,
rule => this._scopeAnimationRule(rule, scopeSelector, unscopedKeyframesSet));
}
/**
* Scopes local keyframes names, returning the updated css rule and it also
* adds the original keyframe name to a provided set to collect all keyframes names
* so that it can later be used to scope the animation rules.
*
* For example, it takes a rule such as:
*
* ```
* @keyframes box-animation {
* to {
* background-color: green;
* }
* }
* ```
*
* and returns:
*
* ```
* @keyframes scopeName_box-animation {
* to {
* background-color: green;
* }
* }
* ```
* and as a side effect it adds "box-animation" to the `unscopedKeyframesSet` set
*
* @param cssRule the css rule to process.
* @param scopeSelector the component's scope selector.
* @param unscopedKeyframesSet the set of unscoped keyframes names (which can be
* modified as a side effect)
*
* @returns the css rule modified with the scoped keyframes name.
*/
private _scopeLocalKeyframeDeclarations(
rule: CssRule, scopeSelector: string, unscopedKeyframesSet: Set<string>): CssRule {
return {
...rule,
selector: rule.selector.replace(
/(^@(?:-webkit-)?keyframes(?:\s+))(['"]?)(.+)\2(\s*)$/,
(_, start, quote, keyframeName, endSpaces) => {
unscopedKeyframesSet.add(unescapeQuotes(keyframeName, quote));
return `${start}${quote}${scopeSelector}_${keyframeName}${quote}${endSpaces}`;
}),
};
}
/**
* Function used to scope a keyframes name (obtained from an animation declaration)
* using an existing set of unscopedKeyframes names to discern if the scoping needs to be
* performed (keyframes names of keyframes not defined in the component's css need not to be
* scoped).
*
* @param keyframe the keyframes name to check.
* @param scopeSelector the component's scope selector.
* @param unscopedKeyframesSet the set of unscoped keyframes names.
*
* @returns the scoped name of the keyframe, or the original name is the name need not to be
* scoped.
*/
private _scopeAnimationKeyframe(
keyframe: string, scopeSelector: string, unscopedKeyframesSet: ReadonlySet<string>): string {
return keyframe.replace(/^(\s*)(['"]?)(.+?)\2(\s*)$/, (_, spaces1, quote, name, spaces2) => {
name = `${unscopedKeyframesSet.has(unescapeQuotes(name, quote)) ? scopeSelector + '_' : ''}${
name}`;
return `${spaces1}${quote}${name}${quote}${spaces2}`;
});
}
/**
* Regular expression used to extrapolate the possible keyframes from an
* animation declaration (with possibly multiple animation definitions)
*
* The regular expression can be divided in three parts
* - (^|\s+)
* simply captures how many (if any) leading whitespaces are present
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
* captures two different possible keyframes, ones which are quoted or ones which are valid css
* idents (custom properties excluded)
* - (?=[,\s;]|$)
* simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
* semicolon or the end of the string
*/
private _animationDeclarationKeyframesRe =
/(^|\s+)(?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))(?=[,\s]|$)/g;
/**
* Scope an animation rule so that the keyframes mentioned in such rule
* are scoped if defined in the component's css and left untouched otherwise.
*
* It can scope values of both the 'animation' and 'animation-name' properties.
*
* @param rule css rule to scope.
* @param scopeSelector the component's scope selector.
* @param unscopedKeyframesSet the set of unscoped keyframes names.
*
* @returns the updated css rule.
**/
private _scopeAnimationRule(
rule: CssRule, scopeSelector: string, unscopedKeyframesSet: ReadonlySet<string>): CssRule {
let content = rule.content.replace(
/((?:^|\s+)(?:-webkit-)?animation(?:\s*):(?:\s*))([^;]+)/g,
(_, start, animationDeclarations) => start +
animationDeclarations.replace(
this._animationDeclarationKeyframesRe,
(original: string, leadingSpaces: string, quote = '', quotedName: string,
nonQuotedName: string) => {
if (quotedName) {
return `${leadingSpaces}${
this._scopeAnimationKeyframe(
`${quote}${quotedName}${quote}`, scopeSelector, unscopedKeyframesSet)}`;
} else {
return animationKeywords.has(nonQuotedName) ?
original :
`${leadingSpaces}${
this._scopeAnimationKeyframe(
nonQuotedName, scopeSelector, unscopedKeyframesSet)}`;
}
}));
content = content.replace(
/((?:^|\s+)(?:-webkit-)?animation-name(?:\s*):(?:\s*))([^;]+)/g,
(_match, start, commaSeparatedKeyframes) => `${start}${
commaSeparatedKeyframes.split(',')
.map(
(keyframe: string) =>
this._scopeAnimationKeyframe(keyframe, scopeSelector, unscopedKeyframesSet))
.join(',')}`);
return {...rule, content};
}
/*
* Process styles to convert native ShadowDOM rules that will trip
* up the css parser; we rely on decorating the stylesheet with inert rules.
@ -217,6 +417,7 @@ export class ShadowCss {
cssText = this._convertColonHostContext(cssText);
cssText = this._convertShadowDOMSelectors(cssText);
if (scopeSelector) {
cssText = this._scopeKeyframesRelatedCss(cssText, scopeSelector);
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
}
cssText = cssText + '\n' + unscopedRules;
@ -642,39 +843,39 @@ function extractCommentsWithHash(input: string): string[] {
}
const BLOCK_PLACEHOLDER = '%BLOCK%';
const QUOTE_PLACEHOLDER = '%QUOTED%';
const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
const _quotedRe = /%QUOTED%/g;
const CONTENT_PAIRS = new Map([['{', '}']]);
const QUOTE_PAIRS = new Map([[`"`, `"`], [`'`, `'`]]);
const COMMA_IN_PLACEHOLDER = '%COMMA_IN_PLACEHOLDER%';
const SEMI_IN_PLACEHOLDER = '%SEMI_IN_PLACEHOLDER%';
const COLON_IN_PLACEHOLDER = '%COLON_IN_PLACEHOLDER%';
const _cssCommaInPlaceholderReGlobal = new RegExp(COMMA_IN_PLACEHOLDER, 'g');
const _cssSemiInPlaceholderReGlobal = new RegExp(SEMI_IN_PLACEHOLDER, 'g');
const _cssColonInPlaceholderReGlobal = new RegExp(COLON_IN_PLACEHOLDER, 'g');
export class CssRule {
constructor(public selector: string, public content: string) {}
}
export function processRules(input: string, ruleCallback: (rule: CssRule) => CssRule): string {
const inputWithEscapedQuotes = escapeBlocks(input, QUOTE_PAIRS, QUOTE_PLACEHOLDER);
const inputWithEscapedBlocks =
escapeBlocks(inputWithEscapedQuotes.escapedString, CONTENT_PAIRS, BLOCK_PLACEHOLDER);
const escaped = escapeInStrings(input);
const inputWithEscapedBlocks = escapeBlocks(escaped, CONTENT_PAIRS, BLOCK_PLACEHOLDER);
let nextBlockIndex = 0;
let nextQuoteIndex = 0;
return inputWithEscapedBlocks.escapedString
.replace(
_ruleRe,
(...m: string[]) => {
const selector = m[2];
let content = '';
let suffix = m[4];
let contentPrefix = '';
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
contentPrefix = '{';
}
const rule = ruleCallback(new CssRule(selector, content));
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
})
.replace(_quotedRe, () => inputWithEscapedQuotes.blocks[nextQuoteIndex++]);
const escapedResult = inputWithEscapedBlocks.escapedString.replace(_ruleRe, (...m: string[]) => {
const selector = m[2];
let content = '';
let suffix = m[4];
let contentPrefix = '';
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
contentPrefix = '{';
}
const rule = ruleCallback(new CssRule(selector, content));
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
});
return unescapeInStrings(escapedResult);
}
class StringWithEscapedBlocks {
@ -722,6 +923,113 @@ function escapeBlocks(
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
}
/**
* Object containing as keys characters that should be substituted by placeholders
* when found in strings during the css text parsing, and as values the respective
* placeholders
*/
const ESCAPE_IN_STRING_MAP: {[key: string]: string} = {
';': SEMI_IN_PLACEHOLDER,
',': COMMA_IN_PLACEHOLDER,
':': COLON_IN_PLACEHOLDER
};
/**
* Parse the provided css text and inside strings (meaning, inside pairs of unescaped single or
* double quotes) replace specific characters with their respective placeholders as indicated
* by the `ESCAPE_IN_STRING_MAP` map.
*
* For example convert the text
* `animation: "my-anim:at\"ion" 1s;`
* to
* `animation: "my-anim%COLON_IN_PLACEHOLDER%at\"ion" 1s;`
*
* This is necessary in order to remove the meaning of some characters when found inside strings
* (for example `;` indicates the end of a css declaration, `,` the sequence of values and `:` the
* division between property and value during a declaration, none of these meanings apply when such
* characters are within strings and so in order to prevent parsing issues they need to be replaced
* with placeholder text for the duration of the css manipulation process).
*
* @param input the original css text.
*
* @returns the css text with specific characters in strings replaced by placeholders.
**/
function escapeInStrings(input: string): string {
let result = input;
let currentQuoteChar: string|null = null;
for (let i = 0; i < result.length; i++) {
const char = result[i];
if (char === '\\') {
i++;
} else {
if (currentQuoteChar !== null) {
// index i is inside a quoted sub-string
if (char === currentQuoteChar) {
currentQuoteChar = null;
} else {
const placeholder: string|undefined = ESCAPE_IN_STRING_MAP[char];
if (placeholder) {
result = `${result.substr(0, i)}${placeholder}${result.substr(i + 1)}`;
i += placeholder.length - 1;
}
}
} else if (char === '\'' || char === '"') {
currentQuoteChar = char;
}
}
}
return result;
}
/**
* Replace in a string all occurrences of keys in the `ESCAPE_IN_STRING_MAP` map with their
* original representation, this is simply used to revert the changes applied by the
* escapeInStrings function.
*
* For example it reverts the text:
* `animation: "my-anim%COLON_IN_PLACEHOLDER%at\"ion" 1s;`
* to it's original form of:
* `animation: "my-anim:at\"ion" 1s;`
*
* Note: For the sake of simplicity this function does not check that the placeholders are
* actually inside strings as it would anyway be extremely unlikely to find them outside of strings.
*
* @param input the css text containing the placeholders.
*
* @returns the css text without the placeholders.
*/
function unescapeInStrings(input: string): string {
let result = input.replace(_cssCommaInPlaceholderReGlobal, ',');
result = result.replace(_cssSemiInPlaceholderReGlobal, ';');
result = result.replace(_cssColonInPlaceholderReGlobal, ':');
return result;
}
/**
* Unescape all quotes present in a string, but only if the string was actually already
* quoted.
*
* This generates a "canonical" representation of strings which can be used to match strings
* which would otherwise only differ because of differently escaped quotes.
*
* For example it converts the string (assumed to be quoted):
* `this \\"is\\" a \\'\\\\'test`
* to:
* `this "is" a '\\\\'test`
* (note that the latter backslashes are not removed as they are not actually escaping the single
* quote)
*
*
* @param input the string possibly containing escaped quotes.
* @param isQuoted boolean indicating whether the string was quoted inside a bigger string (if not
* then it means that it doesn't represent an inner string and thus no unescaping is required)
*
* @returns the string in the "canonical" representation without escaped quotes.
*/
function unescapeQuotes(str: string, isQuoted: boolean): string {
return !isQuoted ? str : str.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=['"])/g, '$1');
}
/**
* Combine the `contextSelectors` with the `hostMarker` and the `otherSelectors`
* to create a selector that matches the same as `:host-context()`.

View file

@ -0,0 +1,510 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ShadowCss} from '@angular/compiler/src/shadow_css';
describe('ShadowCss, keyframes and animations', () => {
function s(css: string, contentAttr: string, hostAttr: string = '') {
const shadowCss = new ShadowCss();
return shadowCss.shimCssText(css, contentAttr, hostAttr);
}
it('should scope keyframes rules', () => {
const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}';
const expected = '@keyframes host-a_foo {0% {transform:translate(-50%) scaleX(0);}}';
expect(s(css, 'host-a')).toEqual(expected);
});
it('should scope -webkit-keyframes rules', () => {
const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}} ';
const expected =
'@-webkit-keyframes host-a_foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}';
expect(s(css, 'host-a')).toEqual(expected);
});
it('should scope animations using local keyframes identifiers', () => {
const css = `
button {
animation: foo 10s ease;
}
@keyframes foo {
0% {
transform: translate(-50%) scaleX(0);
}
}
`;
const result = s(css, 'host-a');
expect(result).toContain('animation: host-a_foo 10s ease;');
});
it('should not scope animations using non-local keyframes identifiers', () => {
const css = `
button {
animation: foo 10s ease;
}
`;
const result = s(css, 'host-a');
expect(result).toContain('animation: foo 10s ease;');
});
it('should scope animation-names using local keyframes identifiers', () => {
const css = `
button {
animation-name: foo;
}
@keyframes foo {
0% {
transform: translate(-50%) scaleX(0);
}
}
`;
const result = s(css, 'host-a');
expect(result).toContain('animation-name: host-a_foo;');
});
it('should not scope animation-names using non-local keyframes identifiers', () => {
const css = `
button {
animation-name: foo;
}
`;
const result = s(css, 'host-a');
expect(result).toContain('animation-name: foo;');
});
it('should handle (scope or not) multiple animation-names', () => {
const css = `
button {
animation-name: foo, bar,baz, qux , quux ,corge ,grault ,garply, waldo;
}
@keyframes foo {}
@keyframes baz {}
@keyframes quux {}
@keyframes grault {}
@keyframes waldo {}`;
const result = s(css, 'host-a');
const animationNames = [
'host-a_foo',
' bar',
'host-a_baz',
' qux ',
' host-a_quux ',
'corge ',
'host-a_grault ',
'garply',
' host-a_waldo',
];
const expected = `animation-name: ${animationNames.join(',')};`;
expect(result).toContain(expected);
});
it('should handle (scope or not) multiple animation-names defined over multiple lines', () => {
const css = `
button {
animation-name: foo,
bar,baz,
qux ,
quux ,
grault,
garply, waldo;
}
@keyframes foo {}
@keyframes baz {}
@keyframes quux {}
@keyframes grault {}`;
const result = s(css, 'host-a');
['foo', 'baz', 'quux', 'grault'].forEach(
scoped => expect(result).toContain(`host-a_${scoped}`));
['bar', 'qux', 'garply', 'waldo'].forEach(nonScoped => {
expect(result).toContain(nonScoped);
expect(result).not.toContain(`host-a_${nonScoped}`);
});
});
it('should handle (scope or not) multiple animation definitions in a single declaration', () => {
const css = `
div {
animation: 1s ease foo, 2s bar infinite, forwards baz 3s;
}
p {
animation: 1s "foo", 2s "bar";
}
span {
animation: .5s ease 'quux',
1s foo infinite, forwards "baz'" 1.5s,
2s bar;
}
button {
animation: .5s bar,
1s foo 0.3s, 2s quux;
}
@keyframes bar {}
@keyframes quux {}
@keyframes "baz'" {}`;
const result = s(css, 'host-a');
expect(result).toContain('animation: 1s ease foo, 2s host-a_bar infinite, forwards baz 3s;');
expect(result).toContain('animation: 1s "foo", 2s "host-a_bar";');
expect(result).toContain(`
animation: .5s host-a_bar,
1s foo 0.3s, 2s host-a_quux;`);
expect(result).toContain(`
animation: .5s ease 'host-a_quux',
1s foo infinite, forwards "host-a_baz'" 1.5s,
2s host-a_bar;`);
});
it(`should not modify css variables ending with 'animation' even if they reference a local keyframes identifier`,
() => {
const css = `
button {
--variable-animation: foo;
}
@keyframes foo {}`;
const result = s(css, 'host-a');
expect(result).toContain('--variable-animation: foo;');
});
it(`should not modify css variables ending with 'animation-name' even if they reference a local keyframes identifier`,
() => {
const css = `
button {
--variable-animation-name: foo;
}
@keyframes foo {}`;
const result = s(css, 'host-a');
expect(result).toContain('--variable-animation-name: foo;');
});
it('should maintain the spacing when handling (scoping or not) keyframes and animations', () => {
const css = `
div {
animation-name : foo;
animation: 5s bar 1s backwards;
animation : 3s baz ;
animation-name:foobar ;
animation:1s "foo" , 2s "bar",3s "quux";
}
@-webkit-keyframes bar {}
@keyframes foobar {}
@keyframes quux {}
`;
const result = s(css, 'host-a');
expect(result).toContain('animation-name : foo;');
expect(result).toContain('animation: 5s host-a_bar 1s backwards;');
expect(result).toContain('animation : 3s baz ;');
expect(result).toContain('animation-name:host-a_foobar ;');
expect(result).toContain('@-webkit-keyframes host-a_bar {}');
expect(result).toContain('@keyframes host-a_foobar {}');
expect(result).toContain('animation:1s "foo" , 2s "host-a_bar",3s "host-a_quux"');
});
it('should ignore keywords values when scoping local animations', () => {
const css = `
div {
animation: inherit;
animation: unset;
animation: 3s ease reverse foo;
animation: 5s foo 1s backwards;
animation: none 1s foo;
animation: .5s foo paused;
animation: 1s running foo;
animation: 3s linear 1s infinite running foo;
animation: 5s foo ease;
animation: 3s .5s infinite steps(3,end) foo;
animation: 5s steps(9, jump-start) jump .5s;
animation: 1s step-end steps;
}
@keyframes foo {}
@keyframes inherit {}
@keyframes unset {}
@keyframes ease {}
@keyframes reverse {}
@keyframes backwards {}
@keyframes none {}
@keyframes paused {}
@keyframes linear {}
@keyframes running {}
@keyframes end {}
@keyframes jump {}
@keyframes start {}
@keyframes steps {}
`;
const result = s(css, 'host-a');
expect(result).toContain('animation: inherit;');
expect(result).toContain('animation: unset;');
expect(result).toContain('animation: 3s ease reverse host-a_foo;');
expect(result).toContain('animation: 5s host-a_foo 1s backwards;');
expect(result).toContain('animation: none 1s host-a_foo;');
expect(result).toContain('animation: .5s host-a_foo paused;');
expect(result).toContain('animation: 1s running host-a_foo;');
expect(result).toContain('animation: 3s linear 1s infinite running host-a_foo;');
expect(result).toContain('animation: 5s host-a_foo ease;');
expect(result).toContain('animation: 3s .5s infinite steps(3,end) host-a_foo;');
expect(result).toContain('animation: 5s steps(9, jump-start) host-a_jump .5s;');
expect(result).toContain('animation: 1s step-end host-a_steps;');
});
it('should handle the usage of quotes', () => {
const css = `
div {
animation: 1.5s foo;
}
p {
animation: 1s 'foz bar';
}
@keyframes 'foo' {}
@keyframes "foz bar" {}
@keyframes bar {}
@keyframes baz {}
@keyframes qux {}
@keyframes quux {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes \'host-a_foo\' {}');
expect(result).toContain('@keyframes "host-a_foz bar" {}');
expect(result).toContain('animation: 1.5s host-a_foo;');
expect(result).toContain('animation: 1s \'host-a_foz bar\';');
});
it('should handle the usage of quotes containing escaped quotes', () => {
const css = `
div {
animation: 1.5s "foo\\"bar";
}
p {
animation: 1s 'bar\\' \\'baz';
}
button {
animation-name: 'foz " baz';
}
@keyframes "foo\\"bar" {}
@keyframes "bar' 'baz" {}
@keyframes "foz \\" baz" {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes "host-a_foo\\"bar" {}');
expect(result).toContain(`@keyframes "host-a_bar' 'baz" {}`);
expect(result).toContain('@keyframes "host-a_foz \\" baz" {}');
expect(result).toContain('animation: 1.5s "host-a_foo\\"bar";');
expect(result).toContain('animation: 1s \'host-a_bar\\\' \\\'baz\';');
expect(result).toContain(`animation-name: 'host-a_foz " baz';`);
});
it('should handle the usage of commas in multiple animation definitions in a single declaration',
() => {
const css = `
button {
animation: 1s "foo bar, baz", 2s 'qux quux';
}
div {
animation: 500ms foo, 1s 'bar, baz', 1500ms bar;
}
p {
animation: 3s "bar, baz", 3s 'foo, bar' 1s, 3s "qux quux";
}
@keyframes "qux quux" {}
@keyframes "bar, baz" {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes "host-a_qux quux" {}');
expect(result).toContain('@keyframes "host-a_bar, baz" {}');
expect(result).toContain(`animation: 1s "foo bar, baz", 2s 'host-a_qux quux';`);
expect(result).toContain('animation: 500ms foo, 1s \'host-a_bar, baz\', 1500ms bar;');
expect(result).toContain(
`animation: 3s "host-a_bar, baz", 3s 'foo, bar' 1s, 3s "host-a_qux quux";`);
});
it('should handle the usage of double quotes escaping in multiple animation definitions in a single declaration',
() => {
const css = `
div {
animation: 1s "foo", 1.5s "bar";
animation: 2s "fo\\"o", 2.5s "bar";
animation: 3s "foo\\"", 3.5s "bar", 3.7s "ba\\"r";
animation: 4s "foo\\\\", 4.5s "bar", 4.7s "baz\\"";
animation: 5s "fo\\\\\\"o", 5.5s "bar", 5.7s "baz\\"";
}
@keyframes "foo" {}
@keyframes "fo\\"o" {}
@keyframes 'foo"' {}
@keyframes 'foo\\\\' {}
@keyframes bar {}
@keyframes "ba\\"r" {}
@keyframes "fo\\\\\\"o" {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes "host-a_foo" {}');
expect(result).toContain('@keyframes "host-a_fo\\"o" {}');
expect(result).toContain(`@keyframes 'host-a_foo"' {}`);
expect(result).toContain('@keyframes \'host-a_foo\\\\\' {}');
expect(result).toContain('@keyframes host-a_bar {}');
expect(result).toContain('@keyframes "host-a_ba\\"r" {}');
expect(result).toContain('@keyframes "host-a_fo\\\\\\"o"');
expect(result).toContain('animation: 1s "host-a_foo", 1.5s "host-a_bar";');
expect(result).toContain('animation: 2s "host-a_fo\\"o", 2.5s "host-a_bar";');
expect(result).toContain(
'animation: 3s "host-a_foo\\"", 3.5s "host-a_bar", 3.7s "host-a_ba\\"r";');
expect(result).toContain(
'animation: 4s "host-a_foo\\\\", 4.5s "host-a_bar", 4.7s "baz\\"";');
expect(result).toContain(
'animation: 5s "host-a_fo\\\\\\"o", 5.5s "host-a_bar", 5.7s "baz\\"";');
});
it('should handle the usage of single quotes escaping in multiple animation definitions in a single declaration',
() => {
const css = `
div {
animation: 1s 'foo', 1.5s 'bar';
animation: 2s 'fo\\'o', 2.5s 'bar';
animation: 3s 'foo\\'', 3.5s 'bar', 3.7s 'ba\\'r';
animation: 4s 'foo\\\\', 4.5s 'bar', 4.7s 'baz\\'';
animation: 5s 'fo\\\\\\'o', 5.5s 'bar', 5.7s 'baz\\'';
}
@keyframes foo {}
@keyframes 'fo\\'o' {}
@keyframes 'foo'' {}
@keyframes 'foo\\\\' {}
@keyframes "bar" {}
@keyframes 'ba\\'r' {}
@keyframes "fo\\\\\\'o" {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes host-a_foo {}');
expect(result).toContain('@keyframes \'host-a_fo\\\'o\' {}');
expect(result).toContain('@keyframes \'host-a_foo\'\' {}');
expect(result).toContain('@keyframes \'host-a_foo\\\\\' {}');
expect(result).toContain('@keyframes "host-a_bar" {}');
expect(result).toContain('@keyframes \'host-a_ba\\\'r\' {}');
expect(result).toContain(`@keyframes "host-a_fo\\\\\\'o" {}`);
expect(result).toContain('animation: 1s \'host-a_foo\', 1.5s \'host-a_bar\';');
expect(result).toContain('animation: 2s \'host-a_fo\\\'o\', 2.5s \'host-a_bar\';');
expect(result).toContain(
'animation: 3s \'host-a_foo\\\'\', 3.5s \'host-a_bar\', 3.7s \'host-a_ba\\\'r\';');
expect(result).toContain(
'animation: 4s \'host-a_foo\\\\\', 4.5s \'host-a_bar\', 4.7s \'baz\\\'\';');
expect(result).toContain(
'animation: 5s \'host-a_fo\\\\\\\'o\', 5.5s \'host-a_bar\', 5.7s \'baz\\\'\'');
});
it('should handle the usage of mixed single and double quotes escaping in multiple animation definitions in a single declaration',
() => {
const css = `
div {
animation: 1s 'f\\"oo', 1.5s "ba\\'r";
animation: 2s "fo\\"\\"o", 2.5s 'b\\\\"ar';
animation: 3s 'foo\\\\', 3.5s "b\\\\\\"ar", 3.7s 'ba\\'\\"\\'r';
animation: 4s 'fo\\'o', 4.5s 'b\\"ar\\"', 4.7s "baz\\'";
}
@keyframes 'f"oo' {}
@keyframes 'fo""o' {}
@keyframes 'foo\\\\' {}
@keyframes 'fo\\'o' {}
@keyframes 'ba\\'r' {}
@keyframes 'b\\\\"ar' {}
@keyframes 'b\\\\\\"ar' {}
@keyframes 'b"ar"' {}
@keyframes 'ba\\'\\"\\'r' {}
`;
const result = s(css, 'host-a');
expect(result).toContain(`@keyframes 'host-a_f"oo' {}`);
expect(result).toContain(`@keyframes 'host-a_fo""o' {}`);
expect(result).toContain('@keyframes \'host-a_foo\\\\\' {}');
expect(result).toContain('@keyframes \'host-a_fo\\\'o\' {}');
expect(result).toContain('@keyframes \'host-a_ba\\\'r\' {}');
expect(result).toContain(`@keyframes 'host-a_b\\\\"ar' {}`);
expect(result).toContain(`@keyframes 'host-a_b\\\\\\"ar' {}`);
expect(result).toContain(`@keyframes 'host-a_b"ar"' {}`);
expect(result).toContain(`@keyframes 'host-a_ba\\'\\"\\'r' {}`);
expect(result).toContain(`animation: 1s 'host-a_f\\"oo', 1.5s "host-a_ba\\'r";`);
expect(result).toContain(`animation: 2s "host-a_fo\\"\\"o", 2.5s 'host-a_b\\\\"ar';`);
expect(result).toContain(
`animation: 3s 'host-a_foo\\\\', 3.5s "host-a_b\\\\\\"ar", 3.7s 'host-a_ba\\'\\"\\'r';`);
expect(result).toContain(
`animation: 4s 'host-a_fo\\'o', 4.5s 'host-a_b\\"ar\\"', 4.7s "baz\\'";`);
});
it('should handle the usage of commas inside quotes', () => {
const css = `
div {
animation: 3s 'bar,, baz';
}
p {
animation-name: "bar,, baz", foo,'ease, linear , inherit', bar;
}
@keyframes 'foo' {}
@keyframes 'bar,, baz' {}
@keyframes 'ease, linear , inherit' {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes \'host-a_bar,, baz\' {}');
expect(result).toContain('animation: 3s \'host-a_bar,, baz\';');
expect(result).toContain(
`animation-name: "host-a_bar,, baz", host-a_foo,'host-a_ease, linear , inherit', bar;`);
});
it('should not ignore animation keywords when they are inside quotes', () => {
const css = `
div {
animation: 3s 'unset';
}
button {
animation: 5s "forwards" 1s forwards;
}
@keyframes unset {}
@keyframes forwards {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes host-a_unset {}');
expect(result).toContain('@keyframes host-a_forwards {}');
expect(result).toContain('animation: 3s \'host-a_unset\';');
expect(result).toContain('animation: 5s "host-a_forwards" 1s forwards;');
});
it('should handle css functions correctly', () => {
const css = `
div {
animation: foo 0.5s alternate infinite cubic-bezier(.17, .67, .83, .67);
}
button {
animation: calc(2s / 2) calc;
}
@keyframes foo {}
@keyframes cubic-bezier {}
@keyframes calc {}
`;
const result = s(css, 'host-a');
expect(result).toContain('@keyframes host-a_cubic-bezier {}');
expect(result).toContain('@keyframes host-a_calc {}');
expect(result).toContain(
'animation: host-a_foo 0.5s alternate infinite cubic-bezier(.17, .67, .83, .67);');
expect(result).toContain('animation: calc(2s / 2) host-a_calc;');
});
});

View file

@ -126,17 +126,6 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
expect(s(css, 'contenta')).toEqual(expected);
});
// Check that the browser supports unprefixed CSS animation
it('should handle keyframes rules', () => {
const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}';
expect(s(css, 'contenta')).toEqual(css);
});
it('should handle -webkit-keyframes rules', () => {
const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}';
expect(s(css, 'contenta')).toEqual(css);
});
it('should handle complicated selectors', () => {
expect(s('one::before {}', 'contenta')).toEqual('one[contenta]::before {}');
expect(s('one two {}', 'contenta')).toEqual('one[contenta] two[contenta] {}');