Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions packages/compiler/src/shadow_css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,18 +596,30 @@ export class ShadowCss {
let selector = rule.selector;
let content = rule.content;
if (rule.selector[0] !== '@') {
selector = this._scopeSelector({
selector,
scopeSelector,
hostSelector,
isParentSelector: true,
});
if (rule.isBlock) {
const selectorParts = selector.split(_selectorSplitRe);
const containsDeep = selectorParts.some((part) => _shadowDeepSelectors.test(part));

selector = this._scopeSelector({
selector,
scopeSelector,
hostSelector,
isParentSelector: true,
});

content =
// Only recurse into content if there might be any child blocks.
// TODO: support something like `.parent { ::ng-deep { .child {} } }`
!containsDeep && rule.content.includes('{')
? this._scopeSelectors(rule.content, scopeSelector, hostSelector)
: this._stripScopingSelectors(rule.content);
}
} 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')) {
content = this._stripScopingSelectors(rule.content);
}
return new CssRule(selector, content);
return new CssRule(selector, content, rule.isBlock);
});
}

Expand Down Expand Up @@ -637,7 +649,7 @@ export class ShadowCss {
const selector = rule.selector
.replace(_shadowDeepSelectors, ' ')
.replace(_polyfillHostNoCombinatorRe, ' ');
return new CssRule(selector, rule.content);
return new CssRule(selector, rule.content, rule.isBlock);
});
}

Expand All @@ -659,15 +671,8 @@ export class ShadowCss {
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 =
/ ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;

return selector
.split(selectorSplitRe)
.split(_selectorSplitRe)
.map((part) => part.split(_shadowDeepSelectors))
.map((deepParts) => {
const [shallowPart, ...otherParts] = deepParts;
Expand Down Expand Up @@ -748,7 +753,7 @@ export class ShadowCss {
const _scopeSelectorPart = (p: string) => {
let scopedP = p.trim();

if (!scopedP) {
if (!scopedP || scopedP === '&') {
return p;
}

Expand Down Expand Up @@ -1008,6 +1013,12 @@ const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s,]*)/;
// Support for `>>>`, `deep`, `::ng-deep` is then also deprecated and will be removed in the future.
// see https://github.com/angular/angular/pull/17677
const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)|(?:::ng-deep)/g;

// Splits 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 =
/ ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;
const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$';
const _polyfillHostRe = /-shadowcsshost/gim;
const _colonHostRe = /:host(?!\-context)/gim;
Expand All @@ -1021,18 +1032,22 @@ const _commentWithHashPlaceHolderRe = new RegExp(COMMENT_PLACEHOLDER, 'g');

const BLOCK_PLACEHOLDER = '%BLOCK%';
const _ruleRe = new RegExp(
`(\\s*(?:${COMMENT_PLACEHOLDER}\\s*)*)([^;\\{\\}]+?)(\\s*)((?:{%BLOCK%}?\\s*;?)|(?:\\s*;))`,
`(\\s*(?:${COMMENT_PLACEHOLDER}\\s*)*)([^;\\{\\}]+?)(\\s*)((?:{%BLOCK%}?\\s*;?)|(?:\\s*;)|$)`,
'g',
);
const CONTENT_PAIRS = new Map([['{', '}']]);

const COMMA_IN_PLACEHOLDER = '%COMMA_IN_PLACEHOLDER%';
const SEMI_IN_PLACEHOLDER = '%SEMI_IN_PLACEHOLDER%';
const COLON_IN_PLACEHOLDER = '%COLON_IN_PLACEHOLDER%';
const LBRACE_IN_PLACEHOLDER = '%LBRACE_IN_PLACEHOLDER%';
const RBRACE_IN_PLACEHOLDER = '%RBRACE_IN_PLACEHOLDER%';

const _cssCommaInPlaceholderReGlobal = new RegExp(COMMA_IN_PLACEHOLDER, 'g');
const _cssSemiInPlaceholderReGlobal = new RegExp(SEMI_IN_PLACEHOLDER, 'g');
const _cssColonInPlaceholderReGlobal = new RegExp(COLON_IN_PLACEHOLDER, 'g');
const _cssLbraceInPlaceholderReGlobal = new RegExp(LBRACE_IN_PLACEHOLDER, 'g');
const _cssRbraceInPlaceholderReGlobal = new RegExp(RBRACE_IN_PLACEHOLDER, 'g');

// Matches any CSS variable name, defined by a double-hyphen followed by any valid ident.
// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
Expand Down Expand Up @@ -1067,8 +1082,9 @@ export function namespaceCssVariables(cssText: string): string {

export class CssRule {
constructor(
public selector: string,
public content: string,
readonly selector: string,
readonly content: string,
readonly isBlock: boolean,
) {}
}

Expand All @@ -1081,12 +1097,14 @@ export function processRules(input: string, ruleCallback: (rule: CssRule) => Css
let content = '';
let suffix = m[4];
let contentPrefix = '';
let hasBlock = false;
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
contentPrefix = '{';
hasBlock = true;
}
const rule = ruleCallback(new CssRule(selector, content));
const rule = ruleCallback(new CssRule(selector, content, hasBlock));
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
});
return unescapeInStrings(escapedResult);
Expand Down Expand Up @@ -1155,6 +1173,8 @@ const ESCAPE_IN_STRING_MAP: {[key: string]: string} = {
';': SEMI_IN_PLACEHOLDER,
',': COMMA_IN_PLACEHOLDER,
':': COLON_IN_PLACEHOLDER,
'{': LBRACE_IN_PLACEHOLDER,
'}': RBRACE_IN_PLACEHOLDER,
};

/**
Expand Down Expand Up @@ -1225,6 +1245,8 @@ function unescapeInStrings(input: string): string {
let result = input.replace(_cssCommaInPlaceholderReGlobal, ',');
result = result.replace(_cssSemiInPlaceholderReGlobal, ';');
result = result.replace(_cssColonInPlaceholderReGlobal, ':');
result = result.replace(_cssLbraceInPlaceholderReGlobal, '{');
result = result.replace(_cssRbraceInPlaceholderReGlobal, '}');
return result;
}

Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/test/shadow_css/at_rules_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ describe('ShadowCss, at-rules', () => {
expect(css).toEqualCss(styleStr);
});

it('should pass through @import that contains an unquoted URL with colons and semicolons', () => {
const styleStr =
'@import url(https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap);';
const css = shim(styleStr, 'contenta');
expect(css).toEqualCss(styleStr);
});

it('should shim rules after @import with colons and semicolons', () => {
const styleStr =
'@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap"); div {}';
Expand Down
Loading