From 58e8f4b7087eacc6bba07b42a2ec76e011aa340a Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Wed, 27 Apr 2022 14:55:50 -0700 Subject: [PATCH] Revert "fix(compiler): scope css keyframes in emulated view encapsulation (#42608)" (#45786) This reverts commit 4d6a1d672210219328b33f4f96210870563066ee. PR Close #45786 --- packages/compiler/src/shadow_css.ts | 360 +------------ .../test/shadow_css/keyframes_spec.ts | 510 ------------------ .../test/{shadow_css => }/shadow_css_spec.ts | 11 + 3 files changed, 37 insertions(+), 844 deletions(-) delete mode 100644 packages/compiler/test/shadow_css/keyframes_spec.ts rename packages/compiler/test/{shadow_css => }/shadow_css_spec.ts (98%) diff --git a/packages/compiler/src/shadow_css.ts b/packages/compiler/src/shadow_css.ts index 318dc94e9d5..9c793c2a358 100644 --- a/packages/compiler/src/shadow_css.ts +++ b/packages/compiler/src/shadow_css.ts @@ -7,27 +7,7 @@ */ /** - * 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. + * This file is a port of shadowCSS from webcomponents.js to TypeScript. * * Please make sure to keep to edits in sync with the source file. * @@ -151,6 +131,7 @@ const animationKeywords = new Set([ 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; @@ -176,187 +157,6 @@ 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(); - 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): 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 { - 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): 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. @@ -417,7 +217,6 @@ 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; @@ -843,39 +642,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 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'); +const QUOTE_PAIRS = new Map([[`"`, `"`], [`'`, `'`]]); export class CssRule { constructor(public selector: string, public content: string) {} } export function processRules(input: string, ruleCallback: (rule: CssRule) => CssRule): string { - const escaped = escapeInStrings(input); - const inputWithEscapedBlocks = escapeBlocks(escaped, CONTENT_PAIRS, BLOCK_PLACEHOLDER); + const inputWithEscapedQuotes = escapeBlocks(input, QUOTE_PAIRS, QUOTE_PLACEHOLDER); + const inputWithEscapedBlocks = + escapeBlocks(inputWithEscapedQuotes.escapedString, CONTENT_PAIRS, BLOCK_PLACEHOLDER); let nextBlockIndex = 0; - 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); + 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++]); } class StringWithEscapedBlocks { @@ -923,113 +722,6 @@ 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()`. diff --git a/packages/compiler/test/shadow_css/keyframes_spec.ts b/packages/compiler/test/shadow_css/keyframes_spec.ts deleted file mode 100644 index 7e2ccfa3c93..00000000000 --- a/packages/compiler/test/shadow_css/keyframes_spec.ts +++ /dev/null @@ -1,510 +0,0 @@ -/** - * @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;'); - }); -}); diff --git a/packages/compiler/test/shadow_css/shadow_css_spec.ts b/packages/compiler/test/shadow_css_spec.ts similarity index 98% rename from packages/compiler/test/shadow_css/shadow_css_spec.ts rename to packages/compiler/test/shadow_css_spec.ts index edc14bcb42a..90f50d876e3 100644 --- a/packages/compiler/test/shadow_css/shadow_css_spec.ts +++ b/packages/compiler/test/shadow_css_spec.ts @@ -126,6 +126,17 @@ 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] {}');