refactor(compiler): don’t rely on external css parser

We used to use different external css parsers,
depending on the `DomAdapter`. This lead to
inconsistent behavior and environment specific errors.

Closes #5006
Closes #4993
This commit is contained in:
Tobias Bosch
2015-10-29 10:57:54 -07:00
parent 5f2eb3e078
commit 993b3d62de
16 changed files with 115 additions and 432 deletions

View File

@ -1,4 +1,3 @@
import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {
StringWrapper,
@ -144,8 +143,7 @@ export class ShadowCss {
* Shim a style element with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimStyle(style: string, selector: string, hostSelector: string = ''): string {
var cssText = DOM.getText(style);
shimStyle(cssText: string, selector: string, hostSelector: string = ''): string {
return this.shimCssText(cssText, selector, hostSelector);
}
@ -231,8 +229,7 @@ export class ShadowCss {
cssText = this._convertColonHostContext(cssText);
cssText = this._convertShadowDOMSelectors(cssText);
if (isPresent(scopeSelector)) {
_withCssRules(cssText,
(rules) => { cssText = this._scopeRules(rules, scopeSelector, hostSelector); });
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
}
cssText = cssText + '\n' + unscoped;
return cssText.trim();
@ -347,50 +344,37 @@ export class ShadowCss {
// change a selector like 'div' to 'name div'
/** @internal */
_scopeRules(cssRules, scopeSelector: string, hostSelector: string): string {
var cssText = '';
if (isPresent(cssRules)) {
for (var i = 0; i < cssRules.length; i++) {
var rule = cssRules[i];
if (DOM.isStyleRule(rule) || DOM.isPageRule(rule)) {
cssText += this._scopeSelector(rule.selectorText, scopeSelector, hostSelector,
this.strictStyling) +
' {\n';
cssText += this._propertiesFromRule(rule) + '\n}\n\n';
} else if (DOM.isMediaRule(rule)) {
cssText += '@media ' + rule.media.mediaText + ' {\n';
cssText += this._scopeRules(rule.cssRules, scopeSelector, hostSelector);
cssText += '\n}\n\n';
} else {
// KEYFRAMES_RULE in IE throws when we query cssText
// when it contains a -webkit- property.
// if this happens, we fallback to constructing the rule
// from the CSSRuleSet
// https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception
try {
if (isPresent(rule.cssText)) {
cssText += rule.cssText + '\n\n';
}
} catch (x) {
if (DOM.isKeyframesRule(rule) && isPresent(rule.cssRules)) {
cssText += this._ieSafeCssTextFromKeyFrameRule(rule);
}
}
_scopeSelectors(cssText: string, scopeSelector: string, hostSelector: string): string {
var parts = splitCurlyBlocks(cssText);
var result = [];
for (var i = 0; i < parts.length; i += 2) {
var selectorTextWithCommands = parts[i];
var selectorStart = selectorTextWithCommands.lastIndexOf(';') + 1;
var selectorText =
selectorTextWithCommands.substring(selectorStart, selectorTextWithCommands.length);
var ruleContent = parts[i + 1];
var selectorMatch = RegExpWrapper.firstMatch(_singleSelectorRe, selectorText);
if (isPresent(selectorMatch) && ruleContent.length > 0) {
var selPrefix = selectorMatch[1];
var selAt = isPresent(selectorMatch[2]) ? selectorMatch[2] : '';
var selector = selectorMatch[3];
var selSuffix = selectorMatch[4];
if (selAt.length === 0 || selAt == '@page') {
var scopedSelector =
this._scopeSelector(selector, scopeSelector, hostSelector, this.strictStyling);
selectorText = `${selPrefix}${selAt}${scopedSelector}${selSuffix}`;
} else if (selAt == '@media' && ruleContent[0] == OPEN_CURLY &&
ruleContent[ruleContent.length - 1] == CLOSE_CURLY) {
var scopedContent = this._scopeSelectors(ruleContent.substring(1, ruleContent.length - 1),
scopeSelector, hostSelector);
ruleContent = `${OPEN_CURLY}${scopedContent}${CLOSE_CURLY}`;
}
}
result.push(selectorTextWithCommands.substring(0, selectorStart));
result.push(selectorText);
result.push(ruleContent);
}
return cssText;
}
/** @internal */
_ieSafeCssTextFromKeyFrameRule(rule): string {
var cssText = '@keyframes ' + rule.name + ' {';
for (var i = 0; i < rule.cssRules.length; i++) {
var r = rule.cssRules[i];
cssText += ' ' + r.keyText + ' {' + r.style.cssText + '}';
}
cssText += ' }';
return cssText;
return result.join('');
}
/** @internal */
@ -477,36 +461,6 @@ export class ShadowCss {
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
return selector;
}
/** @internal */
_propertiesFromRule(rule): string {
var cssText = rule.style.cssText;
// TODO(sorvell): Safari cssom incorrectly removes quotes from the content
// property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
// don't replace attr rules
var attrRe = /['"]+|attr/g;
if (rule.style.content.length > 0 &&
!isPresent(RegExpWrapper.firstMatch(attrRe, rule.style.content))) {
var contentRe = /content:[^;]*;/g;
cssText =
StringWrapper.replaceAll(cssText, contentRe, 'content: \'' + rule.style.content + '\';');
}
// TODO(sorvell): we can workaround this issue here, but we need a list
// of troublesome properties to fix https://github.com/Polymer/platform/issues/53
//
// inherit rules can be omitted from cssText
// TODO(sorvell): remove when Blink bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=358273
// var style = rule.style;
// for (var i = 0; i < style.length; i++) {
// var name = style.item(i);
// var value = style.getPropertyValue(name);
// if (value == 'initial') {
// cssText += name + ': initial; ';
// }
//}
return cssText;
}
}
var _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim;
@ -539,13 +493,34 @@ var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
var _colonHostRe = /:host/gim;
var _colonHostContextRe = /:host-context/gim;
function _cssToRules(cssText: string) {
return DOM.cssToRules(cssText);
var _singleSelectorRe = /^(\s*)(@\S+)?(.*?)(\s*)$/g;
var _curlyRe = /([{}])/g;
var OPEN_CURLY = '{';
var CLOSE_CURLY = '}';
export function splitCurlyBlocks(cssText:string):string[] {
var parts = StringWrapper.split(cssText, _curlyRe);
var result = [];
var bracketCount = 0;
var currentCurlyParts = [];
for (var partIndex = 0; partIndex<parts.length; partIndex++) {
var part = parts[partIndex];
currentCurlyParts.push(part);
if (part == OPEN_CURLY) {
bracketCount++;
} else if (part == CLOSE_CURLY) {
bracketCount--;
}
if (bracketCount === 0) {
result.push(currentCurlyParts.join(''));
currentCurlyParts = [];
}
}
result.push(currentCurlyParts.join(''));
if (result.length >= 2 && result[result.length-1] == '' && result[result.length-2] == '') {
result = result.slice(0, result.length-2);
}
return result;
}
function _withCssRules(cssText: string, callback: Function) {
// Difference from webcomponentjs: remove the workaround for an old bug in Chrome
if (isBlank(callback)) return;
var rules = _cssToRules(cssText);
callback(rules);
}

View File

@ -4,7 +4,6 @@ import 'package:html/parser.dart' as parser;
import 'package:html/dom.dart';
import 'dom_adapter.dart';
import 'emulated_css.dart';
import '../compiler/xhr.dart';
const _attrToPropMap = const {
@ -348,14 +347,6 @@ abstract class AbstractHtml5LibAdapter implements DomAdapter {
throw 'not implemented';
}
bool isPageRule(rule) => (rule.type == 6);
bool isStyleRule(rule) => (rule.type == 1);
bool isMediaRule(rule) => (rule.type == 4);
bool isKeyframesRule(rule) => (rule.type == 7);
String getHref(element) {
throw 'not implemented';
}
@ -364,10 +355,6 @@ abstract class AbstractHtml5LibAdapter implements DomAdapter {
throw 'not implemented';
}
List cssToRules(String css) {
return parseAndEmulateCssRules(css);
}
List getDistributedNodes(Node) {
throw 'not implemented';
}
@ -380,13 +367,6 @@ abstract class AbstractHtml5LibAdapter implements DomAdapter {
return false;
}
bool supportsUnprefixedCssAnimation() {
// Currently during code transformation we do not know what
// browsers we are targetting. To play it safe, we assume
// unprefixed animations are not supported.
return false;
}
getHistory() {
throw 'not implemented';
}

View File

@ -410,10 +410,6 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
return document.adoptNode(node);
}
bool isPageRule(CssRule rule) => rule is CssPageRule;
bool isStyleRule(CssRule rule) => rule is CssStyleRule;
bool isMediaRule(CssRule rule) => rule is CssMediaRule;
bool isKeyframesRule(CssRule rule) => rule is CssKeyframesRule;
String getHref(AnchorElement element) {
return element.href;
}

View File

@ -273,10 +273,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
return document.importNode(toImport, true);
}
adoptNode(node: Node): any { return document.adoptNode(node); }
isPageRule(rule): boolean { return rule.type === CSSRule.PAGE_RULE; }
isStyleRule(rule): boolean { return rule.type === CSSRule.STYLE_RULE; }
isMediaRule(rule): boolean { return rule.type === CSSRule.MEDIA_RULE; }
isKeyframesRule(rule): boolean { return rule.type === CSSRule.KEYFRAMES_RULE; }
getHref(el: Element): string { return (<any>el).href; }
getEventKey(event): string {
var key = event.key;

View File

@ -111,17 +111,11 @@ export abstract class DomAdapter {
abstract isShadowRoot(node): boolean;
abstract importIntoDoc /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/;
abstract adoptNode /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/;
abstract isPageRule(rule): boolean;
abstract isStyleRule(rule): boolean;
abstract isMediaRule(rule): boolean;
abstract isKeyframesRule(rule): boolean;
abstract getHref(element): string;
abstract getEventKey(event): string;
abstract resolveAndSetHref(element, baseUrl: string, href: string);
abstract cssToRules(css: string): any[];
abstract supportsDOMEvents(): boolean;
abstract supportsNativeShadowDOM(): boolean;
abstract supportsUnprefixedCssAnimation(): boolean;
abstract getGlobalEventTarget(target: string): any;
abstract getHistory(): History;
abstract getLocation(): Location;

View File

@ -1,110 +0,0 @@
/**
* Emulates browser CSS API.
*
* WARNING: this is a very incomplete emulation; it only has enough to support
* Angular's CSS scoping (a.k.a. shimming).
*/
library angular2.dom.emulated_css;
import 'package:csslib/parser.dart' as cssp;
import 'package:csslib/visitor.dart' as cssv;
/// Parses [css] string and emits the list of top-level CSS rules in it via
/// data structures that mimick browser CSS APIs.
List<EmulatedCssRule> parseAndEmulateCssRules(String css) {
var stylesheet = cssp.parse(css);
return emulateRules(stylesheet.topLevels);
}
/// Converts `csslib` [rules] to their emulated counterparts.
List<EmulatedCssRule> emulateRules(Iterable<cssv.TreeNode> rules) {
return rules.map((cssv.TreeNode node) {
if (node is cssv.RuleSet) {
if (node.declarationGroup.span.text.isEmpty) {
// Skip CSS matchers with no bodies
return null;
}
return new EmulatedCssStyleRule(node);
} else if (node is cssv.MediaDirective) {
return new EmulatedCssMedialRule(node);
} else if (node is cssv.ImportDirective) {
return new EmulatedCssImportRule(node);
}
}).where((r) => r != null).toList();
}
/// Emulates [CSSRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSRule)
abstract class EmulatedCssRule {
int type;
String cssText;
}
/// Emulates [CSSStyleRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleRule)
class EmulatedCssStyleRule extends EmulatedCssRule {
String selectorText;
EmulatedCssStyleDeclaration style;
EmulatedCssStyleRule(cssv.RuleSet ruleSet) {
final declarationText = new StringBuffer();
ruleSet.declarationGroup.declarations.forEach((d) {
if (d is! cssv.Declaration) {
// Nested selectors not supported
return;
}
// TODO: expression spans are currently broken in csslib; see:
// https://github.com/dart-lang/csslib/pull/14
var declarationSpan = d.span.text;
var colonIdx = declarationSpan.indexOf(':');
var expression = declarationSpan.substring(colonIdx + 1);
declarationText.write('${d.property}: ${expression};');
});
final style = new EmulatedCssStyleDeclaration()
..cssText = declarationText.toString();
this
..type = 1
..cssText = ruleSet.span.text
..selectorText = ruleSet.selectorGroup.span.text
..style = style;
}
}
/// Emulates [CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration)
class EmulatedCssStyleDeclaration {
final String content = '';
String cssText;
}
/// Emulates [CSSMediaRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule)
class EmulatedCssMedialRule extends EmulatedCssRule {
List<EmulatedCssStyleRule> cssRules;
EmulatedMediaList media;
EmulatedCssMedialRule(cssv.MediaDirective directive) {
this
..type = 4
..media = new EmulatedMediaList(directive)
..cssText = directive.span.text
..cssRules = emulateRules(directive.rulesets);
}
}
/// Emulates [MediaList](https://developer.mozilla.org/en-US/docs/Web/API/MediaList)
class EmulatedMediaList {
String mediaText;
EmulatedMediaList(cssv.MediaDirective directive) {
this.mediaText =
directive.mediaQueries.map((q) => q.span.text).join(' and ');
}
}
/// Emulates [CSSImportRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSImportRule)
class EmulatedCssImportRule extends EmulatedCssRule {
EmulatedCssImportRule(cssv.ImportDirective directive) {
this
..type = 3
..cssText = '@${directive.span.text};';
}
}

View File

@ -47,37 +47,10 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter {
resolveAndSetHref(el: HTMLAnchorElement, baseUrl: string, href: string) {
el.href = href == null ? baseUrl : baseUrl + '/../' + href;
}
cssToRules(css: string): any[] {
var style = this.createStyleElement(css);
this.appendChild(this.defaultDoc().head, style);
var rules = [];
if (isPresent(style.sheet)) {
// TODO(sorvell): Firefox throws when accessing the rules of a stylesheet
// with an @import
// https://bugzilla.mozilla.org/show_bug.cgi?id=625013
try {
var rawRules = (<any>style.sheet).cssRules;
rules = ListWrapper.createFixedSize(rawRules.length);
for (var i = 0; i < rawRules.length; i++) {
rules[i] = rawRules[i];
}
} catch (e) {
//
}
} else {
// console.warn('sheet not found', style);
}
this.remove(style);
return rules;
}
supportsDOMEvents(): boolean { return true; }
supportsNativeShadowDOM(): boolean {
return isFunction((<any>this.defaultDoc().body).createShadowRoot);
}
supportsUnprefixedCssAnimation(): boolean {
return isPresent(this.defaultDoc().body.style) &&
isPresent(this.defaultDoc().body.style.animationName);
}
getAnimationPrefix(): string {
return isPresent(this._animationPrefix) ? this._animationPrefix : "";
}

View File

@ -3,8 +3,6 @@ var parser = new parse5.Parser(parse5.TreeAdapters.htmlparser2);
var serializer = new parse5.Serializer(parse5.TreeAdapters.htmlparser2);
var treeAdapter = parser.treeAdapter;
var cssParse = require('css/lib/parse/index');
import {MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
import {DomAdapter, setRootDomAdapter} from './dom_adapter';
import {
@ -474,18 +472,6 @@ export class Parse5DomAdapter extends DomAdapter {
isShadowRoot(node): boolean { return this.getShadowRoot(node) == node; }
importIntoDoc(node): any { return this.clone(node); }
adoptNode(node): any { return node; }
isPageRule(rule): boolean {
return rule.type === 6; // CSSRule.PAGE_RULE
}
isStyleRule(rule): boolean {
return rule.type === 1; // CSSRule.MEDIA_RULE
}
isMediaRule(rule): boolean {
return rule.type === 4; // CSSRule.MEDIA_RULE
}
isKeyframesRule(rule): boolean {
return rule.type === 7; // CSSRule.KEYFRAMES_RULE
}
getHref(el): string { return el.href; }
resolveAndSetHref(el, baseUrl: string, href: string) {
if (href == null) {
@ -531,15 +517,6 @@ export class Parse5DomAdapter extends DomAdapter {
}
return rules;
}
cssToRules(css: string): any[] {
css = css.replace(/url\(\'(.+)\'\)/g, 'url($1)');
var rules = [];
var parsedCSS = cssParse(css, {silent: true});
if (parsedCSS.stylesheet && parsedCSS.stylesheet.rules) {
rules = this._buildRules(parsedCSS.stylesheet.rules, css);
}
return rules;
}
supportsDOMEvents(): boolean { return false; }
supportsNativeShadowDOM(): boolean { return false; }
getGlobalEventTarget(target: string): any {
@ -551,12 +528,6 @@ export class Parse5DomAdapter extends DomAdapter {
return this.defaultDoc().body;
}
}
supportsUnprefixedCssAnimation(): boolean {
// Currently during offline code transformation we do not know
// what browsers we are targetting. To play it safe, we assume
// unprefixed animations are not supported.
return false;
}
getBaseHref(): string { throw 'not implemented'; }
resetBaseElement(): void { throw 'not implemented'; }
getHistory(): History { throw 'not implemented'; }