Skip to content

Commit

Permalink
Revert fix(compiler): transform pseudo selectors correctly for the en…
Browse files Browse the repository at this point in the history
…capsulated view. (#58417)

This commit reverts #57796 for v18

PR Close #58417
  • Loading branch information
JeanMeche authored and alxhub committed Oct 30, 2024
1 parent 1f13a5e commit 69dce38
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 353 deletions.
170 changes: 30 additions & 140 deletions packages/compiler/src/shadow_css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ export class ShadowCss {
* captures how many (if any) leading whitespaces are present or a comma
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
* captures two different possible keyframes, ones which are quoted or ones which are valid css
* indents (custom properties excluded)
* 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
Expand Down Expand Up @@ -459,7 +459,7 @@ export class ShadowCss {
*/
private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
const unscopedRules = this._extractUnscopedRulesFromCssText(cssText);
// replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively
// replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
Expand Down Expand Up @@ -539,7 +539,7 @@ export class ShadowCss {
* .foo<scopeName> .bar { ... }
*/
private _convertColonHostContext(cssText: string): string {
return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => {
return cssText.replace(_cssColonHostContextReGlobal, (selectorText) => {
// We have captured a selector that contains a `:host-context` rule.

// For backward compatibility `:host-context` may contain a comma separated list of selectors.
Expand Down Expand Up @@ -594,12 +594,10 @@ export class ShadowCss {
}

// The context selectors now must be combined with each other to capture all the possible
// selectors that `:host-context` can match. See `_combineHostContextSelectors()` for more
// selectors that `:host-context` can match. See `combineHostContextSelectors()` for more
// info about how this is done.
return contextSelectorGroups
.map((contextSelectors) =>
_combineHostContextSelectors(contextSelectors, selectorText, pseudoPrefix),
)
.map((contextSelectors) => combineHostContextSelectors(contextSelectors, selectorText))
.join(', ');
});
}
Expand All @@ -618,12 +616,7 @@ export class ShadowCss {
let selector = rule.selector;
let content = rule.content;
if (rule.selector[0] !== '@') {
selector = this._scopeSelector({
selector,
scopeSelector,
hostSelector,
isParentSelector: true,
});
selector = this._scopeSelector(rule.selector, scopeSelector, hostSelector);
} else if (scopedAtRuleIdentifiers.some((atRule) => rule.selector.startsWith(atRule))) {
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
} else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) {
Expand Down Expand Up @@ -663,44 +656,15 @@ export class ShadowCss {
});
}

private _safeSelector: SafeSelector | undefined;
private _shouldScopeIndicator: boolean | undefined;

// `isParentSelector` is used to distinguish the selectors which are coming from
// the initial selector string and any nested selectors, parsed recursively,
// for example `selector = 'a:where(.one)'` could be the parent, while recursive call
// would have `selector = '.one'`.
private _scopeSelector({
selector,
scopeSelector,
hostSelector,
isParentSelector = false,
}: {
selector: string;
scopeSelector: string;
hostSelector: string;
isParentSelector?: boolean;
}): string {
// Split the selector into independent parts by `,` (comma) unless
// comma is within parenthesis, for example `:is(.one, two)`.
// Negative lookup after comma allows not splitting inside nested parenthesis,
// up to three levels (((,))).
const selectorSplitRe =
/ ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;

private _scopeSelector(selector: string, scopeSelector: string, hostSelector: string): string {
return selector
.split(selectorSplitRe)
.split(/ ?, ?/)
.map((part) => part.split(_shadowDeepSelectors))
.map((deepParts) => {
const [shallowPart, ...otherParts] = deepParts;
const applyScope = (shallowPart: string) => {
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
return this._applySelectorScope({
selector: shallowPart,
scopeSelector,
hostSelector,
isParentSelector,
});
return this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
} else {
return shallowPart;
}
Expand Down Expand Up @@ -733,9 +697,9 @@ export class ShadowCss {
if (_polyfillHostRe.test(selector)) {
const replaceBy = `[${hostSelector}]`;
return selector
.replace(_polyfillHostNoCombinatorReGlobal, (_hnc, selector) => {
.replace(_polyfillHostNoCombinatorRe, (hnc, selector) => {
return selector.replace(
/([^:\)]*)(:*)(.*)/,
/([^:]*)(:*)(.*)/,
(_: string, before: string, colon: string, after: string) => {
return before + replaceBy + colon + after;
},
Expand All @@ -749,17 +713,11 @@ 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 _applySelectorScope({
selector,
scopeSelector,
hostSelector,
isParentSelector,
}: {
selector: string;
scopeSelector: string;
hostSelector: string;
isParentSelector?: boolean;
}): string {
private _applySelectorScope(
selector: string,
scopeSelector: string,
hostSelector: string,
): string {
const isRe = /\[is=([^\]]*)\]/g;
scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]);

Expand All @@ -774,10 +732,6 @@ export class ShadowCss {

if (p.includes(_polyfillHostNoCombinator)) {
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
if (_polyfillHostNoCombinatorWithinPseudoFunction.test(p)) {
const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)(.*)/)!;
scopedP = before + attrName + colon + after;
}
} else {
// remove :host since it should be unnecessary
const t = p.replace(_polyfillHostRe, '');
Expand All @@ -792,60 +746,13 @@ export class ShadowCss {
return scopedP;
};

// Wraps `_scopeSelectorPart()` to not use it directly on selectors with
// pseudo selector functions like `:where()`. Selectors within pseudo selector
// functions are recursively sent to `_scopeSelector()`.
const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => {
let scopedPart = '';

const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match(
_cssPrefixWithPseudoSelectorFunction,
);
if (cssPrefixWithPseudoSelectorFunctionMatch) {
const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;

// Unwrap the pseudo selector to scope its contents.
// For example,
// - `:where(selectorToScope)` -> `selectorToScope`;
// - `:is(.foo, .bar)` -> `.foo, .bar`.
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);

if (selectorToScope.includes(_polyfillHostNoCombinator)) {
this._shouldScopeIndicator = true;
}

const scopedInnerPart = this._scopeSelector({
selector: selectorToScope,
scopeSelector,
hostSelector,
});

// Put the result back into the pseudo selector function.
scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
} else {
this._shouldScopeIndicator =
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
scopedPart = this._shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart;
}

return scopedPart;
};

if (isParentSelector) {
this._safeSelector = new SafeSelector(selector);
selector = this._safeSelector.content();
}
const safeContent = new SafeSelector(selector);
selector = safeContent.content();

let scopedSelector = '';
let startIndex = 0;
let res: RegExpExecArray | null;
// Combinators aren't used as a delimiter if they are within parenthesis,
// for example `:where(.one .two)` stays intact.
// Similarly to selector separation by comma initially, negative lookahead
// is used here to not break selectors within nested parenthesis up to three
// nested layers.
const sep =
/( |>|\+|~(?!=))(?!([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)))\s*/g;
const sep = /( |>|\+|~(?!=))\s*/g;

// If a selector appears before :host it should not be shimmed as it
// matches on ancestor elements and not on elements in the host's shadow
Expand All @@ -859,13 +766,8 @@ export class ShadowCss {
// - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
// `:host-context(tag)`)
const hasHost = selector.includes(_polyfillHostNoCombinator);
// Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator`
// when it is present. The selector has the same level when it is a part of a pseudo
// selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo`
// being scoped.
if (isParentSelector || this._shouldScopeIndicator) {
this._shouldScopeIndicator = !hasHost;
}
// Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
let shouldScope = !hasHost;

while ((res = sep.exec(selector)) !== null) {
const separator = res[1];
Expand All @@ -884,17 +786,18 @@ export class ShadowCss {
continue;
}

const scopedPart = _pseudoFunctionAwareScopeSelectorPart(part);
shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator);
const scopedPart = shouldScope ? _scopeSelectorPart(part) : part;
scopedSelector += `${scopedPart} ${separator} `;
startIndex = sep.lastIndex;
}

const part = selector.substring(startIndex);
scopedSelector += _pseudoFunctionAwareScopeSelectorPart(part);
shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator);
scopedSelector += shouldScope ? _scopeSelectorPart(part) : part;

// replace the placeholders with their original values
// using values stored inside the `safeSelector` instance.
return this._safeSelector!.restore(scopedSelector);
return safeContent.restore(scopedSelector);
}

private _insertPolyfillHostInCssText(selector: string): string {
Expand Down Expand Up @@ -959,8 +862,6 @@ class SafeSelector {
}
}

const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
const _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
Expand All @@ -971,17 +872,10 @@ const _polyfillHost = '-shadowcsshost';
const _polyfillHostContext = '-shadowcsscontext';
const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)';
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
const _cssColonHostContextReGlobal = new RegExp(
_cssScopedPseudoFunctionPrefix + '(' + _polyfillHostContext + _parenSuffix + ')',
'gim',
);
const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim');
const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im');
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp(
`:.*\\(.*${_polyfillHostNoCombinator}.*\\)`,
);
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g');
const _shadowDOMSelectorsRe = [
/::shadow/g,
/::content/g,
Expand Down Expand Up @@ -1232,11 +1126,7 @@ function unescapeQuotes(str: string, isQuoted: boolean): string {
* @param contextSelectors an array of context selectors that will be combined.
* @param otherSelectors the rest of the selectors that are not context selectors.
*/
function _combineHostContextSelectors(
contextSelectors: string[],
otherSelectors: string,
pseudoPrefix = '',
): string {
function combineHostContextSelectors(contextSelectors: string[], otherSelectors: string): string {
const hostMarker = _polyfillHostNoCombinator;
_polyfillHostRe.lastIndex = 0; // reset the regex to ensure we get an accurate test
const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors);
Expand Down Expand Up @@ -1265,8 +1155,8 @@ function _combineHostContextSelectors(
return combined
.map((s) =>
otherSelectorsHasHost
? `${pseudoPrefix}${s}${otherSelectors}`
: `${pseudoPrefix}${s}${hostMarker}${otherSelectors}, ${pseudoPrefix}${s} ${hostMarker}${otherSelectors}`,
? `${s}${otherSelectors}`
: `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`,
)
.join(',');
}
Expand Down
36 changes: 0 additions & 36 deletions packages/compiler/test/shadow_css/host_and_host_context_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,42 +107,6 @@ describe('ShadowCss, :host and :host-context', () => {
});

describe(':host-context', () => {
it('should transform :host-context with pseudo selectors', () => {
expect(
shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'),
).toEqualCss(
'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}',
);
expect(shim(':where(:host-context(backdrop)) {}', 'contenta', 'hosta')).toEqualCss(
':where(backdrop[hosta]), :where(backdrop [hosta]) {}',
);
expect(shim(':where(:host-context(outer1)) :host(bar) {}', 'contenta', 'hosta')).toEqualCss(
':where(outer1) bar[hosta] {}',
);
expect(
shim(':where(:host-context(.one)) :where(:host-context(.two)) {}', 'contenta', 'a-host'),
).toEqualCss(
':where(.one.two[a-host]), ' + // `one` and `two` both on the host
':where(.one.two [a-host]), ' + // `one` and `two` are both on the same ancestor
':where(.one .two[a-host]), ' + // `one` is an ancestor and `two` is on the host
':where(.one .two [a-host]), ' + // `one` and `two` are both ancestors (in that order)
':where(.two .one[a-host]), ' + // `two` is an ancestor and `one` is on the host
':where(.two .one [a-host])' + // `two` and `one` are both ancestors (in that order)
' {}',
);
expect(
shim(':where(:host-context(backdrop)) .foo ~ .bar {}', 'contenta', 'hosta'),
).toEqualCss(
':where(backdrop[hosta]) .foo[contenta] ~ .bar[contenta], :where(backdrop [hosta]) .foo[contenta] ~ .bar[contenta] {}',
);
expect(shim(':where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
':where(backdrop) [hosta] {}',
);
expect(shim('div:where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
'div:where(backdrop) [hosta] {}',
);
});

it('should handle tag selector', () => {
expect(shim(':host-context(div) {}', 'contenta', 'a-host')).toEqualCss(
'div[a-host], div [a-host] {}',
Expand Down
Loading

0 comments on commit 69dce38

Please sign in to comment.