fix(ShadowCss): fix attribute selectors in :host and :host-context (#12056)

Fix a regression introduced in #11917 while fixing #6249
This commit is contained in:
Victor Berchet 2016-10-04 15:40:31 -07:00 committed by Chuck Jazdzewski
parent 43d3a84df3
commit a63359689f
2 changed files with 80 additions and 49 deletions

View File

@ -209,7 +209,8 @@ export class ShadowCss {
* scopeName .foo { ... } * scopeName .foo { ... }
*/ */
private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string { private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
const unscoped = this._extractUnscopedRulesFromCssText(cssText); const unscopedRules = this._extractUnscopedRulesFromCssText(cssText);
// replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
cssText = this._insertPolyfillHostInCssText(cssText); cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText); cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText); cssText = this._convertColonHostContext(cssText);
@ -217,7 +218,7 @@ export class ShadowCss {
if (scopeSelector) { if (scopeSelector) {
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector); cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
} }
cssText = cssText + '\n' + unscoped; cssText = cssText + '\n' + unscopedRules;
return cssText.trim(); return cssText.trim();
} }
@ -280,15 +281,15 @@ export class ShadowCss {
} }
private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string { private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
// m[1] = :host, m[2] = contents of (), m[3] rest of rule // m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
return cssText.replace(regExp, function(...m: string[]) { return cssText.replace(regExp, function(...m: string[]) {
if (m[2]) { if (m[2]) {
const parts = m[2].split(','); const parts = m[2].split(',');
const r: string[] = []; const r: string[] = [];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
let p = parts[i]; let p = parts[i].trim();
if (!p) break; if (!p) break;
r.push(partReplacer(_polyfillHostNoCombinator, p.trim(), m[3])); r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
} }
return r.join(','); return r.join(',');
} else { } else {
@ -314,8 +315,7 @@ export class ShadowCss {
* by replacing with space. * by replacing with space.
*/ */
private _convertShadowDOMSelectors(cssText: string): string { private _convertShadowDOMSelectors(cssText: string): string {
return _shadowDOMSelectorsRe.reduce( return _shadowDOMSelectorsRe.reduce((result, pattern) => result.replace(pattern, ' '), cssText);
(result, pattern) => { return result.replace(pattern, ' '); }, cssText);
} }
// change a selector like 'div' to 'name div' // change a selector like 'div' to 'name div'
@ -420,38 +420,38 @@ export class ShadowCss {
return scopedP; return scopedP;
}; };
const sep = /( |>|\+|~(?!=)|\[|\])\s*/g; let attrSelectorIndex = 0;
const scopeAfter = selector.indexOf(_polyfillHostNoCombinator); const attrSelectors: string[] = [];
let scoped = ''; // replace attribute selectors with placeholders to avoid issue with white space being treated
// as separator
selector = selector.replace(/\[[^\]]*\]/g, (attrSelector) => {
const replaceBy = `__attr_sel_${attrSelectorIndex}__`;
attrSelectors.push(attrSelector);
attrSelectorIndex++;
return replaceBy;
});
let scopedSelector = '';
let startIndex = 0; let startIndex = 0;
let res: RegExpExecArray; let res: RegExpExecArray;
let inAttributeSelector: boolean = false; const sep = /( |>|\+|~(?!=))\s*/g;
const scopeAfter = selector.indexOf(_polyfillHostNoCombinator);
while ((res = sep.exec(selector)) !== null) { while ((res = sep.exec(selector)) !== null) {
const separator = res[1]; const separator = res[1];
if (separator === '[') { const part = selector.slice(startIndex, res.index).trim();
inAttributeSelector = true; // if a selector appears before :host-context it should not be shimmed as it
scoped += selector.slice(startIndex, res.index).trim() + '['; // matches on ancestor elements and not on elements in the host's shadow
startIndex = sep.lastIndex; const scopedPart = startIndex >= scopeAfter ? _scopeSelectorPart(part) : part;
} scopedSelector += `${scopedPart} ${separator} `;
if (!inAttributeSelector) { startIndex = sep.lastIndex;
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;
} else if (separator === ']') {
const part = selector.slice(startIndex, res.index).trim() + ']';
const scopedPart = startIndex >= scopeAfter ? _scopeSelectorPart(part) : part;
scoped += `${scopedPart} `;
startIndex = sep.lastIndex;
inAttributeSelector = false;
}
} }
return scoped + _scopeSelectorPart(selector.substring(startIndex)); scopedSelector += _scopeSelectorPart(selector.substring(startIndex));
// replace the placeholders with their original values
return scopedSelector.replace(/__attr_sel_(\d+)__/g, (ph, index) => attrSelectors[+index]);
} }
private _insertPolyfillHostInCssText(selector: string): string { private _insertPolyfillHostInCssText(selector: string): string {

View File

@ -108,30 +108,61 @@ export function main() {
expect(s('[is="one"] {}', 'a')).toEqual('[is="one"][a] {}'); expect(s('[is="one"] {}', 'a')).toEqual('[is="one"][a] {}');
}); });
it('should handle :host', () => { describe((':host'), () => {
expect(s(':host {}', 'a', 'a-host')).toEqual('[a-host] {}'); it('should handle no context',
() => { expect(s(':host {}', 'a', 'a-host')).toEqual('[a-host] {}'); });
expect(s(':host(.x) {}', 'a', 'a-host')).toEqual('.x[a-host] {}'); it('should handle tag selector', () => {
expect(s(':host(ul) {}', 'a', 'a-host')).toEqual('ul[a-host] {}'); expect(s(':host(ul) {}', 'a', 'a-host')).toEqual('ul[a-host] {}');
expect(s(':host(.x,.y) {}', 'a', 'a-host')).toEqual('.x[a-host], .y[a-host] {}'); });
expect(s(':host(ul,li) {}', 'a', 'a-host')).toEqual('ul[a-host], li[a-host] {}');
expect(s(':host(.x,.y) > .z {}', 'a', 'a-host')) it('should handle class selector',
.toEqual('.x[a-host] > .z[a], .y[a-host] > .z[a] {}'); () => { expect(s(':host(.x) {}', 'a', 'a-host')).toEqual('.x[a-host] {}'); });
expect(s(':host(ul,li) > .z {}', 'a', 'a-host'))
.toEqual('ul[a-host] > .z[a], li[a-host] > .z[a] {}'); it('should handle attribute selector', () => {
expect(s(':host([a="b"]) {}', 'a', 'a-host')).toEqual('[a="b"][a-host] {}');
expect(s(':host([a=b]) {}', 'a', 'a-host')).toEqual('[a="b"][a-host] {}');
});
it('should handle multiple tag selectors', () => {
expect(s(':host(ul,li) {}', 'a', 'a-host')).toEqual('ul[a-host], li[a-host] {}');
expect(s(':host(ul,li) > .z {}', 'a', 'a-host'))
.toEqual('ul[a-host] > .z[a], li[a-host] > .z[a] {}');
});
it('should handle multiple class selectors', () => {
expect(s(':host(.x,.y) {}', 'a', 'a-host')).toEqual('.x[a-host], .y[a-host] {}');
expect(s(':host(.x,.y) > .z {}', 'a', 'a-host'))
.toEqual('.x[a-host] > .z[a], .y[a-host] > .z[a] {}');
});
it('should handle multiple attribute selectors', () => {
expect(s(':host([a="b"],[c=d]) {}', 'a', 'a-host'))
.toEqual('[a="b"][a-host], [c="d"][a-host] {}');
});
}); });
it('should handle :host-context', () => { describe((':host-context'), () => {
expect(s(':host-context(.x) {}', 'a', 'a-host')).toEqual('.x[a-host], .x [a-host] {}'); it('should handle tag selector', () => {
expect(s(':host-context(div) {}', 'a', 'a-host')).toEqual('div[a-host], div [a-host] {}'); expect(s(':host-context(div) {}', 'a', 'a-host')).toEqual('div[a-host], div [a-host] {}');
expect(s(':host-context(ul) > .y {}', 'a', 'a-host'))
.toEqual('ul[a-host] > .y[a], ul [a-host] > .y[a] {}');
});
expect(s(':host-context(.x) > .y {}', 'a', 'a-host')) it('should handle class selector', () => {
.toEqual('.x[a-host] > .y[a], .x [a-host] > .y[a] {}'); expect(s(':host-context(.x) {}', 'a', 'a-host')).toEqual('.x[a-host], .x [a-host] {}');
expect(s(':host-context(ul) > .y {}', 'a', 'a-host'))
.toEqual('ul[a-host] > .y[a], ul [a-host] > .y[a] {}');
expect(s(':host-context(.x) > .y {}', 'a', 'a-host'))
.toEqual('.x[a-host] > .y[a], .x [a-host] > .y[a] {}');
});
it('should handle attribute selector', () => {
expect(s(':host-context([a="b"]) {}', 'a', 'a-host'))
.toEqual('[a="b"][a-host], [a="b"] [a-host] {}');
expect(s(':host-context([a=b]) {}', 'a', 'a-host'))
.toEqual('[a=b][a-host], [a="b"] [a-host] {}');
});
}); });
it('should support polyfill-next-selector', () => { it('should support polyfill-next-selector', () => {
@ -142,7 +173,7 @@ export function main() {
expect(css).toEqual('x[a] > y[a]{}'); expect(css).toEqual('x[a] > y[a]{}');
css = s(`polyfill-next-selector {content: 'button[priority="1"]'} z {}`, 'a'); css = s(`polyfill-next-selector {content: 'button[priority="1"]'} z {}`, 'a');
expect(css).toEqual('button[priority="1"][a] {}'); expect(css).toEqual('button[priority="1"][a]{}');
}); });
it('should support polyfill-unscoped-rule', () => { it('should support polyfill-unscoped-rule', () => {