diff --git a/modules/@angular/compiler/src/shadow_css.ts b/modules/@angular/compiler/src/shadow_css.ts index f5fa08de52..c3db3148f2 100644 --- a/modules/@angular/compiler/src/shadow_css.ts +++ b/modules/@angular/compiler/src/shadow_css.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {ListWrapper} from './facade/collection'; import {StringWrapper, isBlank, isPresent} from './facade/lang'; /** @@ -52,7 +51,7 @@ import {StringWrapper, isBlank, isPresent} from './facade/lang'; background: red; } - * encapsultion: Styles defined within ShadowDOM, apply only to + * encapsulation: Styles defined within ShadowDOM, apply only to dom inside the ShadowDOM. Polymer uses one of two techniques to implement this feature. @@ -345,13 +344,13 @@ export class ShadowCss { private _scopeSelector( selector: string, scopeSelector: string, hostSelector: string, strict: boolean): string { return selector.split(',') - .map((part) => { return StringWrapper.split(part.trim(), _shadowDeepSelectors); }) + .map(part => part.trim().split(_shadowDeepSelectors)) .map((deepParts) => { const [shallowPart, ...otherParts] = deepParts; const applyScope = (shallowPart: string) => { if (this._selectorNeedsScoping(shallowPart, scopeSelector)) { - return strict && !StringWrapper.contains(shallowPart, _polyfillHostNoCombinator) ? - this._applyStrictSelectorScope(shallowPart, scopeSelector) : + return strict ? + this._applyStrictSelectorScope(shallowPart, scopeSelector, hostSelector) : this._applySelectorScope(shallowPart, scopeSelector, hostSelector); } else { return shallowPart; @@ -377,7 +376,7 @@ export class ShadowCss { private _applySelectorScope(selector: string, scopeSelector: string, hostSelector: string): string { - // Difference from webcomponentsjs: scopeSelector could not be an array + // Difference from webcomponents.js: scopeSelector could not be an array return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector); } @@ -395,38 +394,58 @@ export class ShadowCss { // return a selector with [name] suffix on each simple selector // e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */ - private _applyStrictSelectorScope(selector: string, scopeSelector: string): string { + private _applyStrictSelectorScope(selector: string, scopeSelector: string, hostSelector: string): + string { const isRe = /\[is=([^\]]*)\]/g; - scopeSelector = - StringWrapper.replaceAllMapped(scopeSelector, isRe, (m: any /** TODO #9100 */) => m[1]); - const splits = [' ', '>', '+', '~']; - let scoped = selector; + scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]); + const attrName = '[' + scopeSelector + ']'; - for (let i = 0; i < splits.length; i++) { - const sep = splits[i]; - const parts = scoped.split(sep); - scoped = parts - .map(p => { - // remove :host since it should be unnecessary - const t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, ''); - if (t.length > 0 && !ListWrapper.contains(splits, t) && - !StringWrapper.contains(t, attrName)) { - const m = t.match(/([^:]*)(:*)(.*)/); - if (m !== null) { - p = m[1] + attrName + m[2] + m[3]; - } - } - return p; - }) - .join(sep); + + const _scopeSelectorPart = (p: string) => { + var scopedP = p.trim(); + + if (scopedP.length == 0) { + return ''; + } + + if (p.indexOf(_polyfillHostNoCombinator) > -1) { + scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector); + } else { + // remove :host since it should be unnecessary + var t = p.replace(_polyfillHostRe, ''); + if (t.length > 0) { + const matches = t.match(/([^:]*)(:*)(.*)/); + if (matches !== null) { + scopedP = matches[1] + attrName + matches[2] + matches[3]; + } + } + } + + return scopedP; + }; + + const sep = /( |>|\+|~)\s*/g; + const scopeAfter = selector.indexOf(_polyfillHostNoCombinator); + + let scoped = ''; + let startIndex = 0; + let res: RegExpExecArray; + + while ((res = sep.exec(selector)) !== null) { + const separator = res[1]; + const part = selector.slice(startIndex, res.index).trim(); + // if a selector appears before :host-context it should not be shimmed as it + // matches on ancestor elements and not on elements in the host's shadow + const scopedPart = startIndex >= scopeAfter ? _scopeSelectorPart(part) : part; + scoped += `${scopedPart} ${separator} `; + startIndex = sep.lastIndex; } - return scoped; + return scoped + _scopeSelectorPart(selector.substring(startIndex)); } private _insertPolyfillHostInCssText(selector: string): string { - selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext); - selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost); - return selector; + return selector.replace(_colonHostContextRe, _polyfillHostContext) + .replace(_colonHostRe, _polyfillHost); } } const _cssContentNextSelectorRe = @@ -444,30 +463,28 @@ const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim'); const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim'); const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator'; const _shadowDOMSelectorsRe = [ - /::shadow/g, /::content/g, + /::shadow/g, + /::content/g, // Deprecated selectors - // TODO(vicb): see https://github.com/angular/clang-format/issues/16 - // clang-format off - /\/shadow-deep\//g, // former /deep/ - /\/shadow\//g, // former ::shadow - // clanf-format on + /\/shadow-deep\//g, + /\/shadow\//g, ]; const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)/g; const _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$'; -const _polyfillHostRe = new RegExp(_polyfillHost, 'im'); +const _polyfillHostRe = /-shadowcsshost/gim; const _colonHostRe = /:host/gim; const _colonHostContextRe = /:host-context/gim; const _commentRe = /\/\*\s*[\s\S]*?\*\//g; -function stripComments(input:string):string { +function stripComments(input: string): string { return StringWrapper.replaceAllMapped(input, _commentRe, (_: any /** TODO #9100 */) => ''); } // all comments except inline source mapping ("/* #sourceMappingURL= ... */") const _sourceMappingUrlRe = /[\s\S]*(\/\*\s*#\s*sourceMappingURL=[\s\S]+?\*\/)\s*$/; -function extractSourceMappingUrl(input:string):string { +function extractSourceMappingUrl(input: string): string { const matcher = input.match(_sourceMappingUrlRe); return matcher ? matcher[1] : ''; } @@ -479,38 +496,39 @@ const CLOSE_CURLY = '}'; const BLOCK_PLACEHOLDER = '%BLOCK%'; export class CssRule { - constructor(public selector:string, public content:string) {} + constructor(public selector: string, public content: string) {} } -export function processRules(input:string, ruleCallback:Function):string { +export function processRules(input: string, ruleCallback: Function): string { const inputWithEscapedBlocks = escapeBlocks(input); let nextBlockIndex = 0; - return StringWrapper.replaceAllMapped(inputWithEscapedBlocks.escapedString, _ruleRe, function(m: any /** TODO #9100 */) { - const selector = m[2]; - let content = ''; - let suffix = m[4]; - let contentPrefix = ''; - if (isPresent(m[4]) && m[4].startsWith('{'+BLOCK_PLACEHOLDER)) { - content = inputWithEscapedBlocks.blocks[nextBlockIndex++]; - suffix = m[4].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 StringWrapper.replaceAllMapped( + inputWithEscapedBlocks.escapedString, _ruleRe, function(m: any /** TODO #9100 */) { + const selector = m[2]; + let content = ''; + let suffix = m[4]; + let contentPrefix = ''; + if (isPresent(m[4]) && m[4].startsWith('{' + BLOCK_PLACEHOLDER)) { + content = inputWithEscapedBlocks.blocks[nextBlockIndex++]; + suffix = m[4].substring(BLOCK_PLACEHOLDER.length + 1); + contentPrefix = '{'; + } + const rule = ruleCallback(new CssRule(selector, content)); + return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`; + }); } class StringWithEscapedBlocks { - constructor(public escapedString:string, public blocks:string[]) {} + constructor(public escapedString: string, public blocks: string[]) {} } -function escapeBlocks(input:string):StringWithEscapedBlocks { +function escapeBlocks(input: string): StringWithEscapedBlocks { const inputParts = StringWrapper.split(input, _curlyRe); const resultParts: any[] /** TODO #9100 */ = []; const escapedBlocks: any[] /** TODO #9100 */ = []; let bracketCount = 0; let currentBlockParts: any[] /** TODO #9100 */ = []; - for (let partIndex = 0; partIndex { expect(s('', 'a')).toEqual(''); }); @@ -99,15 +97,17 @@ export function main() { it('should handle :host', () => { expect(s(':host {}', 'a', 'a-host')).toEqual('[a-host] {}'); + expect(s(':host(.x,.y) {}', 'a', 'a-host')).toEqual('[a-host].x, [a-host].y {}'); + expect(s(':host(.x,.y) > .z {}', 'a', 'a-host')) - .toEqual('[a-host].x > .z, [a-host].y > .z {}'); + .toEqual('[a-host].x > .z[a], [a-host].y > .z[a] {}'); }); it('should handle :host-context', () => { expect(s(':host-context(.x) {}', 'a', 'a-host')).toEqual('[a-host].x, .x [a-host] {}'); expect(s(':host-context(.x) > .y {}', 'a', 'a-host')) - .toEqual('[a-host].x > .y, .x [a-host] > .y {}'); + .toEqual('[a-host].x > .y[a], .x [a-host] > .y[a] {}'); }); it('should support polyfill-next-selector', () => { @@ -120,10 +120,10 @@ export function main() { it('should support polyfill-unscoped-rule', () => { let css = s('polyfill-unscoped-rule {content: \'#menu > .bar\';color: blue;}', 'a'); - expect(StringWrapper.contains(css, '#menu > .bar {;color:blue;}')).toBeTruthy(); + expect(css).toContain('#menu > .bar {;color:blue;}'); css = s('polyfill-unscoped-rule {content: "#menu > .bar";color: blue;}', 'a'); - expect(StringWrapper.contains(css, '#menu > .bar {;color:blue;}')).toBeTruthy(); + expect(css).toContain('#menu > .bar {;color:blue;}'); }); it('should support multiple instances polyfill-unscoped-rule', () => { @@ -131,16 +131,16 @@ export function main() { s('polyfill-unscoped-rule {content: \'foo\';color: blue;}' + 'polyfill-unscoped-rule {content: \'bar\';color: blue;}', 'a'); - expect(StringWrapper.contains(css, 'foo {;color:blue;}')).toBeTruthy(); - expect(StringWrapper.contains(css, 'bar {;color:blue;}')).toBeTruthy(); + expect(css).toContain('foo {;color:blue;}'); + expect(css).toContain('bar {;color:blue;}'); }); it('should support polyfill-rule', () => { let css = s('polyfill-rule {content: \':host.foo .bar\';color: blue;}', 'a', 'a-host'); - expect(css).toEqual('[a-host].foo .bar {;color:blue;}'); + expect(css).toEqual('[a-host].foo .bar[a] {;color:blue;}'); css = s('polyfill-rule {content: ":host.foo .bar";color:blue;}', 'a', 'a-host'); - expect(css).toEqual('[a-host].foo .bar {;color:blue;}'); + expect(css).toEqual('[a-host].foo .bar[a] {;color:blue;}'); }); it('should handle ::shadow', () => {