Skip to content

Commit acbf1d8

Browse files
vicbvikerman
authored andcommitted
fix(core): fix pseudo-selector shimming (#12754)
fixes #12730 fixes #12354
1 parent f3793b5 commit acbf1d8

File tree

2 files changed

+52
-17
lines changed

2 files changed

+52
-17
lines changed

modules/@angular/compiler/src/shadow_css.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -378,13 +378,18 @@ export class ShadowCss {
378378
string {
379379
// In Android browser, the lastIndex is not reset when the regex is used in String.replace()
380380
_polyfillHostRe.lastIndex = 0;
381-
382381
if (_polyfillHostRe.test(selector)) {
383382
const replaceBy = this.strictStyling ? `[${hostSelector}]` : scopeSelector;
384383
return selector
385384
.replace(
386385
_polyfillHostNoCombinatorRe,
387-
(hnc, selector) => selector[0] === ':' ? replaceBy + selector : selector + replaceBy)
386+
(hnc, selector) => {
387+
return selector.replace(
388+
/([^:]*)(:*)(.*)/,
389+
(_: string, before: string, colon: string, after: string) => {
390+
return before + replaceBy + colon + after;
391+
});
392+
})
388393
.replace(_polyfillHostRe, replaceBy + ' ');
389394
}
390395

@@ -411,10 +416,10 @@ export class ShadowCss {
411416
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
412417
} else {
413418
// remove :host since it should be unnecessary
414-
var t = p.replace(_polyfillHostRe, '');
419+
const t = p.replace(_polyfillHostRe, '');
415420
if (t.length > 0) {
416421
const matches = t.match(/([^:]*)(:*)(.*)/);
417-
if (matches !== null) {
422+
if (matches) {
418423
scopedP = matches[1] + attrName + matches[2] + matches[3];
419424
}
420425
}
@@ -423,17 +428,8 @@ export class ShadowCss {
423428
return scopedP;
424429
};
425430

426-
let attrSelectorIndex = 0;
427-
const attrSelectors: string[] = [];
428-
429-
// replace attribute selectors with placeholders to avoid issue with white space being treated
430-
// as separator
431-
selector = selector.replace(/\[[^\]]*\]/g, (attrSelector) => {
432-
const replaceBy = `__attr_sel_${attrSelectorIndex}__`;
433-
attrSelectors.push(attrSelector);
434-
attrSelectorIndex++;
435-
return replaceBy;
436-
});
431+
const safeContent = new SafeSelector(selector);
432+
selector = safeContent.content();
437433

438434
let scopedSelector = '';
439435
let startIndex = 0;
@@ -454,14 +450,47 @@ export class ShadowCss {
454450
scopedSelector += _scopeSelectorPart(selector.substring(startIndex));
455451

456452
// replace the placeholders with their original values
457-
return scopedSelector.replace(/__attr_sel_(\d+)__/g, (ph, index) => attrSelectors[+index]);
453+
return safeContent.restore(scopedSelector);
458454
}
459455

460456
private _insertPolyfillHostInCssText(selector: string): string {
461457
return selector.replace(_colonHostContextRe, _polyfillHostContext)
462458
.replace(_colonHostRe, _polyfillHost);
463459
}
464460
}
461+
462+
class SafeSelector {
463+
private placeholders: string[] = [];
464+
private index = 0;
465+
private _content: string;
466+
467+
constructor(selector: string) {
468+
// Replaces attribute selectors with placeholders.
469+
// The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
470+
selector = selector.replace(/(\[[^\]]*\])/g, (_, keep) => {
471+
const replaceBy = `__ph-${this.index}__`;
472+
this.placeholders.push(keep);
473+
this.index++;
474+
return replaceBy;
475+
});
476+
477+
// Replaces the expression in `:nth-child(2n + 1)` with a placeholder.
478+
// WS and "+" would otherwise be interpreted as selector separators.
479+
this._content = selector.replace(/(:nth-[-\w]+)(\([^)]+\))/g, (_, pseudo, exp) => {
480+
const replaceBy = `__ph-${this.index}__`;
481+
this.placeholders.push(exp);
482+
this.index++;
483+
return pseudo + replaceBy;
484+
});
485+
};
486+
487+
restore(content: string): string {
488+
return content.replace(/__ph-(\d+)__/g, (ph, index) => this.placeholders[+index]);
489+
}
490+
491+
content(): string { return this._content; }
492+
}
493+
465494
const _cssContentNextSelectorRe =
466495
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
467496
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;

modules/@angular/compiler/test/shadow_css_spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,15 @@ export function main() {
140140
.toEqual('[a="b"][a-host], [c="d"][a-host] {}');
141141
});
142142

143-
it('should handle pseudo selector', () => {
143+
it('should handle pseudo selectors', () => {
144144
expect(s(':host(:before) {}', 'a', 'a-host')).toEqual('[a-host]:before {}');
145145
expect(s(':host:before {}', 'a', 'a-host')).toEqual('[a-host]:before {}');
146+
expect(s(':host:nth-child(8n+1) {}', 'a', 'a-host')).toEqual('[a-host]:nth-child(8n+1) {}');
147+
expect(s(':host:nth-of-type(8n+1) {}', 'a', 'a-host'))
148+
.toEqual('[a-host]:nth-of-type(8n+1) {}');
149+
expect(s(':host(.class):before {}', 'a', 'a-host')).toEqual('.class[a-host]:before {}');
150+
expect(s(':host.class:before {}', 'a', 'a-host')).toEqual('.class[a-host]:before {}');
151+
expect(s(':host(:not(p)):before {}', 'a', 'a-host')).toEqual('[a-host]:not(p):before {}');
146152
});
147153
});
148154

0 commit comments

Comments
 (0)