fix(compiler): don't choke on unbalanced parens in declaration block

Following https://github.com/angular/angular/pull/64509 we started
choking on unbalanced closing parentheses in declaration blocks,
specifically in quoted background-image urls. This was reported in
https://github.com/angular/angular/issues/65137.

This occured because we previously (and now again) traverse the entire
declaration block when selecting for :host-context() selectors to shim.
This is an oddity of how we parse styles today, and is likely something
we'd want to remove if we parsed selectors properly.

This change adds a new flag to _splitOnTopLevelCommas which allows it to
continue past unbalanced closing parentheses in the declaration block,
returning _convertColonHostContext to its previous behavior while
keeping support for the extra nesting in :host-context().
This commit is contained in:
Matthew Beck 2025-11-16 22:24:24 -08:00 committed by Jessica Janiuk
parent c15836c8c7
commit 9e7ddcaa10
2 changed files with 14 additions and 6 deletions

View file

@ -512,7 +512,7 @@ export class ShadowCss {
return cssText.replace(_cssColonHostRe, (_, hostSelectors: string, otherSelectors: string) => {
if (hostSelectors) {
const convertedSelectors: string[] = [];
for (const hostSelector of this._splitOnTopLevelCommas(hostSelectors)) {
for (const hostSelector of this._splitOnTopLevelCommas(hostSelectors, true)) {
const trimmedHostSelector = hostSelector.trim();
if (!trimmedHostSelector) break;
const convertedSelector =
@ -533,8 +533,9 @@ export class ShadowCss {
* Yields each part of the string between top-level commas. Terminates if an extra closing paren is found.
*
* @param text The string to split
* @param returnOnClosingParen Whether to return when exiting the current level of parentheses nesting
*/
private *_splitOnTopLevelCommas(text: string): Generator<string> {
private *_splitOnTopLevelCommas(text: string, returnOnClosingParen: boolean): Generator<string> {
const length = text.length;
let parens = 0;
let prev = 0;
@ -546,8 +547,8 @@ export class ShadowCss {
parens++;
} else if (charCode === chars.$RPAREN) {
parens--;
if (parens < 0) {
// Found an extra closing paren. Assume we want the list terminated here
if (parens < 0 && returnOnClosingParen) {
// Found an extra closing paren.
yield text.slice(prev, i);
return;
}
@ -582,7 +583,7 @@ export class ShadowCss {
// individually and stitches them back together. This ensures that individual selectors don't
// affect each other.
const results: string[] = [];
for (const part of this._splitOnTopLevelCommas(cssText)) {
for (const part of this._splitOnTopLevelCommas(cssText, false)) {
results.push(this._convertColonHostContextInSelectorPart(part));
}
return results.join(',');
@ -615,7 +616,7 @@ export class ShadowCss {
// Extract comma-separated selectors between the parentheses
const newContextSelectors: string[] = [];
let endIndex = 0; // Index of the closing paren of the :host-context()
for (const selector of this._splitOnTopLevelCommas(afterPrefix.substring(1))) {
for (const selector of this._splitOnTopLevelCommas(afterPrefix.substring(1), true)) {
endIndex = endIndex + selector.length + 1;
const trimmed = selector.trim();
if (trimmed) {

View file

@ -339,6 +339,13 @@ describe('ShadowCss', () => {
expect(css).toEqualCss('div[contenta] {background-image:url("a.jpg"); color:red;}');
});
it('should handle when quoted content contains a closing parenthesis', () => {
// Regression test for https://github.com/angular/angular/issues/65137
expect(shim('p { background-image: url(")") } p { color: red }', 'contenta')).toEqualCss(
'p[contenta] { background-image: url(")") } p[contenta] { color: red }',
);
});
it('should shim rules with an escaped quote inside quoted content', () => {
const styleStr = 'div::after { content: "\\"" }';
const css = shim(styleStr, 'contenta');