fix(compiler): preserve @page rules in encapsulated styles (#41915)
Currently the compiler treats `@page` rules in the same way as `@media`, however that is incorrect and it results in invalid CSS, because `@page` allows style declarations at the root (e.g. `@page (margin: 50%) {}`) and it only allows a limited set of at-rules to be nested into it. Given these restrictions, we can't really encapsulate the styles since they apply at the document level when the user tries to print. These changes make it so that `@page` rules are preserved so that we don't break the user's CSS. More information: https://www.w3.org/TR/css-page-3 Fixes #26269. PR Close #41915
This commit is contained in:
parent
efe8566321
commit
abcd4bbfaa
@ -135,8 +135,6 @@
|
|||||||
export class ShadowCss {
|
export class ShadowCss {
|
||||||
strictStyling: boolean = true;
|
strictStyling: boolean = true;
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Shim some cssText with the given selector. Returns cssText that can
|
* Shim some cssText with the given selector. Returns cssText that can
|
||||||
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
|
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
|
||||||
@ -367,15 +365,15 @@ export class ShadowCss {
|
|||||||
return processRules(cssText, (rule: CssRule) => {
|
return processRules(cssText, (rule: CssRule) => {
|
||||||
let selector = rule.selector;
|
let selector = rule.selector;
|
||||||
let content = rule.content;
|
let content = rule.content;
|
||||||
if (rule.selector[0] != '@') {
|
if (rule.selector[0] !== '@') {
|
||||||
selector =
|
selector =
|
||||||
this._scopeSelector(rule.selector, scopeSelector, hostSelector, this.strictStyling);
|
this._scopeSelector(rule.selector, scopeSelector, hostSelector, this.strictStyling);
|
||||||
} else if (
|
} else if (
|
||||||
rule.selector.startsWith('@media') || rule.selector.startsWith('@supports') ||
|
rule.selector.startsWith('@media') || rule.selector.startsWith('@supports') ||
|
||||||
rule.selector.startsWith('@page') || rule.selector.startsWith('@document')) {
|
rule.selector.startsWith('@document')) {
|
||||||
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
|
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
|
||||||
} else if (rule.selector.startsWith('@font-face')) {
|
} else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) {
|
||||||
content = this._stripScopingSelectors(rule.content, scopeSelector, hostSelector);
|
content = this._stripScopingSelectors(rule.content);
|
||||||
}
|
}
|
||||||
return new CssRule(selector, content);
|
return new CssRule(selector, content);
|
||||||
});
|
});
|
||||||
@ -396,15 +394,17 @@ export class ShadowCss {
|
|||||||
* :host ::ng-deep {
|
* :host ::ng-deep {
|
||||||
* import 'some/lib/containing/font-face';
|
* import 'some/lib/containing/font-face';
|
||||||
* }
|
* }
|
||||||
|
*
|
||||||
|
* Similar logic applies to `@page` rules which can contain a particular set of properties,
|
||||||
|
* as well as some specific at-rules. Since they can't be encapsulated, we have to strip
|
||||||
|
* any scoping selectors from them. For more information: https://www.w3.org/TR/css-page-3
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
private _stripScopingSelectors(cssText: string, scopeSelector: string, hostSelector: string):
|
private _stripScopingSelectors(cssText: string): string {
|
||||||
string {
|
|
||||||
return processRules(cssText, rule => {
|
return processRules(cssText, rule => {
|
||||||
const selector = rule.selector.replace(_shadowDeepSelectors, ' ')
|
const selector = rule.selector.replace(_shadowDeepSelectors, ' ')
|
||||||
.replace(_polyfillHostNoCombinatorRe, ' ');
|
.replace(_polyfillHostNoCombinatorRe, ' ');
|
||||||
const content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
|
return new CssRule(selector, rule.content);
|
||||||
return new CssRule(selector, content);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -792,7 +792,7 @@ function combineHostContextSelectors(contextSelectors: string[], otherSelectors:
|
|||||||
* in-place.
|
* in-place.
|
||||||
* @param multiples The number of times the current groups should appear.
|
* @param multiples The number of times the current groups should appear.
|
||||||
*/
|
*/
|
||||||
export function repeatGroups<T>(groups: string[][], multiples: number): void {
|
export function repeatGroups(groups: string[][], multiples: number): void {
|
||||||
const length = groups.length;
|
const length = groups.length;
|
||||||
for (let i = 1; i < multiples; i++) {
|
for (let i = 1; i < multiples; i++) {
|
||||||
for (let j = 0; j < length; j++) {
|
for (let j = 0; j < length; j++) {
|
||||||
|
@ -7,15 +7,28 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {NgModuleResolver} from '@angular/compiler/src/ng_module_resolver';
|
import {NgModuleResolver} from '@angular/compiler/src/ng_module_resolver';
|
||||||
import {ɵstringify as stringify} from '@angular/core';
|
import {Component, Directive, Injectable, NgModule, ɵstringify as stringify} from '@angular/core';
|
||||||
import {NgModule} from '@angular/core/src/metadata';
|
|
||||||
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
|
||||||
|
|
||||||
class SomeClass1 {}
|
@Directive()
|
||||||
class SomeClass2 {}
|
class SomeClass1 {
|
||||||
class SomeClass3 {}
|
}
|
||||||
class SomeClass4 {}
|
|
||||||
class SomeClass5 {}
|
@NgModule()
|
||||||
|
class SomeClass2 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule()
|
||||||
|
class SomeClass3 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
class SomeClass4 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
class SomeClass5 {
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [SomeClass1],
|
declarations: [SomeClass1],
|
||||||
|
@ -14,8 +14,11 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
|||||||
function s(css: string, contentAttr: string, hostAttr: string = '') {
|
function s(css: string, contentAttr: string, hostAttr: string = '') {
|
||||||
const shadowCss = new ShadowCss();
|
const shadowCss = new ShadowCss();
|
||||||
const shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
|
const shim = shadowCss.shimCssText(css, contentAttr, hostAttr);
|
||||||
const nlRegexp = /\n/g;
|
return normalize(shim);
|
||||||
return normalizeCSS(shim.replace(nlRegexp, ''));
|
}
|
||||||
|
|
||||||
|
function normalize(value: string): string {
|
||||||
|
return normalizeCSS(value.replace(/\n/g, '')).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should handle empty string', () => {
|
it('should handle empty string', () => {
|
||||||
@ -53,10 +56,49 @@ import {normalizeCSS} from '@angular/platform-browser/testing/src/browser_util';
|
|||||||
expect(s(css, 'contenta')).toEqual(expected);
|
expect(s(css, 'contenta')).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle page rules', () => {
|
// @page rules use a special set of at-rules and selectors and they can't be scoped.
|
||||||
const css = '@page {div {font-size:50px;}}';
|
// See: https://www.w3.org/TR/css-page-3
|
||||||
const expected = '@page {div[contenta] {font-size:50px;}}';
|
it('should preserve @page rules', () => {
|
||||||
expect(s(css, 'contenta')).toEqual(expected);
|
const contentAttr = 'contenta';
|
||||||
|
const css = `
|
||||||
|
@page {
|
||||||
|
margin-right: 4in;
|
||||||
|
|
||||||
|
@top-left {
|
||||||
|
content: "Hamlet";
|
||||||
|
}
|
||||||
|
|
||||||
|
@top-right {
|
||||||
|
content: "Page " counter(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@page main {
|
||||||
|
margin-left: 4in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page :left {
|
||||||
|
margin-left: 3cm;
|
||||||
|
margin-right: 4cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page :right {
|
||||||
|
margin-left: 4cm;
|
||||||
|
margin-right: 3cm;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = s(css, contentAttr);
|
||||||
|
expect(result).toEqual(normalize(css));
|
||||||
|
expect(result).not.toContain(contentAttr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ::ng-deep and :host from within @page rules', () => {
|
||||||
|
expect(s('@page { margin-right: 4in; }', 'contenta', 'h'))
|
||||||
|
.toEqual('@page { margin-right:4in;}');
|
||||||
|
expect(s('@page { ::ng-deep @top-left { content: "Hamlet";}}', 'contenta', 'h'))
|
||||||
|
.toEqual('@page { @top-left { content:"Hamlet";}}');
|
||||||
|
expect(s('@page { :host ::ng-deep @top-left { content:"Hamlet";}}', 'contenta', 'h'))
|
||||||
|
.toEqual('@page { @top-left { content:"Hamlet";}}');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle document rules', () => {
|
it('should handle document rules', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user