feat(ShadowCss): port implementation from webcomponent.js

This commit is contained in:
Victor Berchet 2015-02-18 10:06:31 +01:00 committed by Misko Hevery
parent 89b3995756
commit d67f0299cd
6 changed files with 715 additions and 1 deletions

View File

@ -0,0 +1,555 @@
import {
StyleElement,
DOM,
CssRule,
CssKeyframesRule,
CSSRuleWrapper
} from 'angular2/src/facade/dom';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {
StringWrapper,
RegExp,
RegExpWrapper,
RegExpMatcherWrapper,
isPresent,
isBlank,
BaseException,
int
} from 'angular2/src/facade/lang';
/**
* This file is a port of shadowCSS from webcomponents.js to AtScript.
*
* Please make sure to keep to edits in sync with the source file.
*
* Source: https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
*
* The original file level comment is reproduced below
*/
/*
This is a limited shim for ShadowDOM css styling.
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
The intention here is to support only the styling features which can be
relatively simply implemented. The goal is to allow users to avoid the
most obvious pitfalls and do so without compromising performance significantly.
For ShadowDOM styling that's not covered here, a set of best practices
can be provided that should allow users to accomplish more complex styling.
The following is a list of specific ShadowDOM styling features and a brief
discussion of the approach used to shim.
Shimmed features:
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
element using the :host rule. To shim this feature, the :host styles are
reformatted and prefixed with a given scope name and promoted to a
document level stylesheet.
For example, given a scope name of .foo, a rule like this:
:host {
background: red;
}
}
becomes:
.foo {
background: red;
}
* encapsultion: Styles defined within ShadowDOM, apply only to
dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
this feature.
By default, rules are prefixed with the host element tag name
as a descendant selector. This ensures styling does not leak out of the 'top'
of the element's ShadowDOM. For example,
div {
font-weight: bold;
}
becomes:
x-foo div {
font-weight: bold;
}
becomes:
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
selectors are scoped by adding an attribute selector suffix to each
simple selector that contains the host element tag name. Each element
in the element's ShadowDOM template is also given the scope attribute.
Thus, these rules match only elements that have the scope attribute.
For example, given a scope name of x-foo, a rule like this:
div {
font-weight: bold;
}
becomes:
div[x-foo] {
font-weight: bold;
}
Note that elements that are dynamically added to a scope must have the scope
selector added to them manually.
* upper/lower bound encapsulation: Styles which are defined outside a
shadowRoot should not cross the ShadowDOM boundary and should not apply
inside a shadowRoot.
This styling behavior is not emulated. Some possible ways to do this that
were rejected due to complexity and/or performance concerns include: (1) reset
every possible property for every possible selector for a given scope name;
(2) re-implement css in javascript.
As an alternative, users should make sure to use selectors
specific to the scope in which they are working.
* ::distributed: This behavior is not emulated. It's often not necessary
to style the contents of a specific insertion point and instead, descendants
of the host element can be styled selectively. Users can also create an
extra node around an insertion point and style that node's contents
via descendent selectors. For example, with a shadowRoot like this:
<style>
::content(div) {
background: red;
}
</style>
<content></content>
could become:
<style>
/ *@polyfill .content-container div * /
::content(div) {
background: red;
}
</style>
<div class="content-container">
<content></content>
</div>
Note the use of @polyfill in the comment above a ShadowDOM specific style
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;
constructor() {
this.strictStyling = true;
}
/*
* Shim a style element with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimStyle(style: StyleElement, selector: string): string {
var cssText = DOM.getText(style);
return this.shimCssText(cssText, selector);
}
/*
* Shim some cssText with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimCssText(cssText: string, selector: string): string {
cssText = this._insertDirectives(cssText);
return this._scopeCssText(cssText, selector);
}
_insertDirectives(cssText: string): string {
cssText = this._insertPolyfillDirectivesInCssText(cssText);
return this._insertPolyfillRulesInCssText(cssText);
}
/*
* Process styles to convert native ShadowDOM rules that will trip
* up the css parser; we rely on decorating the stylesheet with inert rules.
*
* For example, we convert this rule:
*
* polyfill-next-selector { content: ':host menu-item'; }
* ::content menu-item {
*
* to this:
*
* scopeName menu-item {
*
**/
_insertPolyfillDirectivesInCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
return StringWrapper.replaceAllMapped(cssText, _cssContentNextSelectorRe, function(m) {
return m[1] + '{';
});
}
/*
* Process styles to add rules which will only apply under the polyfill
*
* For example, we convert this rule:
*
* polyfill-rule {
* content: ':host menu-item';
* ...
* }
*
* to this:
*
* scopeName menu-item {...}
*
**/
_insertPolyfillRulesInCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
return StringWrapper.replaceAllMapped(cssText, _cssContentRuleRe, function(m) {
var rule = m[0];
rule = StringWrapper.replace(rule, m[1], '');
rule = StringWrapper.replace(rule, m[2], '');
return m[3] + rule;
});
}
/* Ensure styles are scoped. Pseudo-scoping takes a rule like:
*
* .foo {... }
*
* and converts this to
*
* scopeName .foo { ... }
*/
_scopeCssText(cssText: string, scopeSelector: string): string {
var unscoped = this._extractUnscopedRulesFromCssText(cssText);
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
cssText = this._convertShadowDOMSelectors(cssText);
if (isPresent(scopeSelector)) {
_withCssRules(cssText, (rules) => {
cssText = this._scopeRules(rules, scopeSelector);
});
}
cssText = cssText + '\n' + unscoped;
return cssText.trim();
}
/*
* Process styles to add rules which will only apply under the polyfill
* and do not process via CSSOM. (CSSOM is destructive to rules on rare
* occasions, e.g. -webkit-calc on Safari.)
* For example, we convert this rule:
*
* @polyfill-unscoped-rule {
* content: 'menu-item';
* ... }
*
* to this:
*
* menu-item {...}
*
**/
_extractUnscopedRulesFromCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
var r = '', m;
var matcher = RegExpWrapper.matcher(_cssContentUnscopedRuleRe, cssText);
while (isPresent(m = RegExpMatcherWrapper.next(matcher))) {
var rule = m[0];
rule = StringWrapper.replace(rule, m[2], '');
rule = StringWrapper.replace(rule, m[1], m[3]);
r = rule + '\n\n';
}
return r;
}
/*
* convert a rule like :host(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar
*/
_convertColonHost(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostRe,
this._colonHostPartReplacer);
}
/*
* convert a rule like :host-context(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar, .foo scopeName > .bar { }
*
* and
*
* :host-context(.foo:host) .bar { ... }
*
* to
*
* scopeName.foo .bar { ... }
*/
_convertColonHostContext(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostContextRe,
this._colonHostContextPartReplacer);
}
_convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
// p1 = :host, p2 = contents of (), p3 rest of rule
return StringWrapper.replaceAllMapped(cssText, regExp, function(m) {
if (isPresent(m[2])) {
var parts = m[2].split(','), r = [];
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
if (isBlank(p)) break;
p = p.trim();
ListWrapper.push(r, partReplacer(_polyfillHostNoCombinator, p, m[3]));
}
return r.join(',');
} else {
return _polyfillHostNoCombinator + m[3];
}
});
}
_colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
if (StringWrapper.contains(part, _polyfillHost)) {
return this._colonHostPartReplacer(host, part, suffix);
} else {
return host + part + suffix + ', ' + part + ' ' + host + suffix;
}
}
_colonHostPartReplacer(host: string, part: string, suffix: string): string {
return host + StringWrapper.replace(part, _polyfillHost, '') + suffix;
}
/*
* Convert combinators like ::shadow and pseudo-elements like ::content
* by replacing with space.
*/
_convertShadowDOMSelectors(cssText: string): string {
for (var i = 0; i < _shadowDOMSelectorsRe.length; i++) {
cssText = StringWrapper.replaceAll(cssText, _shadowDOMSelectorsRe[i], ' ');
}
return cssText;
}
// change a selector like 'div' to 'name div'
_scopeRules(cssRules, scopeSelector: string): string {
var cssText = '';
if (isPresent(cssRules)) {
for (var i = 0; i < cssRules.length; i++) {
var rule = cssRules[i];
if (CSSRuleWrapper.isStyleRule(rule) || CSSRuleWrapper.isPageRule(rule)) {
cssText += this._scopeSelector(rule.selectorText, scopeSelector,
this.strictStyling) + ' {\n';
cssText += this._propertiesFromRule(rule) + '\n}\n\n';
} else if (CSSRuleWrapper.isMediaRule(rule)) {
cssText += '@media ' + rule.media.mediaText + ' {\n';
cssText += this._scopeRules(rule.cssRules, scopeSelector);
cssText += '\n}\n\n';
} else {
// KEYFRAMES_RULE in IE throws when we query cssText
// when it contains a -webkit- property.
// if this happens, we fallback to constructing the rule
// from the CSSRuleSet
// https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception
try {
if (isPresent(rule.cssText)) {
cssText += rule.cssText + '\n\n';
}
} catch(x) {
if (CSSRuleWrapper.isKeyframesRule(rule) && isPresent(rule.cssRules)) {
cssText += this._ieSafeCssTextFromKeyFrameRule(rule);
}
}
}
}
}
return cssText;
}
_ieSafeCssTextFromKeyFrameRule(rule: CssKeyframesRule): string {
var cssText = '@keyframes ' + rule.name + ' {';
for (var i = 0; i < rule.cssRules.length; i++) {
var r = rule.cssRules[i];
cssText += ' ' + r.keyText + ' {' + r.style.cssText + '}';
}
cssText += ' }';
return cssText;
}
_scopeSelector(selector: string, scopeSelector: string, strict: boolean): string {
var r = [], parts = selector.split(',');
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
p = p.trim();
if (this._selectorNeedsScoping(p, scopeSelector)) {
p = strict && !StringWrapper.contains(p, _polyfillHostNoCombinator) ?
this._applyStrictSelectorScope(p, scopeSelector) :
this._applySelectorScope(p, scopeSelector);
}
ListWrapper.push(r, p);
}
return r.join(', ');
}
_selectorNeedsScoping(selector: string, scopeSelector: string): boolean {
var re = this._makeScopeMatcher(scopeSelector);
return !isPresent(RegExpWrapper.firstMatch(re, selector));
}
_makeScopeMatcher(scopeSelector: string): RegExp {
var lre = RegExpWrapper.create('\\[');
var rre = RegExpWrapper.create('\\]');
scopeSelector = StringWrapper.replaceAll(scopeSelector, lre, '\\[');
scopeSelector = StringWrapper.replaceAll(scopeSelector, rre, '\\]');
return RegExpWrapper.create('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
}
_applySelectorScope(selector: string, scopeSelector: string): string {
// Difference from webcomponentsjs: scopeSelector could not be an array
return this._applySimpleSelectorScope(selector, scopeSelector);
}
// scope via name and [is=name]
_applySimpleSelectorScope(selector: string, scopeSelector: string): string {
if (isPresent(RegExpWrapper.firstMatch(_polyfillHostRe, selector))) {
selector = StringWrapper.replace(selector, _polyfillHostNoCombinator, scopeSelector);
return StringWrapper.replaceAll(selector, _polyfillHostRe, scopeSelector + ' ');
} else {
return scopeSelector + ' ' + selector;
}
}
// return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
_applyStrictSelectorScope(selector: string, scopeSelector: string): string {
var isRe = RegExpWrapper.create('\\[is=([^\\]]*)\\]');
scopeSelector = StringWrapper.replaceAllMapped(scopeSelector, isRe, (m) => m[1]);
var splits = [' ', '>', '+', '~'],
scoped = selector,
attrName = '[' + scopeSelector + ']';
for (var i = 0; i < splits.length; i++) {
var sep = splits[i];
var parts = scoped.split(sep);
scoped = ListWrapper.map(parts, function(p) {
// remove :host since it should be unnecessary
var t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, '');
if (t.length > 0 &&
!ListWrapper.contains(splits, t) &&
!StringWrapper.contains(t, attrName)) {
var re = RegExpWrapper.create('([^:]*)(:*)(.*)');
var m = RegExpWrapper.firstMatch(re, t);
if (isPresent(m)) {
p = m[1] + attrName + m[2] + m[3];
}
}
return p;
}).join(sep);
}
return scoped;
}
_insertPolyfillHostInCssText(selector: string): string {
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext);
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
return selector;
}
_propertiesFromRule(rule): string {
var cssText = rule.style.cssText;
// TODO(sorvell): Safari cssom incorrectly removes quotes from the content
// property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
// don't replace attr rules
var attrRe = RegExpWrapper.create('[\'"]+|attr');
if (rule.style.content.length > 0 &&
!isPresent(RegExpWrapper.firstMatch(attrRe, rule.style.content))) {
var contentRe = RegExpWrapper.create('content:[^;]*;');
cssText = StringWrapper.replaceAll(cssText, contentRe, 'content: \'' +
rule.style.content + '\';');
}
// TODO(sorvell): we can workaround this issue here, but we need a list
// of troublesome properties to fix https://github.com/Polymer/platform/issues/53
//
// inherit rules can be omitted from cssText
// TODO(sorvell): remove when Blink bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=358273
//var style = rule.style;
//for (var i = 0; i < style.length; i++) {
// var name = style.item(i);
// var value = style.getPropertyValue(name);
// if (value == 'initial') {
// cssText += name + ': initial; ';
// }
//}
return cssText;
}
}
var _cssContentNextSelectorRe = RegExpWrapper.create(
'polyfill-next-selector[^}]*content:[\\s]*?[\'"](.*?)[\'"][;\\s]*}([^{]*?){', 'im');
var _cssContentRuleRe = RegExpWrapper.create(
'(polyfill-rule)[^}]*(content:[\\s]*[\'"](.*?)[\'"])[;\\s]*[^}]*}', 'im');
var _cssContentUnscopedRuleRe = RegExpWrapper.create(
'(polyfill-unscoped-rule)[^}]*(content:[\\s]*[\'"](.*?)[\'"])[;\\s]*[^}]*}', 'im');
var _polyfillHost = '-shadowcsshost';
// note: :host-context pre-processed to -shadowcsshostcontext.
var _polyfillHostContext = '-shadowcsscontext';
var _parenSuffix = ')(?:\\((' +
'(?:\\([^)(]*\\)|[^)(]*)+?' +
')\\))?([^,{]*)';
var _cssColonHostRe = RegExpWrapper.create('(' + _polyfillHost + _parenSuffix, 'im');
var _cssColonHostContextRe = RegExpWrapper.create('(' + _polyfillHostContext + _parenSuffix, 'im');
var _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
var _shadowDOMSelectorsRe = [
RegExpWrapper.create('/shadow/'),
RegExpWrapper.create('/shadow-deep/'),
RegExpWrapper.create('::shadow'),
RegExpWrapper.create('/deep/'),
RegExpWrapper.create('::content'),
];
var _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
var _colonHostRe = RegExpWrapper.create(':host', 'im');
var _colonHostContextRe = RegExpWrapper.create(':host-context', 'im');
function _cssTextToStyle(cssText: string) {
return DOM.createStyleElement(cssText);
}
function _cssToRules(cssText: string) {
var style = _cssTextToStyle(cssText);
DOM.appendChild(DOM.defaultDoc().head, style);
var rules = [];
if (isPresent(style.sheet)) {
// TODO(sorvell): Firefox throws when accessing the rules of a stylesheet
// with an @import
// https://bugzilla.mozilla.org/show_bug.cgi?id=625013
try {
rules = style.sheet.cssRules;
} catch(e) {
//
}
} else {
// console.warn('sheet not found', style);
}
DOM.remove(style);
return rules;
}
function _withCssRules(cssText: string, callback: Function) {
// Difference from webcomponentjs: remove the workaround for an old bug in Chrome
if (isBlank(callback)) return;
var rules = _cssToRules(cssText);
callback(rules);
}

View File

@ -4,6 +4,8 @@ import 'dart:html';
import 'dart:js' show JsObject, context;
export 'dart:html' show
CssRule,
CssKeyframesRule,
document,
DocumentFragment,
Element,
@ -109,6 +111,9 @@ class DOM {
if (doc == null) doc = document;
return doc.createElement(tagName);
}
static createTextNode(String text, [HtmlDocument doc = null]) {
return new Text(text);
}
static createScriptTag(String attrName, String attrValue,
[HtmlDocument doc = null]) {
if (doc == null) doc = document;
@ -183,3 +188,10 @@ class DOM {
static bool isElementNode(Node node) =>
node.nodeType == Node.ELEMENT_NODE;
}
class CSSRuleWrapper {
static isPageRule(CssRule rule) => rule is CssPageRule;
static isStyleRule(CssRule rule) => rule is CssStyleRule;
static isMediaRule(CssRule rule) => rule is CssMediaRule;
static isKeyframesRule(CssRule rule) => rule is CssKeyframesRule;
}

View File

@ -11,6 +11,8 @@ export var StyleElement = window.HTMLStyleElement;
export var document = window.document;
export var location = window.location;
export var gc = window.gc ? () => window.gc() : () => null;
export var CssRule = window.CSSRule;
export var CssKeyframesRule = window.CSSKeyframesRule;
export class DOM {
static query(selector) {
@ -129,6 +131,9 @@ export class DOM {
static createElement(tagName, doc=document) {
return doc.createElement(tagName);
}
static createTextNode(text: string, doc=document) {
return doc.createTextNode(text);
}
static createScriptTag(attrName:string, attrValue:string, doc=document) {
var el = doc.createElement("SCRIPT");
el.setAttribute(attrName, attrValue);
@ -215,3 +220,18 @@ export class DOM {
return node.nodeType === Node.ELEMENT_NODE;
}
}
export class CSSRuleWrapper {
static isPageRule(rule) {
return rule.type === CSSRule.PAGE_RULE;
}
static isStyleRule(rule) {
return rule.type === CSSRule.STYLE_RULE;
}
static isMediaRule(rule) {
return rule.type === CSSRule.MEDIA_RULE;
}
static isKeyframesRule(rule) {
return rule.type === CSSRule.KEYFRAMES_RULE;
}
}

View File

@ -59,6 +59,10 @@ class StringWrapper {
return s == s2;
}
static String replace(String s, Pattern from, String replace) {
return s.replaceFirst(from, replace);
}
static String replaceAll(String s, RegExp from, String replace) {
return s.replaceAll(from, replace);
}

View File

@ -77,6 +77,14 @@ export class StringWrapper {
return s === s2;
}
static replace(s:string, from , replace:string): string {
if (typeof(from) === "string") {
return s.replace(from, replace);
} else {
return s.replace(from.single, replace);
}
}
static replaceAll(s:string, from:RegExp, replace:string):string {
return s.replace(from.multiple, replace);
}
@ -91,6 +99,9 @@ export class StringWrapper {
static replaceAllMapped(s:string, from:RegExp, cb:Function): string {
return s.replace(from.multiple, function(...matches) {
// Remove offset & string from the result array
matches.splice(-2, 2);
// The callback receives match, p1, ..., pn
return cb(matches);
});
}
@ -171,7 +182,7 @@ export class NumberWrapper {
}
}
var RegExp;
export var RegExp;
if (assertionsEnabled_) {
RegExp = assert.define('RegExp', function(obj) {
assert(obj).is(assert.structure({

View File

@ -0,0 +1,112 @@
import {describe, beforeEach, it, expect, ddescribe, iit, SpyObject, el} from 'angular2/test_lib';
import {ShadowCss} from 'angular2/src/core/compiler/shadow_dom_emulation/shadow_css';
import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
export function main() {
describe('ShadowCss', function() {
function s(css: string, tag:string) {
var shadowCss = new ShadowCss();
var shim = shadowCss.shimCssText(css, tag);
var nlRegexp = RegExpWrapper.create('\\n');
return StringWrapper.replaceAll(shim, nlRegexp, '');
}
it('should handle empty string', () => {
expect(s('', 'a')).toEqual('');
});
it('should add an attribute to every rule', () => {
var css = 'one {color: red;}two {color: red;}';
var expected = 'one[a] {color: red;}two[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should hanlde invalid css', () => {
var css = 'one {color: red;}garbage';
var expected = 'one[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should add an attribute to every selector', () => {
var css = 'one, two {color: red;}';
var expected = 'one[a], two[a] {color: red;}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules', () => {
var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}}';
var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle media rules with simple rules', () => {
var css = '@media screen and (max-width: 800px) {div {font-size: 50px;}} div {}';
var expected = '@media screen and (max-width: 800px) {div[a] {font-size: 50px;}}div[a] {}';
expect(s(css, 'a')).toEqual(expected);
});
it('should handle complicated selectors', () => {
expect(s('one::before {}', 'a')).toEqual('one[a]::before {}');
expect(s('one two {}', 'a')).toEqual('one[a] two[a] {}');
expect(s('one>two {}', 'a')).toEqual('one[a] > two[a] {}');
expect(s('one+two {}', 'a')).toEqual('one[a] + two[a] {}');
expect(s('one~two {}', 'a')).toEqual('one[a] ~ two[a] {}');
expect(s('.one.two > three {}', 'a')).toEqual('.one.two[a] > three[a] {}');
expect(s('one[attr="value"] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr=value] {}', 'a')).toEqual('one[attr="value"][a] {}');
expect(s('one[attr^="value"] {}', 'a')).toEqual('one[attr^="value"][a] {}');
expect(s('one[attr$="value"] {}', 'a')).toEqual('one[attr$="value"][a] {}');
expect(s('one[attr*="value"] {}', 'a')).toEqual('one[attr*="value"][a] {}');
expect(s('one[attr|="value"] {}', 'a')).toEqual('one[attr|="value"][a] {}');
expect(s('one[attr] {}', 'a')).toEqual('one[attr][a] {}');
expect(s('[is="one"] {}', 'a')).toEqual('[is="one"][a] {}');
});
it('should handle :host', () => {
expect(s(':host {}', 'a')).toEqual('a {}');
expect(s(':host(.x,.y) {}', 'a')).toEqual('a.x, a.y {}');
expect(s(':host(.x,.y) > .z {}', 'a')).toEqual('a.x > .z, a.y > .z {}');
});
it('should handle :host-context', () => {
expect(s(':host-context(.x) {}', 'a')).toEqual('a.x, .x a {}');
expect(s(':host-context(.x) > .y {}', 'a')).toEqual('a.x > .y, .x a > .y {}');
});
it('should support polyfill-next-selector', () => {
var css = s("polyfill-next-selector {content: 'x > y'} z {}", 'a');
expect(css).toEqual('x[a] > y[a] {}');
css = s('polyfill-next-selector {content: "x > y"} z {}', 'a');
expect(css).toEqual('x[a] > y[a] {}');
});
it('should support polyfill-unscoped-rule', () => {
var css = s("polyfill-unscoped-rule {content: '#menu > .bar';background: blue;}", 'a');
expect(StringWrapper.contains(css, '#menu > .bar {;background: blue;}')).toBeTruthy();
css = s('polyfill-unscoped-rule {content: "#menu > .bar";background: blue;}', 'a');
expect(StringWrapper.contains(css, '#menu > .bar {;background: blue;}')).toBeTruthy();
});
it('should support polyfill-rule', () => {
var css = s("polyfill-rule {content: ':host.foo .bar';background: blue;}", 'a');
expect(css).toEqual('a.foo .bar {background: blue;}');
css = s('polyfill-rule {content: ":host.foo .bar";background: blue;}', 'a');
expect(css).toEqual('a.foo .bar {background: blue;}');
});
it('should handle ::shadow', () => {
var css = s('x::shadow > y {}', 'a');
expect(css).toEqual('x[a] > y[a] {}');
});
it('should handle /deep/', () => {
var css = s('x /deep/ y {}', 'a');
expect(css).toEqual('x[a] y[a] {}');
});
});
}